Skip to content

Commit befd7c9

Browse files
authored
Merge pull request #87 from jitendra-ky/43-2-unactive-user
now user is not created until verified. fix #43
2 parents b11c060 + 7471c2f commit befd7c9

File tree

7 files changed

+174
-49
lines changed

7 files changed

+174
-49
lines changed

zserver/admin.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
from django.contrib import admin
22

3-
from zserver.models import Message, Session, SignUpOTP, UserProfile
3+
from zserver.models import (
4+
Message,
5+
Session,
6+
SignUpOTP,
7+
UnverifiedUserProfile,
8+
UserProfile,
9+
VerifyUserOTP,
10+
)
411

512
# Register your models here.
613
admin.site.register(UserProfile)
714
admin.site.register(SignUpOTP)
815
admin.site.register(Session)
16+
admin.site.register(VerifyUserOTP)
17+
admin.site.register(UnverifiedUserProfile)
918

1019
admin.site.register(Message)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Generated by Django 5.1.5 on 2025-05-19 19:51
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("zserver", "0004_message"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="UnverifiedUserProfile",
16+
fields=[
17+
("id", models.AutoField(primary_key=True, serialize=False)),
18+
("fullname", models.CharField(max_length=100)),
19+
("password", models.CharField(max_length=100)),
20+
("created_at", models.DateTimeField(auto_now_add=True)),
21+
("updated_at", models.DateTimeField(auto_now=True)),
22+
("email", models.EmailField(max_length=100)),
23+
],
24+
options={
25+
"abstract": False,
26+
},
27+
),
28+
migrations.CreateModel(
29+
name="VerifyUserOTP",
30+
fields=[
31+
(
32+
"id",
33+
models.BigAutoField(
34+
auto_created=True,
35+
primary_key=True,
36+
serialize=False,
37+
verbose_name="ID",
38+
),
39+
),
40+
("otp", models.CharField(max_length=6)),
41+
("created_at", models.DateTimeField(auto_now_add=True)),
42+
(
43+
"user",
44+
models.ForeignKey(
45+
on_delete=django.db.models.deletion.CASCADE,
46+
to="zserver.unverifieduserprofile",
47+
),
48+
),
49+
],
50+
),
51+
]

zserver/models/user_profile.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,51 @@
44
from django.db import models
55

66

7-
# the UserProfile is the model that will be used to store the user's profile information
8-
class UserProfile(models.Model):
7+
# Base model for user profiles
8+
class BaseUserProfile(models.Model):
99
id = models.AutoField(primary_key=True)
1010
fullname = models.CharField(max_length=100, blank=False, null=False)
1111
email = models.EmailField(max_length=100, blank=False, null=False, unique=True)
1212
password = models.CharField(max_length=100, blank=False, null=False)
1313
created_at = models.DateTimeField(auto_now_add=True, blank=False, null=False)
1414
updated_at = models.DateTimeField(auto_now=True, blank=False, null=False)
15-
is_active = models.BooleanField(default=False, blank=False, null=False)
15+
16+
class Meta:
17+
"""make this model abstract so that it doesn't create a table."""
18+
19+
abstract = True # don't create a table for this model
1620

1721
def __str__(self) -> str:
1822
"""Return the email of the user."""
1923
return self.email
2024

25+
def is_password_valid(self, password: str) -> bool:
26+
"""Check if the provided password is valid."""
27+
return self.password == password
28+
29+
30+
# Model for verified user profiles
31+
class UserProfile(BaseUserProfile):
32+
is_active = models.BooleanField(default=False, blank=False, null=False)
33+
2134
def generate_otp(self) -> str:
2235
"""Generate a 6-digit OTP for the user."""
2336
generated_opt = "".join(random.choices(string.digits, k=6))
2437
otp = SignUpOTP(user=self, otp=generated_opt)
2538
otp.save()
2639
return generated_opt
2740

