From d8387d341e748a9a44405d9d00446cae6143e02f Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 25 Jan 2026 16:35:29 +0000 Subject: [PATCH] editing email --- config/__pycache__/settings.cpython-311.pyc | Bin 6800 -> 7040 bytes config/settings.py | 12 ++- core/__pycache__/admin.cpython-311.pyc | Bin 8317 -> 8372 bytes core/__pycache__/mail.cpython-311.pyc | Bin 1603 -> 3960 bytes core/__pycache__/urls.cpython-311.pyc | Bin 2592 -> 2568 bytes core/__pycache__/views.cpython-311.pyc | Bin 22573 -> 22715 bytes .../whatsapp_utils.cpython-311.pyc | Bin 11229 -> 11178 bytes core/admin.py | 11 ++- core/mail.py | 59 ++++++++++-- core/templates/emails/base_email.html | 86 ++++++++++++++++++ core/urls.py | 5 +- core/views.py | 49 +++++----- core/whatsapp_utils.py | 21 ++--- 13 files changed, 193 insertions(+), 50 deletions(-) create mode 100644 core/templates/emails/base_email.html diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index ccdc8c4d0a111135c49875ea2313dcba4531670f..00e611d7796a265d9728573bf5f1b35a6d5703e4 100644 GIT binary patch delta 447 zcmbPW+F;JNoR^o20SG)?%Q7!ZPUMqdoU>8gkZp1TqXJiyU8-G?LaN;Y`^kcwB1S3E z%NQ9LRs%5v6tkzqq}t79n9CI9kis0ykZK2^Q(_^sX58jDws%b8E19a;GfGMdtn~GN zaex@bKvt3H=48%kENt>Xvx>Af|L48KqFf{b;)nu?Dp9BgLnCuN10XcCvM?|(&~)5< zSLiciy%k8EHITT)oS9crWCP;bfe3pL;Q%5WL4-4qxW!srkds+b1QFgK!WSgP zAL8K{?&#+k?-<}29~$IS6abP61Q9_XA{eAd$i>yoG1Mm{-Z3=9Ki zn0Y=huraFN;1;>gEq{qy{(^$}MQ)2L+!h}gm|5j+h-l5Iyda_lL>&?!_&`J!%HUUP L@BqOgL7*i7o#Jyc delta 216 zcmZoLpJ2+joR^o20SJUF$}-PLOyrYbG}@?c$i|k!9L%5@z4-*&J0@{WrYiP~l9B=| zef?h?AVx8eRV1?cEax;9HaVc?BF)X2e0NxwH0?HL2!CdDu>^@(0f}48nRz8e)*!Ae zh_C|@_8`ImL^uJ7Tdc(eIhiFzZXf{<5a9(Pd?wEq6&3IYF#|wEAczQ>d_>e|vWl1_ ccN37y2*kw?ST=iz-C^Orz#t4nMS?&v0MX1dEdT%j 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 3c03fa2b607964de08f138352aab9567f6385508..3418d8bde9f6dea3e36282559f27eafabcb540f5 100644 GIT binary patch delta 1688 zcmah}ZA@EL7`~^U>-TL-p)HgiXbTh=n}V9np`(JVn`0n22X1Lf@1b0F+s<=uK?nw2 z7LA&jEU$k|RQQGQBbtbb#>5{i`on3W{=@szL`_Ug^p|nb#B*+K_?Xd?d!KXPujjn? zocFwco&0df`=-a^6xjGe5`75Qvo|Hcc!^_SOax_Qlry4w2OV`$3QCjx`|nFFLqW*DMe> z={UHCVa#@kfM5X+ks1cChU#U8TVA&e4F{K#OvZsezdKsk6G!02x+O>xl zF4+1a2dKJu0EaNF+d67gU0Yw;woPaNt~)+LF&J@9*^FgR!Md{-^}$!p3~Jx&OwYw>DhHjUQVcnYM9WpFq7FdJbCr>7c-}(j z7@YP=4Bz(+qVw>J?`|{(Sd55S7DrEWIL2WbvSO^3$vDd-3eSlFbOK%#e?aZ9=8vFT z@QJ4ty$;`b?mMhF1kKK({uz=*vaXtnNV2V>ckP>2R!Hov4J5$*-Uws^t$uf1CkRP_ z9(mdf>)wdjsrliRK-S~fwJtc|=RhnH+SRNI3$D7f;Wi4n+v$hlUGIp;pIvdtAt=}o?xX?tl)fE!ezNY@LYwK@|q@>i@jLZ@KS|dR@-hn zr--WI5nfYit8Q1TvkRo4>Gl#))qIiYUP=n`6`7Q^oFc25Zq;N>A;u=Iv`khD1?G1g z8zeB;a0cb!g@&VRUi!F^9ES@W8aV_wOf%3OBTp5`6-};`=?s_6sz&J(+7$7 zLUW;)6y-NTD@P9}<^5>2f! z?fo}}J2`e8jLNV7BP)H-BfadX?a`@zes$AmEbXIvt|QYKq*Uc!r#SGNLdQ8wz{}Bd zQG*gO`DAU&`niciZ0q;vCFC+%0+-X>kZXAj#o^DE>knFZc^GG?+gVW+Sq4rjJkC0#yMgkUyf{0X!9YR9l-Eo|xDH2bbGxwhR zd3WC2`#w(nGwix1iVlIVpQB64cmB(+5c%|S*Uf++y9%+nm`NBHWcTZW?4cyz3TH{M zM>4oZ&UvYM!z>8b`BPnUal_{0Hb3`oLw|R>!38+iw96&9)KQCC?}Sa058fe7&ONRR zxpskCXne!O_ux-;5#gTLnqNI_|9?L?Y`Sj`c;P>|tM_xw0&Sp)gL}xXr-Wn>?wWps z-_3tIg1knC;D%r79=65}6+X9wVZd_e(J-nJgl5CQLANQzf>NX^laMk3+E7Z=zz^?Q zTC157JkyH!09txDB+aZFMy*NbzPzw(?F&ojv6rd~RfHhiwsuxibyfbbZkosd+^~N} zT0nMWtj4zEaNF@9>4j~_2#HtvoRfq+0xvbRk~F;6aFQH{yA8qclRTGN$3c>$SD6tv z$+gueyyXelb5zTgSwS6uBjRg3$rs{Bgbae>mN|aoK1ybx!P8Hs;0aIIbDYPq34}p} z)9}0}TCHS;R}z5_JYMoR-17WN;_!ZBnEV7kh%s^v{t^$5F4+am#-nzYTqj$!`kE#P zc}q=RGv$bEymZ}>iEnf`owVpSwR|B@ z=QZ87NLBTGj_QsDR$9!`#q&x*cQHDzTvTXL%N7(>)5QhOW>uxYnZgR>{Ab8H_}c%_ zD<1Z=ksaX_f*&EkLH8aTOQ&WhGuiaXnW^m9RBB?9dC__rt@g1O=jlaFDHYi>sGC)d zu{kuKL6|~#76DH%X<>Um^c*L2LfKjVZc_2sdAmqbwO>wfbv42JCE_%`B)g>0o=ZJ8&m3m`Vtn3CBp(yxG?=5;A`kvW!G4cbd|q zf7OwpswR!$sqndIMdLkHE;5?q9YmR4Uy}}8Nq^TLSuDxr0v%#+ V@O)T}TCQ}&tfX?ZRV8&;!@tKTSTFzp diff --git a/core/__pycache__/mail.cpython-311.pyc b/core/__pycache__/mail.cpython-311.pyc index cd23f61b2128fda748683c19df82eb847eff7bcd..a080a1bad601425b60113859c7f12cadcde37578 100644 GIT binary patch literal 3960 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 delta 148 zcmeAWSs=o@oR^o20SH)Z%QB~I=8zG=UL_ixVd=V870IV{-+EE+gL+QS}v>7g#JVvRGVUv6#GxQ;ElL g2j2x2_lqp diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 3d1e86183a514e7571af7b6fad57e1388d9e024b..e80985306caa48f94115db82e1e940148b5b000e 100644 GIT binary patch delta 4641 zcmb7HYiu0V72dJe>*uc5kM(1{em~+}C+in>Y{zj7egs!ZNC-*DB(SV^#`ZchyPG?+ z2}#qMpg^S(5Yhcns){O7A(0E^wY25Yv_-TOwE`R|w9%-b(jWa%6$usf4^pL`@6I|l zHni$!{rS#0=iYl>_uMl-c}aTnWyyWj?JjcA?a;5H*eL|){tJ16GYQ09T(QD;e-7ouz?obM~ zIz1rQi*b=wcj=wRC5PhHTJ<)$O-%G??RtmYAp}abEqbTiDaKwcsE6c`7?){Xau+RD zrfrqCl0xO$Ha#qd^=`Ra?~!}-Ub$EAll#Pa6-cc!$zVIH9xDS2 z%H2J6kCi*Gn#|2V&I=cV%tC-JUhSI5>!)EIkGS6B31?wLD%auop5vTjk*M~gNGEbI zdQxKF<>zVlcU}ynp(5QT&!IkD;1+OGza*ly#XF6YAoBn%5ji}ichKH*C%~a zHGj6~S5s%pj;LBJsjE$9vT1MBh_dG8JS*Qw&n~Jl)5>436fO^OuVX_0@90c4APysL&F^$E@RjeB%;io*?PteZVnxCcv6NxKAF)KG|ri?sdtoexT z!$iQo%x4uH&N5g)%Pz!IDV33VC;66|2cTr&~!Ko(kBZaPDeB#YRww^w(vBqfg< zGK<_@+c1heV#gq!Z_6o+fpi9NmSAJR*gQCZbY{M`K$2!NDgUJ$R|@2lJik6vA|&Ac zX*S4r*6&|JJ_IY+E!EgJAuQr>4x_Jiz7JdxNa0=K*pr|&0W?4mu->UKhAja^8dBv5 zJ2p7f^1^pI&JM#z-t9ENsCmj0@U%Vil&MET(G6ku~d35;xf8v)oZi zwUimLCoov_G(X((e%6+;*0YjV90swa@NAmD+t$(y(}JaJO(zkZbtixqxBF+;bZjMg zJ8mKhJ$S{T2|Df6egp)lXcZ;Y(-FgrnrXv;1oGNG`tEd!>X4dcvd4(|BL7bNRZ6Mu zjt6q9Z@GKb(<1bz_?2K>8sgs2jH?2>gn1-X*Y;I8w85Hs{=-m>D}Xuo^IwE|UBcT9 z{PWOoy*LL@+VZC8KvTujBiYKxLVTucU!YGgN#H4e zZN}Wzk`wp=Ho7+E8S;S{k;_wQC&9ax~IqQKLxhXvKb>`qwR10P*0^# zD1B=y`;vT$FZa}!LRt3sD*TCm*z>uq9{+3ah3z8PPJ6lgAapBy%aRD6AraM%5_g)>f|H+T_2PmEo_8*X>W!^n> zU&XSN^@-(7M0ItU9K=TkLQ)@B2l}Ni`1c0}$7gBk^fL7lMs!XM7O*up;j(33Kp22) zpM48ZN3eSTEc4RAKm|7OHB@LwaK+naKZbbSMque7QNR%NB$BB_7VpYgDRKR*0OHeiQG{AJH#{`#o# z^COj|Zxc0-;?1stjqpacZjvOH<)zzBH_*%}q)#$ou-_87fOqep2#eHLcf2Dti=y%( z)j-@R&WoD35=um8z^Wi8rX9ej9UueN7iUF$b{GXj!gYklE)OLt z<@T+@|7YU2P^^s;IT_#X_oS^~-;(xR9up1e@OA&l4gUx=q`ceH%ZFzssjz%|CO>aN zB)CxLkF@MGQwZ4{;6uRw2rxu$@OSsMNyqr7`?i!Y=*AWRaUQ%Yu(TgTv4?QpnnOj2 z6u0>ah=>4*U~Je?#7i6n;rMmQg=secDqzE*6k+Ic>?KY&*fqU;FW&uF%&#m)6Z6S1J|bCsHXKcx3t>H}sG1>K za{BVIU)Ni}Ni$L>fQjyGXAnu7aiZ0OwmOe&m}68&4puESV*5h?oC3>pdLe2W(Nros ztqGiFKntK1fU{sZ@xg4@NO25MSH!22{gp6}*Yki!0qChji()qePvG?m;LbOJZvoy0 zpp=VhDN3E4T?oZ8T?t_I5qwcFO0G_8>KOZsUg=;sj0^nrdltQ2D~>PyGR=pkrK+J7 Q$M(&?%<03gNKP#JA5tVF7XSbN delta 4493 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 delta 1919 zcmb`Ie`s4(6vy9f`r7s7N0Tf`Uz(py(q`#?w)-iT)iz7r*w&VHbHCiOtnW2TOwxE? zx;f`I!R-%4(Rx4!f-rD%n~FjNHy!A@O;A)eELehr{UQ6qKcs^Rq6nVz(v>kQD0tv~ z-#zEvckg|lbKZRyw%^)re%EZ41T;_JVCLmq%0mTs#LKWT)I=96kZmC02sTpi97Gisrf z%w@Dv#XN%1q?XBHBzG9{@(`O3SLAK|W?Y}sKB_NDh&b`!O(+#npN-d2z(4`}xY#!7I~ zi|ie^kr~Sxb@pYiEvmD>A7?_LCoGgG5i2AX6-=JV|xygAW2;=7+BXK4}Dd z&)}uK33mf8ZIr~F$iAUrXO;cZkJ=_X*WZ-aPs!nb%U(~wZ-?KRU62dGzz&2}!!H@&IVVm>eELbrMUf2=! zK{9NCPkj;`3PX!d*}jfv4yruZqiLCRK~)g(Rz8R?*B`UOqY?j3oudx;p(_7Jn8WyV z%)kj`#i;1hCkv^(%KL~JeFoK%J{R#yS@f<`xDCFGoZTRaHX>=$i+ItwgOf`WH|4~X z?3n{(Xs#lj1^G#9Ph&^iBwRH~akt^B%;IiB$licJ+fPWZHX2%Avg67#mer44a-I{i zv`5{t_4iw8riky?ot=xU3&}2ICO8p&^>~%(5EhPJy4#9Ms-!-^)Lbpj-8gNvLjOD2 z8l7wvW)03m#{=Ffj>2dOPA#xYIrM(SOZyW(UU2k(W!fFN(%I1)5hfy1Z--%G8AFY^ zco(f-pBqi5Gleu?3qQm*Sz0MpmmgKsoW|>+tKGdp|H||~gnmrw=leb!#_5CNR0^E- zeqP{FYSUZ&KCcU>7|W?^`53zN&(rP+@OFC^niuwM3^41oFe|;x&$_3DS$T{(*{W$_ NR+c!({k!PP{ROsy!?XYZ 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 %} + + {% 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