mirror of
https://github.com/i701/sarlink-portal-api.git
synced 2025-04-19 23:46:53 +00:00
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
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 2m39s
This commit is contained in:
parent
ddfbeba2f4
commit
43f9b7ef7c
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -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
|
||||||
}
|
}
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
17
api/migrations/0014_alter_user_email.py
Normal file
17
api/migrations/0014_alter_user_email.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -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)
|
||||||
|
116
api/views.py
116
api/views.py
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user