37794-vm/core/views.py
2026-02-02 06:38:01 +00:00

1257 lines
50 KiB
Python

from django.contrib.auth.views import LoginView
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, authenticate, logout
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, DriverRejection
from .forms import LoginForm, UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm, DriverRatingForm, ShipperRegistrationForm, DriverRegistrationForm, DriverReportForm
from django.utils.translation import gettext_lazy as _
from django.utils.translation import get_language
from django.contrib import messages
from django.http import JsonResponse, HttpResponse
from django.urls import reverse
from .payment_utils import ThawaniPay
from django.conf import settings
from django.core.mail import send_mail
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.views.decorators.http import require_POST
from django.db.models import Avg, Count
from django.template.loader import render_to_string
import random
import string
from .notifications import get_notification_content
from .whatsapp_utils import (
notify_shipment_created,
notify_payment_received,
notify_driver_assigned,
notify_status_change,
send_whatsapp_message
)
from .mail import send_contact_message, send_html_email
import json
from ai.local_ai_api import LocalAIApi
import weasyprint
import qrcode
from io import BytesIO
import base64
from .pricing import get_pricing_breakdown # Import pricing logic
def logout_view(request):
logout(request)
messages.success(request, _("You have been logged out."))
return redirect("index")
def index(request):
# If tracking_id is present, redirect to the new track view
tracking_id = request.GET.get('tracking_id')
if tracking_id:
return redirect(f"{reverse('track')}?tracking_number={tracking_id}")
testimonials = Testimonial.objects.filter(is_active=True)
platform_profile = PlatformProfile.objects.first()
ticker_limit = platform_profile.ticker_limit if platform_profile else 10
recent_shipments = Parcel.objects.exclude(status="cancelled").order_by("-created_at")[:ticker_limit]
# Top 5 Drivers (by Average Rating)
top_drivers = Profile.objects.filter(role='car_owner').annotate(
avg_rating=Avg('user__received_ratings__rating'),
rating_count=Count('user__received_ratings')
).filter(rating_count__gt=0).order_by('-avg_rating')[:5]
# Top 5 Shippers (by Shipment Count)
top_shippers = Profile.objects.filter(role='shipper').annotate(
shipment_count=Count('user__sent_parcels')
).order_by('-shipment_count')[:5]
return render(request, 'core/index.html', {
'testimonials': testimonials,
'top_drivers': top_drivers,
'top_shippers': top_shippers,
'recent_shipments': recent_shipments
})
def track_parcel(request):
tracking_number = request.GET.get('tracking_number')
parcel = None
error = None
if tracking_number:
try:
parcel = Parcel.objects.get(tracking_number__iexact=tracking_number.strip())
except Parcel.DoesNotExist:
error = _("Parcel not found with this Tracking ID.")
return render(request, 'core/track.html', {
'parcel': parcel,
'error': error,
'tracking_number': tracking_number
})
def register(request):
return render(request, 'core/register_choice.html')
def register_shipper(request):
if request.method == 'POST':
form = ShipperRegistrationForm(request.POST)
if form.is_valid():
# Save user but inactive
user = form.save(commit=True)
user.is_active = False
user.save()
# Auto-approve Shippers
if hasattr(user, 'profile'):
user.profile.is_approved = True
user.profile.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')
subj, email_msg, wa_msg = get_notification_content('otp_registration', {'code': code}, language=get_language())
if method == 'whatsapp':
phone = user.profile.phone_number
send_whatsapp_message(phone, wa_msg)
messages.info(request, _("Verification code sent to WhatsApp."))
else:
send_html_email(
subject=subj,
message=email_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 = 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()
# Drivers are NOT approved by default (model default is False)
# We can rely on the default, but explicit is fine too if we want to be sure
if hasattr(user, 'profile'):
user.profile.is_approved = False
user.profile.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')
subj, email_msg, wa_msg = get_notification_content('otp_registration', {'code': code}, language=get_language())
if method == 'whatsapp':
phone = user.profile.phone_number
send_whatsapp_message(phone, wa_msg)
messages.info(request, _("Verification code sent to WhatsApp."))
else:
send_html_email(
subject=subj,
message=email_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:
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)
# Set user's language preference
if hasattr(user, 'profile') and user.profile.language:
request.session["_language"] = user.profile.language
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
profile, created = Profile.objects.get_or_create(user=request.user)
# Check for Ban
if profile.is_banned:
logout(request)
messages.error(request, _("Your account is banned. Reason: ") + profile.ban_reason)
return redirect('login')
if profile.role == 'shipper':
all_parcels = Parcel.objects.filter(shipper=request.user).order_by('-created_at')
active_parcels_list = all_parcels.exclude(status__in=['delivered', 'cancelled'])
# Split history into delivered and cancelled
delivered_parcels = all_parcels.filter(status='delivered')
cancelled_parcels = all_parcels.filter(status='cancelled')
# Pagination for Active Shipments
page = request.GET.get('page', 1)
paginator = Paginator(active_parcels_list, 9) # Show 9 parcels per page
try:
active_parcels = paginator.page(page)
except PageNotAnInteger:
active_parcels = paginator.page(1)
except EmptyPage:
active_parcels = paginator.page(paginator.num_pages)
platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True
return render(request, 'core/shipper_dashboard.html', {
'active_parcels': active_parcels,
'delivered_parcels': delivered_parcels,
'cancelled_parcels': cancelled_parcels,
'payments_enabled': payments_enabled,
'platform_profile': platform_profile # Pass full profile just in case
})
else:
# Car Owner view
# Search logic
search_query = request.GET.get('q', '').strip()
platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True
# Filter out parcels this driver already rejected
rejected_parcel_ids = DriverRejection.objects.filter(driver=request.user).values_list('parcel_id', flat=True)
if payments_enabled:
available_parcels_list = Parcel.objects.filter(status='pending', payment_status='paid').exclude(id__in=rejected_parcel_ids).order_by('-created_at')
else:
available_parcels_list = Parcel.objects.filter(status='pending').exclude(id__in=rejected_parcel_ids).order_by('-created_at')
# Check Approval Status
if not profile.is_approved:
messages.warning(request, _("We are currently revising your documents. Please be patient. We will inform once we finish."))
# Empty list if not approved
available_parcels_list = Parcel.objects.none()
# Pagination for Available Shipments
page = request.GET.get('page', 1)
paginator = Paginator(available_parcels_list, 9) # Show 9 parcels per page
try:
available_parcels = paginator.page(page)
except PageNotAnInteger:
available_parcels = paginator.page(1)
except EmptyPage:
available_parcels = paginator.page(paginator.num_pages)
# Apply search filter if query exists
if search_query:
available_parcels_list = available_parcels_list.filter(tracking_number__icontains=search_query)
# We need to re-paginate if we filtered
paginator = Paginator(available_parcels_list, 9)
try:
available_parcels = paginator.page(page)
except (PageNotAnInteger, EmptyPage):
available_parcels = paginator.page(1)
# Active: Picked up or In Transit
my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status__in=['delivered', 'cancelled']).order_by('-created_at')
if search_query: my_parcels = my_parcels.filter(tracking_number__icontains=search_query)
# History: Delivered
completed_parcels = Parcel.objects.filter(carrier=request.user, status='delivered').order_by('-created_at')
if search_query: completed_parcels = completed_parcels.filter(tracking_number__icontains=search_query)
# Cancelled
cancelled_parcels = Parcel.objects.filter(carrier=request.user, status='cancelled').order_by('-created_at')
if search_query: cancelled_parcels = cancelled_parcels.filter(tracking_number__icontains=search_query)
# Statistics for Driver Dashboard
stats = {
'accepted_count': Parcel.objects.filter(carrier=request.user).exclude(status='cancelled').count(),
'rejected_count': DriverRejection.objects.filter(driver=request.user).count(),
'average_rating': profile.get_average_rating(),
'rating_count': profile.get_rating_count(),
'driver_grade': profile.get_driver_grade_display(),
'is_recommended': profile.is_recommended,
'warning_count': request.user.warnings.count(),
'max_warnings': platform_profile.max_warnings_before_ban if platform_profile else 3,
}
return render(request, 'core/driver_dashboard.html', {
'available_parcels': available_parcels,
'my_parcels': my_parcels,
'completed_parcels': completed_parcels,
'cancelled_parcels': cancelled_parcels,
'is_approved': profile.is_approved,
'stats': stats,
'search_query': search_query,
})
@login_required
def shipment_request(request):
from .models import PlatformProfile
platform_profile = PlatformProfile.objects.first()
if platform_profile and not platform_profile.accepting_shipments:
messages.warning(request, platform_profile.maintenance_message or _("The platform is currently not accepting new shipments."))
return redirect("dashboard")
profile, created = Profile.objects.get_or_create(user=request.user)
if profile.role != 'shipper':
messages.error(request, _("Only shippers can request shipments."))
return redirect('dashboard')
if request.method == 'POST':
form = ParcelForm(request.POST)
if form.is_valid():
parcel = form.save(commit=False)
parcel.shipper = request.user
# Recalculate price on backend to ensure integrity
# We trust the form's distance/weight if populated, but good to verify
# Ideally we recalculate from PricingRule here too
breakdown = get_pricing_breakdown(parcel.distance_km, parcel.weight)
if 'error' not in breakdown:
parcel.price = breakdown['price']
parcel.platform_fee = breakdown['platform_fee']
parcel.platform_fee_percentage = breakdown['platform_fee_percentage']
parcel.driver_amount = breakdown['driver_amount']
parcel.save()
# WhatsApp Notification
notify_shipment_created(parcel)
messages.success(request, _("Shipment requested successfully! Tracking ID: ") + parcel.tracking_number)
return redirect('dashboard')
else:
form = ParcelForm()
platform_profile = PlatformProfile.objects.first()
google_maps_api_key = platform_profile.google_maps_api_key if platform_profile else None
# DEBUG: Verify reverse
try:
url1 = reverse('api_pricing')
url2 = reverse('api_calculate_price')
print(f"DEBUG REVERSE: api_pricing={url1}, api_calculate_price={url2}")
except Exception as e:
print(f"DEBUG REVERSE ERROR: {e}")
return render(request, 'core/shipment_request.html', {
'form': form,
'google_maps_api_key': google_maps_api_key
})
@login_required
def accept_parcel(request, parcel_id):
profile, created = Profile.objects.get_or_create(user=request.user)
if profile.role != 'car_owner':
messages.error(request, _("Only car owners can accept shipments."))
return redirect('dashboard')
# Check Approval and Ban
if profile.is_banned:
messages.error(request, _("Your account is banned."))
return redirect('dashboard')
if not profile.is_approved:
messages.error(request, _("We are currently revising your documents. Please be patient. We will inform once we finish."))
return redirect('dashboard')
platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True
if payments_enabled:
parcel = get_object_or_404(Parcel, id=parcel_id, status='pending', payment_status='paid')
else:
parcel = get_object_or_404(Parcel, id=parcel_id, status='pending')
parcel.carrier = request.user
parcel.status = 'picked_up'
parcel.save()
# WhatsApp Notification
notify_driver_assigned(parcel)
messages.success(request, _("You have accepted the shipment!"))
return redirect('dashboard')
@login_required
@require_POST
def reject_parcel(request, parcel_id):
profile, created = Profile.objects.get_or_create(user=request.user)
if profile.role != 'car_owner':
messages.error(request, _("Only car owners can reject shipments."))
return redirect('dashboard')
parcel = get_object_or_404(Parcel, id=parcel_id, status='pending')
reason = request.POST.get('reason')
if not reason:
messages.error(request, _("Please provide a reason for rejection."))
return redirect('dashboard')
# Record Rejection
DriverRejection.objects.create(
driver=request.user,
parcel=parcel,
reason=reason
)
# Check Auto-Ban
platform_profile = PlatformProfile.objects.first()
if platform_profile and platform_profile.auto_ban_on_rejections:
rejection_count = DriverRejection.objects.filter(driver=request.user).count()
if rejection_count >= platform_profile.rejection_limit:
profile.is_banned = True
profile.is_approved = False # Also unapprove to be safe
profile.ban_reason = _("Automated ban: Exceeded rejection limit ({}).").format(platform_profile.rejection_limit)
profile.save()
logout(request)
messages.error(request, _("Your account has been banned due to excessive shipment rejections."))
return redirect('login')
messages.success(request, _("Shipment rejected successfully."))
return redirect('dashboard')
@login_required
def update_status(request, parcel_id):
parcel = get_object_or_404(Parcel, id=parcel_id, carrier=request.user)
if request.method == 'POST':
new_status = request.POST.get('status')
if new_status in dict(Parcel.STATUS_CHOICES):
parcel.status = new_status
parcel.save()
# WhatsApp Notification
notify_status_change(parcel)
messages.success(request, _("Status updated successfully!"))
return redirect('dashboard')
@login_required
def initiate_payment(request, parcel_id):
# Check if payments are enabled
platform_profile = PlatformProfile.objects.first()
if platform_profile and not platform_profile.enable_payment:
messages.error(request, _("Payments are currently disabled by the administrator."))
return redirect('dashboard')
parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user, payment_status='pending')
thawani = ThawaniPay()
success_url = request.build_absolute_uri(reverse('payment_success')) + f"?session_id={{CHECKOUT_SESSION_ID}}&parcel_id={parcel.id}"
cancel_url = request.build_absolute_uri(reverse('payment_cancel')) + f"?parcel_id={parcel.id}"
session_id = thawani.create_checkout_session(parcel, success_url, cancel_url)
if session_id:
parcel.thawani_session_id = session_id
parcel.save()
checkout_url = f"{settings.THAWANI_API_URL.replace('/api/v1', '')}/pay/{session_id}?key={settings.THAWANI_PUBLISHABLE_KEY}"
return redirect(checkout_url)
else:
messages.error(request, _("Could not initiate payment. Please try again later."))
return redirect('dashboard')
@login_required
def payment_success(request):
session_id = request.GET.get('session_id')
parcel_id = request.GET.get('parcel_id')
parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user)
thawani = ThawaniPay()
session_data = thawani.get_checkout_session(session_id)
if session_data and session_data.get('payment_status') == 'paid':
parcel.payment_status = 'paid'
parcel.save()
# WhatsApp Notification
notify_payment_received(parcel)
messages.success(request, _("Payment successful! Your shipment is now active."))
else:
messages.warning(request, _("Payment status is pending or failed. Please check your dashboard."))
return redirect('dashboard')
@login_required
def payment_cancel(request):
messages.info(request, _("Payment was cancelled."))
return redirect('dashboard')
def article_detail(request):
return render(request, 'core/article_detail.html')
def get_governates(request):
country_id = request.GET.get('country_id')
lang = get_language()
field_name = 'name_ar' if lang == 'ar' else 'name_en'
governates = Governate.objects.filter(country_id=country_id).order_by(field_name)
data = [{'id': g.id, 'name': getattr(g, field_name)} for g in governates]
return JsonResponse(data, safe=False)
def get_cities(request):
governate_id = request.GET.get('governate_id')
lang = get_language()
field_name = 'name_ar' if lang == 'ar' else 'name_en'
cities = City.objects.filter(governate_id=governate_id).order_by(field_name)
data = [{'id': c.id, 'name': getattr(c, field_name)} for c in cities]
return JsonResponse(data, safe=False)
def privacy_policy(request):
return render(request, 'core/privacy_policy.html')
def terms_conditions(request):
return render(request, 'core/terms_conditions.html')
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Send email
sent = send_contact_message(
name=form.cleaned_data['name'],
email=form.cleaned_data['email'],
message=form.cleaned_data['message']
)
if sent:
messages.success(request, _("Your message has been sent successfully!"))
else:
messages.error(request, _("There was an error sending your message. Please try again later."))
return redirect('contact')
else:
form = ContactForm()
return render(request, 'core/contact.html', {'form': form})
@login_required
def profile_view(request):
profile = request.user.profile
reviews = []
if profile.role == 'car_owner':
reviews = request.user.received_ratings.all().order_by('-created_at')
return render(request, 'core/profile.html', {
'profile': profile,
'reviews': reviews
})
@login_required
def edit_profile(request):
if request.method == 'POST':
form = UserProfileForm(request.POST, request.FILES, instance=request.user.profile)
if form.is_valid():
# 1. Handle Image immediately (easier than session storage)
if 'profile_picture' in request.FILES:
request.user.profile.profile_picture = request.FILES['profile_picture']
request.user.profile.save()
# 2. Store other data in session for verification
data = form.cleaned_data
# Remove objects that can't be serialized or we've already handled
safe_data = {
'first_name': data['first_name'],
'last_name': data['last_name'],
'email': data['email'],
'phone_number': data['phone_number'],
'address': data['address'],
'country_id': data['country'].id if data['country'] else None,
'governate_id': data['governate'].id if data['governate'] else None,
'city_id': data['city'].id if data['city'] else None,
'language': data['language'],
}
request.session['pending_profile_update'] = safe_data
# 3. Generate OTP
code = ''.join(random.choices(string.digits, k=6))
OTPVerification.objects.create(user=request.user, code=code, purpose='profile_update')
# 4. Send OTP
method = data.get('otp_method', 'email')
subj, email_msg, wa_msg = get_notification_content('otp_profile_update', {'code': code}, language=get_language())
if method == 'whatsapp':
# Use current phone if available, else new phone
phone = request.user.profile.phone_number or data['phone_number']
send_whatsapp_message(phone, wa_msg)
messages.info(request, _("Verification code sent to WhatsApp."))
else:
# Default to email
# Send to the NEW email address (from the form), not the old one
target_email = data['email']
send_html_email(
subject=subj,
message=email_msg,
recipient_list=[target_email],
title=subj,
request=request
)
messages.info(request, _("Verification code sent to email."))
return redirect('verify_otp')
else:
form = UserProfileForm(instance=request.user.profile)
return render(request, 'core/edit_profile.html', {'form': form})
@login_required
def verify_otp_view(request):
if request.method == 'POST':
code = request.POST.get('code')
try:
otp = OTPVerification.objects.filter(
user=request.user,
code=code,
purpose='profile_update',
is_verified=False
).latest('created_at')
if otp.is_valid():
# Apply changes
data = request.session.get('pending_profile_update')
if data:
# Update User
request.user.first_name = data['first_name']
request.user.last_name = data['last_name']
request.user.email = data['email']
request.user.save()
# Update Profile
profile = request.user.profile
profile.phone_number = data['phone_number']
profile.address = data['address']
if data.get('country_id'):
profile.country_id = data['country_id']
if data.get('governate_id'):
profile.governate_id = data['governate_id']
if data.get('city_id'):
profile.city_id = data['city_id']
if data.get('language'):
profile.language = data['language']
request.session["_language"] = data['language']
profile.save()
# Cleanup
otp.is_verified = True
otp.save()
del request.session['pending_profile_update']
messages.success(request, _("Profile updated successfully!"))
return redirect('profile')
else:
messages.error(request, _("Session expired. Please try again."))
return redirect('edit_profile')
else:
messages.error(request, _("Invalid or expired code."))
except OTPVerification.DoesNotExist:
messages.error(request, _("Invalid code."))
return render(request, 'core/verify_otp.html')
@login_required
def rate_driver(request, parcel_id):
parcel = get_object_or_404(Parcel, id=parcel_id)
# Validation
if parcel.shipper != request.user:
messages.error(request, _("You are not authorized to rate this shipment."))
return redirect('dashboard')
if parcel.status != 'delivered':
messages.error(request, _("You can only rate delivered shipments."))
return redirect('dashboard')
if not parcel.carrier:
messages.error(request, _("No driver was assigned to this shipment."))
return redirect('dashboard')
if hasattr(parcel, 'rating'):
messages.info(request, _("You have already rated this shipment."))
return redirect('dashboard')
if request.method == 'POST':
form = DriverRatingForm(request.POST)
if form.is_valid():
rating = form.save(commit=False)
rating.parcel = parcel
rating.driver = parcel.carrier
rating.shipper = request.user
rating.save()
messages.success(request, _("Thank you for your feedback!"))
return redirect('dashboard')
else:
form = DriverRatingForm()
return render(request, 'core/rate_driver.html', {
'form': form,
'parcel': parcel
})
@require_POST
def request_login_otp(request):
identifier = request.POST.get('identifier')
if not identifier:
return JsonResponse({'success': False, 'message': _('Please enter an email or phone number.')})
# Clean identifier
identifier = identifier.strip()
user = None
method = 'email'
# Try to find user by email
user = User.objects.filter(email__iexact=identifier).first()
# If not found, try by phone number
if not user:
profile = Profile.objects.filter(phone_number=identifier).first()
if profile:
user = profile.user
method = 'whatsapp'
else:
# Fallback: maybe they entered a phone without country code or with?
# For now, simplistic search
pass
if not user:
# Don't reveal if user exists or not for security, but for UX on this project we can be a bit more helpful
return JsonResponse({'success': False, 'message': _('User not found with this email or phone number.')})
if not user.is_active:
return JsonResponse({'success': False, 'message': _('Account is inactive. Please verify registration first.')})
# Generate OTP
code = ''.join(random.choices(string.digits, k=6))
subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language())
OTPVerification.objects.create(user=user, code=code, purpose='login')
# Send OTP
subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language())
try:
if method == 'whatsapp':
phone = user.profile.phone_number
send_whatsapp_message(phone, wa_msg)
message_sent = _("OTP sent to your WhatsApp.")
else:
send_html_email(
subject=subj,
message=email_msg,
recipient_list=[user.email],
title=_('Login Verification'),
request=request
)
message_sent = _("OTP sent to your email.")
return JsonResponse({'success': True, 'message': message_sent, 'user_id': user.id})
except Exception as e:
return JsonResponse({'success': False, 'message': _('Failed to send OTP. Please try again.')})
@require_POST
def verify_login_otp(request):
user_id = request.POST.get('user_id')
code = request.POST.get('code')
if not user_id or not code:
return JsonResponse({'success': False, 'message': _('Invalid request.')})
try:
user = User.objects.get(id=user_id)
otp = OTPVerification.objects.filter(
user=user,
code=code,
purpose='login',
is_verified=False
).latest('created_at')
if otp.is_valid():
# Cleanup
otp.is_verified = True
otp.save()
# Login
login(request, user)
# Set user's language preference
if hasattr(user, 'profile') and user.profile.language:
request.session["_language"] = user.profile.language
return JsonResponse({'success': True, 'redirect_url': reverse('dashboard')})
else:
return JsonResponse({'success': False, 'message': _('Invalid or expired OTP.')})
except (User.DoesNotExist, OTPVerification.DoesNotExist):
return JsonResponse({'success': False, 'message': _('Invalid OTP.')})
@require_POST
def chatbot(request):
try:
data = json.loads(request.body)
user_message = data.get("message", "")
language = data.get("language", "en")
if not user_message:
return JsonResponse({"success": False, "error": "Empty message"})
system_prompt = (
"You are MasarX AI, a helpful and professional assistant for the Masar logistics platform. "
"The platform connects shippers with drivers for small parcel deliveries. "
"Answer the user's questions about shipping, tracking, becoming a driver, or general support. "
"If the user speaks Arabic, reply in Arabic. If English, reply in English. "
"Keep responses concise and helpful."
)
if language == "ar":
system_prompt += " The user is currently browsing in Arabic."
else:
system_prompt += " The user is currently browsing in English."
import logging
logger = logging.getLogger(__name__)
logger.info(f'Chatbot request: {user_message}')
response = LocalAIApi.create_response({
"input": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
]
})
logger.info(f'Chatbot response: {response}')
if response.get("success"):
text = LocalAIApi.extract_text(response)
return JsonResponse({"success": True, "response": text})
else:
return JsonResponse({"success": False, "error": response.get("error", "AI Error")})
except json.JSONDecodeError:
return JsonResponse({"success": False, "error": "Invalid JSON"})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
@login_required
def generate_parcel_label(request, parcel_id):
parcel = get_object_or_404(Parcel, id=parcel_id)
# Security check: only shipper or carrier can print label
if parcel.shipper != request.user and parcel.carrier != request.user:
messages.error(request, _("You are not authorized to print this label."))
return redirect('dashboard')
# Generate QR Code
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(parcel.tracking_number)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format="PNG")
qr_image_base64 = base64.b64encode(buffer.getvalue()).decode()
# Get Logo Base64
logo_base64 = None
platform_profile = PlatformProfile.objects.first()
if platform_profile and platform_profile.logo:
try:
with open(platform_profile.logo.path, "rb") as image_file:
logo_base64 = base64.b64encode(image_file.read()).decode()
except Exception:
pass
# Render Template
html_string = render_to_string('core/parcel_label.html', {
'parcel': parcel,
'qr_code': qr_image_base64,
'logo_base64': logo_base64,
'platform_profile': platform_profile,
})
# Generate PDF
html = weasyprint.HTML(string=html_string, base_url=request.build_absolute_uri())
pdf_file = html.write_pdf()
response = HttpResponse(pdf_file, content_type='application/pdf')
response['Content-Disposition'] = f'inline; filename="label_{parcel.tracking_number}.pdf"'
return response
@login_required
def generate_invoice(request, parcel_id):
parcel = get_object_or_404(Parcel, id=parcel_id)
# Security check: only shipper can view invoice (or admin)
if parcel.shipper != request.user and not request.user.is_staff:
messages.error(request, _("You are not authorized to view this invoice."))
return redirect('dashboard')
# Get Logo Base64
logo_base64 = None
platform_profile = PlatformProfile.objects.first()
if platform_profile and platform_profile.logo:
try:
with open(platform_profile.logo.path, "rb") as image_file:
logo_base64 = base64.b64encode(image_file.read()).decode()
except Exception:
pass
# Render Template
html_string = render_to_string('core/invoice.html', {
'parcel': parcel,
'logo_base64': logo_base64,
'platform_profile': platform_profile,
'request': request,
})
# Generate PDF
html = weasyprint.HTML(string=html_string, base_url=request.build_absolute_uri())
pdf_file = html.write_pdf()
response = HttpResponse(pdf_file, content_type='application/pdf')
response['Content-Disposition'] = f'inline; filename="invoice_{parcel.tracking_number}.pdf"'
return response
@login_required
def scan_qr_view(request):
"""Renders the QR Scanner page for drivers."""
# Optional: Restrict to drivers only
# if request.user.profile.role != 'car_owner':
# messages.error(request, _("Only drivers can access the scanner."))
# return redirect('dashboard')
return render(request, 'core/scan_qr.html')
@login_required
def get_parcel_details(request):
"""API to fetch parcel details by tracking number."""
tracking_number = request.GET.get('tracking_number')
if not tracking_number:
return JsonResponse({'success': False, 'error': _('Tracking number required')})
try:
parcel = Parcel.objects.get(tracking_number__iexact=tracking_number.strip())
# Check permissions: Is user the assigned driver? Or is it pending (for acceptance)?
is_driver = request.user.profile.role == 'car_owner'
is_assigned = parcel.carrier == request.user
can_update = False
if is_driver:
if is_assigned:
can_update = True
elif parcel.status == 'pending':
# Check if payments are enabled and paid
platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True
if not payments_enabled or parcel.payment_status == 'paid':
can_update = True
data = {
'success': True,
'parcel': {
'id': parcel.id,
'tracking_number': parcel.tracking_number,
'status': parcel.status,
'status_display': parcel.get_status_display(),
'price': float(parcel.price),
'from': f"{parcel.pickup_governate.name} / {parcel.pickup_city.name}",
'to': f"{parcel.delivery_governate.name} / {parcel.delivery_city.name}",
'description': parcel.description,
'can_update': can_update
}
}
return JsonResponse(data)
except Parcel.DoesNotExist:
return JsonResponse({'success': False, 'error': _('Parcel not found')})
@login_required
@require_POST
def update_parcel_status_ajax(request):
"""API to update parcel status from scanner."""
try:
data = json.loads(request.body)
parcel_id = data.get('parcel_id')
action = data.get('action')
parcel = get_object_or_404(Parcel, id=parcel_id)
# Logic for actions
if action == 'accept':
# Similar to accept_parcel view
if request.user.profile.role != 'car_owner':
return JsonResponse({'success': False, 'error': _('Only drivers can accept shipments')})
if parcel.status != 'pending':
return JsonResponse({'success': False, 'error': _('Parcel is not available')})
# Check payment status if enabled
platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True
if payments_enabled and parcel.payment_status != 'paid':
return JsonResponse({'success': False, 'error': _('Payment pending')})
# Check Approval and Ban for Driver via AJAX
if request.user.profile.is_banned:
return JsonResponse({'success': False, 'error': _('Account is banned')})
if not request.user.profile.is_approved:
return JsonResponse({'success': False, 'error': _('Account pending approval')})
parcel.carrier = request.user
parcel.status = 'picked_up' # Or 'assigned'? Logic says 'picked_up' in accept_parcel
parcel.save()
notify_driver_assigned(parcel)
elif action == 'delivered':
if parcel.carrier != request.user:
return JsonResponse({'success': False, 'error': _('Not authorized')})
parcel.status = 'delivered'
parcel.save()
notify_status_change(parcel)
else:
return JsonResponse({'success': False, 'error': _('Invalid action')})
return JsonResponse({'success': True})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
@login_required
def edit_parcel(request, parcel_id):
parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user)
if parcel.status != 'pending':
messages.error(request, _("You can only edit pending shipments."))
return redirect('dashboard')
if request.method == 'POST':
form = ParcelForm(request.POST, instance=parcel)
if form.is_valid():
parcel_updated = form.save(commit=False)
# Recalculate if fields changed
breakdown = get_pricing_breakdown(parcel_updated.distance_km, parcel_updated.weight)
if 'error' not in breakdown:
parcel_updated.price = breakdown['price']
parcel_updated.platform_fee = breakdown['platform_fee']
parcel_updated.platform_fee_percentage = breakdown['platform_fee_percentage']
parcel_updated.driver_amount = breakdown['driver_amount']
parcel_updated.save()
messages.success(request, _("Shipment updated successfully."))
return redirect('dashboard')
else:
form = ParcelForm(instance=parcel)
platform_profile = PlatformProfile.objects.first()
google_maps_api_key = platform_profile.google_maps_api_key if platform_profile else None
return render(request, 'core/edit_parcel.html', {'form': form, 'parcel': parcel, 'google_maps_api_key': google_maps_api_key})
@login_required
def cancel_parcel(request, parcel_id):
parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user)
if parcel.status != 'pending':
messages.error(request, _("You can only cancel pending shipments."))
else:
parcel.status = 'cancelled'
parcel.save()
messages.success(request, _("Shipment cancelled successfully."))
return redirect('dashboard')
class CustomLoginView(LoginView):
authentication_form = LoginForm
template_name = 'core/login.html'
def form_valid(self, form):
# Authenticate checks are done by the form
user = form.get_user()
# Store user ID in session for 2FA step
self.request.session['pre_2fa_user_id'] = user.id
self.request.session.set_expiry(600) # 10 minutes expiry for this session part
return redirect('select_2fa_method')
def select_2fa_method(request):
user_id = request.session.get('pre_2fa_user_id')
if not user_id:
return redirect('login')
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
return redirect('login')
if request.method == 'POST':
method = request.POST.get('method')
code = ''.join(random.choices(string.digits, k=6))
subj, email_msg, wa_msg = get_notification_content('otp_login', {'code': code}, language=get_language())
# Invalidate old login OTPs
OTPVerification.objects.filter(user=user, purpose='login').delete()
OTPVerification.objects.create(user=user, code=code, purpose='login')
if method == 'email':
if user.email:
try:
send_html_email(
subject=_("Your Login OTP"),
message=email_msg,
recipient_list=[user.email],
title=_("Login Verification")
)
messages.success(request, _("OTP sent to your email."))
return redirect('verify_2fa_otp')
except Exception as e:
messages.error(request, _("Failed to send email: ") + str(e))
else:
messages.error(request, _("No email address associated with this account."))
elif method == 'whatsapp':
if hasattr(user, 'profile') and user.profile.phone_number:
if send_whatsapp_message(user.profile.phone_number, wa_msg):
messages.success(request, _("OTP sent to your WhatsApp."))
return redirect('verify_2fa_otp')
else:
messages.error(request, _("Failed to send WhatsApp message. Please check the logs."))
else:
messages.error(request, _("No phone number found for this account."))
return render(request, 'core/select_2fa_method.html')
def verify_2fa_otp(request):
user_id = request.session.get('pre_2fa_user_id')
if not user_id:
return redirect('login')
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
return redirect('login')
if request.method == 'POST':
code = request.POST.get('otp')
try:
# Find the most recent valid login OTP for this user
otp_record = OTPVerification.objects.filter(
user=user,
purpose='login',
is_verified=False
).latest('created_at')
if otp_record.code == code and otp_record.is_valid():
otp_record.is_verified = True
otp_record.save()
# ACTUAL LOGIN HAPPENS HERE
if otp_record.code == code and otp_record.is_valid():
otp_record.is_verified = True
otp_record.save()
# ACTUAL LOGIN HAPPENS HERE
login(request, user)
# Set user's language preference
if hasattr(user, 'profile') and user.profile.language:
request.session["_language"] = user.profile.language
# Clean up session
if 'pre_2fa_user_id' in request.session:
del request.session['pre_2fa_user_id']
request.session.set_expiry(None)
messages.success(request, _("Logged in successfully."))
return redirect('dashboard')
else:
messages.error(request, _("Invalid or expired OTP."))
except OTPVerification.DoesNotExist:
messages.error(request, _("No valid OTP found. Please request a new one."))
return render(request, 'core/verify_2fa_otp.html')
@login_required
def report_driver(request, parcel_id):
parcel = get_object_or_404(Parcel, id=parcel_id)
# Validation: Only shipper of the parcel can report the carrier
if parcel.shipper != request.user:
messages.error(request, _("You are not authorized to report for this shipment."))
return redirect('dashboard')
if not parcel.carrier:
messages.error(request, _("No driver was assigned to this shipment."))
return redirect('dashboard')
if request.method == 'POST':
form = DriverReportForm(request.POST)
if form.is_valid():
report = form.save(commit=False)
report.reporter = request.user
report.driver = parcel.carrier
report.parcel = parcel
report.save()
messages.success(request, _("Thank you. Your report has been submitted and will be investigated."))
return redirect('dashboard')
else:
form = DriverReportForm()
return render(request, 'core/report_driver.html', {
'form': form,
'parcel': parcel
})