From e0ace678dafc6d5325363957f6e2ffbfb16113da Mon Sep 17 00:00:00 2001 From: Imotechs Date: Fri, 19 Jul 2024 09:17:50 +0100 Subject: [PATCH 1/8] CREATED[Custom authentication class in app/core] --- app/core/custom_auth.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/core/custom_auth.py diff --git a/app/core/custom_auth.py b/app/core/custom_auth.py new file mode 100644 index 0000000..dd0f053 --- /dev/null +++ b/app/core/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,request,name =None, password = None): + try: + user = User.objects.get(email=name) + success = user.check_password(password) + if success: + return user + except User.DoesNotExist: + try: + user = User.objects.get(name=name) + success = user.check_password(password) + if success: + return user + except User.DoesNotExist: + return None From 65ef98731032dbcec2eca21f38d1449780763d58 Mon Sep 17 00:00:00 2001 From: Imotechs Date: Fri, 19 Jul 2024 09:24:21 +0100 Subject: [PATCH 2/8] ADDED[inventory app] --- .gitignore | 2 + app/app/settings.py | 59 ++++++++++- app/app/urls.py | 3 +- app/core/models.py | 3 +- app/inventory/__init__.py | 0 app/inventory/admin.py | 3 + app/inventory/apps.py | 6 ++ app/inventory/migrations/__init__.py | 0 app/inventory/models.py | 99 +++++++++++++++++++ .../registration/account-activation.html | 39 ++++++++ app/inventory/tests.py | 3 + app/inventory/views.py | 57 +++++++++++ requirements.txt | 1 + 13 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 app/inventory/__init__.py create mode 100644 app/inventory/admin.py create mode 100644 app/inventory/apps.py create mode 100644 app/inventory/migrations/__init__.py create mode 100644 app/inventory/models.py create mode 100644 app/inventory/templates/registration/account-activation.html create mode 100644 app/inventory/tests.py create mode 100644 app/inventory/views.py 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..705fa93 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,12 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'core' + #... + 'rest_framework', + 'rest_framework_simplejwt', + #apps + 'core', + 'inventory' ] MIDDLEWARE = [ @@ -79,6 +85,15 @@ 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } + + # 'default': { + # 'ENGINE': 'django.db.backends.postgresql', + # 'NAME': 'your_db_name', + # 'USER': 'your_db_user', + # 'PASSWORD': 'your_db_password', + # 'HOST': 'localhost', + # 'PORT': '5432', + # } } @@ -123,4 +138,44 @@ 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', + ], +} + + + +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), +} \ No newline at end of file diff --git a/app/app/urls.py b/app/app/urls.py index 7c49775..ad88344 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('inventory.urls')), ] diff --git a/app/core/models.py b/app/core/models.py index a417f39..493ebcd 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.is_email_verified =True return user @@ -36,6 +36,7 @@ class User(AbstractBaseUser, PermissionsMixin): name = models.CharField(max_length=255) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) + is_email_verified=models.BooleanField(default=False) objects = UserManager() 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..8c38f3f --- /dev/null +++ b/app/inventory/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/inventory/apps.py b/app/inventory/apps.py new file mode 100644 index 0000000..905749f --- /dev/null +++ b/app/inventory/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class InventoryConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'inventory' 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..2ec5f7b --- /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) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE) + 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.products.name}' + def save(self, *args, **kwargs): + if not self.slug: + slug_text = slugify(f'Order-from-{self.order.user.name}') + 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() + 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.name}') + self.slug = unique_slugify(self, slug_text) + super().save(*args, **kwargs) \ 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..7ce503c --- /dev/null +++ b/app/inventory/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/inventory/views.py b/app/inventory/views.py new file mode 100644 index 0000000..317af74 --- /dev/null +++ b/app/inventory/views.py @@ -0,0 +1,57 @@ +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 core.custom_auth import EmailNameAuthBackend +from rest_framework.response import Response +from .signals import send_verification_email +from .serializers import UserSerializer + +# Create your views here. + + +class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + 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): + email = request.data.get('email', '') + password = request.data.get('password', '') + user = EmailNameAuthBackend.authenticate(email, password) + if user is not None: + if user.is_email_verified: + if user.is_active: + refresh = RefreshToken.for_user(user) + token = { + 'refresh': str(refresh), + 'access': str(refresh.access_token), + } + return Response(token, status=status.HTTP_200_OK) + return Response({'error': 'Account disabled!'}, status=status.HTTP_401_UNAUTHORIZED) + else: + send_verification_email(user) + return Response({'error': 'Account not verified!'}, status=status.HTTP_401_UNAUTHORIZED) + return Response({'error': 'Invalid login Details'}, status=status.HTTP_401_UNAUTHORIZED) + + +class UserRegistrationView(generics.GenericAPIView): + serializer_class = UserSerializer + def post(self, request,*args,**kwargs): + req = request.data.copy() + data = req['data'] + serializer = self.serializer_class(data=data) + if serializer.is_valid(): + serializer = serializer.save() + message = {'message':f'Account created succesffuly!'} + return Response({message,serializer.data},status=status.HTTP_200_OK) + return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST) + + diff --git a/requirements.txt b/requirements.txt index 843358c..764d61a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Django>=3.0,<4.0.2 djangorestframework==3.13.1 +djangorestframework-simplejwt==5.3.1 # flake8>=3.6.0,<3.7.0 \ No newline at end of file From 74bee86e044d9b2f35a51f485782ff4ea5b83b3d Mon Sep 17 00:00:00 2001 From: Imotechs Date: Fri, 19 Jul 2024 09:26:18 +0100 Subject: [PATCH 3/8] ADDED[ signals,tokens,urls in app/inventory ] --- app/inventory/serializers.py | 35 +++++++++++++++++++++++++++++++++++ app/inventory/signals.py | 35 +++++++++++++++++++++++++++++++++++ app/inventory/tokens.py | 11 +++++++++++ app/inventory/urls.py | 5 +++++ 4 files changed, 86 insertions(+) create mode 100644 app/inventory/serializers.py create mode 100644 app/inventory/signals.py create mode 100644 app/inventory/tokens.py create mode 100644 app/inventory/urls.py diff --git a/app/inventory/serializers.py b/app/inventory/serializers.py new file mode 100644 index 0000000..ef6b682 --- /dev/null +++ b/app/inventory/serializers.py @@ -0,0 +1,35 @@ +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 = ['slug','email', 'name', 'password'] + extra_kwargs = {'password': {'write_only': True}} + + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = ['__all__'] + +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__'] \ No newline at end of file diff --git a/app/inventory/signals.py b/app/inventory/signals.py new file mode 100644 index 0000000..fb6b358 --- /dev/null +++ b/app/inventory/signals.py @@ -0,0 +1,35 @@ +from django.core.mail import EmailMessage +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.template.loader import render_to_string +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode + +from django.conf import settings +from .tokens import account_activation_token + +from django.core.mail import send_mail +from django.contrib.auth import get_user_model + +User = get_user_model() +host_user = settings.EMAIL_HOST_USER + + + +def send_verification_email(user): + mail_subject = "Email verification" + message = render_to_string( "registration/account-activation.html", { + 'user': user, + 'domain': settings.FRONTEND_URL, + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + 'token': account_activation_token.make_token(user), + }) + from_email = f'no-Reply <{host_user}>' + to_email = user.email + send_mail(mail_subject, message, from_email, [to_email], html_message=message) + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + """Send verification email on registration""" + if created: + send_verification_email(instance) \ No newline at end of file diff --git a/app/inventory/tokens.py b/app/inventory/tokens.py new file mode 100644 index 0000000..c849cc3 --- /dev/null +++ b/app/inventory/tokens.py @@ -0,0 +1,11 @@ +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from six import text_type +# from .models import UserToken + +class TokenGenerator(PasswordResetTokenGenerator): + def _make_hash_value(self, user, timestamp): + # UserToken.objects.create(token) + return ( + text_type(user.pk) +text_type(timestamp) + text_type(user.is_active) + ) +account_activation_token = TokenGenerator() \ No newline at end of file diff --git a/app/inventory/urls.py b/app/inventory/urls.py new file mode 100644 index 0000000..480ad3b --- /dev/null +++ b/app/inventory/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +urlpatterns = [ + path('api/',), +] \ No newline at end of file From fe04a49ca80522ffca52342c0cc0a22dd3f9fbd9 Mon Sep 17 00:00:00 2001 From: Imotechs Date: Fri, 19 Jul 2024 20:12:23 +0100 Subject: [PATCH 4/8] ADDED[Added more contents to views, models and serializers] --- app/app/settings.py | 18 +- app/app/urls.py | 4 +- app/core/admin.py | 5 +- app/core/custom_auth.py | 1 + .../migrations/0002_user_is_email_verified.py | 18 ++ app/core/models.py | 1 + app/inventory/admin.py | 6 + app/inventory/apps.py | 3 + app/inventory/migrations/0001_initial.py | 83 ++++++++ app/inventory/models.py | 16 +- app/inventory/serializers.py | 30 ++- app/inventory/urls.py | 23 ++- app/inventory/views.py | 195 +++++++++++++++++- 13 files changed, 376 insertions(+), 27 deletions(-) create mode 100644 app/core/migrations/0002_user_is_email_verified.py create mode 100644 app/inventory/migrations/0001_initial.py diff --git a/app/app/settings.py b/app/app/settings.py index 705fa93..4fe0947 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -12,6 +12,7 @@ from pathlib import Path from datetime import timedelta +from decouple import config # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -26,8 +27,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = [ ] +FRONTEND_URL ='http://localhost:8000' # Application definition @@ -151,6 +153,7 @@ ], } +AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend','core.custom_auth.EmailNameAuthBackend'] SIMPLE_JWT = { @@ -178,4 +181,15 @@ "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", "SLIDING_TOKEN_LIFETIME": timedelta(minutes=60), "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), -} \ No newline at end of file +} + + +# EMAIL SENDING CONFIGURATION +EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT=587 +EMAIL_USE_TLS=True +DUMMY_EMAIL = 'liampeter127@gmail.com' +DUMMY_PASS = 'jazgtjosrtnvnjfq' +EMAIL_HOST_USER=config('EMAIL_HOST_USER',default =DUMMY_EMAIL) +EMAIL_HOST_PASSWORD=config('EMAIL_HOST_PASSWORD',default = DUMMY_PASS) diff --git a/app/app/urls.py b/app/app/urls.py index ad88344..ad7fe07 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -15,8 +15,10 @@ """ from django.contrib import admin from django.urls import path,include +from inventory.views import endpoints urlpatterns = [ path('admin/', admin.site.urls), - path('api/',include('inventory.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/custom_auth.py b/app/core/custom_auth.py index dd0f053..4d33fd5 100644 --- a/app/core/custom_auth.py +++ b/app/core/custom_auth.py @@ -10,6 +10,7 @@ ''' class EmailNameAuthBackend(ModelBackend): def authenticate(self,request,name =None, password = None): + print(name,'name...',password) try: user = User.objects.get(email=name) success = user.check_password(password) diff --git a/app/core/migrations/0002_user_is_email_verified.py b/app/core/migrations/0002_user_is_email_verified.py new file mode 100644 index 0000000..44242cc --- /dev/null +++ b/app/core/migrations/0002_user_is_email_verified.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2024-07-19 11:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_email_verified', + field=models.BooleanField(default=False), + ), + ] diff --git a/app/core/models.py b/app/core/models.py index 493ebcd..15dc368 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -26,6 +26,7 @@ def create_superuser(self, email, password): user.is_staff = True user.is_superuser = True user.is_email_verified =True + user.save() return user diff --git a/app/inventory/admin.py b/app/inventory/admin.py index 8c38f3f..1f2bd6f 100644 --- a/app/inventory/admin.py +++ b/app/inventory/admin.py @@ -1,3 +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 index 905749f..2c8aaa0 100644 --- a/app/inventory/apps.py +++ b/app/inventory/apps.py @@ -4,3 +4,6 @@ class InventoryConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'inventory' + def ready(self): + import inventory.signals + 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/models.py b/app/inventory/models.py index 2ec5f7b..ededdc2 100644 --- a/app/inventory/models.py +++ b/app/inventory/models.py @@ -40,10 +40,10 @@ class Product(models.Model): null=False, blank=False, editable=False, allow_unicode=True) - name = models.CharField(max_length=100) - description = models.TextField() - price = models.DecimalField(max_digits=10, decimal_places=2) - category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE) + 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,blank=True, null=True) + 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: @@ -70,10 +70,10 @@ class Meta: db_table ='inventory_order' verbose_name_plural ='orders' def __str__(self): - return f'order-for{self.products.name}' + return f'order-for-{self.user.email}' def save(self, *args, **kwargs): if not self.slug: - slug_text = slugify(f'Order-from-{self.order.user.name}') + slug_text = slugify(f'Order-from-{self.user.email}') self.slug = unique_slugify(self, slug_text) super().save(*args, **kwargs) @@ -84,7 +84,7 @@ class OrderItem(models.Model): 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() + quantity = models.PositiveIntegerField(default=1) date_created = models.DateTimeField(auto_now_add =True) class Meta: @@ -94,6 +94,6 @@ 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.name}') + 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 index ef6b682..e198c85 100644 --- a/app/inventory/serializers.py +++ b/app/inventory/serializers.py @@ -4,19 +4,31 @@ User = get_user_model() + +class SlugRelatedField(serializers.SlugRelatedField): + def to_internal_value(self, data): + try: + return self.get_queryset().get(slug=data) + except self.queryset.model.DoesNotExist: + raise serializers.ValidationError("Object with this slug does not exist.") + + def to_representation(self, value): + return value + class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['slug','email', 'name', 'password'] + fields = ['email', 'name', 'password'] extra_kwargs = {'password': {'write_only': True}} class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category - fields = ['__all__'] + fields = ['slug','name'] class ProductSerializer(serializers.ModelSerializer): + class Meta: model = Product fields = ['slug', 'name', 'description', 'price', 'category','date_created'] @@ -27,9 +39,21 @@ class OrderItemSerializer(serializers.ModelSerializer): 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 = ['__all__'] \ No newline at end of file + fields = ['slug', 'date_created', 'order_items'] \ No newline at end of file diff --git a/app/inventory/urls.py b/app/inventory/urls.py index 480ad3b..6a22a88 100644 --- a/app/inventory/urls.py +++ b/app/inventory/urls.py @@ -1,5 +1,26 @@ from django.urls import path +from rest_framework_simplejwt.views import TokenRefreshView +from .views import CustomTokenObtain,UserRegistrationView,ProductView,CategoryView,OrderView urlpatterns = [ - path('api/',), + # 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()), + #products urls + path('inventory/products/',ProductView.as_view()),# list all products + path('inventory/product/create/',ProductView.as_view()),#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()),#list all categories + path('inventory/category//',CategoryView.as_view()),#list category item + path('inventory/category/create/',CategoryView.as_view()),#create a category item + #Order view + path('inventory/order/create/',OrderView.as_view()),# 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 index 317af74..88f06b3 100644 --- a/app/inventory/views.py +++ b/app/inventory/views.py @@ -4,10 +4,22 @@ from rest_framework import generics, status from rest_framework_simplejwt.tokens import RefreshToken from core.custom_auth import EmailNameAuthBackend +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.decorators import permission_classes +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from django.db import transaction + from rest_framework.response import Response from .signals import send_verification_email -from .serializers import UserSerializer +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 .serializers import UserSerializer,ProductSerializer,\ + OrderItemSerializer,OrderSerializer,CategorySerializer,OrderHistorySerializer +from .models import Category, Product,Order,OrderItem # Create your views here. @@ -23,9 +35,10 @@ def get_token(cls, user): class CustomTokenObtain(TokenObtainPairView): serializer_class = CustomTokenObtainPairSerializer def post(self, request, *args, **kwargs): - email = request.data.get('email', '') - password = request.data.get('password', '') - user = EmailNameAuthBackend.authenticate(email, password) + data = request.data + email = data['email']or None + password = data['password'] or None + user = EmailNameAuthBackend.authenticate(self,request,email, password) if user is not None: if user.is_email_verified: if user.is_active: @@ -38,20 +51,182 @@ def post(self, request, *args, **kwargs): return Response({'error': 'Account disabled!'}, status=status.HTTP_401_UNAUTHORIZED) else: send_verification_email(user) - return Response({'error': 'Account not verified!'}, status=status.HTTP_401_UNAUTHORIZED) + return Response({'error': 'Account not verified!,checkout we resend verification email'}, 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): - req = request.data.copy() - data = req['data'] + data = request.data.copy() serializer = self.serializer_class(data=data) if serializer.is_valid(): serializer = serializer.save() - message = {'message':f'Account created succesffuly!'} - return Response({message,serializer.data},status=status.HTTP_200_OK) + + 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: + 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) + return Response(serializer.data) + def post(self, request,*args,**kwargs): + data = request.data + 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) + 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): + 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 From 03195735a136e269303e15cd1daf32dccc8c6648 Mon Sep 17 00:00:00 2001 From: Imotechs Date: Fri, 19 Jul 2024 21:12:58 +0100 Subject: [PATCH 5/8] ADDED[Tests and refactored views] --- app/core/custom_auth.py | 1 - app/inventory/models.py | 2 +- app/inventory/tests.py | 98 ++++++++++++++++++++++++++++++++++++++++- app/inventory/urls.py | 12 ++--- app/inventory/views.py | 15 +++---- 5 files changed, 110 insertions(+), 18 deletions(-) diff --git a/app/core/custom_auth.py b/app/core/custom_auth.py index 4d33fd5..dd0f053 100644 --- a/app/core/custom_auth.py +++ b/app/core/custom_auth.py @@ -10,7 +10,6 @@ ''' class EmailNameAuthBackend(ModelBackend): def authenticate(self,request,name =None, password = None): - print(name,'name...',password) try: user = User.objects.get(email=name) success = user.check_password(password) diff --git a/app/inventory/models.py b/app/inventory/models.py index ededdc2..575d38d 100644 --- a/app/inventory/models.py +++ b/app/inventory/models.py @@ -42,7 +42,7 @@ class Product(models.Model): 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,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) diff --git a/app/inventory/tests.py b/app/inventory/tests.py index 7ce503c..286eaf2 100644 --- a/app/inventory/tests.py +++ b/app/inventory/tests.py @@ -1,3 +1,97 @@ -from django.test import TestCase +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 -# Create your tests here. +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 index 6a22a88..5c9014c 100644 --- a/app/inventory/urls.py +++ b/app/inventory/urls.py @@ -8,19 +8,19 @@ path("token/refresh/", TokenRefreshView.as_view(), name="refresh-token"), # other views - path("register/", UserRegistrationView.as_view()), + path("register/", UserRegistrationView.as_view(),name='user_register'), #products urls - path('inventory/products/',ProductView.as_view()),# list all products - path('inventory/product/create/',ProductView.as_view()),#Create a new product + 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()),#list all categories + 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()),#create a 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()),# create order + 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 index 88f06b3..0c58fb3 100644 --- a/app/inventory/views.py +++ b/app/inventory/views.py @@ -146,17 +146,16 @@ class CategoryView(APIView): def get(self, request, slug=None,*args,**kwargs): if slug: try: - product = self.model_class.objects.get(slug=slug) - serializer = self.serializer_class(product,many =False) - return Response(serializer.data) + 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) - else: - products = self.model_class.objects.all() - serializer = self.serializer_class(products, many=True) - return Response(serializer.data) + 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 + data = request.data.copy() serializer = self.serializer_class(data=data) if serializer.is_valid(): serializer.save() From 0ef498b76ce61511ad673b13d814fe82646c7708 Mon Sep 17 00:00:00 2001 From: Imotechs Date: Sat, 20 Jul 2024 14:57:49 +0100 Subject: [PATCH 6/8] ADDED[Added Custom Login and emailverification] --- app/app/settings.py | 37 +++++---- app/app/urls.py | 3 +- app/core/custom_auth.py | 6 +- ...uantity_alter_product_category_and_more.py | 39 ++++++++++ app/inventory/serializers.py | 17 ++--- app/inventory/views.py | 75 +++++++++++++++---- requirements.txt | 12 ++- 7 files changed, 139 insertions(+), 50 deletions(-) create mode 100644 app/inventory/migrations/0002_alter_orderitem_quantity_alter_product_category_and_more.py diff --git a/app/app/settings.py b/app/app/settings.py index 4fe0947..989b715 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -13,7 +13,7 @@ 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 @@ -22,12 +22,12 @@ # 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' @@ -83,19 +83,19 @@ # 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.postgresql', - # 'NAME': 'your_db_name', - # 'USER': 'your_db_user', - # 'PASSWORD': 'your_db_password', - # 'HOST': 'localhost', - # 'PORT': '5432', + # 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': BASE_DIR / 'db.sqlite3', # } + + 'default': { + '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'), + } } @@ -189,7 +189,6 @@ EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT=587 EMAIL_USE_TLS=True -DUMMY_EMAIL = 'liampeter127@gmail.com' -DUMMY_PASS = 'jazgtjosrtnvnjfq' -EMAIL_HOST_USER=config('EMAIL_HOST_USER',default =DUMMY_EMAIL) -EMAIL_HOST_PASSWORD=config('EMAIL_HOST_PASSWORD',default = DUMMY_PASS) + +EMAIL_HOST_USER=config('HOST_USER',default ='') +EMAIL_HOST_PASSWORD=config('HOST_PASS',default = '') diff --git a/app/app/urls.py b/app/app/urls.py index ad7fe07..7e477b9 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -15,10 +15,11 @@ """ from django.contrib import admin from django.urls import path,include -from inventory.views import endpoints +from inventory.views import endpoints,EmailVerificationView urlpatterns = [ path('admin/', admin.site.urls), path('',endpoints), + path("verify-email/",EmailVerificationView.as_view(),name='email_verification'), path('api/v1/',include('inventory.urls')), ] diff --git a/app/core/custom_auth.py b/app/core/custom_auth.py index dd0f053..512fd81 100644 --- a/app/core/custom_auth.py +++ b/app/core/custom_auth.py @@ -9,15 +9,15 @@ permiting them to use their names or email to login ''' class EmailNameAuthBackend(ModelBackend): - def authenticate(self,request,name =None, password = None): + def authenticate(self,requst,email =None, password = None): try: - user = User.objects.get(email=name) + user = User.objects.get(email=email) success = user.check_password(password) if success: return user except User.DoesNotExist: try: - user = User.objects.get(name=name) + user = User.objects.get(name=email) success = user.check_password(password) if success: return user 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..db704ea --- /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-20 11:11 + +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/serializers.py b/app/inventory/serializers.py index e198c85..dffef85 100644 --- a/app/inventory/serializers.py +++ b/app/inventory/serializers.py @@ -5,21 +5,18 @@ User = get_user_model() -class SlugRelatedField(serializers.SlugRelatedField): - def to_internal_value(self, data): - try: - return self.get_queryset().get(slug=data) - except self.queryset.model.DoesNotExist: - raise serializers.ValidationError("Object with this slug does not exist.") - - def to_representation(self, value): - return value - 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): diff --git a/app/inventory/views.py b/app/inventory/views.py index 0c58fb3..c2173f9 100644 --- a/app/inventory/views.py +++ b/app/inventory/views.py @@ -9,23 +9,30 @@ from rest_framework.decorators import permission_classes from rest_framework.permissions import IsAuthenticatedOrReadOnly from django.db import transaction - from rest_framework.response import Response -from .signals import send_verification_email 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.utils.http import urlsafe_base64_decode +from .tokens import account_activation_token +from django.http import HttpResponse +from django.contrib.auth import authenticate +from django.contrib.auth import get_user_model + +#local imports +from .signals import send_verification_email 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): + 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 @@ -35,21 +42,22 @@ def get_token(cls, user): class CustomTokenObtain(TokenObtainPairView): serializer_class = CustomTokenObtainPairSerializer def post(self, request, *args, **kwargs): - data = request.data - email = data['email']or None + data = request.data #get the request data and retrieve email/name and password + name = data['email']or None password = data['password'] or None - user = EmailNameAuthBackend.authenticate(self,request,email, password) - if user is not None: - if user.is_email_verified: + 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 + user.save() + if user.is_email_verified:# if user verify his email if user.is_active: - refresh = RefreshToken.for_user(user) + 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) + 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) - else: + else:# else email is not verified resend him the verification email and throw him an error message send_verification_email(user) return Response({'error': 'Account not verified!,checkout we resend verification email'}, status=status.HTTP_401_UNAUTHORIZED) return Response({'error': 'Invalid login Details'}, status=status.HTTP_401_UNAUTHORIZED) @@ -68,7 +76,46 @@ def post(self, request,*args,**kwargs): return Response(data,status=status.HTTP_200_OK) return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST) - +class EmailVerificationView(generics.GenericAPIView): + serializer_class = UserSerializer + permission_classes =[] + authentication_classes=[] + def post(self, request):# handle verification via post data and not directly on our backend domain + data = request.data.copy() + uidb64 = data.get('uidb64','') + token = data.get("token", '') + try: + uid = urlsafe_base64_decode(uidb64).decode() + user = User.objects.get(id=uid) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + return Response({"message": "User dose not exist."}, status=status.HTTP_404_NOT_FOUND) + if user is not None and account_activation_token.check_token(user,token): + user.is_email_verified = True + user.is_active = True + user.save() + message = {'message':'Verification completed!!'} + return Response(message,status=status.HTTP_200_OK) + message = {'message':'Verification link already used or has Expired!!'} + return Response(message,status=status.HTTP_400_BAD_REQUEST) + def get(self,request,*args,**kwargs):# handle verification on get directly on the backend domain + uidb64 = request.GET.get('uid', '') + token = request.GET.get('confirmation_token', '') + try: + uid = urlsafe_base64_decode(uidb64).decode() + user = User.objects.get(id=uid) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + return Response({"message": "User dose not exist."}, status=status.HTTP_404_NOT_FOUND) + if user is not None and account_activation_token.check_token(user,token): + user.is_email_verified = True + user.is_active = True + user.save() + html_content = "