28-
def is_password_valid(self, password: str) -> bool:
29-
"""Check if the provided password is valid."""
30-
return self.password == password
41+
42+
# Model for unverified user profiles
43+
class UnverifiedUserProfile(BaseUserProfile):
44+
email = models.EmailField(max_length=100, blank=False, null=False, unique=False)
45+
46+
def generate_otp(self) -> str:
47+
"""Generate a 6-digit OTP for the user."""
48+
generated_opt = "".join(random.choices(string.digits, k=6))
49+
otp = VerifyUserOTP(user=self, otp=generated_opt)
50+
otp.save()
51+
return generated_opt
3152

3253

3354
# let's create a signup otp model
@@ -41,6 +62,17 @@ def __str__(self) -> str:
4162
return self.user.email
4263

4364

65+
# let's create a VerifyUserOTP model
66+
class VerifyUserOTP(models.Model):
67+
user = models.ForeignKey(UnverifiedUserProfile, on_delete=models.CASCADE)
68+
otp = models.CharField(max_length=6, blank=False, null=False)
69+
created_at = models.DateTimeField(auto_now_add=True, blank=False, null=False)
70+
71+
def __str__(self) -> str:
72+
"""Return the email of the user associated with the OTP."""
73+
return self.user.email + " " + self.otp
74+
75+
4476
# let's create a modle for login sessions
4577
class Session(models.Model):
4678
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)

zserver/serializers/user_profile.py

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from rest_framework import serializers
22

3-
from zserver.models import Session, SignUpOTP, UserProfile
3+
from zserver.models import Session, SignUpOTP, UnverifiedUserProfile, UserProfile, VerifyUserOTP
44

55

66
# Serializer class for the UserProfile model
@@ -19,12 +19,6 @@ class Meta:
1919
"password": {"write_only": True}, # Make the password field write-only
2020
}
2121

22-
def create(self, validated_data: dict) -> UserProfile:
23-
"""Create a new user profile and generate OTP."""
24-
user = UserProfile.objects.create(**validated_data)
25-
user.generate_otp()
26-
return user
27-
2822
def update(self, instance: UserProfile, validated_data: dict) -> UserProfile:
2923
"""Update an existing user profile."""
3024
instance.fullname = validated_data.get("fullname", instance.fullname)
@@ -34,14 +28,46 @@ def update(self, instance: UserProfile, validated_data: dict) -> UserProfile:
3428
return instance
3529

3630

37-
# Serializer class for the SignUpOTP model
38-
class SignUpOTPSerializer(serializers.ModelSerializer):
31+
# Serializer class for the UnverifiedUserProfile model
32+
class UnverifiedUserProfileSerializer(serializers.ModelSerializer):
33+
class Meta:
34+
"""Meta class to specify the model and fields to be serialized."""
35+
36+
model = UnverifiedUserProfile
37+
fields = [
38+
"fullname",
39+
"email",
40+
"password",
41+
] # Fields to be included in the serialization
42+
extra_kwargs = {
43+
"password": {"write_only": True}, # Make the password field write-only
44+
}
45+
46+
def validate_email(self, value: str) -> str:
47+
"""Validate that the email is not already in use."""
48+
if UserProfile.objects.filter(email=value).exists():
49+
raise serializers.ValidationError("Email is already in use.")
50+
return value
51+
52+
def create(self, validated_data: dict) -> UnverifiedUserProfile:
53+
"""Create a new unverified user profile."""
54+
if UnverifiedUserProfile.objects.filter(email=validated_data["email"]).exists():
55+
UnverifiedUserProfile.objects.filter(email=validated_data["email"]).delete()
56+
# Create a new unverified user profile
57+
# and generate an OTP for verification
58+
user = UnverifiedUserProfile.objects.create(**validated_data)
59+
user.generate_otp()
60+
return user
61+
62+
63+
# serializer for VerifyUserOTP model
64+
class VerifyUserOTPSerializer(serializers.ModelSerializer):
3965
email = serializers.EmailField(max_length=100)
4066

