adding variviction
This commit is contained in:
parent
241aa3abd2
commit
b77b8b1619
Binary file not shown.
@ -211,5 +211,6 @@ WHATSAPP_ENABLED = os.getenv("WHATSAPP_ENABLED", "true").lower() == "true"
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
LOGIN_URL = 'login'
|
||||
LOGIN_REDIRECT_URL = 'dashboard'
|
||||
LOGOUT_REDIRECT_URL = 'index'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -15,6 +15,7 @@ class UserRegistrationForm(forms.ModelForm):
|
||||
password_confirm = forms.CharField(widget=forms.PasswordInput, label=_("Confirm Password"))
|
||||
role = forms.ChoiceField(choices=Profile.ROLE_CHOICES, label=_("Register as"))
|
||||
phone_number = forms.CharField(max_length=20, label=_("Phone Number"))
|
||||
verification_method = forms.ChoiceField(choices=[('email', _('Email')), ('whatsapp', _('WhatsApp'))], label=_("Verify via"), widget=forms.RadioSelect, initial='email')
|
||||
|
||||
country = forms.ModelChoiceField(queryset=Country.objects.all(), required=False, label=_("Country"))
|
||||
governate = forms.ModelChoiceField(queryset=Governate.objects.none(), required=False, label=_("Governate"))
|
||||
|
||||
18
core/migrations/0014_alter_otpverification_purpose.py
Normal file
18
core/migrations/0014_alter_otpverification_purpose.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 15:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_platformprofile_enable_payment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='otpverification',
|
||||
name='purpose',
|
||||
field=models.CharField(choices=[('profile_update', 'Profile Update'), ('password_reset', 'Password Reset'), ('registration', 'Registration')], default='profile_update', max_length=20),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -198,6 +198,7 @@ class OTPVerification(models.Model):
|
||||
PURPOSE_CHOICES = (
|
||||
('profile_update', _('Profile Update')),
|
||||
('password_reset', _('Password Reset')),
|
||||
('registration', _('Registration')),
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
code = models.CharField(max_length=6)
|
||||
|
||||
@ -8,6 +8,13 @@
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<!-- Back to Profile -->
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'profile' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Profile" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold mb-4 text-center">{% trans "Edit Profile" %}</h2>
|
||||
@ -91,4 +98,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -8,6 +8,13 @@
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5">
|
||||
<!-- Back to Home -->
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'index' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Home" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold mb-4 text-center">{% trans "Login to masarX" %}</h2>
|
||||
@ -47,4 +54,4 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -7,24 +7,33 @@
|
||||
<section class="py-5 bg-light" style="min-height: 80vh;">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-10 col-lg-8">
|
||||
<!-- Back to Home -->
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'index' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Home" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold mb-4 text-center">{% trans "Join masarX" %}</h2>
|
||||
<form method="post" id="registrationForm">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text small">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<div class="text-danger small">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
{% for field in form %}
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text small">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<div class="text-danger small">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-masarx-primary w-100 py-2 mt-3">{% trans "Create Account" %}</button>
|
||||
</form>
|
||||
<div class="text-center mt-4">
|
||||
@ -43,47 +52,50 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const governateSelect = document.getElementById('id_governate');
|
||||
const citySelect = document.getElementById('id_city');
|
||||
|
||||
countrySelect.addEventListener('change', function() {
|
||||
const countryId = this.value;
|
||||
governateSelect.innerHTML = '<option value="">{% trans "Select Governate" %}</option>';
|
||||
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||
|
||||
if (countryId) {
|
||||
fetch(`{% url 'get_governates' %}?country_id=${countryId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.forEach(gov => {
|
||||
const option = document.createElement('option');
|
||||
option.value = gov.id;
|
||||
option.textContent = gov.name;
|
||||
governateSelect.appendChild(option);
|
||||
// Only attach listeners if elements exist (safety check)
|
||||
if (countrySelect && governateSelect && citySelect) {
|
||||
countrySelect.addEventListener('change', function() {
|
||||
const countryId = this.value;
|
||||
governateSelect.innerHTML = '<option value="">{% trans "Select Governate" %}</option>';
|
||||
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||
|
||||
if (countryId) {
|
||||
fetch(`{% url 'get_governates' %}?country_id=${countryId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.forEach(gov => {
|
||||
const option = document.createElement('option');
|
||||
option.value = gov.id;
|
||||
option.textContent = gov.name;
|
||||
governateSelect.appendChild(option);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
governateSelect.addEventListener('change', function() {
|
||||
const governateId = this.value;
|
||||
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||
|
||||
if (governateId) {
|
||||
fetch(`{% url 'get_cities' %}?governate_id=${governateId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.forEach(city => {
|
||||
const option = document.createElement('option');
|
||||
option.value = city.id;
|
||||
option.textContent = city.name;
|
||||
citySelect.appendChild(option);
|
||||
governateSelect.addEventListener('change', function() {
|
||||
const governateId = this.value;
|
||||
citySelect.innerHTML = '<option value="">{% trans "Select City" %}</option>';
|
||||
|
||||
if (governateId) {
|
||||
fetch(`{% url 'get_cities' %}?governate_id=${governateId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.forEach(city => {
|
||||
const option = document.createElement('option');
|
||||
option.value = city.id;
|
||||
option.textContent = city.name;
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
input, select {
|
||||
input:not([type=radio]):not([type=checkbox]), select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
@ -98,6 +110,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
border-radius: 8px;
|
||||
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||
}
|
||||
#id_verification_method {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
#id_verification_method li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.btn-masarx-primary {
|
||||
background-color: var(--accent-orange);
|
||||
border-color: var(--accent-orange);
|
||||
@ -106,4 +129,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -6,6 +6,13 @@
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<!-- Back to Dashboard -->
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'dashboard' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Dashboard" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm p-4" style="border-radius: 20px;">
|
||||
<h2 class="mb-4">{% trans "Request a Shipment" %}</h2>
|
||||
<form method="POST">
|
||||
|
||||
@ -8,6 +8,13 @@
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<!-- Back to Edit Profile -->
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'edit_profile' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Edit Profile" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold mb-4 text-center">{% trans "Verification Required" %}</h2>
|
||||
@ -44,4 +51,4 @@
|
||||
letter-spacing: 0.5em;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
53
core/templates/core/verify_registration.html
Normal file
53
core/templates/core/verify_registration.html
Normal file
@ -0,0 +1,53 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Verify Account" %} | masarX{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="py-5 bg-light" style="min-height: 80vh;">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<!-- Back to Register -->
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'register' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Register" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold mb-4 text-center">{% trans "Account Verification" %}</h2>
|
||||
<p class="text-center text-muted mb-4">
|
||||
{% trans "We have sent a verification code to verify your account. Please enter it below to complete registration." %}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold" for="code">{% trans "Verification Code" %}</label>
|
||||
<input type="text" name="code" id="code" class="form-control text-center fs-3 tracking-widest" maxlength="6" required placeholder="000000">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-masarx-primary py-2">{% trans "Verify Account" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.btn-masarx-primary {
|
||||
background-color: var(--accent-orange);
|
||||
border-color: var(--accent-orange);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.tracking-widest {
|
||||
letter-spacing: 0.5em;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -7,6 +7,7 @@ urlpatterns = [
|
||||
path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
|
||||
path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
|
||||
path('register/', views.register, name='register'),
|
||||
path('register/verify/', views.verify_registration, name='verify_registration'),
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
path('shipment-request/', views.shipment_request, name='shipment_request'),
|
||||
path('accept-parcel/<int:parcel_id>/', views.accept_parcel, name='accept_parcel'),
|
||||
|
||||
@ -43,13 +43,76 @@ def register(request):
|
||||
if request.method == 'POST':
|
||||
form = UserRegistrationForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
return redirect('dashboard')
|
||||
# Save user but inactive
|
||||
user = form.save(commit=True)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
|
||||
# Generate OTP
|
||||
code = ''.join(random.choices(string.digits, k=6))
|
||||
OTPVerification.objects.create(user=user, code=code, purpose='registration')
|
||||
|
||||
# Send OTP
|
||||
method = form.cleaned_data.get('verification_method', 'email')
|
||||
if method == 'whatsapp':
|
||||
phone = user.profile.phone_number
|
||||
send_whatsapp_message(phone, f"Your verification code is: {code}")
|
||||
messages.info(request, _("Verification code sent to WhatsApp."))
|
||||
else:
|
||||
send_mail(
|
||||
_('Verification Code'),
|
||||
f'Your verification code is: {code}',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[user.email],
|
||||
fail_silently=False,
|
||||
)
|
||||
messages.info(request, _("Verification code sent to email."))
|
||||
|
||||
request.session['registration_user_id'] = user.id
|
||||
return redirect('verify_registration')
|
||||
else:
|
||||
form = UserRegistrationForm()
|
||||
return render(request, 'core/register.html', {'form': form})
|
||||
|
||||
def verify_registration(request):
|
||||
if 'registration_user_id' not in request.session:
|
||||
messages.error(request, _("Session expired or invalid."))
|
||||
return redirect('register')
|
||||
|
||||
if request.method == 'POST':
|
||||
code = request.POST.get('code')
|
||||
user_id = request.session['registration_user_id']
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
otp = OTPVerification.objects.filter(
|
||||
user=user,
|
||||
code=code,
|
||||
purpose='registration',
|
||||
is_verified=False
|
||||
).latest('created_at')
|
||||
|
||||
if otp.is_valid():
|
||||
# Activate User
|
||||
user.is_active = True
|
||||
user.save()
|
||||
|
||||
# Cleanup
|
||||
otp.is_verified = True
|
||||
otp.save()
|
||||
del request.session['registration_user_id']
|
||||
|
||||
# Login
|
||||
login(request, user)
|
||||
|
||||
messages.success(request, _("Account verified successfully!"))
|
||||
return redirect('dashboard')
|
||||
else:
|
||||
messages.error(request, _("Invalid or expired code."))
|
||||
except (User.DoesNotExist, OTPVerification.DoesNotExist):
|
||||
messages.error(request, _("Invalid code."))
|
||||
|
||||
return render(request, 'core/verify_registration.html')
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
# Ensure profile exists
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user