Account Verified!!

" + return HttpResponse(html_content,status=status.HTTP_200_OK) + html_content = "

Verification link already used or has Expired!!

" + return HttpResponse(html_content,status=status.HTTP_400_BAD_REQUEST) + class ProductView(APIView):#One View for all CRUD on Product model authentication_classes=[] @@ -195,7 +242,7 @@ def post(self, request, *args, **kwargs): quantity = item_data.get('quantity', 1) OrderItem.objects.create(order=order, product=product, quantity=quantity) except Product.DoesNotExist: - transaction.set_rollback(True) + 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) @@ -205,7 +252,7 @@ def post(self, request, *args, **kwargs): @api_view(['GET']) @permission_classes([AllowAny]) -def endpoints(request): +def endpoints(request):# endpoint to keep all out endpoint at the root url for easy access and development data = [ "api/v1/register/", diff --git a/requirements.txt b/requirements.txt index 764d61a..2d0bc5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,11 @@ -Django>=3.0,<4.0.2 +asgiref==3.8.1 +Django==4.0.1 djangorestframework==3.13.1 djangorestframework-simplejwt==5.3.1 - -# flake8>=3.6.0,<3.7.0 \ No newline at end of file +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 From 1050fcd59cda9524a38faac21fc9312c1d712959 Mon Sep 17 00:00:00 2001 From: Imotechs Date: Sun, 21 Jul 2024 10:16:55 +0100 Subject: [PATCH 7/8] ADDED[Added Custom Login and and refactor views] --- app/app/settings.py | 15 +---- app/app/urls.py | 3 +- app/core/migrations/0001_initial.py | 2 +- .../migrations/0002_user_is_email_verified.py | 18 ----- app/core/models.py | 2 - app/inventory/apps.py | 3 +- ...uantity_alter_product_category_and_more.py | 2 +- app/inventory/signals.py | 35 ---------- app/inventory/tokens.py | 11 --- app/inventory/views.py | 67 +++---------------- 10 files changed, 16 insertions(+), 142 deletions(-) delete mode 100644 app/core/migrations/0002_user_is_email_verified.py delete mode 100644 app/inventory/signals.py delete mode 100644 app/inventory/tokens.py diff --git a/app/app/settings.py b/app/app/settings.py index 989b715..02bed2a 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -40,12 +40,12 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - #... + #...third party apps 'rest_framework', 'rest_framework_simplejwt', - #apps + # local apps 'core', - 'inventory' + 'inventory', ] MIDDLEWARE = [ @@ -183,12 +183,3 @@ "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), } - -# EMAIL SENDING CONFIGURATION -EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.gmail.com' -EMAIL_PORT=587 -EMAIL_USE_TLS=True - -EMAIL_HOST_USER=config('HOST_USER',default ='') -EMAIL_HOST_PASSWORD=config('HOST_PASS',default = '') diff --git a/app/app/urls.py b/app/app/urls.py index 7e477b9..ad7fe07 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -15,11 +15,10 @@ """ from django.contrib import admin from django.urls import path,include -from inventory.views import endpoints,EmailVerificationView +from inventory.views import endpoints urlpatterns = [ path('admin/', admin.site.urls), path('',endpoints), - path("verify-email/",EmailVerificationView.as_view(),name='email_verification'), path('api/v1/',include('inventory.urls')), ] 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/migrations/0002_user_is_email_verified.py b/app/core/migrations/0002_user_is_email_verified.py deleted file mode 100644 index 44242cc..0000000 --- a/app/core/migrations/0002_user_is_email_verified.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.1 on 2024-07-19 11:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='is_email_verified', - field=models.BooleanField(default=False), - ), - ] diff --git a/app/core/models.py b/app/core/models.py index 15dc368..3bcbca4 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -25,7 +25,6 @@ def create_superuser(self, email, password): user = self.create_user(email, password) user.is_staff = True user.is_superuser = True - user.is_email_verified =True user.save() return user @@ -37,7 +36,6 @@ class User(AbstractBaseUser, PermissionsMixin): name = models.CharField(max_length=255) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) - is_email_verified=models.BooleanField(default=False) objects = UserManager() diff --git a/app/inventory/apps.py b/app/inventory/apps.py index 2c8aaa0..3d0d059 100644 --- a/app/inventory/apps.py +++ b/app/inventory/apps.py @@ -4,6 +4,5 @@ class InventoryConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'inventory' - def ready(self): - import inventory.signals + 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 index db704ea..eca71ab 100644 --- 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 @@ -1,4 +1,4 @@ -# Generated by Django 4.0.1 on 2024-07-20 11:11 +# Generated by Django 4.0.1 on 2024-07-21 08:33 from django.db import migrations, models import django.db.models.deletion diff --git a/app/inventory/signals.py b/app/inventory/signals.py deleted file mode 100644 index fb6b358..0000000 --- a/app/inventory/signals.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.core.mail import EmailMessage -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.template.loader import render_to_string -from django.utils.encoding import force_bytes -from django.utils.http import urlsafe_base64_encode - -from django.conf import settings -from .tokens import account_activation_token - -from django.core.mail import send_mail -from django.contrib.auth import get_user_model - -User = get_user_model() -host_user = settings.EMAIL_HOST_USER - - - -def send_verification_email(user): - mail_subject = "Email verification" - message = render_to_string( "registration/account-activation.html", { - 'user': user, - 'domain': settings.FRONTEND_URL, - 'uid': urlsafe_base64_encode(force_bytes(user.pk)), - 'token': account_activation_token.make_token(user), - }) - from_email = f'no-Reply <{host_user}>' - to_email = user.email - send_mail(mail_subject, message, from_email, [to_email], html_message=message) - -@receiver(post_save, sender=User) -def create_user_profile(sender, instance, created, **kwargs): - """Send verification email on registration""" - if created: - send_verification_email(instance) \ No newline at end of file diff --git a/app/inventory/tokens.py b/app/inventory/tokens.py deleted file mode 100644 index c849cc3..0000000 --- a/app/inventory/tokens.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.contrib.auth.tokens import PasswordResetTokenGenerator -from six import text_type -# from .models import UserToken - -class TokenGenerator(PasswordResetTokenGenerator): - def _make_hash_value(self, user, timestamp): - # UserToken.objects.create(token) - return ( - text_type(user.pk) +text_type(timestamp) + text_type(user.is_active) - ) -account_activation_token = TokenGenerator() \ No newline at end of file diff --git a/app/inventory/views.py b/app/inventory/views.py index c2173f9..b737757 100644 --- a/app/inventory/views.py +++ b/app/inventory/views.py @@ -7,22 +7,17 @@ from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import permission_classes -from rest_framework.permissions import IsAuthenticatedOrReadOnly 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.utils.http import urlsafe_base64_decode -from .tokens import account_activation_token -from django.http import HttpResponse from django.contrib.auth import authenticate from django.contrib.auth import get_user_model #local imports -from .signals import send_verification_email from .serializers import UserSerializer,ProductSerializer,\ OrderItemSerializer,OrderSerializer,CategorySerializer,OrderHistorySerializer from .models import Category, Product,Order,OrderItem @@ -47,19 +42,14 @@ def post(self, request, *args, **kwargs): 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 - user.save() - if user.is_email_verified:# if user verify his email - 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) - else:# else email is not verified resend him the verification email and throw him an error message - send_verification_email(user) - return Response({'error': 'Account not verified!,checkout we resend verification email'}, status=status.HTTP_401_UNAUTHORIZED) + 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) @@ -76,46 +66,7 @@ def post(self, request,*args,**kwargs): return Response(data,status=status.HTTP_200_OK) return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST) -class EmailVerificationView(generics.GenericAPIView): - serializer_class = UserSerializer - permission_classes =[] - authentication_classes=[] - def post(self, request):# handle verification via post data and not directly on our backend domain - data = request.data.copy() - uidb64 = data.get('uidb64','') - token = data.get("token", '') - try: - uid = urlsafe_base64_decode(uidb64).decode() - user = User.objects.get(id=uid) - except (TypeError, ValueError, OverflowError, User.DoesNotExist): - user = None - return Response({"message": "User dose not exist."}, status=status.HTTP_404_NOT_FOUND) - if user is not None and account_activation_token.check_token(user,token): - user.is_email_verified = True - user.is_active = True - user.save() - message = {'message':'Verification completed!!'} - return Response(message,status=status.HTTP_200_OK) - message = {'message':'Verification link already used or has Expired!!'} - return Response(message,status=status.HTTP_400_BAD_REQUEST) - def get(self,request,*args,**kwargs):# handle verification on get directly on the backend domain - uidb64 = request.GET.get('uid', '') - token = request.GET.get('confirmation_token', '') - try: - uid = urlsafe_base64_decode(uidb64).decode() - user = User.objects.get(id=uid) - except (TypeError, ValueError, OverflowError, User.DoesNotExist): - user = None - return Response({"message": "User dose not exist."}, status=status.HTTP_404_NOT_FOUND) - if user is not None and account_activation_token.check_token(user,token): - user.is_email_verified = True - user.is_active = True - user.save() - html_content = "

