From 43f9b7ef7cf5cb156de0fd07369148edf3278487 Mon Sep 17 00:00:00 2001 From: i701 Date: Fri, 28 Mar 2025 22:25:30 +0500 Subject: [PATCH] Enhance User model: add email field with unique constraint, update id_card field to allow null values, and include verified field. Update UserAdmin to display verified field. Improve device listing to filter by logged-in user. --- .vscode/settings.json | 11 +- api/admin.py | 2 + api/managers.py | 15 ++- ..._alter_user_managers_alter_user_id_card.py | 24 ++++ api/migrations/0014_alter_user_email.py | 17 +++ api/models.py | 3 +- api/views.py | 116 ++++++++++-------- devices/views.py | 15 +++ djangopasswordlessknox/utils.py | 1 - 9 files changed, 145 insertions(+), 59 deletions(-) create mode 100644 api/migrations/0013_alter_user_managers_alter_user_id_card.py create mode 100644 api/migrations/0014_alter_user_email.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 91761a5..5fd0503 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,12 @@ { - "djlint.showInstallError": false + "djlint.showInstallError": false, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./api", + "-p", + "*test.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true } \ No newline at end of file diff --git a/api/admin.py b/api/admin.py index c70b58e..655efe0 100644 --- a/api/admin.py +++ b/api/admin.py @@ -11,6 +11,7 @@ class UserAdmin(BaseUserAdmin): "email", "first_name", "last_name", + "verified", "is_active", "is_staff", "mobile", @@ -36,6 +37,7 @@ class UserAdmin(BaseUserAdmin): "email", "mobile", "address", + "verified", "wallet_balance", "acc_no", "id_card", diff --git a/api/managers.py b/api/managers.py index aef1f55..30fb14b 100644 --- a/api/managers.py +++ b/api/managers.py @@ -1,20 +1,23 @@ -from django.contrib.auth.models import BaseUserManager +from typing import Optional +from django.contrib.auth.models import UserManager as BaseUserManager class CustomUserManager(BaseUserManager): - def create_user(self, username, password=None, **extra_fields): + def create_user( + self, username, email=None, password: Optional[str] = None, **extra_fields + ): """Create and return a user with an email and password.""" if not username: raise ValueError("The Username field must be set") - user = self.model(username=username, **extra_fields) + user = self.model(username=username, email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user - def create_superuser(self, username, password=None, **extra_fields): + def create_superuser(self, username, email=None, password=None, **extra_fields): """Create and return a superuser with an email and password.""" - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) return self.create_user(username, password, **extra_fields) diff --git a/api/migrations/0013_alter_user_managers_alter_user_id_card.py b/api/migrations/0013_alter_user_managers_alter_user_id_card.py new file mode 100644 index 0000000..26a0989 --- /dev/null +++ b/api/migrations/0013_alter_user_managers_alter_user_id_card.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2025-03-26 23:55 + +import api.managers +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0012_alter_user_id_card"), + ] + + operations = [ + migrations.AlterModelManagers( + name="user", + managers=[ + ("objects", api.managers.CustomUserManager()), + ], + ), + migrations.AlterField( + model_name="user", + name="id_card", + field=models.CharField(blank=True, max_length=255, null=True, unique=True), + ), + ] diff --git a/api/migrations/0014_alter_user_email.py b/api/migrations/0014_alter_user_email.py new file mode 100644 index 0000000..03ea825 --- /dev/null +++ b/api/migrations/0014_alter_user_email.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2025-03-28 16:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0013_alter_user_managers_alter_user_id_card"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="email", + field=models.EmailField(blank=True, max_length=254, null=True, unique=True), + ), + ] diff --git a/api/models.py b/api/models.py index 7bbf148..22accab 100644 --- a/api/models.py +++ b/api/models.py @@ -10,10 +10,11 @@ from django.utils import timezone class User(AbstractUser): address = models.CharField(max_length=255, blank=True) + email = models.EmailField(blank=True, null=True, unique=True) mobile = models.CharField(max_length=255, blank=True, unique=True, null=True) designation = models.CharField(max_length=255, blank=True) acc_no = models.CharField(max_length=255, blank=True) - id_card = models.CharField(max_length=255, blank=True, unique=True) + id_card = models.CharField(max_length=255, blank=True, unique=True, null=True) verified = models.BooleanField(default=False) dob = models.DateField(blank=True, null=True) terms_accepted = models.BooleanField(default=False) diff --git a/api/views.py b/api/views.py index 43e635a..1525695 100644 --- a/api/views.py +++ b/api/views.py @@ -32,6 +32,17 @@ from .serializers import ( CustomReadOnlyUserByIDCardSerializer, ) +ID_CARD_PATTERN = r"^[A-Z]{1,2}[0-9]{6,7}$" +MOBILE_PATTERN = r"^[7|9][0-9]{6}$" +ACCOUNT_NUMBER_PATTERN = r"^(7\d{12}|9\d{16})$" + + +class ErrorMessages: + USERNAME_EXISTS = "Username already exists." + INVALID_ID_CARD = "Please enter a valid ID card number." + INVALID_MOBILE = "Please enter a valid mobile number." + INVALID_ACCOUNT = "Please enter a valid account number." + class CreateUserView(generics.CreateAPIView): # Create user API view @@ -42,65 +53,39 @@ class CreateUserView(generics.CreateAPIView): def post(self, request, *args, **kwargs): # Extract required fields from request data password = request.data.get("password") - username = request.data.get("username") # This can be None + username = request.data.get("username") address = request.data.get("address") mobile = request.data.get("mobile") acc_no = request.data.get("acc_no") id_card = request.data.get("id_card") dob = request.data.get("dob") - atoll_id = request.data.get("atoll") # Get the atoll ID - island_id = request.data.get("island") # Get the island ID + atoll_id = request.data.get("atoll") + island_id = request.data.get("island") terms_accepted = request.data.get("terms_accepted") policy_accepted = request.data.get("policy_accepted") firstname = request.data.get("firstname") lastname = request.data.get("lastname") - # Validate required fields - existing_username = User.objects.filter(username=username).first() - if existing_username: - return Response({"message": "Username already exists."}, status=400) + # Validate required fields first + validation_error = self.validate_required_fields(request.data) + if validation_error: + return validation_error - if not firstname: - return Response({"message": "firstname is required."}, status=400) - if not lastname: - return Response({"message": "lastname is required."}, status=400) - if not password: - return Response({"message": "Password is required."}, status=400) - if not username: - return Response({"message": "Username is required."}, status=400) - if not address: - return Response({"message": "Address is required."}, status=400) - if not mobile: - return Response({"message": "Mobile number is required."}, status=400) - if not acc_no: - return Response({"message": "Account number is required."}, status=400) - if not id_card: - return Response({"message": "ID card is required."}, status=400) - if not dob: - return Response({"message": "Date of birth is required."}, status=400) - if not atoll_id: - return Response({"message": "Atoll is required."}, status=400) - if not island_id: - return Response({"message": "Island is required."}, status=400) - if terms_accepted is None: - return Response({"message": "Terms acceptance is required."}, status=400) - if policy_accepted is None: - return Response({"message": "Policy acceptance is required."}, status=400) + # Check username uniqueness after validation + if User.objects.filter(username=username).exists(): + return Response({"message": ErrorMessages.USERNAME_EXISTS}, status=400) - if not re.match(r"^[A-Z]{1,2}[0-9]{6,7}$", id_card): - return Response( - {"message": "Please enter a valid ID card number."}, status=400 - ) + if id_card and not re.match(ID_CARD_PATTERN, id_card): + return Response({"message": ErrorMessages.INVALID_ID_CARD}, status=400) - if not re.match(r"^[7|9][0-9]{6}$", mobile): - return Response( - {"message": "Please enter a valid mobile number."}, status=400 - ) + if User.objects.filter(id_card=id_card).exists(): + return Response({"message": "ID card already exists."}, status=400) - if not re.match(r"^(7\d{12}|9\d{16})$", acc_no): - return Response( - {"message": "Please enter a valid account number."}, status=400 - ) + if mobile is None or not re.match(MOBILE_PATTERN, mobile): + return Response({"message": ErrorMessages.INVALID_MOBILE}, status=400) + + if acc_no is None or not re.match(ACCOUNT_NUMBER_PATTERN, acc_no): + return Response({"message": ErrorMessages.INVALID_ACCOUNT}, status=400) # Fetch Atoll and Island instances try: @@ -111,19 +96,20 @@ class CreateUserView(generics.CreateAPIView): except Island.DoesNotExist: return Response({"message": "Island not found."}, status=404) - # Create user without email + # Create user user = User.objects.create_user( first_name=firstname, last_name=lastname, - username=username, + username=str(username), password=password, + email=None, address=address, mobile=mobile, acc_no=acc_no, id_card=id_card, dob=dob, - atoll=atoll, # Assign the Atoll instance - island=island, # Assign the Island instance + atoll=atoll, + island=island, terms_accepted=terms_accepted, policy_accepted=policy_accepted, ) @@ -133,6 +119,32 @@ class CreateUserView(generics.CreateAPIView): serializer.data, status=status.HTTP_201_CREATED, headers=headers ) + def validate_required_fields(self, data): + required_fields = { + "firstname": "First name", + "lastname": "Last name", + "password": "Password", + "username": "Username", + "address": "Address", + "mobile": "Mobile number", + "acc_no": "Account number", + "id_card": "ID card", + "dob": "Date of birth", + "atoll": "Atoll", + "island": "Island", + } + + for field, label in required_fields.items(): + if not data.get(field): + return Response({"message": f"{label} is required."}, status=400) + + if data.get("terms_accepted") is None: + return Response({"message": "Terms acceptance is required."}, status=400) + if data.get("policy_accepted") is None: + return Response({"message": "Policy acceptance is required."}, status=400) + + return None + class LoginView(KnoxLoginView): # login view extending KnoxLoginView @@ -216,7 +228,11 @@ def filter_user(request): print(f"Querying with filters: {filters}") print(f"Found user: {user}") - return Response({"ok": True if user else False}) + return Response( + {"ok": True, "verified": user.verified} + if user + else {"ok": False, "verified": False} + ) class ListUserByIDCardView(generics.ListAPIView): diff --git a/devices/views.py b/devices/views.py index 0bdeeb1..adf7c40 100644 --- a/devices/views.py +++ b/devices/views.py @@ -22,6 +22,21 @@ class DeviceListCreateAPIView( filterset_fields = "__all__" filterset_class = DeviceFilter + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + + # Filter devices by the logged-in user unless the user is a superuser + if not request.user.is_superuser: + queryset = queryset.filter(user=request.user) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + def get_serializer_class(self) -> type: if self.request.method == "POST": return CreateDeviceSerializer diff --git a/djangopasswordlessknox/utils.py b/djangopasswordlessknox/utils.py index ea01f06..529b064 100644 --- a/djangopasswordlessknox/utils.py +++ b/djangopasswordlessknox/utils.py @@ -44,7 +44,6 @@ def authenticate_by_token(callback_token): def create_callback_token_for_user(user, token_type): - token = None token_type = token_type.upper()