From 91858c53d5ad0c3db698978ae4678d0c8d842c13 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Feb 2026 06:00:35 +0000 Subject: [PATCH] Autosave: 20260204-060035 --- config/__pycache__/settings.cpython-311.pyc | Bin 7489 -> 7779 bytes config/settings.py | 13 +- configuration/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 166 bytes .../__pycache__/admin.cpython-311.pyc | Bin 0 -> 4371 bytes .../__pycache__/apps.cpython-311.pyc | Bin 0 -> 584 bytes .../__pycache__/models.cpython-311.pyc | Bin 0 -> 1787 bytes configuration/admin.py | 71 ++++ configuration/apps.py | 6 + configuration/migrations/0001_initial.py | 54 +++ configuration/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 1268 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 177 bytes configuration/models.py | 20 ++ core/__pycache__/admin.cpython-311.pyc | Bin 6699 -> 6755 bytes core/__pycache__/forms.cpython-311.pyc | Bin 4292 -> 6365 bytes core/__pycache__/models.cpython-311.pyc | Bin 8209 -> 13242 bytes core/__pycache__/thawani.cpython-311.pyc | Bin 0 -> 2999 bytes core/__pycache__/urls.cpython-311.pyc | Bin 1081 -> 1860 bytes core/__pycache__/views.cpython-311.pyc | Bin 5039 -> 13208 bytes core/__pycache__/wablas.cpython-311.pyc | Bin 0 -> 1712 bytes core/admin.py | 15 +- core/forms.py | 53 ++- .../create_test_users.cpython-311.pyc | Bin 0 -> 3503 bytes core/management/commands/create_test_users.py | 60 ++++ ...code_student_is_email_verified_and_more.py | 33 ++ core/migrations/0007_wablasconfiguration.py | 26 ++ core/migrations/0008_thawaniconfiguration.py | 26 ++ ...ove_student_is_mobile_verified_and_more.py | 70 ++++ ...dent_moderate_remove_city_name_and_more.py | 57 ++++ ...is_email_verified_and_more.cpython-311.pyc | Bin 0 -> 1340 bytes .../0007_wablasconfiguration.cpython-311.pyc | Bin 0 -> 1400 bytes .../0008_thawaniconfiguration.cpython-311.pyc | Bin 0 -> 1308 bytes ...lter_city_options_and_more.cpython-311.pyc | Bin 0 -> 7519 bytes ...s_mobile_verified_and_more.cpython-311.pyc | Bin 0 -> 3131 bytes ..._remove_city_name_and_more.cpython-311.pyc | Bin 0 -> 2451 bytes core/models.py | 94 +++++- core/templates/base.html | 2 +- core/templates/core/error.html | 11 + core/templates/core/login.html | 51 +++ core/templates/core/profile.html | 6 +- core/templates/core/registration.html | 314 ++++++++++-------- core/templates/core/student_dashboard.html | 182 ++++++++++ core/templates/core/teacher_dashboard.html | 129 +++++++ core/templates/core/verify_otp.html | 52 +++ core/thawani.py | 69 ++++ core/urls.py | 7 + core/views.py | 203 ++++++++++- core/wablas.py | 44 +++ requirements.txt | 1 + 50 files changed, 1482 insertions(+), 187 deletions(-) create mode 100644 configuration/__init__.py create mode 100644 configuration/__pycache__/__init__.cpython-311.pyc create mode 100644 configuration/__pycache__/admin.cpython-311.pyc create mode 100644 configuration/__pycache__/apps.cpython-311.pyc create mode 100644 configuration/__pycache__/models.cpython-311.pyc create mode 100644 configuration/admin.py create mode 100644 configuration/apps.py create mode 100644 configuration/migrations/0001_initial.py create mode 100644 configuration/migrations/__init__.py create mode 100644 configuration/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 configuration/migrations/__pycache__/__init__.cpython-311.pyc create mode 100644 configuration/models.py create mode 100644 core/__pycache__/thawani.cpython-311.pyc create mode 100644 core/__pycache__/wablas.cpython-311.pyc create mode 100644 core/management/commands/__pycache__/create_test_users.cpython-311.pyc create mode 100644 core/management/commands/create_test_users.py create mode 100644 core/migrations/0006_student_email_otp_code_student_is_email_verified_and_more.py create mode 100644 core/migrations/0007_wablasconfiguration.py create mode 100644 core/migrations/0008_thawaniconfiguration.py create mode 100644 core/migrations/0009_platformsettings_remove_student_is_mobile_verified_and_more.py create mode 100644 core/migrations/0010_governorate_remove_student_moderate_remove_city_name_and_more.py create mode 100644 core/migrations/__pycache__/0006_student_email_otp_code_student_is_email_verified_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0007_wablasconfiguration.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0008_thawaniconfiguration.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0009_platformsettings_alter_city_options_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0009_platformsettings_remove_student_is_mobile_verified_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0010_governorate_remove_student_moderate_remove_city_name_and_more.cpython-311.pyc create mode 100644 core/templates/core/error.html create mode 100644 core/templates/core/login.html create mode 100644 core/templates/core/student_dashboard.html create mode 100644 core/templates/core/teacher_dashboard.html create mode 100644 core/templates/core/verify_otp.html create mode 100644 core/thawani.py create mode 100644 core/wablas.py diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 7ccceb4f5ad8a4fb106fac133e993a29736956f3..fa65535a2598f9fbbbb393875709ecbc7b148c97 100644 GIT binary patch delta 1084 zcmZXQ?Q0WB9LHyN-5g0xO*W~ic2latkw6FZ+ne2J zjSo@(g22A9-Oql%zo%v9_0bPw znayZ4g82IM?jhOpBSIg9*jHxqGQ4``VP%Fi(OHaM^bzXJsqJgC48xC!fU3#uDkJbN4NaNdP8AoH#AeQzW5 z9Op+tUO?GeLkn%nIPCuse!i<6Ejo6Im=&sf#IYMBV#=X?{4VHV<6ZP03BU52u?M>q z%Q1=F{$Y(eC1M$Eh1g)MH)i7n99j|8m;IOOdN?c7y~6eR2NO@0s%lAh2TJ--ks3g} zp`uRVYs@K^4ci;iEw9?OUFVo?wu_p4L^Z5eywzOAHE1;w$kWuRtkDbp--&_RvZ&}T zI@HLGTe|B~$0=9S-<>44xT@dOZL(l^9^LdkC zl=O<_6$qbF@DC-Q;-ihX$sTEBOq}-^M_VSgM~@48Lg1vpDS>f;NshMSu35yRQ+|J{ z_vi5R%;~A~g{ArFw%U>NlM_?(ljDm)EWu+lON)hXgZ@ZrF!GpB!u)mrYFtlU#qo$L zF|>{Q-K@9qzW0q(3!!N8ez?`s-$ZI_ce;rZYzM(fX=_KUo@0uTEP@oZeuQ-eCWe$a z&%^_xA*J_`#?pb=kCa3+`4c-3xC~Nab&X{MljEuSl_!N}a)ccWBEw?lFq1}zuWF;1 z_|sZuA;?fHiYJ(h1G~=c-bcgiR>$7sHda?ztz#!7G3OAQl!D?j+-lAA@y^4}O2q$D z+Xt=eu~ufRl|9T-JG(;+3CwjmEZ))A)J9pk<82A`53!vny`LkIYQ=lDw1KTuADfed QKcKH{qFCplD!=D{0Z80SBLDyZ delta 821 zcmZXQO-vI(6o6-T*@l*%bhpJ;=o&;1B}k11{6UO}_y_&~ML|%Y^mVsIpt##Aa1iyT zrzU%l1BV_YTs3?0=*`5$?8z7orU_@w7!JmpZ>ACt&g_2szW3hFWM;SbLr;9Wp&>wU ze7f=?=N%rS^jqhG^8PDn#8$IA&VahIhcosVuJ-( zgeAzsGCTqcY$!l+vlcrZ!xQ8R41$bP!@0WY+oF@FKu(hOH8CW>~(S__Eq zs+BJ;Tcf;IpUzvE#8@Vk&SJT0$>Q96CS9I(y-_^tAh?sY&WS z$9L>S?8v#Lf_0wj1&%(BiyZwN1Be~PUS7x*tZUAXNYv?xb_8Bymf~xFcJt9$s`@pF zk$pUCWCz^J!H?2(B z5n*wn1WPg2E+m1a(sX&o3wN{QBGbj!>|)(Q`YBPu6*W@Uy}n_mM~_p(N$c?uQM9la zDh)833SB{M-sobhReg#YENN`Lssj>V_awV4iRMAn*oILyJiU_*Nrv-HKRm@$f1?i% zju@%78Q9k1#m+Lb>RE{#>SXmqZ$pG4U)82cthEwpVMDT5mi(hfg4M^;@Mix2_PgxG diff --git a/config/settings.py b/config/settings.py index 04a4eca..7c015e4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -58,6 +58,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'core', + 'configuration', ] MIDDLEWARE = [ @@ -196,6 +197,11 @@ if EMAIL_USE_SSL: DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# Authentication Redirects +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'profile' +LOGOUT_REDIRECT_URL = 'index' + JAZZMIN_SETTINGS = { # title of the window (Will default to current_admin_site.site_title if absent or None) "site_title": "School Admin", @@ -265,7 +271,7 @@ JAZZMIN_SETTINGS = { "hide_models": [], # List of apps (and/or models) to base side menu ordering off of (does not need to contain all apps/models) - "order_with_respect_to": ["core", "auth"], + "order_with_respect_to": ["core", "configuration", "auth"], # Custom icons for side menu apps/models See https://fontawesome.com/icons?d=gallery&m=free&v=5.0.0,5.0.1,5.0.10,5.0.11,5.0.12,5.0.13,5.1.0,5.1.1,5.2.0,5.3.0,5.3.1,5.4.0,5.4.1,5.4.2,5.13.0,5.12.0,5.11.2,5.11.1,5.10.0,5.5.0,5.6.0,5.6.1,5.6.3,5.7.0,5.7.1,5.7.2,5.8.0,5.8.1,5.8.2,5.9.0 "icons": { @@ -277,6 +283,9 @@ JAZZMIN_SETTINGS = { "core.Subject": "fas fa-book", "core.Resource": "fas fa-file-alt", "core.Classroom": "fas fa-layer-group", + "configuration.ThawaniSettings": "fas fa-credit-card", + "configuration.WablasSettings": "fas fa-comment-alt", + "configuration.PlatformProfile": "fas fa-cogs", }, # Icons that are used when one is not manually specified "default_icon_parents": "fas fa-chevron-circle-right", @@ -298,4 +307,4 @@ JAZZMIN_SETTINGS = { "use_google_fonts_cdn": True, # Whether to show the UI customizer on the sidebar "show_ui_builder": True, -} \ No newline at end of file +} diff --git a/configuration/__init__.py b/configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/configuration/__pycache__/__init__.cpython-311.pyc b/configuration/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..336d30614f8c643e7e3dee67efc40474c247ac52 GIT binary patch literal 166 zcmZ3^%ge<81e>lkWrFC(AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFH8N5{M=Oi z(xlS7l2ZNDiqz!NlKdk5^8BLg;)2BFRQ=@qytK^p(xSwY%=|q4`1s7c%#!$cy@JYL k95%W6DWy57c15f}lR$PA^8<+w%#4hT9~fXn5i?K>0Ek*B)c^nh literal 0 HcmV?d00001 diff --git a/configuration/__pycache__/admin.cpython-311.pyc b/configuration/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72a8709dff83cff1ec77a260f3cec10603e582ad GIT binary patch literal 4371 zcmb_f&2JmW6`%bgm!zasBt^0#+giDjUADD^s&&#Dj#1Zk0v##~Pz)C|oXu)?D6O>I zWoDO_%*udU7_AEU;6sW8Xbkw|*su@%6LRRGM^YeQc7dFV9CD)}ABvp%-Yl0QCDlPc zhP#jR`R2{9SrotNE4f#Hn2zdr?Tnagnz-V96 zrs>D9=QDWY!g;8`BSUk&D<8vzXUQ~D=-&m@ML21`hP)}`>O`(tmxiitg5=h|E(DyE)yJuABR=`8A9aYqhOf_4dWNsWM^0Bf4Qti(Rw&qyY%(ws z*)VN8pN*7~Z&No)`^#%!UO|+in@#}#O3`6K$YeM%Q-DN@QKF$4Wz2HfaLr{{ljTbd z&n(eMEU)L4NDio5L<9Y}LW3~sr<)-&4a@gJti6G3B-kiGhB}sS-=1CdOLVrfT=Bxn zEZwA5CG^?shR@c6vT4y-SX^{gD$EQW-<#zJnknCowD>qK$ZFhj=tlKKujS3`8%Lc6 zUjVsFnu8;sWVe-OZt(uQFLDz*xrusivXPr?5jkTE&7rYR&b%0!-Wi&%51nodo!-9m z)$sVk_iI!3lWWg!G|v1IYVdq{?XNc)S8l`&q^{p^S#3S29SiHn!UoXcN@KWE>#KYn zuQxmE&DxaNS&t2P>c^HF$Cm5ER%6(z^;x?hDzQ4Y1P!!se0P5jt&VHqh{Bh&{#tSXKqvfBRu4{7*ZLX%yu?bim5etnmDHsGe z$R9z4gDjT?huo1NM98*y9(u!0m^uJk*9c5(mn~rng2%v;njJ(~k&dVaV{Szch{z#}iRN%)I$u|;!h&!@V9j9u4o>`28ZZ&aAeSF-QH zx&V)au9o2YeP~zT+J|o!?-5*-0DEM*cI1aohPL~gXU}c-*G7L_SLeP?@I1MroqY7p zlli}F{_RdpJ6YFmHnf{H^(F__>KqjKA;cWk523v2bmp3H~B>B)s4l610Heb08pe@@t7H#THuR03DQ+pR~v! zBr7W)!h+Y*K1o@GIsXmi$Z&BjkhD`0=iEi5btNZ)E!vVTe=Zh9Td`A+tnhkKEJ(J> zQ<0Hy)$iv#>G4oW6QbmWlb`a%k8d%IJ<82k?u6VZ`=6faB3@+ZR6M&<8_OicO z)j#l8AW_nn;#;AmZ6HN)UyxvV>&sg*cM&~G`W9`7{6&Vq!Ix&&zjbHdx~ZD^t<0t)&IQOOJJ z9P}}a>|K~*GR%;ms|A;U^kRCVIXcrEzY56dh#V&|rX$V1kq3TD5=UV`L^Rc2G?8l$ zn&_Tbc2T(3pqZF^+E(pM+uY%c%Fcqh=c18)Nj?WRQs=p$hqvK7LFS2??D{J{!w`n4 z;D%#Uh()+_l;+nl4XVR{b7)$jKJVdu!ZR;e-w&x6b{n(=O4@6dSACCy7uhzWK~U9} zd<#5FUxXI*EQ%|OkjX9k{<`5h-g?v*Za5)0R@@qJydX3|cO~8isZ`5%U9%j}!*z2R zgk?y625*2+_UdM&IeNA^rh{%!1u=KwTK!a_!$gEqP49Fn>6GaINv?^h-YYs(Z>D5eM(SdGEvntuE*dL2rp!A=M%W z$!8Z{V(%*?FC`*Qb|xQ5SPayKiH0!I5;Mr+Ao*u?JG|lT!&L%N@_#cb$Q0==qLxK{=T=85jWt6%k-s zqza^IJG#+ZpCi{mQB7m;S=l{%pVEwVzaF-ts1Jw;wis+ZL7y8%r- zo?*(LG22p3U7IRVKXSrIq1RMrGGDoJ{qw-nbgC>|N6|blki8q9_cu+vc|n_{;qQny zw#VXnTxkvu;znzD!iT>)7jz{dspKu-Za#O&{8l(hH?A=>EA3=Y-1X~>@~#KREKLN2 bPz;;n#W@iL)SkfK^dNolzu*6q(!+iLDgB$h literal 0 HcmV?d00001 diff --git a/configuration/__pycache__/models.cpython-311.pyc b/configuration/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e06df4b0c1b51e62ac3eef0fa5c35961a084124b GIT binary patch literal 1787 zcmb`HOK;Oa5XX1@NYgYafusdNRTUMXra=ikajB3H2gHG>QY#_(Vma|94fqk(Hhr8B z;?QrdX#cxEHn0O)*at3!eluQtl zA(mlMA~qAmW+0Yjaw3-1XSYcu_XlMv)X$i<&J3p_`sJNnZBMhhuWhT{?RZ>sySC+* z-fK-mb0!yyTZZPgZEkKe*X>#zrxj%td`Zz{vJ%Oq6bWuQl(~%mM=Vo&$6Rgaa4u3`dA0Snahmb7s%mK_ zQ&m5wssO<=(9f&tCr>k?h}`4${(+zW%y`pwSnz;fx;|5T29PpN$q4hid{N)EO;-1s zp5=OVw$ECgYxDY^%|ALlt;OoC>u~EK`&#e7moQ0=eS;n1tbqA)^%)e)NAlqQlcW4# zx!TV@3NK!QO?&}N<5c2#oD|T^$IJX4dcm$TdfC`uft*b+Hgk8=1GIhH;{QU5Hd%(MrGEY)jw!f$FAv3F2a&O z*RnaQh3A)J9XOs_ot;93Zu)f! z;=!Z0UOZPsPyQc*hazE4f+sy~X-@^gx0_8rVyka5Z|BXt-|v0Q(3i{oC-_MifzOqdG2Qj3hLVsPr09nc-C__Cq@|LLXsvq%6_IGSZcwp{Zk` z)h&}7zU4YzHFk+U+1S|s42x~VP#IGMU|g1|Wa4r{LQGjzyAUdyh$diQ6TwP(r%?4% zawh?yVRobkkpjOojAMlMA4CQaSs2HNzFveWmydLbtM~x+D@+moss%R4UDp zhHdyYm)AY!`<7#RgtNN4#)#)PDRX>cd8F=EESr%v#;uygC@~xgd%)1Y6oZ$zTLYJ^ zf$qj+yrU~33%H8wG2$3?CWcO05WL9^TPO|gZfuIAM4;TQK5tgjX;-Z6FNskmXMcC!w&NW3ktP- z3kuIt)?iSyQ?;1K!*?k%4>&V?w%}4`3(ak?_(6U^`{Bzud{|s@>#W$UG#$TLgmzV% zzRQd2E`Q-Qj4CUh%BXn!qKk0dGbD~&Xl(NE&l9Qe;d;M7wT{O3++b@yP*M6B4rFOq zZ9NGzl%9B_?WV4_FI)>GJe-5${A_z_u01;59vg2@TZlX-=wL5i8J8khR79K;i>4BO~Jn1{hJq3={(Z1Y<3i literal 0 HcmV?d00001 diff --git a/configuration/models.py b/configuration/models.py new file mode 100644 index 0000000..55ced39 --- /dev/null +++ b/configuration/models.py @@ -0,0 +1,20 @@ +from django.db import models +from core.models import ThawaniConfiguration, WablasConfiguration, PlatformSettings + +class ThawaniSettings(ThawaniConfiguration): + class Meta: + proxy = True + verbose_name = "Thawani API" + verbose_name_plural = "Thawani API" + +class WablasSettings(WablasConfiguration): + class Meta: + proxy = True + verbose_name = "Wablas API" + verbose_name_plural = "Wablas API" + +class PlatformProfile(PlatformSettings): + class Meta: + proxy = True + verbose_name = "Platform Profile" + verbose_name_plural = "Platform Profile" diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index e3f5397a6e03bbeb9f0f9e7c0e6dd05ac99ff6de..b27f09156c22e466c82872945393f88e7b1ad1ac 100644 GIT binary patch delta 859 zcmZ2&^4NrLIWI340}#mEY04Dh-N>iS%)#xRUzS>wm%rJL`3(!>%E{Usp^Tp=H*(ZS z=>ioNse=dw5FrO548g=?aZXi6!^w_Zl9R(Y4cW9of;y9XIn^}HKuilDp~+MP5eBmT zG}(%jKoZIzLIp(VO}@ja&t?i@nobtv(q}XUo952LSD(Td#hk(v#gf7t%%I6~i_GcPu!WaBP^Jh(=z|#g zo6WfU7#XcLALZd=WHg(6g;yU)vhc+-+D%U3I|pPN^II?)Z_eTO2TAV{SkLG+IY-c$ zO%J5oa`IL|HAd&jcLcAR8G!^qfqjcNH$N#eCpA8=G&d===oU*dFu1@W0SOgmu&Sa= zkb2w6Z-n|-bAZg?#r49BGT_L{0&+DuihMw3aiph~#HZyK0B&d3c; zLEwnDnanM2!&p7pLp<6f^a2CPX!CV(5ypBZM&%C-n8XDE#g8E2FCgM0GXs;@6-Lny zOdy3E3`_zIqAhY0xGyrwTw#>?z{JRC^ML`AxFD|a5hVNtM1ahYg{x-cWAtbIz`(~i IIZN^&08Km68~^|S delta 731 zcmb7BO=}ZT6rDRs$C;#=d`{woVEw9))?}&~{Aev$v#Lgmg{~ArYNk4x$;eB(PznxG zx214@fw<7%LS+!w{R2|5f@GuM))}`GcxXtz>^3cbCiFE7MJ&q;(7{!C1&$*qY02PPW?)_!1q0737o{;G#&KOa zrkNC;;!qUX4$WiS7*0b8KPiS(p?n215d$i`R>zU3o>!`N_ufp?vD-bdA8t1si;ZyI z6&Cz2GD$TAW9Z2lwdB=kvx5P>Ym$vo%dFA00wXrXvt)2JT9C@5oc24>HGs3e7*hdE zOsWRQ8MO}6_*i|*Or z6Nk2m{!XwR62$BlXakXik{~8LZS}yq{0{Uj(0AB>d1OZa^vLh2B^BmiAgF-PbQNFf F{x4`!y%qof diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index aa5bbba8f2291c3c812addee601dc6cc18e4b471..7abaee64742e277bcc919b6346b23389eebdd5d5 100644 GIT binary patch literal 6365 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 delta 2184 zcmb7FU2GIp6uvXFv%AxF+g-ctZlTx${hLxsB!twUNL#z0p$)M}DaK5uI~SN`cDK1R z3xq6bLkK~m3C#s1(ZrgVTBXl~H>vvIi^-DgLo*|}%# zJ?Fdkp84)McYk{6`(4dnhC&Slq<{6gIj1Z%CxkN#z5Bxg5y>%P$Tx`Lr$Rcokc|t< z!p+@+wD8r=cnW7!#b4<>dAP8CY|fBZd`l7`_o0>LtY0KlO8cr~!*7Tyfh7?z&?=aK zhY424fCw%LfPhv(DD|trieB941+-FGk3SNAi5ac8$T`7y;f8WHev4cY&L$>_8s)3v z_QZ<-KXo;d%v)CHgr1?DeMytqj>>-*I}Tk9Z?d1z!Tj4tfLBG<1TPlCzEFONuK%)W zoTm;8@e|T+A;fc1cQ{gou`u?U`4`e%Ihe^Aln?t_g#>@ww<<(=T;3TtT8!Ys56k`j zZUp!8i*lPdB=Vc`5PuLE;6KaFCkrGkgVPg3XsdRKRwW(29Wa5a;}T&^Ar8h-k1~u%;c7B#JH>hc5)^s$D zA+>HGHmxYtz-s*63K%FnNR5AZ}N!>@-r z`FwMXKL`z_i%kRYtV@Do3hU%)ZMVV(tF6QFkpc4UKx9;qz9RzYRIT2?kF|#9{^V;2 z;jZv8abQrDG}y|*KWmB20j0GR*_?)Tc5x#;>+I4NcHb6udBeWqUjo+dhiWx&O>`7~ zTTY0JG9>D@@xas9#5JiP7Nm{xsEb6|SQ#+2qA3XM<$~m`7{#iC%da4%Yj=@XHPLY0 zN)2s{TF?9YOUPs-vie zK_yke^`~-~jOj2;C#Hf@8yFTS8wBMxrWn;7s^J!HQ_7-xmKvI&J31Rczt7e$QP+oV zZw0s2&d=B>X3hW%x@qbquVhmP;uykpM^Ghka=>=-2F*I`FpQo~(lM}IZJ#V7-lFGC z+hMw6=Ca8g%M9YS7_9vW6l5{~Bsv(~QHu91x0m9F)?(XBvEJpzQtTlAF0)^5ad@f}p5pJulKfHZ{dbyVEb1+EdsvRIlmDR@^6o`B zp*HYu;$sJ0#Ul%k(6h5HM0hZ~nf?yG0{>WfiymR8VTj*rU*jW*!KwX4vgNbxJ19V4 T|5o?tr@24S{^v8C-md=u?v=yv diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index d0c89de0c58aa048b89cc18e650ce4734996d511..35542839b2c3d9561b16f954ca97edf495fa2d3c 100644 GIT binary patch literal 13242 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 delta 2497 zcma)8T})eL7(QQD+Ee;VX-9v6m7fh7el|9SiVQXp#-gj+Se#vc>*9{m%Qo z=lkCGd4EoS99ul;_{wfC7ubvZbW{GpvFPxT&TpHJ5mUlFEQBjTBc0IZv8=prxvv51kBN>nxWBX&UsOa#ji@VeM?}4dt$Tr7!Qhs zUGTG^5gv*SBmny?gTWA!E(x@r{m=sl<^q(#m}S|-6=rzY;)kyCzsP<#Vyz=~$W@QP zH&$0;2ea17`<0lg(sVkhMwU#p0e5IbIEWC2U#(4n!?@f&MKhk2GBNrx6IMdV)~O$J z0;YzMhfODS85}LI}Gfp~wIS2lUwQ>w$Ev zJhjIStrn)bxMZa05p^Rt5UdDC89aJ`t4@UD@S!Wnc>ThaBrd3|7!D!@v=5;qc=({g zGjs@5rx1|UNI0-pgj)Gvno9Cm>J4dH_q2%_YnFH+Vbl!2R8)C)6G30YZJWX3eucC^ zzq`9Ogo2{L2T+C(SozF7L-fc8R%$$xq#7ePDbzfW8dGHTYM2h;9i6c3ZSuO% zz&*Qg!-@b>c5hlwU;@aE^{-gQAz(^(pq3aQ65s5_~j&5PAr#8mD@ z3_Xv&@DMm7W;J_dT#9FC%MKNBW>WAYMm5WztVn~hq$D)sC|lAISlfSaPiijX<_KY^ zs`^c5<}&sWNkGgLgtV^=dVCe|d3i;*$sn8-62@CXqU=k9Abicf`fJ3TFx?VH#Dtj* z15q>f%b97-7-1bjk1-2IWUg9jRoGH9F`X=_5!#K4qCD}+O*6$ytkZnZP*eS!ZZl%H zl~x(BM>AdLEsa~?|N%an+D!NzFN&QU7M-RqA5wIoHaRzhS?{#Z% zzH7}IynTwjNAH}uduF*IZ|z#OGGW);>+pSTYqftvAZ5kCU8Bn0?4S6*?2Bj935mv7 zxhB!c?F5YU+6$U?KGG(cTC)^nFG|TYvpj_=|K%{-jRCZtqGbE^6x zdYz_uAM!D)iCbcoo@Qa>Z0D|~Q&K3BO(C-pWsHc!VyFR2z)H@M+)EL z0?PFMf#2(ok^u-d_=k9niOE7m@)2N+vks*#Xy1wuLMTe~EXojY5Uafel9n*6Hr#A= zhUO>MoS}QZ<+s0!KIqI}dM`hI4L{1n>iFc^_+;KWwd$N&b56|-!D8ci_{ZY6VQ74} zNZ1MMN6ru9gH$WS3RLue5j+lFfqWqH_U6UL9og;NQ?Z8ylvSi&`Z3!F|JJC}1U~8L zgJ&JfH$=kvx)kQS7J4>O{$xk#DK+D_&QC1Nl-Sxx--b{M^P>yOCd!}aO7|`kp4EQ= DpH@GV diff --git a/core/__pycache__/thawani.cpython-311.pyc b/core/__pycache__/thawani.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03b715c5197e092ccd262ea0ec512c4abf571c15 GIT binary patch literal 2999 zcmbtW-*4MQ96$d^;-p#IG~LQ**QIMm6`=Lln1HEl3v3_+bV%D%Uo6+Yw$2he+@0B4 zIodSsA)raTAZ=rrRHSJPjRzk2Gtv=hk=ofU`de^a+tYw$z%yMkdD-<(k z*~`?}9vQrZ?Z#FPD%TN43xbIjL{qqm79>*yDVq|=m?^_7F@IO+y<}mV8R2m5g@+`; zv-KFnb;Ll1r5M?*M}dcZ{CyXm`;nq5cmmyHA`fZYUr>TpxMi?a&=(8)>?uzuMwZ>9 zKp%|({nNToj7E2R^3YQ~Ntz<`#lu-lbQX@|pPIZpe{Sui6~-#`n5w!un=w|fao%y+ zEbE-ZtW#F?ilu(`+S-B8ZfBc(g7B6231_j04MzRgl0_)<6W~HSk5~PHin~~{=!(8r z!hs@XQH|=hx#(QWWjws=Mgg%vJG4Y`DxJ z>IH|Krxo46s^JikUHZwC1Ge;g|j%;L( zHZw=bA|uEDiAb~3DP!Nkaj&Jr3)cmfoD4Vb$Q7%Fm;pwIypp#%Yj z?t;br2$@#^Q^g3Kw;&B~i`7{7cvCXvx>${~MCcKrZiJb!HHf6wYc;|8x~#=wgr{CR zGY+02zh=Tqm`Yt*7Cgz5dprm`dFZj)d7fllG*k6>RbiQ@aTWQl1;vYbiWmRB?=I}r z{b;wUNiVq`P+4dfzI!uQKw#Ik-q7lw84gsEo_}&+8&SE!Cm-ZK#H%@r=4^-MI*@e< zHfPr|YM3i3#gqb-tqtmxO35+;UQ>${$W44QunpN6NRt^zleIL^9|MM(Ik#HD)?-+S zpQz}oWl;Ubk328=vaOe~uYBt2HnZ5O-{*3m>tzRW1M(+D-5>=u%k&d2#RMw(xSurw z=F!wvW1o0papg^izfoW~HF6lrhsyo#{M5dF*~-1?wgz zG@mAXgZ<>`iw3T6&L+EfTe58C$121!urE~Gq>(LbWDCt~ zp{BHy!H3Gk17%`EnQSVP4P~-5F!E*fi|Xf>u3c&<<75byY(#Ve4gPWB3Afb*@&Aa+ z6KeM#cAPdPM-pPSX zj)0&%_yDUNoDReIXF(cr(7Qoa&pyxQ7@Qn-Kf6Y&88W32i(?d9=lh?U; z6U28YKv}SS!jC8rd|T*g5SOE?Ee^yALK}6-0~4|RZ4`<3gg)MWR&-|I&hzokIrZ^h zh*?gECEEFy=`)MEz3j|#UMC#ponYp8ji*5n;1gx8v#8_b1n4+NQyWBE5(J@z(wBp) fg~ literal 0 HcmV?d00001 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 544cde0351b72ae9f21710e01c552a7c99c02969..bf0527e2c891024117759c395d9d6dc72a1d5eb6 100644 GIT binary patch delta 1001 zcmY+C-%ry}6vuDdJJ=8R1IiE>193*dxYgi;5f+RIATmXwQNm1Yy0(rkTf4L!y2V7| z6TZ1LK^}QR5_!-^ebqN#I@3hc7+*+Ccr)=I@Z9|h-nRFi^ZA~f-h0md==~DPz6}Qb z2*&!)m&#|1&=GMuqrYwZU7-8!HzEQN$vznuPIb^;uw$KErT#;9pF-#z zWi3-9_4l4|6w-z|A<`h^37-ZO?xG=rh5<#oXoR2`puR2|B`6N4b8IAy5i|~n?M9se zG(Q1opo`8P_UtpTA^2J&AIe6h#Cf~H(57a<9+3MJX~G{sr~aw|`&NzE*CuD|8*;8?SC_C+mbRC0!>D><1u0 zsspy~;=zJ*DP!L&*;s90)xoOum5HafhhEP(?1k?v_l@Q3xw|&YHP}^$UA5O9+n8@) z-ogA2?A@B*j_-P$p~-{iHcmHi+QDh-h*^Bp)NJ4R1CKL#-CkU`akhc84$iiP*zN-- tK5buHwsEF`GY-zQg^}F_XY{gt^`?!N8hFXUOMl|wFufHZk3J43`UgBV8$AF3 delta 327 zcmX@Yx0AzfIWI340}!m;*_7GK#K7}I z`xb9mz@ZhTIDdVXoiWGhx*6)~VV$lT&DK;i>4BO~Jt2AK<}X!2gx%E|t05|dZ3 TISPCb1Gg@BPP*693*pQU8fw3Xdfp`T9CdQFkex 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 delta 1753 zcmah}U2GIp6ux(6c4oVOGy6lk?k?S}T?%U&eiQ_iijkj!CM|yjS;ckf3}wpf4!JW+ zffTzTeW*VPlxqSHJ`mzhlm|>u6Jkt=KJdEL#;lW&@Inl4V+=`SjPcyrZ9^r-n>$}l zzH{!m=bky|Tphc)I`Vlq96+%Cx_v?aAutJa8hr*HZ!ovsDyCE!bZJ!q%xGaX`h>+w zH3qtFHGTjU5)TogpW?cL9nmny%$ofWgP(#x2f4m<0Xtm>?3$`o?fu%AZrP-4>t=PA zNhgWMY3q5gHL> zPF)rc;|zU6+=o}uFU2)DNq-jmlPNgsge=W28RhC&tvsfY4L~Jmm(-8%(;bp3-4Dj= z!z4p*N;WRgynp>b1S|*-P~b_D$sD4UsW9AJ4UE5_i7?QpEpv9l#ZJFQ6u*$$R@dN*(hLMn!P@QHJ)_STLW`e;t8 zs;5l7YUhrdBu5_m8H}h97Wyvo0=w-c23r{n064x=L?6}2Hjo#CV#fapihdW$D7%2~ zhtIMB>gYcGEkrUIkVjcfEG`iQme)W2M)Z3n$&vl^SEVSD-bsATN2AfF>CtF}zF3G+ zEuM|bi^mX}L@KWelbG)7j)#3ij<9$5z(K<5hQ5}_DtCI3@czJi+dkapBEDl)von{vqF>f$PkwZAX7E8geRXgyo}Z8B8=<@_ zfx)8y{id^b>c3M&;NB~!1D-w(QfrMbCG$M4cJ(5gy|KN<*;BohN5ls`kIv&Gs47gL zGyD-Wf$fe(y_Zg`a+6r6`@3UP{Ls1Rs7bVSr-%s!sjWR>R}4pPXQI?*3qD7vSsEex zzz_Q)WUH51p|&#OV;yC+s*qjijUaDYc>PIOn|P(=<-xmw z^e~Ry;EkF;{ae9*&Cp&Z?*PZ5vvfdsH^xo$3f;Y7;&@*JE!$1BrSX5eiJoaJ->x9x z%`RGkSO6znv;;|d_Sv;>ig=@omOu&MotW7yVG?nQ%Zd09&;@h8Gt4TML?Kn7PrIb`sLP~ysbj^~geevfj3!7ZSq zbQGB1I^uvQIbU7WmrFXGwXOT+ic556+E$^B7+6MO3NYUy&^|&Phy~gcS`AP_+CA&j zR-!$So}R8Y`5I56HGVa~3yF5Kj<$>-?mfVJdWs2g^9#rj&U%>KrcgsQ&Jcg(H$~Wi zY8(@Zon6&LL%I$dEYaRaXf?GBAgBLm^P)Fw0atwA+Lq!kjy!4 z$69DilIOCB)zD*P#B0)ScxP?I_WOKI9;2Vc3t1}*=p16=?5Tj=3F~+p?|%`XZlj1@ zYTF<*hREg3$T`%mS_FLg4^Ll0#z&ix?;>ICLus8`M-^cSt#irl>QqbvTELb;HCWt1fj#kE$>MK}tscEfftcbPa#?ofyze$BI)!`Ej#f*Uw)hwH7g_Fj^M(U=Ym~mOr zCT2Bk!zAxZPSLioA3Z_$BNP`1W=BC`lY%cAI@A4V5$gsf)Q>W=jBS@4y@GA?3iMHR z@IrtTTTElNR^_L)Z>!0hx>>?TpnwhILOv;Nf$7Yp(^^%&qJ3=}8z52$KEq zh-vLsGC_h)@}u_hDlRbUOZQDencty1W}ew=*n-rEX)!-`oKb!aZ}?J?F{k2-jt!>K zz|vjXHV(rwca0>Gkl3 zk#aWtzMy3J^`HfG$H|{u5*Y!=AbYw1H}C+pXHk6E~Wa>($sb1R7wiJ~3c;~KnIo%R?HMSLPb)x=Hngc^E zNl;@g1PpWvQhL4kPz!M#+2%;n8_8@fZr^PrhW?45-oYQHcZ%Ln>e=GUmHOgRUClMr zoTuityn7SxgTb10zY#gq5*p^Ux#hCgl~{fVHx~;=`~<2f#bL) YQXYg)6Ma@cKU(7F9OQ73kumW87c@${n*aa+ literal 0 HcmV?d00001 diff --git a/core/admin.py b/core/admin.py index 5832a93..3402ea9 100644 --- a/core/admin.py +++ b/core/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django import forms from django.utils.html import format_html from django.urls import reverse -from .models import Classroom, Teacher, Subject, Resource, Student, City, Moderate +from .models import Classroom, Teacher, Subject, Resource, Student, City, Governorate class ActionsModelAdmin(admin.ModelAdmin): """ @@ -41,11 +41,12 @@ class SubjectAdmin(ActionsModelAdmin): @admin.register(City) class CityAdmin(ActionsModelAdmin): - list_display = ('name', 'actions_column') + list_display = ('name_en', 'name_ar', 'governorate', 'actions_column') + list_filter = ('governorate',) -@admin.register(Moderate) -class ModerateAdmin(ActionsModelAdmin): - list_display = ('name', 'actions_column') +@admin.register(Governorate) +class GovernorateAdmin(ActionsModelAdmin): + list_display = ('name_en', 'name_ar', 'actions_column') class ResourceAdminForm(forms.ModelForm): classroom = forms.ModelChoiceField( @@ -75,8 +76,8 @@ class ResourceAdmin(ActionsModelAdmin): @admin.register(Student) class StudentAdmin(ActionsModelAdmin): - list_display = ('user', 'classroom', 'mobile_number', 'city', 'moderate', 'actions_column') - list_filter = ('classroom', 'city', 'moderate') + list_display = ('user', 'classroom', 'mobile_number', 'city', 'governorate', 'actions_column') + list_filter = ('classroom', 'city', 'governorate') filter_horizontal = ('subscribed_subjects',) class Media: diff --git a/core/forms.py b/core/forms.py index d2bdeb4..813bec2 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,13 +1,13 @@ from django import forms from django.contrib.auth.models import User -from .models import Student, Subject, Classroom +from .models import Student, Subject, Classroom, City, Governorate class StudentRegistrationForm(forms.ModelForm): - first_name = forms.CharField(max_length=30, required=True, label="Full Name (First)") - last_name = forms.CharField(max_length=30, required=True, label="Full Name (Last)") + full_name = forms.CharField(max_length=150, required=True, label="Full Name") username = forms.CharField(max_length=150, required=True) email = forms.EmailField(required=True) password = forms.CharField(widget=forms.PasswordInput, required=True) + password_confirm = forms.CharField(widget=forms.PasswordInput, required=True, label="Confirm Password") classroom = forms.ModelChoiceField(queryset=Classroom.objects.all(), required=True, empty_label="Select Classroom") subjects = forms.ModelMultipleChoiceField( @@ -19,13 +19,22 @@ class StudentRegistrationForm(forms.ModelForm): class Meta: model = Student - fields = ['mobile_number', 'moderate', 'city', 'avatar'] + fields = ['mobile_number', 'governorate', 'city', 'avatar'] widgets = { - 'avatar': forms.FileInput(attrs={'accept': 'image/*', 'capture': 'camera'}), # generic hint, but we'll use custom JS + 'avatar': forms.FileInput(attrs={'accept': 'image/*', 'capture': 'camera'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + + # Add Bootstrap classes + for field_name, field in self.fields.items(): + if field.widget.__class__.__name__ == 'CheckboxInput': + field.widget.attrs['class'] = 'form-check-input' + elif field.widget.__class__.__name__ != 'CheckboxSelectMultiple': + field.widget.attrs['class'] = 'form-control' + field.widget.attrs['placeholder'] = field.label # Important for Floating Labels! + # optimize subjects loading self.fields['subjects'].queryset = Subject.objects.none() @@ -38,14 +47,42 @@ class StudentRegistrationForm(forms.ModelForm): elif self.instance.pk and self.instance.classroom: self.fields['subjects'].queryset = Subject.objects.filter(classroom=self.instance.classroom) + # optimize city loading + self.fields['city'].queryset = City.objects.none() + + if 'governorate' in self.data: + try: + governorate_id = int(self.data.get('governorate')) + self.fields['city'].queryset = City.objects.filter(governorate_id=governorate_id) + except (ValueError, TypeError): + pass + elif self.instance.pk and self.instance.governorate: + self.fields['city'].queryset = City.objects.filter(governorate=self.instance.governorate) + + def clean(self): + cleaned_data = super().clean() + password = cleaned_data.get("password") + password_confirm = cleaned_data.get("password_confirm") + + if password and password_confirm and password != password_confirm: + self.add_error('password_confirm', "Passwords do not match") + + return cleaned_data + def save(self, commit=True): - # We need to save the User first, then the Student + full_name = self.cleaned_data['full_name'] + if " " in full_name: + first_name, last_name = full_name.split(" ", 1) + else: + first_name = full_name + last_name = "" + user = User.objects.create_user( username=self.cleaned_data['username'], email=self.cleaned_data['email'], password=self.cleaned_data['password'], - first_name=self.cleaned_data['first_name'], - last_name=self.cleaned_data['last_name'] + first_name=first_name, + last_name=last_name ) student = super().save(commit=False) student.user = user diff --git a/core/management/commands/__pycache__/create_test_users.cpython-311.pyc b/core/management/commands/__pycache__/create_test_users.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bfe5d188d85398f342050c21dfe920e0107b418 GIT binary patch literal 3503 zcmb_f&2Jk;6rZ)%UVp5U#%Y~4A} zhSqYWf_iXJC5nUu5=F`>2qgzDNIh`iz#rg5ma>e5goHS7bJcR;#G75O+!a^AtLQxR-I8+W9;bPbu zhs&Z7DMpNFF=|Lf$%qwWM!XmoPzaqxSiFI7gb1r)gx28amtq1(XHZW1gOfRdC12KT zGHDrxW?~@4w`@Xl5hh(ET4{k$Ce1h%OiYKxC(D{`Q_C`#IH@~VS!~L}glY~cwMBcK z&?fF3guxexprU|LF@%L;7>914qKLzwUSv{RXSFa%2?((j_&G`i)Z3>N%OpU17ARnt zN87$3r9cB$1Y3uC;djp!xi*K-waa3MZzUAWb@y0U53K^vT8Hm4@LdaE6P@Tb-=0#! zyGn_G6mDC)>?uXW5gf(R?a+E`6|7k6SnSyp1!>-K@x|>aExw-E1(|d$R*TWwHEE>_ zUtl3ZIJt8T95A^isZzj!Q$Tev;U1vk!8efEWnVCh&?*eqI;4P{Oou>SDCF(g?NLp_ zy?Ebx-_GMJpc-0C1zamV!Kk|j4t^%Qva&B2b@w=3lNS4enXeG;|2n)A{Z2|i=Cco} zHyudt?(zPA=smEP-iPvgW>>$*J-@Hi;t%Zifm*!F?+1a}_WL2Aw*4M|K))Z`-i1B= z9pUcJUa5Y z%Y(G38Kj!>rh*_`jh&?$Cd&9&B?+7De)IDU_xqne0=nAcuMNU?F2p1*K-DNqVq%x5 zzU1hZ*_kIMi(xXSRmzUd0kiopr!!nbkf&m*60GoKmVi%JD8cbJYk>a7wjT(aMl_@KuFe(QS^i zonAbg)&4g14>+1hs=YIY?kp(B+j%j^5`cf=sS_ukerD`qE=mVr_gTCY>^2iiUa~aW z$&#fEYgR84)vOq^fTK^hRY0nj)ei{O=X8R%QM8RXrR4Tgegm0g%`TD>Seu$Bj%rc0 zJDKM+qqgM@DO-wa`M%g8QRZzrFUL>bNbx$S<3HbF2O{bl;@bJ(RmxJ^v;t_vz${< z=?L_6m=jS)^}HwfsxavCCTyNQlur3;s<2=gq)?fym`m2v1kEK+k5tsRw9Lx zMM(kPEZRIV;B^AD_kyttzF^ObzU2JURTf=*)7%=JV05 z^yp@K^t;Cz>Cuh!>1O)$vQ!uA+Ao=GGjnV!GqIVOxKn6kCN?tXo0;>=$$IS8;a>)n z=D_i-ffJhpC*1v6_i(=P!t0F#Z$NED(7}^;QTSluK5v%gdi>V-pE4R6xjniyII%f6 zamQ>7PHYTLH3z4bd+X6#lABDoP*41_;L7POd1O-_SxIfk`KFw2$a!~YtbXR^Yp!yv zex@l8yCZqtKLV_uUijp~^3=_#`qb}o|IOF8gZi0rh4x4 zrsXvWlAVM`v~l!w2>w->jvW1HEr#yI}*?VnWW{JN1P%`DlF=bG|d!>hl_`*vIQ z?w!mJBaQKQ{koAYHnYVI`BGE9)Q~Uz8Ql|e--BX?E!YF|;8FL$A@|T>H~Xl2~ZvE%-0^VGE$%gVTnl;cXpLy<4cNETO6p7b*}9R3@wH`xUM1MP`+W z3#7b6Pr{0C5q%mudV)7Rw0jM|u=eB7T<`q3$>*-W(h~EL%R&q7-ujZbkm+2K4dF%2 zoVOrUQq!7xwUTZFW)_r{{~K8L%#q3 literal 0 HcmV?d00001 diff --git a/core/management/commands/create_test_users.py b/core/management/commands/create_test_users.py new file mode 100644 index 0000000..b9dbcc3 --- /dev/null +++ b/core/management/commands/create_test_users.py @@ -0,0 +1,60 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User +from core.models import Teacher, Student, Classroom, City, Governorate + +class Command(BaseCommand): + help = 'Creates test users: one teacher and one student' + + def handle(self, *args, **options): + # 1. Setup Dependencies + gov, _ = Governorate.objects.get_or_create( + name_en="Muscat", + defaults={'name_ar': "مسقط"} + ) + city, _ = City.objects.get_or_create( + name_en="Seeb", + defaults={'name_ar': "السيب", 'governorate': gov} + ) + classroom, _ = Classroom.objects.get_or_create( + name_en="Grade 10", + defaults={'name_ar': "الصف 10", 'description': "Test Classroom"} + ) + + # 2. Create Teacher + t_user, created = User.objects.get_or_create(username='teacher_test') + if created: + t_user.set_password('password123') + t_user.email = 'teacher@example.com' + t_user.first_name = 'John' + t_user.last_name = 'Doe (Teacher)' + t_user.save() + + Teacher.objects.create( + user=t_user, + bio="I am a test teacher specializing in Mathematics.", + specialization="Mathematics" + ) + self.stdout.write(self.style.SUCCESS(f"Created teacher: {t_user.username} (password123)")) + else: + self.stdout.write(self.style.WARNING(f"User {t_user.username} already exists")) + + # 3. Create Student + s_user, created = User.objects.get_or_create(username='student_test') + if created: + s_user.set_password('password123') + s_user.email = 'student@example.com' + s_user.first_name = 'Jane' + s_user.last_name = 'Smith (Student)' + s_user.save() + + Student.objects.create( + user=s_user, + classroom=classroom, + city=city, + governorate=gov, + mobile_number="1234567890", + is_email_verified=True + ) + self.stdout.write(self.style.SUCCESS(f"Created student: {s_user.username} (password123)")) + else: + self.stdout.write(self.style.WARNING(f"User {s_user.username} already exists")) \ No newline at end of file diff --git a/core/migrations/0006_student_email_otp_code_student_is_email_verified_and_more.py b/core/migrations/0006_student_email_otp_code_student_is_email_verified_and_more.py new file mode 100644 index 0000000..14357b1 --- /dev/null +++ b/core/migrations/0006_student_email_otp_code_student_is_email_verified_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-02-04 04:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_city_moderate_student_mobile_number_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='email_otp_code', + field=models.CharField(blank=True, max_length=6, null=True), + ), + migrations.AddField( + model_name='student', + name='is_email_verified', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='student', + name='is_mobile_verified', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='student', + name='mobile_otp_code', + field=models.CharField(blank=True, max_length=6, null=True), + ), + ] diff --git a/core/migrations/0007_wablasconfiguration.py b/core/migrations/0007_wablasconfiguration.py new file mode 100644 index 0000000..43dd6d0 --- /dev/null +++ b/core/migrations/0007_wablasconfiguration.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2026-02-04 04:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_student_email_otp_code_student_is_email_verified_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='WablasConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('api_url', models.URLField(default='https://kudus.wablas.com/api/send-message', verbose_name='API URL')), + ('api_token', models.CharField(max_length=500, verbose_name='API Token')), + ('secret_key', models.CharField(blank=True, max_length=500, null=True, verbose_name='Secret Key')), + ], + options={ + 'verbose_name': 'Wablas Configuration', + 'verbose_name_plural': 'Wablas Configuration', + }, + ), + ] diff --git a/core/migrations/0008_thawaniconfiguration.py b/core/migrations/0008_thawaniconfiguration.py new file mode 100644 index 0000000..7907936 --- /dev/null +++ b/core/migrations/0008_thawaniconfiguration.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2026-02-04 05:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_wablasconfiguration'), + ] + + operations = [ + migrations.CreateModel( + name='ThawaniConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('api_key', models.CharField(max_length=500, verbose_name='Secret Key')), + ('publishable_key', models.CharField(max_length=500, verbose_name='Publishable Key')), + ('is_sandbox', models.BooleanField(default=True, verbose_name='Sandbox Mode')), + ], + options={ + 'verbose_name': 'Thawani Configuration', + 'verbose_name_plural': 'Thawani Configuration', + }, + ), + ] diff --git a/core/migrations/0009_platformsettings_remove_student_is_mobile_verified_and_more.py b/core/migrations/0009_platformsettings_remove_student_is_mobile_verified_and_more.py new file mode 100644 index 0000000..ae9d621 --- /dev/null +++ b/core/migrations/0009_platformsettings_remove_student_is_mobile_verified_and_more.py @@ -0,0 +1,70 @@ +# Generated by Django 5.2.7 on 2026-02-04 05:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_thawaniconfiguration'), + ] + + operations = [ + migrations.CreateModel( + name='PlatformSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='My School Platform', max_length=100)), + ('logo', models.ImageField(blank=True, null=True, upload_to='platform/')), + ('description', models.TextField(blank=True, null=True)), + ('contact_email', models.EmailField(blank=True, max_length=254, null=True)), + ('contact_phone', models.CharField(blank=True, max_length=20, null=True)), + ('address', models.TextField(blank=True, null=True)), + ('facebook_link', models.URLField(blank=True, null=True)), + ('twitter_link', models.URLField(blank=True, null=True)), + ('instagram_link', models.URLField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Platform Profile', + 'verbose_name_plural': 'Platform Profile', + }, + ), + migrations.RemoveField( + model_name='student', + name='is_mobile_verified', + ), + migrations.RemoveField( + model_name='student', + name='mobile_otp_code', + ), + migrations.AlterField( + model_name='thawaniconfiguration', + name='api_key', + field=models.CharField(help_text='Thawani Secret Key', max_length=255), + ), + migrations.AlterField( + model_name='thawaniconfiguration', + name='is_sandbox', + field=models.BooleanField(default=True, help_text='Check to use Sandbox environment'), + ), + migrations.AlterField( + model_name='thawaniconfiguration', + name='publishable_key', + field=models.CharField(help_text='Thawani Publishable Key', max_length=255), + ), + migrations.AlterField( + model_name='wablasconfiguration', + name='api_token', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='wablasconfiguration', + name='api_url', + field=models.URLField(default='https://texas.wablas.com'), + ), + migrations.AlterField( + model_name='wablasconfiguration', + name='secret_key', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/core/migrations/0010_governorate_remove_student_moderate_remove_city_name_and_more.py b/core/migrations/0010_governorate_remove_student_moderate_remove_city_name_and_more.py new file mode 100644 index 0000000..af87c2e --- /dev/null +++ b/core/migrations/0010_governorate_remove_student_moderate_remove_city_name_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 5.2.7 on 2026-02-04 05:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_platformsettings_remove_student_is_mobile_verified_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Governorate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(default='', max_length=100, verbose_name='Name (English)')), + ('name_ar', models.CharField(default='', max_length=100, verbose_name='Name (Arabic)')), + ], + options={ + 'verbose_name': 'Governorate', + 'verbose_name_plural': 'Governorates', + }, + ), + migrations.RemoveField( + model_name='student', + name='moderate', + ), + migrations.RemoveField( + model_name='city', + name='name', + ), + migrations.AddField( + model_name='city', + name='name_ar', + field=models.CharField(default='', max_length=100, verbose_name='Name (Arabic)'), + ), + migrations.AddField( + model_name='city', + name='name_en', + field=models.CharField(default='', max_length=100, verbose_name='Name (English)'), + ), + migrations.AddField( + model_name='city', + name='governorate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cities', to='core.governorate', verbose_name='Governorate'), + ), + migrations.AddField( + model_name='student', + name='governorate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.governorate', verbose_name='Governorate'), + ), + migrations.DeleteModel( + name='Moderate', + ), + ] diff --git a/core/migrations/__pycache__/0006_student_email_otp_code_student_is_email_verified_and_more.cpython-311.pyc b/core/migrations/__pycache__/0006_student_email_otp_code_student_is_email_verified_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7145eb7e33e231b00575813f63198bc86ffbb95a GIT binary patch literal 1340 zcmbtT%}>-o6rX9k-ECb^8jJ^w!Uha`VKG5Z#6(Tl#3&byhh8S#&aiap$I=gl8!;X{ za`Qhx4EzDc#KentNcOOqt0!(nIC1h#ce@7HsOYqPeZTkmKIZqP??y&+1nbk6SI$Qn zp|1kutI$_Y7D0K7D5BU!9yYNjnG!}4sv|1BMpS0F1+l%g1KQpInKD(LqH5ttII9xZ zJ*OF3u@m?aSG|BTH?sSI*{j?KC$m_TWXQx6nG(fKY|0X1N-J*)N>gD`Tg5y9RcL`K z`|9}?_KWB7P?l1br8LN*?dv;GeGi`A`eTMm|DB=yFFR=}rbb7A-chbS$$15m6*~wS zzdb)ce}~vkyh#Mnz&ayQoKWV+U}!ilBYxsFSV%0Nf(1f~eLo*%p5?eCh}*=5*k?y7 zFj19xq2XHo8cfmJATINp@hVsR#C5@?XVWCUn~SzULsj;Lby)Eh7#zX@G>FmiRFrV0#VjxKJ3tTSQLsw z$V>MrT@rnSih$a~RV&OeFFyzZmsx%f=|NjiBo5|#bRy<{wq9EeJXT8@i6195w!!Qq z4#L{YAY6;umd$E{=Gu<~T?4}J4k$j);J`dY#(@2~_GTz(&V*2+Zy>e?SE?dy38%6& zu5LX}b);Q?qjyRZ-D{KGv8k?6*}l~=D!bz!ran&}81+M=-ZAQFQPK2c1maYPpQ5I_ z#*B!X*<}aD!lALyF&2IwF>~RF%3pRib=6R}9*HZwnUkR_8U?86p=f|24UoEUkvczZousao1FbvBwD@Np zX-k@nqC>}Q@sQa?Q4Jk4cJxw!1~dqqf_BQrz@9So9Vt0K?j8B~-o5X=yZ7DWZ?m&1 zf>AvF*?68o=wEJ3M|Pr|-T>tZB8UhkvV^8!iA_;JBKigq@fSogRM>&obR2>9$GO$a z5NQL|voE-_E=HHANzHgl0xWRN9#y z3brPEMc&Cx6A%ezmdL~0kpzc;Hyv$}G3f%e-ALide5Xro z<4izkpm5*=ZYMpW_pnLrj=!CvRYu{gK0n_;-AMGMb=rqQvQVep_2zj_$qr022^f@9 z0+!xlpf0A_=HDy}yCx81)-w>!tCWFBldve~xJi@VtItGvoG^fKq+%eL1SSQ&fbnji zn>>nJgt|~UwOa;d9&i6>W;NmWE^l&=_hMA|%IMqz=GXWhQ67j%HDz^M_fl1LTsrwb zen+PegkG&{+m1!GU@NfwK%+g{f)t*%?|5D2>Mg4Ap3z?aR~mHBb(~&H>&+HuxP4EC zjEUe1`y0f;`HW?xYzT2iEXxOXVihU1U)52*Ixby#URnq%*Be-VZ+m*M7LzJ6Y_F7JFf_KQ1kX)y83aRBb%_=y*M>Zk|*( zN7c=+_|v%bL0Hoc_eM4CPx2QFzke7KH>~ZR)OJU;-LUAzO13-~&!G>mk3ab`&SnrL+}n9z`jJpvJD1VI2+gnEdsUVLJ?EXXmsC?2b$;_^%GIQt)OvhRQQOpvhv literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0008_thawaniconfiguration.cpython-311.pyc b/core/migrations/__pycache__/0008_thawaniconfiguration.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..881ab2137f7252537d94bd5ecbb70067950d0023 GIT binary patch literal 1308 zcmZuwy>Ht_6hD555-C}h5*aEArE*iDsRAc50t3wu1ZW&MXueRyL0t%NiIeUu(V<9& zAO6r}6dgKdD3A;tIyng9Kfq(RdZbYf!VN`%PT3gPQ>MHlC8w@)M?SuH_db8`@#WN% zhG4D!^^@_dgwVfSn5@j0Il2MNb3_mkOf(SM!a!_`0us?3M8uyFk*Kf^w&~gh?)8b( zmWaHJnwdBJvMDlcVDwz<8@A;!WndF(dfl;M^2v{dqrZW9jwosi1hqvX^n|t~A}X&> zl7i5LpD63ui31|T&JblUyDLKy@TO}`B)LR0H#s3H(f0DYB51*TqE$GfHT4m#Vxpx^ z>>%d!Ug`h7dNL`)KKGlrcm6aaP>Dt=0LlzgA572yXymT#QufLG{QTGYChnNn>)O_u z(F;<%SmkjaZ(_?>JasA>xaLv5LYxj%c4ZYRycTe?ZDp5J$;pw$w|)2nn6wCv8?SRlk~8eXE{BoN7tFA>o8aWWT>-(u0IX1$*s6Vs6#D6t*${m zm(M86FD7X53{9603@h9+dfxz!OFRZs7yH;v*}^T`HYv7JNwXa~9^_Z(h|j{i`>nn` zpsk=2Sbor=TeJ&q+}5V;KJgsfr7hmz)`uUU1+BiW`)OBC_4=x_?JD3hA)N941hR9o zV+E;KVo9thJNIJ^sr6s9DEHZ@eCeO^Ojy1AY$K{(-j@#=VOtL^H>`SxRWGV~VaXqr zKMiYhFP5U(+`e=$7uHq|Yb#N0B`p0gDqjp63%@T#jfFp?@KHBxki!Ov8YC>yQTakx zzxHAys$bh*IIzO{>S28~s;`EnA7eFBDaJ+g`SsDIn{h^(DaMGCazWMJA|S_{#8UB` zvU8Vj_lrqo4`F6ivW~5ueU)^Qh9o^mYrn|V65R%H?{YWNuHJON1QCW6dEWzxB|#7X f51|>Nug*TPQW2CGoh8q-sML7NFOENQo5Xz&vh8o7 literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0009_platformsettings_alter_city_options_and_more.cpython-311.pyc b/core/migrations/__pycache__/0009_platformsettings_alter_city_options_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19891b6dfaf57c7bb7324dfa9b3db8476fc7c767 GIT binary patch literal 7519 zcmb_hO;8hA9?usc;Y)a;h=PQGj5taV-C3EP50P(062|69q^vCb_ z|M@ce(yUPfA-=@!;nXIQX**yRch!ExF;ZNA@JVOWuTU z$>(-?T;ne6{k;qOh z$Xcw3Wq(ldYFgXOCIcwyO+Fi@JW2TzOV;-;fjB?x?}?T;8TVD+3M#$hy%WS zTlU}pLk3D$a1hsIYoB`n1>Z8Nj-i63E6iSfw!xkiSUbjWOhbyLxNPGa)|wa!GT|Gb z5a5o#VXfH_n-8DZ7k9EEuPqE!x5w*#v7M?2)mjNEUV~4==geO`**1G_e{=)iw_0D0 zrCoAm&)Ri*1Jh9fCUhXCq3o*zAIo;`>uVPi#$6R)x+}n(I~3D{Kd1n6z5+~d1(*wm zVm`zd55OENYuHmsrDregJL-*PEA4O0zM5>`b2rRA`10*g8yz^pYH@c4zl)9j_qGxG z6>_Lk5kL^#7Llq3;j`#BTyLa&Ti9>H6{`Adf zMg@_F-YfFY-XZeOna7s0-adRC4`pxId&r@Y_uye>$6hTXN3NEhU!kw`;P>wS%7#_zH*~ug7@*%#-?7Veta;Ul=;#%jbsYXFe|NP zU3_>pJ9lI&^Nv{Vy+bVb@q)w3-6MC(`*@Mr*zMU5vP(yN7T(7X9dY>Z-AZQv(rNq! ztjE8!g11ZSa9EW4Vv0(v*8cwfYl6NaJ{FTwOi3YSR)%&P$Q}&%2I|{b&rS|RWu?b#RRc@ zbe}=DH$ro<6-ALlB@a6o#u;L&#$;Jc%R0OSfBR*9xr^~++6L=|OCkR@1j-ox`iZ7NGMT=KmgAYc)-@N;sr%d2{9mAbu3>YnNCEB zYE^6L=qicnR>K}K1yQx1lcd!YSClvy#;Wv)a2*#Mla$^^1>0T~s02 z4`@0p<0_VMuUn;KU{%xVppN(fcnE;eUd6}`eOZme zmu)+AAp?^D#wP{@8OjJsQ&Z{6hff?V#U)*{yirN98nhI^=xk&!;8yd#bG=$jNp$XJ z&^&Jip*aF`TMykL8$17_{ybK2g~%yEC+j*NuNtT=O-$mbvYrVIuaMYUNLNB>jfCd- z)euQOl2j#`fJ%firtztFXpqNJ>8LDeD`FJJj(Tk=d7a+9L0wzN9|K0z?2Rc%gVmKa zs1O!+T9q@WR&+h3eH@8Ee4^IJX?-yz0oBEY(O5#%-JRYuRzP673xZW6zzo4@h(xgJ z1mT;sD6>L@GfYw>3G*l>fxPtR4XbvTHN4w&zNz$w2CMFd6d!_CI7TP06&zj>Reo4A zNe8odc^*_NULK_#h#v>;%}(*7rXd-c2{U`QIQwg7NkVy4G!PiVxX5QnejFTwSy8+^ z2DK^0lTYVK-0~uSK5+IAHD=&!zNzcqO(%?&3(p>zEf=#veA8JY z)W7LBL;YL&%Wfkyo(qkep>d;eBHt7;!dEtX%88QZBq?hL?=ShwSbE zy*seo{3>kBEg0d&TzJt8bF-(7wu?WEnQa%hu5M$aZ8+C9Y_<&>jUzzZjBNJ&7}@r{ zylR9ea^VRxJYh6W=9@Y;J2p@L*uetn9M5%*o1Hw6&dtv^Kl<_W?dF#wM%QGnYtrnR zG#YOfPSiFx6i&N3ujD@%EL3?&g=skWT5MASFnQ*U`rTOcKRD~GuXr)SSa6g&Iz03 zuxUFi?1as5*rFZQ;eoHOIMf28(fd+GE&_siUY9AeA*uAE^!X)R;0+-FL z_Iyh_t%edeW}tx_8Zgm-lR3wkU+iJRpq{RP`6XvAADtQJ%+wxcXJyfT!*(2UvmAK!|XFY$4yzp&ZHPE#jx@MwlPU{q_tyN&B?C?8(6ouBoe{@4Teh>IB=)fH|MP)4de0pF${dXw z@?!(7=g_)|)}6Kz+}0zzt+uz=dSalbIrP*-Po1_Nb6eMayMq8(h001cZ-=3KqKO=u zFwumQJ>+APB~KKt50Vh)KW5}o-HPEOj%;Zn@TCAJ_z3puo4q6H^g;q3K2?2Gv( zm^8(HesIPcoOuIQPNi-ROc-F+r;H2m92m`^Q4@_ieGdDX%2ReGq+5E1Ynkl))n$NL!Sa2!^k%DVz9>vmE6h|1 zj(!Pz!q@MgsT!Qp|69;x*cH5Pw;L9U?oPuM;{Wol^TvMv3jP_d+h1_)zkgO^Hg^BS LZoWPM!lL;<-M>A^ literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0009_platformsettings_remove_student_is_mobile_verified_and_more.cpython-311.pyc b/core/migrations/__pycache__/0009_platformsettings_remove_student_is_mobile_verified_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b4563b2ec91461557575b56d9fe2d1d2fe843f1 GIT binary patch literal 3131 zcmb7GOKcNI7~ZuVCw5-;;>4H+ve`VqA;f8ds)7&{@-#1@1e)L;mbGVMZ?fKXch`aB zgsL8T}G$@%>Vl4f6V0P z*47{c&-35ElYfKJpL9}x8ft@gmtgReK@4IQCeNnWyeH*h84okfAkX&<@?v($TUUAs zqhEJsDKGNPG4X~i%8q;NU|!DZk|}GdVf*tM!itfpYtr9vZSw9pn0U%yCdDEqG_AS*xfonup{7a2>1IPz&1M-&|U-|$9eD1U8K~0WiQ+)*mP_Wee(gRZP?MSsa zQi1x4W3RQ;wtq`tt=*Ax0!L-3Tq$(OTG)|l-eZjexnqZ{MGj!|p^hWvIzP&5)RAi0 zXFqvLUHkV2+P@gEkCbTJvC)GB2h(mQ zdr@Dhf4|kzz!5n1+BoglXy38wLuX3yPqZ<30LR%+#xaD>9k9_?8h*(_O~U6s|Fe@J zlyEHUW_zUc`6pT!J%GW7`VLp$K^)mYDul+MvCrFqTlJ2H_V#5o9ow;yk&!P&GbcTi zR5_!mi*mN;HoYCXqe$kWrswCdY07HWust%mJ7s&46LFv2A{9+d%;;D$F|wNrx}2Bv zRdE@w+5rRWvZTms*ltN40XDvBK_78FfFqN&-<2pbt)F3>|O?$JBI zs@@62ZiTIxQpOZBJG!12~nPZQN{`~>_*MO z4+Fz2BCMJ=CmUj3OM@K|^ktAk&iPEwWGAw&f%ck=W){Q@@a)Kud2N&mGR@N(ch^lj zKZhZYCVvC3Zv6vDz;+;qm4ayEN2X5W0}uz`$92=-;IxAP%8*o))*h|#<2jsJ=1q++ z8knDRS9z?i$hxNHVGFQ-jv6=%#BMJX(~4~5q%>@Z0*Tgv+}UMNH1@HFAR`(3>oP#` zo7yr~RR4`;MINK*fH&~Ua&z21(8?IzI)pD5Zv z5h9F=07$f3MDg3Aq|joeGlUCRg*;?rZ0Pj1w42ABvcE;+uhXl`Zn-9BuR=pi(G=Q& z@tmZ)!(cKmWpQo!F2u98JW1=&9XH*Zz3GmcXK`LzsYwT~Do|f8qvaZO3zpQHvR1&g zi}@Gk9k^%S8;P8j$BAOPsG7wDeuOgsMo&D{^kt&}g`1$wp4h$66VUb-p#IzjH);(o z>W+W0R(ImzvJ)Y-5GWlF7glvYcyOK{DeAYDVxrmU`^ z*XK77=^8J0jayygBs2kv>xuP&7l{quoAX4NDGM`}FhfGgO1OKyd%g2Tw*#bSy4*8u z^|(NK)~~F8@#4xxO_qC;R&SDou2(ynBW=|aOwU;5^o44Jx1+7fP;!zB1m7}1 zwkfF&?1VDPSHk@uNZCNPDXBK?V+%*HPtio2tVGXM!Y3-6@MOT^1aj``W`=OHWp38u zW~+@pY7@vdB{X|`lulN-eyY^}3X#hX2q%>}$>O9BD+#-voQ!hwWp3W$=0Bh{R0((0 zwO#?Z(K0t`aibsBfF1Qb8Tc_l1}6wNS>`4!Zt}w#upKuBuOb`0kP$9f=8_hdJgBwI zUgZ3buhUl_uh;!;xCaAhNm8@gFiJaTgL53XopFtdIkFQldf#-(Jzy^DQ!u6fHW&|p nRJ|S_G<_1ZLVa%?=JtPxI0*g)vsg+x literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0010_governorate_remove_student_moderate_remove_city_name_and_more.cpython-311.pyc b/core/migrations/__pycache__/0010_governorate_remove_student_moderate_remove_city_name_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..245a1c524d00197aae0528b22c53fa78310387bd GIT binary patch literal 2451 zcmbVMO>7fK6rNrG|0LKOQUfs|kceUw#VJw`v=wTaAJjIa5+O)+O;>Brc(Y->Yi8G! zL~4bq9(qK*w1=Kks`$W>BL`G*+(>({Mm<#OiQ6J_;?y^@u?b*))Y;v)GxOfO_r1UQ zV`wOiVEpy>kH&Nop?~?JH)8vfrwcIo6=8%W6ICSm%ck6nR3go2B`P5qT|_wY6T(pv z+31}?+Yy9z;0s43hNV?hiaqAbm6FWTO+%-eYuJ{<;!PV9)2Z*vieG+z@$?s&+a|(p zMaGgYRU$GX(dU-1jQLJ{BhgcUqwvIVyp!0D0t)!T(U5UcNP?%~z$u)@nNIdav>-P$ zKyLU|IXwc_58>SJawq?y4uLV%x1->Z&f#tFWe2`+yd*J--y0|)^21Ny(RV}DDGWez zP|G{T;H_GYhzMo&oq_B_{<9lHz4b=CKO64P4)tdEC_aYe&e%3c?erzzVpGBY$7Zf? zlUpoqLxt}2t8}2nUhN@#yfeNnfjoTS(32GfpMXl9V5v*J3Wjoy)@@4Ir?a!O=hc>} zxec2(9pbu%r8_DmP5UNM9k-2%<*J6GHtm{W67?pbM#CUj)hrBqpdoV+B&lUXYY-+I z_{tKKKVK+CSypSiwpynI7?@>R)M#pSQ@u_$S;_%5nrUnimW9JL+aao@H3>^_suGK# z#~9#m4X?m@;>@C@n})MqDn(cVlZMtdU1&$`mTD49ch@OT(*9YEZViQp=BQRP>Lr#v z0MIGPq43B-v*3T5Y9?rp3KVhh(8LZ)*sZYL6Z?T+N#6I|Q*j^gJ3!#3M>v3J-EcR> zv=pJoVJF;<32iK+ABa;NL=EC><#-Zi4sc3w7OR<>bseCzO_QZ;O9d+gVwNy;omloWL!W~TFY#Eh)cm?e!)fNSD4uYUoWnSrrI)}} zL$|(!{KxTzX6ZJQag8N~dvL3=#Qfaq{M^DKORg?nQCHTMmsw^(_%6({wB6#d=NIsw z=qX@8&)Vg6yGhFJTHA8lWpazu0TC^KXVdFWORJMIk8k<;#a*7A{a{u-5bu{O9U>y~ z+uP?U4FlFI_?gxw`Nipbmmkb~R~ueY-!1BXQTKA| zuW*T1Z0r^rezD=@^dJ#EJRD@v$(io^9|p0==x~5IIh0AIpCBN6oCHVucnTBU{1^yw zHjq6|g5-1d@ov5_fL-L?jCBjZKGs!E-8tcg-7#WjENTYs_PwZ9J#nwQsP_+j}5 z|BX=jiJ*IezCkra;kBxD&-?+Dt;pX9omHW;Ds!Dxd4SGdrpF-rJOV)WI0;szX(fL9 z3;xiLdbI {% else %} {% trans "Register" %} - {% trans "Login" %} + {% trans "Login" %} {% endif %} diff --git a/core/templates/core/error.html b/core/templates/core/error.html new file mode 100644 index 0000000..34dd7a2 --- /dev/null +++ b/core/templates/core/error.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Error

+

{{ message }}

+ Go Back to Profile +
+
+{% endblock %} diff --git a/core/templates/core/login.html b/core/templates/core/login.html new file mode 100644 index 0000000..53d488a --- /dev/null +++ b/core/templates/core/login.html @@ -0,0 +1,51 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block title %}{% trans "Login" %}{% endblock %} + +{% block content %} +
+
+
+
+
+

{% trans "Welcome Back" %}

+

{% trans "Sign in to continue your learning journey" %}

+
+ +
+ {% csrf_token %} + + {% if form.errors %} + + {% endif %} + +
+ + +
+ +
+ + +
+ + + +
+

+ {% trans "Don't have an account?" %} + {% trans "Register Here" %} +

+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/profile.html b/core/templates/core/profile.html index 07e1156..3de8d90 100644 --- a/core/templates/core/profile.html +++ b/core/templates/core/profile.html @@ -42,8 +42,8 @@

{{ student_profile.city|default:"-" }}

- -

{{ student_profile.moderate|default:"-" }}

+ +

{{ student_profile.governorate|default:"-" }}

@@ -74,4 +74,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/registration.html b/core/templates/core/registration.html index 01f554d..ffe2c95 100644 --- a/core/templates/core/registration.html +++ b/core/templates/core/registration.html @@ -7,110 +7,150 @@
-
-

{% trans "Student Registration" %}

+
+
+

{% trans "Create Your Account" %}

+

{% trans "Join our learning platform today" %}

+
-
+ {% csrf_token %} {% if form.errors %} -
- {{ form.errors }} +
+
    + {% for field in form %} + {% for error in field.errors %} +
  • {{ field.label }}: {{ error }}
  • + {% endfor %} + {% endfor %} + {% for error in form.non_field_errors %} +
  • {{ error }}
  • + {% endfor %} +
{% endif %} - -
{% trans "Personal Information" %}
-
-
- - {{ form.first_name }} -
-
- - {{ form.last_name }} -
-
- -
- - {{ form.username }} -
- -
- - {{ form.email }} -
- -
- - {{ form.password }} -
- - -
{% trans "Student Details" %}
-
-
- - {{ form.mobile_number }} -
-
- - {{ form.moderate }} -
-
- - {{ form.city }} -
-
- - -
- - {{ form.classroom }} -
- -
- -
-

{% trans "Select a classroom to see subjects." %}

-
-
- -
-

{% trans "Total Amount:" %}

-

0.00

-
- - -
{% trans "Profile Picture" %}
-
-
-
- -
- - + +
+
+
{% trans "Personal Information" %}
+ +
+ {{ form.full_name }} + +
+ +
+ {{ form.username }} + +
+ +
+ {{ form.email }} + +
+ +
+
+
+ {{ form.password }} + +
+
+
+
+ {{ form.password_confirm }} + +
-
- - -
- {% trans "No photo taken" %} -
- -
-
- - {{ form.avatar }} +
+ + +
+
+
{% trans "Student Profile" %}
+
+
+
+ {{ form.mobile_number }} + +
+
+
+
+ {{ form.governorate }} + +
+
+
+
+ {{ form.city }} + +
+
+
- + +
+
+
{% trans "Education" %}
+
+ + {{ form.classroom }} +
+ +
+ +
+

{% trans "Select a classroom first." %}

+
+
+ +
+
{% trans "Total Amount:" %}
+

0.00

+
+
+
+ + +
+
+
{% trans "Profile Picture" %}
+
+
+ +
+ + +
+
+
+ + +
+ {% trans "No photo" %} +
+ +
+
+
+ + {{ form.avatar }} +
+
+
+ +
@@ -121,19 +161,7 @@ {% block extra_js %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/student_dashboard.html b/core/templates/core/student_dashboard.html new file mode 100644 index 0000000..a93be93 --- /dev/null +++ b/core/templates/core/student_dashboard.html @@ -0,0 +1,182 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block title %}{% trans "Student Dashboard" %} - EduPlatform{% endblock %} + +{% block content %} +
+ +
+
+

{% trans "Welcome back," %} {{ user.first_name|default:user.username }}! 👋

+

{% trans "Here is an overview of your learning journey." %}

+
+
+ +
+ +
+
+
+ {% if student_profile.avatar %} + Avatar + {% else %} +
+ {{ user.first_name|first|upper }} +
+ {% endif %} + + {% if student_profile.is_mobile_verified and student_profile.is_email_verified %} + + Verified + + {% endif %} +
+ +

{{ user.get_full_name|default:user.username }}

+

{% trans "Student" %} {% if student_profile.classroom %} • {{ student_profile.classroom.name_en }}{% endif %}

+ +
+ +
+
+ {% trans "Email" %} + {{ user.email }} + {% if student_profile.is_email_verified %} + {% trans "Verified" %} + {% else %} + {% trans "Unverified" %} + {% endif %} +
+
+ {% trans "Phone" %} + {{ student_profile.mobile_number|default:"-" }} + {% if student_profile.is_mobile_verified %} + {% trans "Verified" %} + {% else %} + {% trans "Unverified" %} + {% endif %} +
+
+ {% trans "City" %} + {{ student_profile.city.name|default:"-" }} +
+
+ + +
+
+ + +
+ +
+
+
+
+
{% trans "Enrolled Subjects" %}
+

{{ subscribed_subjects.count }}

+
+
+ + + +
+
+
+
+
+
+
{% trans "Classroom" %}
+
{{ student_profile.classroom.name_en|default:"Not assigned" }}
+
+
+ + + + +
+
+
+
+ + +
+

{% trans "My Subjects" %}

+ {% if subscribed_subjects %} +
+ {% for subject in subscribed_subjects %} +
+
+ {% if subject.image %} +
+ {{ subject.name_en }} +
+ {% else %} +
+ {% trans "No Image" %} +
+ {% endif %} +
+
{{ subject.name_en }}
+

{{ subject.description_en }}

+
+ + {{ subject.teacher.user.get_full_name|default:"No Teacher" }} + + {% trans "Go to Class" %} +
+
+
+
+ {% endfor %} +
+ {% else %} +
+
{% trans "You haven't enrolled in any subjects yet." %}
+
+ {% endif %} +
+ + +
+

{% trans "Available Subjects" %}

+ {% if available_subjects %} +
+ {% for subject in available_subjects %} +
+
+ {% if subject.image %} +
+ {{ subject.name_en }} +
+ {% else %} +
+ {% trans "No Image" %} +
+ {% endif %} +
+
{{ subject.name_en }}
+

{{ subject.description_en }}

+
+ {{ subject.price }} OMR + {% trans "Subscribe" %} +
+
+
+
+ {% endfor %} +
+ {% else %} +
+
{% trans "No new subjects available." %}
+
+ {% endif %} +
+ +
+
+
+{% endblock %} diff --git a/core/templates/core/teacher_dashboard.html b/core/templates/core/teacher_dashboard.html new file mode 100644 index 0000000..b2ca55f --- /dev/null +++ b/core/templates/core/teacher_dashboard.html @@ -0,0 +1,129 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block title %}{% trans "Teacher Dashboard" %} - EduPlatform{% endblock %} + +{% block content %} +
+ +
+
+

