diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index ccdc8c4..00e611d 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index d104fa0..1db02a9 100644 --- a/config/settings.py +++ b/config/settings.py @@ -213,4 +213,14 @@ WHATSAPP_ENABLED = os.getenv("WHATSAPP_ENABLED", "true").lower() == "true" DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' LOGIN_URL = 'login' LOGIN_REDIRECT_URL = 'dashboard' -LOGOUT_REDIRECT_URL = 'index' \ No newline at end of file +LOGOUT_REDIRECT_URL = 'index' + +# Site URL for Emails +HOST_FQDN = os.getenv("HOST_FQDN", "") +if HOST_FQDN: + if not HOST_FQDN.startswith(("http://", "https://")): + SITE_URL = f"https://{HOST_FQDN}" + else: + SITE_URL = HOST_FQDN +else: + SITE_URL = "http://127.0.0.1:8000" diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 3c03fa2..3418d8b 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/mail.cpython-311.pyc b/core/__pycache__/mail.cpython-311.pyc index cd23f61..a080a1b 100644 Binary files a/core/__pycache__/mail.cpython-311.pyc and b/core/__pycache__/mail.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 92cbcc8..78274b5 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 3d1e861..e809853 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/__pycache__/whatsapp_utils.cpython-311.pyc b/core/__pycache__/whatsapp_utils.cpython-311.pyc index 44c265e..327d6f6 100644 Binary files a/core/__pycache__/whatsapp_utils.cpython-311.pyc and b/core/__pycache__/whatsapp_utils.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 45b972e..29a95fc 100644 --- a/core/admin.py +++ b/core/admin.py @@ -10,6 +10,7 @@ from django.contrib import messages from .whatsapp_utils import send_whatsapp_message_detailed from django.core.mail import send_mail from django.conf import settings +from .mail import send_html_email import logging class ProfileInline(admin.StackedInline): @@ -81,12 +82,12 @@ class PlatformProfileAdmin(admin.ModelAdmin): email = request.POST.get('email') if email: try: - send_mail( + send_html_email( subject="Test Email from Platform", - message="This is a test email to verify your platform's email configuration.", - from_email=settings.DEFAULT_FROM_EMAIL, + message="This is a test email to verify your platform's email configuration. If you see the logo and nice formatting, it works!", recipient_list=[email], - fail_silently=False, + title="Test Email", + request=request ) messages.success(request, f"Success: Test email sent to {email}.") except Exception as e: @@ -129,4 +130,4 @@ admin.site.register(Parcel, ParcelAdmin) admin.site.register(Country) admin.site.register(Governate) admin.site.register(City) -admin.site.register(PlatformProfile, PlatformProfileAdmin) \ No newline at end of file +admin.site.register(PlatformProfile, PlatformProfileAdmin) diff --git a/core/mail.py b/core/mail.py index b855004..a66e6a5 100644 --- a/core/mail.py +++ b/core/mail.py @@ -1,9 +1,57 @@ -from django.core.mail import send_mail +from django.core.mail import send_mail, EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils.html import strip_tags from django.conf import settings import logging logger = logging.getLogger(__name__) +def send_html_email(subject, message, recipient_list, title=None, action_url=None, action_text=None, request=None): + """ + Sends a styled HTML email using the platform template. + """ + try: + from .models import PlatformProfile + platform = PlatformProfile.objects.first() + if not platform: + # Create a dummy platform object if none exists, to avoid errors + class DummyPlatform: + name = "Platform" + logo = None + address = "" + platform = DummyPlatform() + + # Determine site URL + site_url = settings.SITE_URL if hasattr(settings, 'SITE_URL') else 'http://127.0.0.1:8000' + if request: + site_url = f"{request.scheme}://{request.get_host()}" + + context = { + 'platform': platform, + 'title': title or subject, + 'message': message, + 'action_url': action_url, + 'action_text': action_text, + 'site_url': site_url, + } + + html_content = render_to_string('emails/base_email.html', context) + text_content = strip_tags(html_content) + + msg = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, recipient_list) + msg.attach_alternative(html_content, "text/html") + msg.send() + return True + except Exception as e: + logger.error(f"Failed to send HTML email: {e}") + # Fallback to plain text + try: + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list) + return True + except Exception as e2: + logger.error(f"Failed to send fallback email: {e2}") + return False + def send_contact_message(name, email, message): """ Sends a contact form message to the platform admins. @@ -25,14 +73,13 @@ def send_contact_message(name, email, message): recipient_list = settings.CONTACT_EMAIL_TO or [settings.DEFAULT_FROM_EMAIL] - send_mail( + # Use HTML email for contact form too, for consistency + return send_html_email( subject=subject, message=full_message, - from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=recipient_list, - fail_silently=False, + title="New Contact Message" ) - return True except Exception as e: logger.error(f"Failed to send contact message: {e}") - return False + return False \ No newline at end of file diff --git a/core/templates/emails/base_email.html b/core/templates/emails/base_email.html new file mode 100644 index 0000000..5b02a65 --- /dev/null +++ b/core/templates/emails/base_email.html @@ -0,0 +1,86 @@ + + + + + + + +
+
+ {% if platform.logo %} + {{ platform.name }} + {% else %} +

