diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc
index af0d53c..950dabf 100644
Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ
diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc
index be16afb..e198ee6 100644
Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ
diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index 4be52ff..fddd747 100644
Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ
diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc
index ea8c7f4..be364c3 100644
Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ
diff --git a/core/forms.py b/core/forms.py
index e14a845..0e82b0d 100644
--- a/core/forms.py
+++ b/core/forms.py
@@ -1,5 +1,5 @@
from django import forms
-from .models import Truck, Shipment, Bid, Profile, Country
+from .models import Truck, Shipment, Bid, Profile, Country, OTPCode
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
@@ -9,7 +9,7 @@ class UserRegistrationForm(UserCreationForm):
confirm_email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control'}), label=_("Confirm Email"))
role = forms.ChoiceField(choices=Profile.ROLE_CHOICES, widget=forms.Select(attrs={'class': 'form-select'}))
country_code = forms.ChoiceField(choices=[], widget=forms.Select(attrs={'class': 'form-select'}))
- phone_number = forms.CharField(max_length=20, required=False, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '123456789'}))
+ phone_number = forms.CharField(max_length=20, required=True, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '123456789'}))
class Meta(UserCreationForm.Meta):
model = User
@@ -33,6 +33,20 @@ class UserRegistrationForm(UserCreationForm):
if not isinstance(field.widget, (forms.CheckboxInput, forms.Select)):
field.widget.attrs.update({'class': 'form-control'})
+ def clean_email(self):
+ email = self.cleaned_data.get('email')
+ if User.objects.filter(email=email).exists():
+ raise forms.ValidationError(_("This email is already in use."))
+ return email
+
+ def clean_phone_number(self):
+ phone_number = self.cleaned_data.get('phone_number')
+ country_code = self.cleaned_data.get('country_code')
+ # We check uniqueness of phone_number in Profile
+ if Profile.objects.filter(phone_number=phone_number).exists():
+ raise forms.ValidationError(_("This phone number is already in use."))
+ return phone_number
+
def clean(self):
cleaned_data = super().clean()
email = cleaned_data.get("email")
@@ -43,6 +57,9 @@ class UserRegistrationForm(UserCreationForm):
return cleaned_data
+class OTPVerifyForm(forms.Form):
+ otp_code = forms.CharField(max_length=6, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '123456'}))
+
class TruckForm(forms.ModelForm):
class Meta:
model = Truck
@@ -98,4 +115,4 @@ class BidForm(forms.ModelForm):
# Only allow bidding with approved trucks
self.fields['truck'].queryset = Truck.objects.filter(owner=user, is_approved=True)
if not self.fields['truck'].queryset.exists():
- self.fields['truck'].help_text = _("You must have an approved truck to place a bid.")
+ self.fields['truck'].help_text = _("You must have an approved truck to place a bid.")
\ No newline at end of file
diff --git a/core/migrations/0009_otpcode_alter_profile_phone_number.py b/core/migrations/0009_otpcode_alter_profile_phone_number.py
new file mode 100644
index 0000000..3a031e9
--- /dev/null
+++ b/core/migrations/0009_otpcode_alter_profile_phone_number.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.2.7 on 2026-01-23 12:41
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0008_country'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='OTPCode',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('phone_number', models.CharField(max_length=20)),
+ ('code', models.CharField(max_length=6)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('is_used', models.BooleanField(default=False)),
+ ],
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='phone_number',
+ field=models.CharField(max_length=20, null=True, unique=True),
+ ),
+ ]
diff --git a/core/migrations/__pycache__/0009_otpcode_alter_profile_phone_number.cpython-311.pyc b/core/migrations/__pycache__/0009_otpcode_alter_profile_phone_number.cpython-311.pyc
new file mode 100644
index 0000000..b29a475
Binary files /dev/null and b/core/migrations/__pycache__/0009_otpcode_alter_profile_phone_number.cpython-311.pyc differ
diff --git a/core/models.py b/core/models.py
index e063def..4f11329 100644
--- a/core/models.py
+++ b/core/models.py
@@ -4,6 +4,9 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.utils.translation import get_language
+from django.utils import timezone
+import random
+import string
class Country(models.Model):
name = models.CharField(_('Country Name'), max_length=100)
@@ -26,7 +29,7 @@ class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='SHIPPER')
country_code = models.CharField(max_length=5, blank=True, default="966")
- phone_number = models.CharField(max_length=20, blank=True)
+ phone_number = models.CharField(max_length=20, unique=True, null=True) # Changed to unique and nullable for migration safety
@property
def full_phone_number(self):
@@ -40,6 +43,21 @@ class Profile(models.Model):
def __str__(self):
return f"{self.user.username} - {self.role}"
+class OTPCode(models.Model):
+ phone_number = models.CharField(max_length=20)
+ code = models.CharField(max_length=6)
+ created_at = models.DateTimeField(auto_now_add=True)
+ is_used = models.BooleanField(default=False)
+
+ def is_valid(self):
+ # Valid for 10 minutes
+ return not self.is_used and (timezone.now() - self.created_at).total_seconds() < 600
+
+ @staticmethod
+ def generate_code(phone_number):
+ code = ''.join(random.choices(string.digits, k=6))
+ return OTPCode.objects.create(phone_number=phone_number, code=code)
+
class Truck(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='trucks')
@@ -166,4 +184,4 @@ def save_user_profile(sender, instance, **kwargs):
if hasattr(instance, 'profile'):
instance.profile.save()
else:
- Profile.objects.create(user=instance)
+ Profile.objects.create(user=instance)
\ No newline at end of file
diff --git a/core/templates/registration/verify_otp.html b/core/templates/registration/verify_otp.html
new file mode 100644
index 0000000..0b0fb5f
--- /dev/null
+++ b/core/templates/registration/verify_otp.html
@@ -0,0 +1,43 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
{% trans "Verify Your Account" %}
+
+ {% if purpose == 'registration' %}
+ {% trans "We have sent a verification code to your WhatsApp number. Please enter it below to complete your registration." %}
+ {% else %}
+ {% trans "We have sent a verification code to your WhatsApp number. Please enter it below to log in." %}
+ {% endif %}
+
+
+
+
{% trans "Didn't receive the code?" %} {% trans "Try again" %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/core/urls.py b/core/urls.py
index 4a39602..597f03a 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -5,7 +5,9 @@ from . import views
urlpatterns = [
path("", views.home, name="home"),
path("register/", views.register, name="register"),
- path("login/", auth_views.LoginView.as_view(), name="login"),
+ path("verify-otp-registration/", views.verify_otp_registration, name="verify_otp_registration"),
+ path("login/", views.custom_login, name="login"),
+ path("verify-otp-login/", views.verify_otp_login, name="verify_otp_login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
path("dashboard/", views.dashboard, name="dashboard"),
path("truck/register/", views.truck_register, name="truck_register"),
@@ -17,4 +19,4 @@ urlpatterns = [
path("shipment//", views.shipment_detail, name="shipment_detail"),
path("shipment//bid/", views.place_bid, name="place_bid"),
path("bid//accept/", views.accept_bid, name="accept_bid"),
-]
\ No newline at end of file
+]
diff --git a/core/views.py b/core/views.py
index 55b07ac..1bda90a 100644
--- a/core/views.py
+++ b/core/views.py
@@ -1,14 +1,15 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
-from django.contrib.auth import login, authenticate
+from django.contrib.auth import login, authenticate, logout
from django.utils import timezone
-from .models import Profile, Truck, Shipment, Bid, Message
-from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm
+from .models import Profile, Truck, Shipment, Bid, Message, OTPCode
+from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm, OTPVerifyForm
from django.contrib import messages
from django.utils.translation import gettext as _
from django.db.models import Q
from django.contrib.auth.models import User
from .whatsapp import send_whatsapp_message
+from django.contrib.auth.forms import AuthenticationForm
def home(request):
"""Render the landing screen for MASAR CARGO."""
@@ -21,21 +22,131 @@ def register(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
- user = form.save()
- profile = user.profile
- profile.role = form.cleaned_data.get('role')
- profile.phone_number = form.cleaned_data.get('phone_number')
- profile.country_code = form.cleaned_data.get('country_code')
- profile.save()
- login(request, user)
- messages.success(request, _("Registration successful. Welcome!"))
- return redirect('dashboard')
+ # Store data in session to be used after OTP verification
+ registration_data = {
+ 'username': form.cleaned_data['username'],
+ 'email': form.cleaned_data['email'],
+ 'password': form.data['password1'], # We need raw password to create user later
+ 'role': form.cleaned_data['role'],
+ 'phone_number': form.cleaned_data['phone_number'],
+ 'country_code': form.cleaned_data['country_code'],
+ }
+ request.session['registration_data'] = registration_data
+
+ # Send OTP
+ full_phone = f"{registration_data['country_code']}{registration_data['phone_number']}"
+ otp = OTPCode.generate_code(full_phone)
+ msg = f"Your verification code for MASAR CARGO is: {otp.code}"
+ if send_whatsapp_message(full_phone, msg):
+ messages.info(request, _("A verification code has been sent to your WhatsApp."))
+ return redirect('verify_otp_registration')
+ else:
+ messages.error(request, _("Failed to send verification code. Please check your phone number."))
else:
messages.error(request, _("Please correct the errors below."))
else:
form = UserRegistrationForm()
return render(request, 'registration/register.html', {'form': form})
+def verify_otp_registration(request):
+ registration_data = request.session.get('registration_data')
+ if not registration_data:
+ return redirect('register')
+
+ if request.method == 'POST':
+ form = OTPVerifyForm(request.POST)
+ if form.is_valid():
+ code = form.cleaned_data['otp_code']
+ full_phone = f"{registration_data['country_code']}{registration_data['phone_number']}"
+ otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
+
+ if otp_record and otp_record.is_valid():
+ otp_record.is_used = True
+ otp_record.save()
+
+ # Create user
+ user = User.objects.create_user(
+ username=registration_data['username'],
+ email=registration_data['email'],
+ password=registration_data['password']
+ )
+ profile = user.profile
+ profile.role = registration_data['role']
+ profile.phone_number = registration_data['phone_number']
+ profile.country_code = registration_data['country_code']
+ profile.save()
+
+ login(request, user)
+ del request.session['registration_data']
+ messages.success(request, _("Registration successful. Welcome!"))
+ return redirect('dashboard')
+ else:
+ messages.error(request, _("Invalid or expired verification code."))
+ else:
+ form = OTPVerifyForm()
+
+ return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'registration'})
+
+def custom_login(request):
+ if request.method == 'POST':
+ form = AuthenticationForm(request, data=request.POST)
+ if form.is_valid():
+ user = form.get_user()
+ profile = user.profile
+ if not profile.phone_number:
+ messages.error(request, _("Your account does not have a phone number. Please contact admin."))
+ return redirect('login')
+
+ # Store user ID in session temporarily
+ request.session['pre_otp_user_id'] = user.id
+
+ # Send OTP
+ full_phone = profile.full_phone_number
+ otp = OTPCode.generate_code(full_phone)
+ msg = f"Your login verification code for MASAR CARGO is: {otp.code}"
+ if send_whatsapp_message(full_phone, msg):
+ messages.info(request, _("A verification code has been sent to your WhatsApp."))
+ return redirect('verify_otp_login')
+ else:
+ # If WhatsApp fails, maybe allow login but warn? Or strictly enforce?
+ # For now, strictly enforce
+ messages.error(request, _("Failed to send verification code. Please check your connection."))
+ else:
+ messages.error(request, _("Invalid username or password."))
+ else:
+ form = AuthenticationForm()
+ return render(request, 'registration/login.html', {'form': form})
+
+def verify_otp_login(request):
+ user_id = request.session.get('pre_otp_user_id')
+ if not user_id:
+ return redirect('login')
+
+ user = get_object_or_404(User, id=user_id)
+ profile = user.profile
+
+ if request.method == 'POST':
+ form = OTPVerifyForm(request.POST)
+ if form.is_valid():
+ code = form.cleaned_data['otp_code']
+ full_phone = profile.full_phone_number
+ otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
+
+ if otp_record and otp_record.is_valid():
+ otp_record.is_used = True
+ otp_record.save()
+
+ login(request, user)
+ del request.session['pre_otp_user_id']
+ messages.success(request, _("Logged in successfully!"))
+ return redirect('dashboard')
+ else:
+ messages.error(request, _("Invalid or expired verification code."))
+ else:
+ form = OTPVerifyForm()
+
+ return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'login'})
+
@login_required
def dashboard(request):
profile, created = Profile.objects.get_or_create(user=request.user)
@@ -234,4 +345,4 @@ def accept_bid(request, bid_id):
send_whatsapp_message(owner_phone, msg)
messages.success(request, _("Bid accepted! Shipment is now in progress."))
- return redirect('shipment_detail', shipment_id=bid.shipment.id)
+ return redirect('shipment_detail', shipment_id=bid.shipment.id)
\ No newline at end of file