{% trans "Welcome, Teacher" %} {{ user.last_name|default:user.username }}! 👨‍🏫

+

{% trans "Manage your classes and students here." %}

+
+
+ +
+ +
+
+
+ {% if teacher_profile.avatar %} + Avatar + {% else %} +
+ {{ user.first_name|first|upper }} +
+ {% endif %} +
+ +

{{ user.get_full_name|default:user.username }}

+

{{ teacher_profile.specialization|default:"Teacher" }}

+ +
+ +
+
{% trans "Bio" %}
+

+ {{ teacher_profile.bio|default:"No bio provided yet." }} +

+
+ + +
+
+ + +
+ +
+
+
+
+
{% trans "Subjects Taught" %}
+

{{ subjects.count }}

+
+
+ + + +
+
+
+ +
+
+
+
{% trans "Status" %}
+
Active
+
+
+ + + + +
+
+
+
+ + +
+

{% trans "My Classes" %}

+ {% if subjects %} +
+ {% for subject in subjects %} +
+
+ {% if subject.image %} +
+ {{ subject.name_en }} +
+ {% else %} +
+ {% trans "No Image" %} +
+ {% endif %} +
+
{{ subject.name_en }}
+

{{ subject.description_en }}

+
+ + {{ subject.classroom.name_en }} + + + {{ subject.subscribers.count }} {% trans "Students" %} + +
+ +
+
+
+ {% endfor %} +
+ {% else %} +
+
{% trans "You are not assigned to any subjects yet." %}
+

