adding track reminder
This commit is contained in:
parent
b70c6b10e0
commit
56126df7d4
Binary file not shown.
@ -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',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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')
|
||||
|
||||
57
core/management/commands/check_subscriptions.py
Normal file
57
core/management/commands/check_subscriptions.py
Normal 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'))
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
53
core/templates/core/subscription_expired.html
Normal file
53
core/templates/core/subscription_expired.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
|
||||
|
||||
@ -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"),
|
||||
]
|
||||
|
||||
@ -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.
@ -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 "الاتصال للتجديد"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user