4167
class Meta:
4268
"""Meta class to specify the model and fields to be serialized."""
4369

44-
model = SignUpOTP
70+
model = VerifyUserOTP
4571
fields = ["otp", "email"]
4672

4773
def validate(self, data: dict) -> dict:
@@ -50,13 +76,13 @@ def validate(self, data: dict) -> dict:
5076
otp = data.get("otp")
5177

5278
try:
53-
user = UserProfile.objects.get(email=email)
54-
except UserProfile.DoesNotExist as err:
79+
user = UnverifiedUserProfile.objects.get(email=email)
80+
except UnverifiedUserProfile.DoesNotExist as err:
5581
raise serializers.ValidationError({"email": "User does not exist."}) from err
5682

5783
try:
58-
user_otp = SignUpOTP.objects.get(user=user)
59-
except SignUpOTP.DoesNotExist as err:
84+
user_otp = VerifyUserOTP.objects.get(user=user)
85+
except VerifyUserOTP.DoesNotExist as err:
6086
raise serializers.ValidationError({"otp": "OTP does not exist."}) from err
6187

6288
if user_otp.otp != otp:
@@ -66,16 +92,18 @@ def validate(self, data: dict) -> dict:
6692
data["user_otp"] = user_otp
6793
return data
6894

69-
def make_user_active(self) -> None:
70-
"""Activate the user."""
95+
def signup_user(self) -> None:
96+
"""Add user to UserProfile table and delete the OTP."""
7197
user = self.validated_data["user"]
72-
user.is_active = True
73-
user.save()
74-
75-
def delete_otp(self) -> None:
76-
"""Delete the OTP."""
77-
user_otp = self.validated_data["user_otp"]
78-
user_otp.delete()
98+
user_profile = UserProfile(
99+
fullname=user.fullname,
100+
email=user.email,
101+
password=user.password,
102+
)
103+
user_profile.is_active = True
104+
user_profile.save()
105+
user.delete()
106+
self.validated_data["user_otp"].delete()
79107

80108

81109
class SessionSerializer(serializers.Serializer):

zserver/tests/user_profile.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from rest_framework import status
44
from rest_framework.test import APIClient
55

6-
from zserver.models import Session, SignUpOTP, UserProfile
6+
from zserver.models import Session, SignUpOTP, UnverifiedUserProfile, UserProfile, VerifyUserOTP
77

88

99
class UserProfileViewTest(TestCase):
@@ -83,24 +83,29 @@ def test_create_user_profile(self):
8383
self.assertEqual(response.data["email"], new_user["email"])
8484
# now check the user is created and opt is generated
8585
try:
86-
user = UserProfile.objects.get(email=new_user["email"])
86+
user = UnverifiedUserProfile.objects.get(email=new_user["email"])
8787
print("User created successfully")
88-
except UserProfile.DoesNotExist:
88+
except UnverifiedUserProfile.DoesNotExist:
8989
self.fail("User not created")
9090
try:
91-
SignUpOTP.objects.get(user=user)
91+
VerifyUserOTP.objects.get(user=user)
9292
print("OTP generated successfully")
93-
except SignUpOTP.DoesNotExist:
93+
except VerifyUserOTP.DoesNotExist:
9494
self.fail("OTP not generated")
9595

9696
# this will send a post request to create a user that already exits
97-
response = self.client.post(self.user_url, new_user)
97+
existed_user = {
98+
"fullname": self.active_user_without_session.fullname,
99+
"email": self.active_user_without_session.email,
100+
"password": self.active_user_without_session.password,
101+
}
102+
response = self.client.post(self.user_url, existed_user)
98103
print(f"POST response status: {response.status_code}")
99104
print(f"POST response data: {response.data}")
100105
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
101106
self.assertEqual(
102107
response.data["email"][0],
103-
"user profile with this email already exists.",
108+
"Email is already in use.",
104109
)
105110

