dem16
This commit is contained in:
parent
3a9d3da108
commit
43c475e4c1
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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.")
|
||||
28
core/migrations/0009_otpcode_alter_profile_phone_number.py
Normal file
28
core/migrations/0009_otpcode_alter_profile_phone_number.py
Normal 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),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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)
|
||||
43
core/templates/registration/verify_otp.html
Normal file
43
core/templates/registration/verify_otp.html
Normal 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 %}
|
||||
@ -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"),
|
||||
]
|
||||
]
|
||||
|
||||
137
core/views.py
137
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)
|
||||
Loading…
x
Reference in New Issue
Block a user