adding track reminder

This commit is contained in:
Flatlogic Bot 2026-01-24 04:08:52 +00:00
parent b70c6b10e0
commit 56126df7d4
17 changed files with 291 additions and 30 deletions

View File

@ -69,6 +69,7 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'core.middleware.SubscriptionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',

View File

@ -4,6 +4,8 @@ from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.utils.html import format_html
from .models import Profile, Truck, Shipment, Bid, Message, WhatsAppConfig, Country, City, TruckType, AppSetting, Banner, HomeSection
from .whatsapp import send_whatsapp_message
@ -25,9 +27,24 @@ class TruckTypeAdmin(admin.ModelAdmin):
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'country_code', 'phone_number', 'subscription_plan', 'is_subscription_active')
list_display = ('user', 'role', 'phone_number', 'subscription_plan', 'subscription_expiry', 'subscription_status')
list_filter = ('role', 'subscription_plan', 'is_subscription_active')
search_fields = ('user__username', 'phone_number')
def subscription_status(self, obj):
if obj.subscription_plan == 'NONE':
return format_html('<span style="color: gray;">{}</span>', _('No Plan'))
if obj.is_expired():
return format_html('<span style="color: red; font-weight: bold;">{}</span>', _('Expired'))
days = obj.days_until_expiry()
if days <= 7:
return format_html('<span style="color: orange; font-weight: bold;">{} ({} {})</span>', _('Expiring soon'), days, _('days'))
return format_html('<span style="color: green; font-weight: bold;">{}</span>', _('Active'))
subscription_status.short_description = _('Subscription Status')
@admin.register(Truck)
class TruckAdmin(admin.ModelAdmin):
@ -117,4 +134,4 @@ class HomeSectionAdmin(admin.ModelAdmin):
list_display = ('title', 'section_type', 'order', 'is_active')
list_editable = ('order', 'is_active')
list_filter = ('section_type', 'is_active', 'background_color')
search_fields = ('title', 'title_ar', 'subtitle', 'subtitle_ar', 'content', 'content_ar')
search_fields = ('title', 'title_ar', 'subtitle', 'subtitle_ar', 'content', 'content_ar')

View File

@ -0,0 +1,57 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from core.models import Profile
from core.whatsapp import send_whatsapp_message
from django.utils.translation import gettext as _
from datetime import timedelta
class Command(BaseCommand):
help = 'Checks for expiring subscriptions and sends reminders via WhatsApp'
def handle(self, *args, **options):
today = timezone.now().date()
# Reminders for 7 days, 3 days, and 1 day before expiry
reminder_days = [7, 3, 1]
for days in reminder_days:
expiry_date = today + timedelta(days=days)
expiring_profiles = Profile.objects.filter(
subscription_expiry=expiry_date,
subscription_plan__in=['MONTHLY', 'ANNUAL']
)
for profile in expiring_profiles:
full_phone = profile.full_phone_number
if full_phone:
message = _(
"Hello %(user)s, your MASAR CARGO subscription (%(plan)s) will expire in %(days)s days on %(date)s. "
"Please renew to avoid account suspension."
) % {
'user': profile.user.username,
'plan': profile.get_subscription_plan_display(),
'days': days,
'date': profile.subscription_expiry.strftime('%d/%m/%Y')
}
self.stdout.write(f"Sending reminder to {profile.user.username} ({full_phone}) - {days} days left")
send_whatsapp_message(full_phone, message)
# Also check for profiles that expired TODAY and send a notification
expired_today = Profile.objects.filter(
subscription_expiry=today,
subscription_plan__in=['MONTHLY', 'ANNUAL']
)
for profile in expired_today:
full_phone = profile.full_phone_number
if full_phone:
message = _(
"Hello %(user)s, your MASAR CARGO subscription has EXPIRED today. "
"Your account is now suspended. Please contact support to renew."
) % {
'user': profile.user.username
}
self.stdout.write(f"Sending expiry notice to {profile.user.username} ({full_phone})")
send_whatsapp_message(full_phone, message)
self.stdout.write(self.style.SUCCESS('Successfully checked subscriptions'))

View File

@ -1,4 +1,8 @@
from django.utils.translation import get_language
from django.shortcuts import redirect
from django.urls import reverse
from django.contrib import messages
from django.utils.translation import gettext as _
import logging
logger = logging.getLogger(__name__)
@ -10,5 +14,43 @@ class LanguageDebugMiddleware:
def __call__(self, request):
response = self.get_response(request)
# Log the current language for debugging
print(f"DEBUG: Path: {request.path}, Lang: {get_language()}, Cookie: {request.COOKIES.get('django_language')}")
# print(f"DEBUG: Path: {request.path}, Lang: {get_language()}, Cookie: {request.COOKIES.get('django_language')}")
return response
class SubscriptionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Whitelisted paths that are always accessible
whitelisted_paths = [
reverse('logout'),
reverse('login'),
reverse('register'),
reverse('subscription_expired'),
reverse('home'),
'/admin/',
'/static/',
'/media/',
'/i18n/',
]
# Check if the current path starts with any whitelisted path
is_whitelisted = any(request.path.startswith(path) for path in whitelisted_paths)
if request.user.is_authenticated and not request.user.is_superuser:
try:
profile = request.user.profile
# If they are an admin role (not superuser but ADMIN role in profile), maybe don't suspend?
# Usually admins are exempted.
if profile.role == 'ADMIN':
return self.get_response(request)
if profile.is_expired() and not is_whitelisted:
# Only redirect if they are not already on a whitelisted page
return redirect('subscription_expired')
except Exception as e:
logger.error(f"SubscriptionMiddleware error: {e}")
response = self.get_response(request)
return response