106111
def test_update_user_profile(self):
@@ -280,7 +285,7 @@ def test_get(self):
280285
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
281286

282287

283-
class SignUpOTPTest(TestCase):
288+
class VerifyUserOTPTest(TestCase):
284289

285290
def setUp(self):
286291
"""Set up test data for SignUpOTPTest."""
@@ -302,7 +307,7 @@ def setUp(self):
302307
def test_post(self):
303308
"""Test verifying OTP and activating the user."""
304309
# first send the post request to create a user on 'user-profile' endpoint
305-
# then read teh otp from the 'SignUpOTP' model
310+
# then read teh otp from the 'VerifyUserOTP' model
306311
# then test the current endpoint by sending post request with the otp
307312

308313
# create a user
@@ -314,8 +319,8 @@ def test_post(self):
314319
response = self.client.post(reverse("user-profile"), new_user)
315320
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
316321
# get the otp
317-
user = UserProfile.objects.get(email=new_user["email"])
318-
otp = SignUpOTP.objects.get(user=user)
322+
user = UnverifiedUserProfile.objects.get(email=new_user["email"])
323+
otp = VerifyUserOTP.objects.get(user=user)
319324
# test the otp
320325
data = {"email": new_user["email"], "otp": otp.otp}
321326
response = self.client.post(self.endpoint, data)

zserver/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313

1414
path("api/user-profile/", views.UserProfileView.as_view(), name="user-profile"),
1515
path("api/sign-in/", views.SignInView.as_view(), name="sign-in"),
16-
path("api/sign-up-otp/", views.SignUpOTPView.as_view(), name="sign-up-otp"),
1716
path("api/forgot-password/", views.ForgotPasswordView.as_view(), name="forgot-password"),
1817
path("api/reset-password/", views.ResetPasswordView.as_view(), name="reset-password"),
1918
path("api/messages/", views.MessageView.as_view(), name="messages"),
2019
path("api/contacts/", views.ContactView.as_view(), name="contacts"),
2120
path("api/all-users/", views.AllUsersView.as_view(), name="all-users"),
21+
path("api/sign-up-otp/", views.VerifyUserOTPView.as_view(), name="sign-up-otp"),
2222

2323
path("google-login/", views.GoogleLoginView.as_view(), name="google_login"),
2424
]

zserver/views/user_profile.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
ForgotPasswordSerializer,
1616
ResetPasswordSerializer,
1717
SessionSerializer,
18-
SignUpOTPSerializer,
18+
UnverifiedUserProfileSerializer,
1919
UserProfileSerializer,
20+
VerifyUserOTPSerializer,
2021
)
2122
from zserver.utils import get_env_var
2223

@@ -39,7 +40,7 @@ def get(self, request: Request) -> Response:
3940

4041
def post(self, request: Request) -> Response:
4142
"""Create a new user profile."""
42-
serializer = UserProfileSerializer(data=request.data)
43+
serializer = UnverifiedUserProfileSerializer(data=request.data)
4344
if serializer.is_valid():
4445
serializer.save()
4546
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -100,14 +101,13 @@ def post(self, request: Request) -> Response:
100101
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
101102

102103

103-
class SignUpOTPView(APIView):
104+
class VerifyUserOTPView(APIView):
104105

105106
def post(self, request: Request) -> Response:
106-
"""Verify OTP and activate the user."""
107-
serializer = SignUpOTPSerializer(data=request.data)
107+
"""Verify OTP and signup user."""
108+
serializer = VerifyUserOTPSerializer(data=request.data)
108109
if serializer.is_valid():
109-
serializer.make_user_active()
110-
serializer.delete_otp()
110+
serializer.signup_user()
111111
return Response(status=status.HTTP_200_OK)
112112
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
113113

0 commit comments

Comments
 (0)