{% trans "Contact the administrator to assign classes." %}

+
+ {% endif %} +
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/verify_otp.html b/core/templates/core/verify_otp.html new file mode 100644 index 0000000..6d62f06 --- /dev/null +++ b/core/templates/core/verify_otp.html @@ -0,0 +1,52 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block title %}{% trans "Verify Account" %}{% endblock %} + +{% block content %} +
+
+
+
+

{% trans "Account Verification" %}

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +

+ {% trans "We have sent verification codes to your mobile number and email address. Please enter them below." %} +

+ +
+ {% csrf_token %} + +
+ + + {% if student.is_mobile_verified %} + {% trans "Mobile Verified" %} + {% else %} + {% trans "Sent to" %} {{ student.mobile_number }} + {% endif %} +
+ +
+ + + {% if student.is_email_verified %} + {% trans "Email Verified" %} + {% else %} + {% trans "Sent to" %} {{ student.user.email }} + {% endif %} +
+ + +
+
+
+
+
+{% endblock %} diff --git a/core/thawani.py b/core/thawani.py new file mode 100644 index 0000000..6c0f16e --- /dev/null +++ b/core/thawani.py @@ -0,0 +1,69 @@ +import httpx +from core.models import ThawaniConfiguration +from django.conf import settings + +class ThawaniClient: + def __init__(self): + config = ThawaniConfiguration.objects.first() + if not config: + # Fallback or error if not configured + self.api_key = None + self.publishable_key = None + self.is_sandbox = True + else: + self.api_key = config.api_key + self.publishable_key = config.publishable_key + self.is_sandbox = config.is_sandbox + + if self.is_sandbox: + self.base_url = "https://uat-checkout.thawani.om/api/v1" + else: + self.base_url = "https://checkout.thawani.om/api/v1" + + def create_checkout_session(self, subject, user, success_url, cancel_url): + if not self.api_key: + raise Exception("Thawani API Key is not configured.") + + url = f"{self.base_url}/checkout/session" + headers = { + "thawani-api-key": self.api_key, + "Content-Type": "application/json" + } + + # Thawani expects amount in Baisa (1 OMR = 1000 Baisa) + amount_baisa = int(subject.price * 1000) + + payload = { + "client_reference_id": str(user.id), + "mode": "payment", + "products": [ + { + "name": subject.name_en, + "quantity": 1, + "unit_amount": amount_baisa + } + ], + "success_url": success_url, + "cancel_url": cancel_url, + "metadata": { + "subject_id": str(subject.id), + "user_id": str(user.id) + } + } + + response = httpx.post(url, json=payload, headers=headers) + response.raise_for_status() + return response.json() + + def get_checkout_session(self, session_id): + if not self.api_key: + raise Exception("Thawani API Key is not configured.") + + url = f"{self.base_url}/checkout/session/{session_id}" + headers = { + "thawani-api-key": self.api_key + } + + response = httpx.get(url, headers=headers) + response.raise_for_status() + return response.json() diff --git a/core/urls.py b/core/urls.py index defe932..ed37225 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,4 +1,5 @@ from django.urls import path +from django.contrib.auth import views as auth_views from . import views urlpatterns = [ @@ -6,8 +7,14 @@ urlpatterns = [ path('set-language//', views.set_language, name='set_language'), path('subject//', views.subject_detail, name='subject_detail'), path('ajax/get-subjects-by-level/', views.get_subjects_by_level, name='get_subjects_by_level'), + path('ajax/get-cities-by-governorate/', views.get_cities_by_governorate, name='get_cities_by_governorate'), path('register/', views.register_student, name='register_student'), + path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'), + path('verify-otp/', views.verify_otp, name='verify_otp'), path('ajax/get-classroom-subjects/', views.get_classroom_subjects, name='get_classroom_subjects'), path('profile/', views.profile, name='profile'), path('logout/', views.custom_logout, name='logout'), + path('subscribe//', views.subscribe_subject, name='subscribe_subject'), + path('payment/success/', views.payment_success, name='payment_success'), + path('payment/cancel/', views.payment_cancel, name='payment_cancel'), ] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 665968d..b1f44dc 100644 --- a/core/views.py +++ b/core/views.py @@ -4,29 +4,28 @@ from django.conf import settings from django.http import JsonResponse from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required -from django.contrib.auth import logout -from .models import Classroom, Subject, Teacher, Student +from django.contrib.auth import logout, login +from django.urls import reverse +from .models import Classroom, Subject, Teacher, Student, City from .forms import StudentRegistrationForm +from .wablas import send_whatsapp_message +from .thawani import ThawaniClient +import random +import string def index(request): - # Fetch levels with their related subjects using prefetch_related for efficiency levels = Classroom.objects.prefetch_related('subjects').all() - - context = { - 'levels': levels, - } + context = {'levels': levels} return render(request, 'core/index.html', context) def set_language(request, lang_code): next_url = request.GET.get('next', '/') response = redirect(next_url) - if lang_code in [lang[0] for lang in settings.LANGUAGES]: translation.activate(lang_code) if hasattr(request, 'session'): request.session['_language'] = lang_code response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code) - return response def subject_detail(request, pk): @@ -42,34 +41,206 @@ def get_subjects_by_level(request): return JsonResponse(list(subjects), safe=False) def get_classroom_subjects(request): - """Public endpoint for registration form""" classroom_id = request.GET.get('classroom_id') if not classroom_id: return JsonResponse([], safe=False) subjects = Subject.objects.filter(classroom_id=classroom_id).values('id', 'name_en', 'price') return JsonResponse(list(subjects), safe=False) +def get_cities_by_governorate(request): + governorate_id = request.GET.get('governorate_id') + if not governorate_id: + return JsonResponse([], safe=False) + cities = City.objects.filter(governorate_id=governorate_id).values('id', 'name_en', 'name_ar') + return JsonResponse(list(cities), safe=False) + +def generate_otp(): + return ''.join(random.choices(string.digits, k=6)) + def register_student(request): if request.method == 'POST': form = StudentRegistrationForm(request.POST, request.FILES) if form.is_valid(): student = form.save() - # Redirect to success page or login - return redirect('index') # Placeholder + + # Generate OTPs + mobile_otp = generate_otp() + email_otp = generate_otp() + + student.mobile_otp_code = mobile_otp + student.email_otp_code = email_otp + student.save() + + # Send OTP via WhatsApp + if student.mobile_number: + # Attempt to send WhatsApp message + # Note: This will only work if Wablas is configured in Admin + send_whatsapp_message( + student.mobile_number, + f"Your Verification Code is: {mobile_otp}" + ) + + # Simulate sending OTPs (Log to console) + print(f"========================================") + print(f"SIMULATED OTP SENDING:") + print(f"User: {student.user.username}") + print(f"Mobile OTP for {student.mobile_number}: {mobile_otp}") + print(f"Email OTP for {student.user.email}: {email_otp}") + print(f"========================================") + + # Log the user in + login(request, student.user) + + # Redirect to verification page + return redirect('verify_otp') else: form = StudentRegistrationForm() return render(request, 'core/registration.html', {'form': form}) @login_required -def profile(request): +def verify_otp(request): try: - student_profile = request.user.student_profile + student = request.user.student_profile except Student.DoesNotExist: - student_profile = None + return redirect('index') + + if student.is_mobile_verified and student.is_email_verified: + return redirect('profile') + + error = None + if request.method == 'POST': + entered_mobile_otp = request.POST.get('mobile_otp') + entered_email_otp = request.POST.get('email_otp') + + mobile_ok = student.is_mobile_verified + email_ok = student.is_email_verified + + if not mobile_ok: + if entered_mobile_otp == student.mobile_otp_code: + student.is_mobile_verified = True + mobile_ok = True + else: + error = "Invalid Mobile OTP" + + if not email_ok and (error is None or "Mobile" not in error): + if entered_email_otp == student.email_otp_code: + student.is_email_verified = True + email_ok = True + else: + error = "Invalid Email OTP" + + if mobile_ok and email_ok: + student.mobile_otp_code = "" # Clear codes + student.email_otp_code = "" + student.save() + return redirect('profile') + else: + student.save() # Save partial verification if any + + return render(request, 'core/verify_otp.html', {'error': error, 'student': student}) + +@login_required +def profile(request): + user = request.user - return render(request, 'core/profile.html', {'student_profile': student_profile}) + # Check for Teacher + if hasattr(user, 'teacher_profile'): + teacher_profile = user.teacher_profile + subjects = teacher_profile.subjects.all() + return render(request, 'core/teacher_dashboard.html', { + 'teacher_profile': teacher_profile, + 'subjects': subjects + }) + + # Check for Student + elif hasattr(user, 'student_profile'): + student_profile = user.student_profile + subscribed_subjects = student_profile.subscribed_subjects.all() + + # Get available subjects (in same classroom, not yet subscribed) + available_subjects = [] + if student_profile.classroom: + available_subjects = Subject.objects.filter( + classroom=student_profile.classroom + ).exclude( + id__in=subscribed_subjects.values_list('id', flat=True) + ) + + return render(request, 'core/student_dashboard.html', { + 'student_profile': student_profile, + 'subscribed_subjects': subscribed_subjects, + 'available_subjects': available_subjects + }) + + # Fallback (Superuser or Admin without profile) + else: + student_profile = None + return render(request, 'core/profile.html', {'student_profile': student_profile}) def custom_logout(request): logout(request) return redirect('index') + +@login_required +def subscribe_subject(request, subject_id): + try: + student = request.user.student_profile + except Student.DoesNotExist: + return redirect('index') + + subject = get_object_or_404(Subject, pk=subject_id) + + # Check if already subscribed + if subject in student.subscribed_subjects.all(): + return redirect('profile') + + try: + thawani = ThawaniClient() + success_url = request.build_absolute_uri(reverse('payment_success')) + '?session_id={session_id}' + cancel_url = request.build_absolute_uri(reverse('payment_cancel')) + + session = thawani.create_checkout_session(subject, request.user, success_url, cancel_url) + + session_id = session.get('data', {}).get('session_id') + if not session_id: + return render(request, 'core/error.html', {'message': 'Could not create payment session.'}) + + return redirect(f"{thawani.checkout_base_url}/{session_id}") + + except Exception as e: + print(f"Payment Error: {e}") + return render(request, 'core/error.html', {'message': f'Payment initialization failed: {str(e)}'}) + +@login_required +def payment_success(request): + session_id = request.GET.get('session_id') + if not session_id: + return redirect('profile') + + try: + thawani = ThawaniClient() + response = thawani.get_checkout_session(session_id) + data = response.get('data', {}) + payment_status = data.get('payment_status') + metadata = data.get('metadata', {}) + subject_id = metadata.get('subject_id') + + if payment_status == 'paid' and subject_id: + try: + student = request.user.student_profile + subject = get_object_or_404(Subject, pk=subject_id) + student.subscribed_subjects.add(subject) + except Exception: + pass # Already handled or user mismatch? + return redirect('profile') + else: + return render(request, 'core/error.html', {'message': f'Payment was not successful. Status: {payment_status}'}) + + except Exception as e: + print(f"Payment Verification Error: {e}") + return render(request, 'core/error.html', {'message': f'Payment verification failed: {str(e)}'}) + +@login_required +def payment_cancel(request): + return redirect('profile') \ No newline at end of file diff --git a/core/wablas.py b/core/wablas.py new file mode 100644 index 0000000..c530bd7 --- /dev/null +++ b/core/wablas.py @@ -0,0 +1,44 @@ +import httpx +from .models import WablasConfiguration + +def send_whatsapp_message(phone_number, message): + """ + Sends a WhatsApp message using the Wablas API. + """ + try: + settings = WablasConfiguration.objects.first() + if not settings or not settings.api_token: + print("Wablas settings not configured.") + return False + + # Wablas API usually takes: phone, message + # And Authorization header. + + headers = { + "Authorization": settings.api_token, + } + + # Depending on the specific Wablas endpoint version, payload might differ. + # Common v2/v3 payload: + payload = { + "phone": phone_number, + "message": message, + } + + # If secret is used, sometimes it's passed in body + if settings.secret_key: + payload["secret"] = settings.secret_key + + # Using a timeout to prevent hanging + response = httpx.post(settings.api_url, data=payload, headers=headers, timeout=10) + + if response.status_code >= 200 and response.status_code < 300: + print(f"WhatsApp message sent to {phone_number}: {response.json()}") + return True + else: + print(f"Failed to send WhatsApp message. Status: {response.status_code}, Body: {response.text}") + return False + + except Exception as e: + print(f"Error sending WhatsApp message: {e}") + return False diff --git a/requirements.txt b/requirements.txt index 7ce7d6c..45b378b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 django-jazzmin +httpx