View File

@ -61,6 +61,18 @@ class Profile(models.Model):
subscription_plan = models.CharField(max_length=20, choices=SUBSCRIPTION_CHOICES, default='NONE')
subscription_expiry = models.DateField(null=True, blank=True)
is_subscription_active = models.BooleanField(default=False)
def is_expired(self):
if self.subscription_plan == "NONE":
return True
if not self.subscription_expiry:
return True
return self.subscription_expiry < timezone.now().date()
def days_until_expiry(self):
if not self.subscription_expiry:
return 0
delta = self.subscription_expiry - timezone.now().date()
return delta.days
country_code = models.CharField(max_length=5, blank=True, default="966")
phone_number = models.CharField(max_length=20, unique=True, null=True) # Changed to unique and nullable for migration safety

View File

@ -0,0 +1,53 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<div class="card shadow-lg border-0 rounded-4 p-5">
<div class="mb-4">
<i class="bi bi-exclamination-triangle-fill text-warning" style="font-size: 4rem;"></i>
</div>
<h2 class="fw-bold mb-3">{% trans "Subscription Expired" %}</h2>
<p class="lead text-muted mb-4">
{% blocktrans with name=request.user.username %}
Hello {{ name }}, your subscription to MASAR CARGO has expired.
{% endblocktrans %}
</p>
<div class="alert alert-warning border-0 rounded-3 mb-4 text-start">
<ul class="mb-0">
<li><strong>{% trans "Expired on:" %}</strong> {{ profile.subscription_expiry|date:"d/m/Y" }}</li>
<li><strong>{% trans "Current Plan:" %}</strong> {{ profile.get_subscription_plan_display }}</li>
</ul>
</div>
<p class="mb-4">
{% trans "To continue using our services, please renew your subscription. You can contact our support team for renewal details." %}
</p>
{% if app_settings.contact_phone or app_settings.contact_email %}
<div class="mb-5">
<h5 class="fw-bold">{% trans "Contact for Renewal" %}</h5>
{% if app_settings.contact_phone %}
<p class="mb-1"><i class="bi bi-whatsapp text-success me-2"></i> {{ app_settings.contact_phone }}</p>
{% endif %}
{% if app_settings.contact_email %}
<p class="mb-0"><i class="bi bi-envelope me-2"></i> {{ app_settings.contact_email }}</p>
{% endif %}
</div>
{% endif %}
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-lg px-4 rounded-pill">
{% trans "Back to Home" %}
</a>
<a href="{% url 'logout' %}" class="btn btn-danger btn-lg px-4 rounded-pill">
{% trans "Logout" %}
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -104,44 +104,70 @@
{% if subscription_enabled %}
<script>
document.addEventListener('DOMContentLoaded', function() {
(function() {
const fees = {{ fees_json|safe }};
const monthlyLabel = "{% trans 'Monthly Plan' %}";
const annualLabel = "{% trans 'Annual Plan' %}";
function updateFees() {
// Try multiple ways to find the elements to be extremely robust
const roleSelect = document.getElementById('id_role') || document.querySelector('[name="role"]');
const planSelect = document.getElementById('id_subscription_plan') || document.querySelector('[name="subscription_plan"]');
// Robustly find role and plan fields
const roleField = document.querySelector('select[name="role"]') || document.getElementById('id_role');
const planField = document.querySelector('select[name="subscription_plan"]') || document.getElementById('id_subscription_plan');
if (!roleSelect || !planSelect) return;
if (!roleField || !planField) return;
const role = roleSelect.value;
// Use case-insensitive match just in case
const roleKey = Object.keys(fees).find(k => k.toUpperCase() === role.toUpperCase());
const currentRole = roleField.value;
if (!currentRole) return;
// Match role key (SHIPPER or TRUCK_OWNER)
const roleKey = Object.keys(fees).find(k => k.toUpperCase() === currentRole.toUpperCase());
const roleFees = fees[roleKey];
if (roleFees) {
for (let i = 0; i < planSelect.options.length; i++) {
const option = planSelect.options[i];
// Update each option in the plan select
Array.from(planField.options).forEach(option => {
if (option.value === 'MONTHLY') {
option.text = monthlyLabel + " (" + roleFees.MONTHLY + ")";
option.textContent = monthlyLabel + " (" + roleFees.MONTHLY + ")";
} else if (option.value === 'ANNUAL') {
option.text = annualLabel + " (" + roleFees.ANNUAL + ")";
option.textContent = annualLabel + " (" + roleFees.ANNUAL + ")";
}
}
});
}
}
const roleField = document.getElementById('id_role') || document.querySelector('[name="role"]');
if (roleField) {
roleField.addEventListener('change', updateFees);
updateFees(); // Run once on load
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
const roleField = document.querySelector('select[name="role"]') || document.getElementById('id_role');
if (roleField) {
roleField.addEventListener('change', updateFees);
}
// Run immediately
updateFees();
// Run after a short delay to catch any late rendering
setTimeout(updateFees, 200);
setTimeout(updateFees, 1000);
// Pulse check: run every 2 seconds for the first 10 seconds
// to ensure it stays correct if any other scripts modify the DOM
let count = 0;
const interval = setInterval(() => {
updateFees();
if (++count > 5) clearInterval(interval);
}, 2000);
});
// Fallback: listen for any change event on the form
const form = document.getElementById('registrationForm');
if (form) {
form.addEventListener('change', function(e) {
if (e.target && e.target.name === 'role') {
updateFees();
}
});
}
// As a final fallback, try to run after a small delay in case of dynamic rendering issues
setTimeout(updateFees, 500);
});
})();
</script>
{% endif %}

