Autosave: 20260128-000421
This commit is contained in:
parent
c728c4e116
commit
5f2219fc0f
BIN
assets/vm-shot-2026-01-27T10-44-32-354Z.jpg
Normal file
BIN
assets/vm-shot-2026-01-27T10-44-32-354Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19
core/apps.py
19
core/apps.py
@ -1,6 +1,25 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'core'
|
||||
verbose_name = _('Masar Express Management')
|
||||
default = True
|
||||
|
||||
def ready(self):
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
# Monkey-patch Permission.__str__ to show a VERY short name
|
||||
# Standard was: "app_label | model | name" (e.g. core | country | Can add country)
|
||||
# Previous fix: "Country | Can add country"
|
||||
# New fix: "add Country", "change Country" (strips "Can " prefix)
|
||||
|
||||
def short_str(self):
|
||||
name = str(self.name)
|
||||
if name.startswith("Can "):
|
||||
return name[4:]
|
||||
return name
|
||||
|
||||
Permission.__str__ = short_str
|
||||
@ -96,14 +96,44 @@ class UserRegistrationForm(forms.ModelForm):
|
||||
user.save()
|
||||
# Profile is created by signal, so we update it
|
||||
profile, created = Profile.objects.get_or_create(user=user)
|
||||
profile.role = self.cleaned_data['role']
|
||||
# Handle role if it exists in cleaned_data (it might be excluded in subclasses)
|
||||
if 'role' in self.cleaned_data:
|
||||
profile.role = self.cleaned_data['role']
|
||||
profile.phone_number = self.cleaned_data['phone_number']
|
||||
profile.country = self.cleaned_data['country']
|
||||
profile.governate = self.cleaned_data['governate']
|
||||
profile.city = self.cleaned_data['city']
|
||||
|
||||
# Save extra driver fields if they exist
|
||||
if 'profile_picture' in self.cleaned_data and self.cleaned_data['profile_picture']:
|
||||
profile.profile_picture = self.cleaned_data['profile_picture']
|
||||
if 'license_front_image' in self.cleaned_data and self.cleaned_data['license_front_image']:
|
||||
profile.license_front_image = self.cleaned_data['license_front_image']
|
||||
if 'license_back_image' in self.cleaned_data and self.cleaned_data['license_back_image']:
|
||||
profile.license_back_image = self.cleaned_data['license_back_image']
|
||||
if 'car_plate_number' in self.cleaned_data:
|
||||
profile.car_plate_number = self.cleaned_data['car_plate_number']
|
||||
|
||||
profile.save()
|
||||
return user
|
||||
|
||||
class ShipperRegistrationForm(UserRegistrationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['role'].widget = forms.HiddenInput()
|
||||
self.fields['role'].initial = 'shipper'
|
||||
|
||||
class DriverRegistrationForm(UserRegistrationForm):
|
||||
profile_picture = forms.ImageField(label=_("Profile Picture (Webcam/Upload)"), required=True, widget=forms.FileInput(attrs={'class': 'form-control', 'capture': 'user', 'accept': 'image/*'}))
|
||||
license_front_image = forms.ImageField(label=_("License Front Image"), required=True, widget=forms.FileInput(attrs={'class': 'form-control', 'accept': 'image/*'}))
|
||||
license_back_image = forms.ImageField(label=_("License Back Image"), required=True, widget=forms.FileInput(attrs={'class': 'form-control', 'accept': 'image/*'}))
|
||||
car_plate_number = forms.CharField(label=_("Car Plate Number"), max_length=20, required=True, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['role'].widget = forms.HiddenInput()
|
||||
self.fields['role'].initial = 'car_owner'
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
first_name = forms.CharField(label=_("First Name"), max_length=150, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||
last_name = forms.CharField(label=_("Last Name"), max_length=150, widget=forms.TextInput(attrs={'class': 'form-control'}))
|
||||
@ -347,4 +377,4 @@ class DriverRatingForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Reverse choices for CSS star rating logic (5 to 1) to ensure left-to-right filling
|
||||
self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)]
|
||||
self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)]
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-27 23:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0018_alter_otpverification_purpose'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='car_plate_number',
|
||||
field=models.CharField(blank=True, max_length=20, verbose_name='Car Plate Number'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='license_back_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='licenses/', verbose_name='License Back Image'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='license_front_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='licenses/', verbose_name='License Front Image'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -73,6 +73,11 @@ class Profile(models.Model):
|
||||
profile_picture = models.ImageField(_('Profile Picture'), upload_to='profile_pics/', blank=True, null=True)
|
||||
address = models.CharField(_('Address'), max_length=255, blank=True)
|
||||
|
||||
# Driver specific fields
|
||||
license_front_image = models.ImageField(_('License Front Image'), upload_to='licenses/', blank=True, null=True)
|
||||
license_back_image = models.ImageField(_('License Back Image'), upload_to='licenses/', blank=True, null=True)
|
||||
car_plate_number = models.CharField(_('Car Plate Number'), max_length=20, blank=True)
|
||||
|
||||
country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Country'))
|
||||
governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Governate'))
|
||||
city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('City'))
|
||||
@ -270,4 +275,4 @@ class DriverRating(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Driver Rating')
|
||||
verbose_name_plural = _('Driver Ratings')
|
||||
verbose_name_plural = _('Driver Ratings')
|
||||
|
||||
73
core/templates/core/register_choice.html
Normal file
73
core/templates/core/register_choice.html
Normal file
@ -0,0 +1,73 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Register" %} | 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 text-center mb-5">
|
||||
<div class="col-lg-8">
|
||||
<h2 class="fw-bold mb-3">{% trans "Welcome to masarX" %}</h2>
|
||||
<p class="text-muted lead">{% trans "Choose how you want to join us today." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center g-4">
|
||||
<!-- Shipper Card -->
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="card h-100 shadow-sm border-0 transition-hover">
|
||||
<div class="card-body p-5 text-center">
|
||||
<div class="mb-4 text-masarx-orange">
|
||||
<i class="bi bi-box-seam display-4"></i>
|
||||
</div>
|
||||
<h3 class="h4 fw-bold mb-3">{% trans "I am a Shipper" %}</h3>
|
||||
<p class="text-muted mb-4">{% trans "I want to send parcels and track my shipments easily." %}</p>
|
||||
<a href="{% url 'register_shipper' %}" class="btn btn-masarx-primary w-100 py-2">{% trans "Register as Shipper" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Driver Card -->
|
||||
<div class="col-md-5 col-lg-4">
|
||||
<div class="card h-100 shadow-sm border-0 transition-hover">
|
||||
<div class="card-body p-5 text-center">
|
||||
<div class="mb-4 text-masarx-dark">
|
||||
<i class="bi bi-truck display-4"></i>
|
||||
</div>
|
||||
<h3 class="h4 fw-bold mb-3">{% trans "I am a Car Owner" %}</h3>
|
||||
<p class="text-muted mb-4">{% trans "I want to deliver parcels and earn money on my own schedule." %}</p>
|
||||
<a href="{% url 'register_driver' %}" class="btn btn-outline-dark w-100 py-2">{% trans "Register as Car Owner" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-5">
|
||||
<p class="text-muted">{% trans "Already have an account?" %} <a href="{% url 'login' %}" class="text-masarx-orange fw-semibold">{% trans "Login here" %}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.transition-hover {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.transition-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 1rem 3rem rgba(0,0,0,.175)!important;
|
||||
}
|
||||
.btn-masarx-primary {
|
||||
background-color: var(--accent-orange);
|
||||
border-color: var(--accent-orange);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
.text-masarx-orange {
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
.text-masarx-dark {
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
222
core/templates/core/register_driver.html
Normal file
222
core/templates/core/register_driver.html
Normal file
@ -0,0 +1,222 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans "Register as Car Owner" %} | 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-10 col-lg-8">
|
||||
<!-- Back -->
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'register' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||
<i class="bi {% if LANGUAGE_BIDI %}bi-arrow-right{% else %}bi-arrow-left{% endif %} me-2"></i>{% trans "Back" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold mb-4 text-center">{% trans "Car Owner Registration" %}</h2>
|
||||
<form method="post" id="registrationForm" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% else %}
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-semibold" for="{{ field.id_for_label }}">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
|
||||
{% if field.name == 'profile_picture' %}
|
||||
<div class="input-group">
|
||||
{{ field }}
|
||||
<button type="button" class="btn btn-secondary" id="openWebcamBtn" title="{% trans 'Take photo with Webcam' %}">
|
||||
<i class="bi bi-camera-video"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
{% trans "Please provide clear photos of your license (front and back) and car plate number for verification." %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-dark w-100 py-2 mt-3">{% trans "Submit Application" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Webcam Modal -->
|
||||
<div class="modal fade" id="webcamModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Take Photo" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center bg-dark p-0">
|
||||
<video id="webcamVideo" style="width: 100%; max-height: 400px;" autoplay playsinline></video>
|
||||
<canvas id="webcamCanvas" style="display:none;"></canvas>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-center">
|
||||
<button type="button" class="btn btn-primary rounded-circle p-3" id="captureBtn">
|
||||
<i class="bi bi-camera-fill fs-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Location Logic
|
||||
const countrySelect = document.getElementById('id_country');
|
||||
const governateSelect = document.getElementById('id_governate');
|
||||
const citySelect = document.getElementById('id_city');
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Webcam Logic
|
||||
const openWebcamBtn = document.getElementById('openWebcamBtn');
|
||||
const webcamModal = new bootstrap.Modal(document.getElementById('webcamModal'));
|
||||
const video = document.getElementById('webcamVideo');
|
||||
const canvas = document.getElementById('webcamCanvas');
|
||||
const captureBtn = document.getElementById('captureBtn');
|
||||
const profileInput = document.getElementById('id_profile_picture');
|
||||
let stream = null;
|
||||
|
||||
if (openWebcamBtn) {
|
||||
openWebcamBtn.addEventListener('click', function() {
|
||||
webcamModal.show();
|
||||
navigator.mediaDevices.getUserMedia({ video: true })
|
||||
.then(function(s) {
|
||||
stream = s;
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error("An error occurred: " + err);
|
||||
alert("{% trans 'Could not access webcam. Please check permissions.' %}");
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('webcamModal').addEventListener('hidden.bs.modal', function () {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
});
|
||||
|
||||
captureBtn.addEventListener('click', function() {
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
canvas.toBlob(function(blob) {
|
||||
// Create a file from the blob
|
||||
const file = new File([blob], "webcam_profile.jpg", { type: "image/jpeg" });
|
||||
|
||||
// Use DataTransfer to set the file input value
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
profileInput.files = dataTransfer.files;
|
||||
|
||||
// Close modal
|
||||
webcamModal.hide();
|
||||
|
||||
// Visual feedback (optional)
|
||||
// alert("{% trans 'Photo captured!' %}");
|
||||
}, 'image/jpeg');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
input:not([type=radio]):not([type=checkbox]), select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 8px;
|
||||
}
|
||||
#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;
|
||||
}
|
||||
.input-group > .form-control {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.input-group > .btn {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
125
core/templates/core/register_shipper.html
Normal file
125
core/templates/core/register_shipper.html
Normal file
@ -0,0 +1,125 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Register as Shipper" %} | 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-10 col-lg-8">
|
||||
<!-- Back -->
|
||||
<div class="mb-4">
|
||||
<a href="{% url 'register' %}" class="btn btn-link text-decoration-none text-muted ps-0">
|
||||
<i class="bi {% if LANGUAGE_BIDI %}bi-arrow-right{% else %}bi-arrow-left{% endif %} me-2"></i>{% trans "Back" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold mb-4 text-center">{% trans "Shipper Registration" %}</h2>
|
||||
<form method="post" id="registrationForm">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
{{ field }}
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-masarx-primary w-100 py-2 mt-3">{% trans "Create Account" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const countrySelect = document.getElementById('id_country');
|
||||
const governateSelect = document.getElementById('id_governate');
|
||||
const citySelect = document.getElementById('id_city');
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
input:not([type=radio]):not([type=checkbox]), select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 8px;
|
||||
}
|
||||
#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);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
14
core/urls.py
14
core/urls.py
@ -7,7 +7,11 @@ urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
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'),
|
||||
|
||||
# Registration Flow
|
||||
path('register/', views.register, name='register'),
|
||||
path('register/shipper/', views.register_shipper, name='register_shipper'),
|
||||
path('register/driver/', views.register_driver, name='register_driver'),
|
||||
path('register/verify/', views.verify_registration, name='verify_registration'),
|
||||
|
||||
# Password Reset URLs
|
||||
@ -66,10 +70,18 @@ urlpatterns = [
|
||||
path('login/request-otp/', views.request_login_otp, name='request_login_otp'),
|
||||
path('login/verify-otp/', views.verify_login_otp, name='verify_login_otp'),
|
||||
|
||||
# API Endpoints
|
||||
# API Endpoints (Standard)
|
||||
path('api/auth/token/', api_views.CustomAuthToken.as_view(), name='api_token_auth'),
|
||||
path('api/parcels/', api_views.ParcelListCreateView.as_view(), name='api_parcel_list'),
|
||||
path('api/parcels/<int:pk>/', api_views.ParcelDetailView.as_view(), name='api_parcel_detail'),
|
||||
path('api/track/<str:tracking_number>/', api_views.PublicParcelTrackView.as_view(), name='api_track_parcel'),
|
||||
path('api/profile/', api_views.UserProfileView.as_view(), name='api_user_profile'),
|
||||
|
||||
# Aliases for mobile app compatibility (API v1)
|
||||
path('api/shipments/', api_views.ParcelListCreateView.as_view(), name='api_shipment_list'),
|
||||
path('api/shipments/<int:pk>/', api_views.ParcelDetailView.as_view(), name='api_shipment_detail'),
|
||||
|
||||
# Root-level Aliases (for apps hardcoded to /shipments/)
|
||||
path('shipments/', api_views.ParcelListCreateView.as_view(), name='root_shipment_list'),
|
||||
path('shipments/<int:pk>/', api_views.ParcelDetailView.as_view(), name='root_shipment_detail'),
|
||||
]
|
||||
@ -4,7 +4,7 @@ from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from .models import Parcel, Profile, Country, Governate, City, OTPVerification, PlatformProfile, Testimonial, DriverRating
|
||||
from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm, DriverRatingForm
|
||||
from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm, DriverRatingForm, ShipperRegistrationForm, DriverRegistrationForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import get_language
|
||||
from django.contrib import messages
|
||||
@ -77,8 +77,11 @@ def track_parcel(request):
|
||||
})
|
||||
|
||||
def register(request):
|
||||
return render(request, 'core/register_choice.html')
|
||||
|
||||
def register_shipper(request):
|
||||
if request.method == 'POST':
|
||||
form = UserRegistrationForm(request.POST)
|
||||
form = ShipperRegistrationForm(request.POST)
|
||||
if form.is_valid():
|
||||
# Save user but inactive
|
||||
user = form.save(commit=True)
|
||||
@ -110,8 +113,45 @@ def register(request):
|
||||
request.session['registration_user_id'] = user.id
|
||||
return redirect('verify_registration')
|
||||
else:
|
||||
form = UserRegistrationForm()
|
||||
return render(request, 'core/register.html', {'form': form})
|
||||
form = ShipperRegistrationForm()
|
||||
return render(request, 'core/register_shipper.html', {'form': form})
|
||||
|
||||
def register_driver(request):
|
||||
if request.method == 'POST':
|
||||
form = DriverRegistrationForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
# 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')
|
||||
otp_msg = _("Your Masar Verification Code is %(code)s") % {'code': code}
|
||||
|
||||
if method == 'whatsapp':
|
||||
phone = user.profile.phone_number
|
||||
send_whatsapp_message(phone, otp_msg)
|
||||
messages.info(request, _("Verification code sent to WhatsApp."))
|
||||
else:
|
||||
send_html_email(
|
||||
subject=_('Verification Code'),
|
||||
message=otp_msg,
|
||||
recipient_list=[user.email],
|
||||
title=_('Welcome to Masar!'),
|
||||
request=request
|
||||
)
|
||||
messages.info(request, _("Verification code sent to email."))
|
||||
|
||||
request.session['registration_user_id'] = user.id
|
||||
return redirect('verify_registration')
|
||||
else:
|
||||
form = DriverRegistrationForm()
|
||||
return render(request, 'core/register_driver.html', {'form': form})
|
||||
|
||||
def verify_registration(request):
|
||||
if 'registration_user_id' not in request.session:
|
||||
@ -888,4 +928,4 @@ def cancel_parcel(request, parcel_id):
|
||||
parcel.save()
|
||||
messages.success(request, _("Shipment cancelled successfully."))
|
||||
|
||||
return redirect('dashboard')
|
||||
return redirect('dashboard')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user