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.
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 2m39s

This commit is contained in:
i701 2025-03-28 22:25:30 +05:00
parent ddfbeba2f4
commit 43f9b7ef7c
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
9 changed files with 145 additions and 59 deletions

11
.vscode/settings.json vendored
View File

@ -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
} }

View File

@ -11,6 +11,7 @@ class UserAdmin(BaseUserAdmin):
"email", "email",
"first_name", "first_name",
"last_name", "last_name",
"verified",
"is_active", "is_active",
"is_staff", "is_staff",
"mobile", "mobile",
@ -36,6 +37,7 @@ class UserAdmin(BaseUserAdmin):
"email", "email",
"mobile", "mobile",
"address", "address",
"verified",
"wallet_balance", "wallet_balance",
"acc_no", "acc_no",
"id_card", "id_card",

View File

@ -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): 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.""" """Create and return a user with an email and password."""
if not username: if not username:
raise ValueError("The Username field must be set") 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.set_password(password)
user.save(using=self._db) user.save(using=self._db)
return user 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.""" """Create and return a superuser with an email and password."""
extra_fields.setdefault('is_staff', True) extra_fields.setdefault("is_staff", True)
extra_fields.setdefault('is_superuser', True) extra_fields.setdefault("is_superuser", True)
return self.create_user(username, password, **extra_fields) return self.create_user(username, password, **extra_fields)

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -10,10 +10,11 @@ from django.utils import timezone
class User(AbstractUser): class User(AbstractUser):
address = models.CharField(max_length=255, blank=True) 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) mobile = models.CharField(max_length=255, blank=True, unique=True, null=True)
designation = models.CharField(max_length=255, blank=True) designation = models.CharField(max_length=255, blank=True)
acc_no = 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) verified = models.BooleanField(default=False)
dob = models.DateField(blank=True, null=True) dob = models.DateField(blank=True, null=True)
terms_accepted = models.BooleanField(default=False) terms_accepted = models.BooleanField(default=False)

View File

@ -32,6 +32,17 @@ from .serializers import (
CustomReadOnlyUserByIDCardSerializer, 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): class CreateUserView(generics.CreateAPIView):
# Create user API view # Create user API view
@ -42,65 +53,39 @@ class CreateUserView(generics.CreateAPIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# Extract required fields from request data # Extract required fields from request data
password = request.data.get("password") password = request.data.get("password")
username = request.data.get("username") # This can be None username = request.data.get("username")
address = request.data.get("address") address = request.data.get("address")
mobile = request.data.get("mobile") mobile = request.data.get("mobile")
acc_no = request.data.get("acc_no") acc_no = request.data.get("acc_no")
id_card = request.data.get("id_card") id_card = request.data.get("id_card")
dob = request.data.get("dob") dob = request.data.get("dob")
atoll_id = request.data.get("atoll") # Get the atoll ID atoll_id = request.data.get("atoll")
island_id = request.data.get("island") # Get the island ID island_id = request.data.get("island")
terms_accepted = request.data.get("terms_accepted") terms_accepted = request.data.get("terms_accepted")
policy_accepted = request.data.get("policy_accepted") policy_accepted = request.data.get("policy_accepted")
firstname = request.data.get("firstname") firstname = request.data.get("firstname")
lastname = request.data.get("lastname") lastname = request.data.get("lastname")
# Validate required fields
existing_username = User.objects.filter(username=username).first() # Validate required fields first
if existing_username: validation_error = self.validate_required_fields(request.data)
return Response({"message": "Username already exists."}, status=400) if validation_error:
return validation_error
if not firstname: # Check username uniqueness after validation
return Response({"message": "firstname is required."}, status=400) if User.objects.filter(username=username).exists():
if not lastname: return Response({"message": ErrorMessages.USERNAME_EXISTS}, status=400)
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)
if not re.match(r"^[A-Z]{1,2}[0-9]{6,7}$", id_card): if id_card and not re.match(ID_CARD_PATTERN, id_card):
return Response( return Response({"message": ErrorMessages.INVALID_ID_CARD}, status=400)
{"message": "Please enter a valid ID card number."}, status=400
)
if not re.match(r"^[7|9][0-9]{6}$", mobile): if User.objects.filter(id_card=id_card).exists():
return Response( return Response({"message": "ID card already exists."}, status=400)
{"message": "Please enter a valid mobile number."}, status=400
)
if not re.match(r"^(7\d{12}|9\d{16})$", acc_no): if mobile is None or not re.match(MOBILE_PATTERN, mobile):
return Response( return Response({"message": ErrorMessages.INVALID_MOBILE}, status=400)
{"message": "Please enter a valid account number."}, 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 # Fetch Atoll and Island instances
try: try:
@ -111,19 +96,20 @@ class CreateUserView(generics.CreateAPIView):
except Island.DoesNotExist: except Island.DoesNotExist:
return Response({"message": "Island not found."}, status=404) return Response({"message": "Island not found."}, status=404)
# Create user without email # Create user
user = User.objects.create_user( user = User.objects.create_user(
first_name=firstname, first_name=firstname,
last_name=lastname, last_name=lastname,
username=username, username=str(username),
password=password, password=password,
email=None,
address=address, address=address,
mobile=mobile, mobile=mobile,
acc_no=acc_no, acc_no=acc_no,
id_card=id_card, id_card=id_card,
dob=dob, dob=dob,
atoll=atoll, # Assign the Atoll instance atoll=atoll,
island=island, # Assign the Island instance island=island,
terms_accepted=terms_accepted, terms_accepted=terms_accepted,
policy_accepted=policy_accepted, policy_accepted=policy_accepted,
) )
@ -133,6 +119,32 @@ class CreateUserView(generics.CreateAPIView):
serializer.data, status=status.HTTP_201_CREATED, headers=headers 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): class LoginView(KnoxLoginView):
# login view extending KnoxLoginView # login view extending KnoxLoginView
@ -216,7 +228,11 @@ def filter_user(request):
print(f"Querying with filters: {filters}") print(f"Querying with filters: {filters}")
print(f"Found user: {user}") 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): class ListUserByIDCardView(generics.ListAPIView):

View File

@ -22,6 +22,21 @@ class DeviceListCreateAPIView(
filterset_fields = "__all__" filterset_fields = "__all__"
filterset_class = DeviceFilter 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: def get_serializer_class(self) -> type:
if self.request.method == "POST": if self.request.method == "POST":
return CreateDeviceSerializer return CreateDeviceSerializer

View File

@ -44,7 +44,6 @@ def authenticate_by_token(callback_token):
def create_callback_token_for_user(user, token_type): def create_callback_token_for_user(user, token_type):
token = None token = None
token_type = token_type.upper() token_type = token_type.upper()