Account Verified!!

" - return HttpResponse(html_content,status=status.HTTP_200_OK) - html_content = "

Verification link already used or has Expired!!

" - return HttpResponse(html_content,status=status.HTTP_400_BAD_REQUEST) - + class ProductView(APIView):#One View for all CRUD on Product model authentication_classes=[] From 00a141e9c0ffde290c914cc1fb756245655c60c1 Mon Sep 17 00:00:00 2001 From: Imotechs Date: Sun, 21 Jul 2024 10:26:44 +0100 Subject: [PATCH 8/8] ADDED[Added Custom Login and and refactor views] --- app/app/settings.py | 2 +- app/{core => inventory}/custom_auth.py | 0 app/inventory/views.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/{core => inventory}/custom_auth.py (100%) diff --git a/app/app/settings.py b/app/app/settings.py index 02bed2a..01784ae 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -153,7 +153,7 @@ ], } -AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend','core.custom_auth.EmailNameAuthBackend'] +AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend','inventory.custom_auth.EmailNameAuthBackend'] SIMPLE_JWT = { diff --git a/app/core/custom_auth.py b/app/inventory/custom_auth.py similarity index 100% rename from app/core/custom_auth.py rename to app/inventory/custom_auth.py diff --git a/app/inventory/views.py b/app/inventory/views.py index b737757..5ce309b 100644 --- a/app/inventory/views.py +++ b/app/inventory/views.py @@ -3,7 +3,7 @@ from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework import generics, status from rest_framework_simplejwt.tokens import RefreshToken -from core.custom_auth import EmailNameAuthBackend +from .custom_auth import EmailNameAuthBackend from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import permission_classes