diff --git a/assets/vm-shot-2026-01-27T10-44-32-354Z.jpg b/assets/vm-shot-2026-01-27T10-44-32-354Z.jpg new file mode 100644 index 0000000..14ea974 Binary files /dev/null and b/assets/vm-shot-2026-01-27T10-44-32-354Z.jpg differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6f131d4..143a227 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 25d2e3b..9c79858 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 987996d..c579a65 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 0394ab0..c895336 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 782558d..ad9397a 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/apps.py b/core/apps.py index 8115ae6..fcb1d96 100644 --- a/core/apps.py +++ b/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 \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index 86188ba..a4477c8 100644 --- a/core/forms.py +++ b/core/forms.py @@ -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)] \ No newline at end of file + self.fields['rating'].choices = [(i, str(i)) for i in range(5, 0, -1)] diff --git a/core/migrations/0019_profile_car_plate_number_profile_license_back_image_and_more.py b/core/migrations/0019_profile_car_plate_number_profile_license_back_image_and_more.py new file mode 100644 index 0000000..0944535 --- /dev/null +++ b/core/migrations/0019_profile_car_plate_number_profile_license_back_image_and_more.py @@ -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'), + ), + ] diff --git a/core/migrations/__pycache__/0019_profile_car_plate_number_profile_license_back_image_and_more.cpython-311.pyc b/core/migrations/__pycache__/0019_profile_car_plate_number_profile_license_back_image_and_more.cpython-311.pyc new file mode 100644 index 0000000..ac07d52 Binary files /dev/null and b/core/migrations/__pycache__/0019_profile_car_plate_number_profile_license_back_image_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 8d068a8..db45946 100644 --- a/core/models.py +++ b/core/models.py @@ -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') \ No newline at end of file + verbose_name_plural = _('Driver Ratings') diff --git a/core/templates/core/register_choice.html b/core/templates/core/register_choice.html new file mode 100644 index 0000000..92d1c89 --- /dev/null +++ b/core/templates/core/register_choice.html @@ -0,0 +1,73 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Register" %} | masarX{% endblock %} + +{% block content %} +
+
+
+
+

{% trans "Welcome to masarX" %}

+

{% trans "Choose how you want to join us today." %}

+
+
+ +
+ +
+
+
+
+ +
+

{% trans "I am a Shipper" %}

+

{% trans "I want to send parcels and track my shipments easily." %}

+ {% trans "Register as Shipper" %} +
+
+
+ + +
+
+
+
+ +
+

{% trans "I am a Car Owner" %}

+

{% trans "I want to deliver parcels and earn money on my own schedule." %}

+ {% trans "Register as Car Owner" %} +
+
+
+
+ +
+

{% trans "Already have an account?" %} {% trans "Login here" %}

+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/register_driver.html b/core/templates/core/register_driver.html new file mode 100644 index 0000000..1120f0a --- /dev/null +++ b/core/templates/core/register_driver.html @@ -0,0 +1,222 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans "Register as Car Owner" %} | masarX{% endblock %} + +{% block content %} +
+
+
+
+ + + +
+
+

{% trans "Car Owner Registration" %}

+
+ {% csrf_token %} +
+ {% for field in form %} + {% if field.is_hidden %} + {{ field }} + {% else %} +
+ + + {% if field.name == 'profile_picture' %} +
+ {{ field }} + +
+ {% else %} + {{ field }} + {% endif %} + + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endif %} + {% endfor %} +
+ +
+ + {% trans "Please provide clear photos of your license (front and back) and car plate number for verification." %} +
+ + +
+
+
+
+
+
+
+ + + + + + + +{% endblock %} diff --git a/core/templates/core/register_shipper.html b/core/templates/core/register_shipper.html new file mode 100644 index 0000000..9eea66d --- /dev/null +++ b/core/templates/core/register_shipper.html @@ -0,0 +1,125 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans "Register as Shipper" %} | masarX{% endblock %} + +{% block content %} +
+
+
+
+ + + +
+
+

{% trans "Shipper Registration" %}

+
+ {% csrf_token %} +
+ {% for field in form %} + {% if field.is_hidden %} + {{ field }} + {% else %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endif %} + {% endfor %} +
+ +
+
+
+
+
+
+
+ + + + +{% endblock %} diff --git a/core/urls.py b/core/urls.py index d6e84f3..685628e 100644 --- a/core/urls.py +++ b/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//', api_views.ParcelDetailView.as_view(), name='api_parcel_detail'), path('api/track//', 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//', 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//', api_views.ParcelDetailView.as_view(), name='root_shipment_detail'), ] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 8229611..ce6cfdb 100644 --- a/core/views.py +++ b/core/views.py @@ -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') \ No newline at end of file + return redirect('dashboard')