{{ platform.name }}

+ {% endif %} +
+
+ {% if title %} +

{{ title }}

+ {% endif %} + + {{ message|safe|linebreaks }} + + {% if action_url %} +
+ {{ action_text|default:"Click here" }} +
+ {% endif %} +
+ +
+ + \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index dc3c824..f5f57ce 100644 --- a/core/urls.py +++ b/core/urls.py @@ -8,6 +8,7 @@ urlpatterns = [ path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'), path('register/', views.register, name='register'), path('register/verify/', views.verify_registration, name='verify_registration'), + path('dashboard/', views.dashboard, name='dashboard'), path('shipment-request/', views.shipment_request, name='shipment_request'), path('accept-parcel//', views.accept_parcel, name='accept_parcel'), @@ -21,9 +22,9 @@ urlpatterns = [ path('ajax/get-cities/', views.get_cities, name='get_cities'), path('privacy-policy/', views.privacy_policy, name='privacy_policy'), path('terms-conditions/', views.terms_conditions, name='terms_conditions'), - path('contact/', views.contact_view, name='contact'), + path('contact/', views.contact, name='contact'), path('profile/', views.profile_view, name='profile'), - path('profile/edit/', views.edit_profile_view, name='edit_profile'), + path('profile/edit/', views.edit_profile, name='edit_profile'), path('profile/verify-otp/', views.verify_otp_view, name='verify_otp'), ] diff --git a/core/views.py b/core/views.py index 3ac78ad..cdb8c84 100644 --- a/core/views.py +++ b/core/views.py @@ -2,6 +2,7 @@ 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 from .forms import UserRegistrationForm, ParcelForm, ContactForm, UserProfileForm from django.utils.translation import gettext_lazy as _ @@ -21,7 +22,7 @@ from .whatsapp_utils import ( notify_status_change, send_whatsapp_message ) -from .mail import send_contact_message +from .mail import send_contact_message, send_html_email def index(request): tracking_id = request.GET.get('tracking_id') @@ -59,12 +60,12 @@ def register(request): send_whatsapp_message(phone, f"Your verification code is: {code}") messages.info(request, _("Verification code sent to WhatsApp.")) else: - send_mail( - _('Verification Code'), - f'Your verification code is: {code}', - settings.DEFAULT_FROM_EMAIL, - [user.email], - fail_silently=False, + send_html_email( + subject=_('Verification Code'), + message=f'Your verification code is: {code}', + recipient_list=[user.email], + title=_('Welcome to Masar!'), + request=request ) messages.info(request, _("Verification code sent to email.")) @@ -117,7 +118,7 @@ def verify_registration(request): def dashboard(request): # Ensure profile exists profile, created = Profile.objects.get_or_create(user=request.user) - + if profile.role == 'shipper': parcels = Parcel.objects.filter(shipper=request.user).order_by('-created_at') return render(request, 'core/shipper_dashboard.html', {'parcels': parcels}) @@ -136,7 +137,7 @@ def shipment_request(request): 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(): @@ -159,7 +160,7 @@ def accept_parcel(request, parcel_id): if profile.role != 'car_owner': messages.error(request, _("Only car owners can accept shipments.")) return redirect('dashboard') - + parcel = get_object_or_404(Parcel, id=parcel_id, status='pending', payment_status='paid') parcel.carrier = request.user parcel.status = 'picked_up' @@ -230,7 +231,7 @@ def payment_success(request): 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 @@ -263,7 +264,7 @@ def privacy_policy(request): def terms_conditions(request): return render(request, 'core/terms_conditions.html') -def contact_view(request): +def contact(request): if request.method == 'POST': form = ContactForm(request.POST) if form.is_valid(): @@ -287,7 +288,7 @@ def profile_view(request): return render(request, 'core/profile.html', {'profile': request.user.profile}) @login_required -def edit_profile_view(request): +def edit_profile(request): if request.method == 'POST': form = UserProfileForm(request.POST, request.FILES, instance=request.user.profile) if form.is_valid(): @@ -310,11 +311,11 @@ def edit_profile_view(request): 'city_id': data['city'].id if data['city'] else None, } 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') if method == 'whatsapp': @@ -326,19 +327,19 @@ def edit_profile_view(request): # Default to email # Send to the NEW email address (from the form), not the old one target_email = data['email'] - send_mail( - _('Verification Code'), - f'Your verification code is: {code}', - settings.DEFAULT_FROM_EMAIL, - [target_email], - fail_silently=False, + send_html_email( + subject=_('Verification Code'), + message=f'Your verification code is: {code}', + recipient_list=[target_email], + title=_('Profile Update Verification'), + 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 @@ -390,4 +391,4 @@ def verify_otp_view(request): except OTPVerification.DoesNotExist: messages.error(request, _("Invalid code.")) - return render(request, 'core/verify_otp.html') \ No newline at end of file + return render(request, 'core/verify_otp.html') diff --git a/core/whatsapp_utils.py b/core/whatsapp_utils.py index 2b4de8f..0c14d41 100644 --- a/core/whatsapp_utils.py +++ b/core/whatsapp_utils.py @@ -5,6 +5,7 @@ from django.conf import settings from django.core.mail import send_mail from django.utils.translation import gettext_lazy as _ from .models import PlatformProfile +from .mail import send_html_email logger = logging.getLogger(__name__) @@ -149,12 +150,11 @@ Please proceed to payment to make it visible to drivers.""" # Email if parcel.shipper.email: try: - send_mail( + send_html_email( subject='Shipment Request Received - ' + parcel.tracking_number, message=message, - from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[parcel.shipper.email], - fail_silently=False + title='Shipment Request Received' ) logger.info(f"Shipment created email sent to {parcel.shipper.email}") except Exception as e: @@ -176,12 +176,11 @@ Your shipment is now visible to available drivers.""" # Email Shipper if parcel.shipper.email: try: - send_mail( + send_html_email( subject='Payment Successful - ' + parcel.tracking_number, message=shipper_msg, - from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[parcel.shipper.email], - fail_silently=False + title='Payment Successful' ) except Exception as e: logger.error(f"Failed to send payment email to {parcel.shipper.email}: {e}") @@ -205,12 +204,11 @@ Status: {parcel.get_status_display()}""" if parcel.shipper.email: try: - send_mail( + send_html_email( subject='Driver Assigned - ' + parcel.tracking_number, message=msg, - from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[parcel.shipper.email], - fail_silently=True + title='Driver Assigned' ) except Exception: pass @@ -227,12 +225,11 @@ New Status: {parcel.get_status_display()}""" if parcel.shipper.email: try: - send_mail( + send_html_email( subject='Shipment Update - ' + parcel.tracking_number, message=msg, - from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[parcel.shipper.email], - fail_silently=True + title='Shipment Update' ) except Exception: pass