This commit is contained in:
Flatlogic Bot 2026-01-23 12:44:51 +00:00
parent 3a9d3da108
commit 43c475e4c1
11 changed files with 239 additions and 20 deletions

View File

@ -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.")

View File

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

View File

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

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-body p-5">
<h2 class="text-center mb-4">{% trans "Verify Your Account" %}</h2>
<p class="text-muted text-center mb-4">
{% 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 %}
</p>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
{% for error in field.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
{% endfor %}
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-primary btn-lg">{% trans "Verify" %}</button>
</div>
</form>
<div class="text-center mt-3">
<small>{% trans "Didn't receive the code?" %} <a href="{% if purpose == 'registration' %}{% url 'register' %}{% else %}{% url 'login' %}{% endif %}">{% trans "Try again" %}</a></small>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -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/<int:shipment_id>/", views.shipment_detail, name="shipment_detail"),
path("shipment/<int:shipment_id>/bid/", views.place_bid, name="place_bid"),
path("bid/<int:bid_id>/accept/", views.accept_bid, name="accept_bid"),
]
]

View File

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