View File

@ -22,4 +22,5 @@ urlpatterns = [
path("bid/<int:bid_id>/reject/", views.reject_bid, name="reject_bid"),
path("privacy-policy/", views.privacy_policy, name="privacy_policy"),
path("terms-of-service/", views.terms_of_service, name="terms_of_service"),
path("subscription-expired/", views.subscription_expired, name="subscription_expired"),
]

View File

@ -1,3 +1,4 @@
from datetime import timedelta
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate, logout
@ -27,15 +28,16 @@ def register(request):
app_settings = AppSetting.objects.first()
subscription_enabled = app_settings.subscription_enabled if app_settings else False
# Fees for JS - using float for cleaner JSON and explicit formatting
# Simplified fees dictionary for JS
# Ensuring keys are exactly as they appear in Profile.ROLE_CHOICES
fees = {
'SHIPPER': {
'MONTHLY': "{:.2f}".format(app_settings.shipper_monthly_fee) if app_settings else "0.00",
'ANNUAL': "{:.2f}".format(app_settings.shipper_annual_fee) if app_settings else "0.00",
'MONTHLY': str(app_settings.shipper_monthly_fee) if app_settings else "0.00",
'ANNUAL': str(app_settings.shipper_annual_fee) if app_settings else "0.00",
},
'TRUCK_OWNER': {
'MONTHLY': "{:.2f}".format(app_settings.truck_owner_monthly_fee) if app_settings else "0.00",
'ANNUAL': "{:.2f}".format(app_settings.truck_owner_annual_fee) if app_settings else "0.00",
'MONTHLY': str(app_settings.truck_owner_monthly_fee) if app_settings else "0.00",
'ANNUAL': str(app_settings.truck_owner_annual_fee) if app_settings else "0.00",
}
}
@ -103,6 +105,10 @@ def verify_otp_registration(request):
profile.subscription_plan = registration_data.get('subscription_plan', 'NONE')
if profile.subscription_plan != 'NONE':
profile.is_subscription_active = True
if profile.subscription_plan == 'MONTHLY':
profile.subscription_expiry = timezone.now().date() + timedelta(days=30)
elif profile.subscription_plan == 'ANNUAL':
profile.subscription_expiry = timezone.now().date() + timedelta(days=365)
profile.save()
login(request, user)
@ -427,3 +433,15 @@ def terms_of_service(request):
}
}
return render(request, 'core/article_detail.html', context)
@login_required
def subscription_expired(request):
profile = request.user.profile
if not profile.is_expired():
return redirect('dashboard')
app_settings = AppSetting.objects.first()
return render(request, 'core/subscription_expired.html', {
'profile': profile,
'app_settings': app_settings
})

Binary file not shown.

View File

@ -1420,4 +1420,38 @@ msgstr "سياسة الخصوصية ستتوفر قريباً."
#: core/views.py:426
msgid "Terms of service are coming soon."
msgstr "شروط الخدمة ستتوفر قريباً."
msgstr "شروط الخدمة ستتوفر قريباً."
msgid "Subscription Expired"
msgstr "انتهى الاشتراك"
msgid "No Plan"
msgstr "بدون خطة"
msgid "Expired"
msgstr "منتهي"
msgid "Expiring soon"
msgstr "تنتهي قريبا"
msgid "days"
msgstr "أيام"
msgid "Active"
msgstr "نشط"
msgid "Hello %(name)s, your subscription to MASAR CARGO has expired."
msgstr "مرحباً %(name)s، لقد انتهى اشتراكك في مسار كارغو."
msgid "Expired on:"
msgstr "انتهى في:"
msgid "Current Plan:"
msgstr "الخطة الحالية:"
msgid "To continue using our services, please renew your subscription. You can contact our support team for renewal details."
msgstr "لمواصلة استخدام خدماتنا، يرجى تجديد اشتراكك. يمكنك الاتصال بفريق الدعم لدينا للحصول على تفاصيل التجديد."
msgid "Contact for Renewal"
msgstr "الاتصال للتجديد"