From f4cb2d0af8f3bc28bfac8238abcb502703e31976 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Feb 2026 16:21:56 +0000 Subject: [PATCH] add packeges and youtube --- core/__pycache__/admin.cpython-311.pyc | Bin 7403 -> 7859 bytes core/__pycache__/models.cpython-311.pyc | Bin 16751 -> 18382 bytes core/__pycache__/thawani.cpython-311.pyc | Bin 2999 -> 3178 bytes core/__pycache__/urls.cpython-311.pyc | Bin 2548 -> 2670 bytes core/__pycache__/views.cpython-311.pyc | Bin 21638 -> 24105 bytes core/admin.py | 10 ++- core/migrations/0015_package.py | 31 ++++++++ core/migrations/0016_package_classroom.py | 19 +++++ .../__pycache__/0015_package.cpython-311.pyc | Bin 0 -> 2073 bytes .../0016_package_classroom.cpython-311.pyc | Bin 0 -> 1051 bytes core/models.py | 38 ++++++++-- core/templates/core/live_classroom.html | 3 +- core/templates/core/student_dashboard.html | 45 ++++++++++++ core/templates/core/subject_detail.html | 12 +++- core/thawani.py | 24 ++++--- core/urls.py | 3 +- core/views.py | 67 +++++++++++++++--- media/resources/Voucher-_2.pdf | Bin 0 -> 9129 bytes media/resources/بسم_الله_الرحمن_الرحيم.docx | Bin 0 -> 15796 bytes test_regex.py | 39 ++++++++++ test_regex_v2.py | 29 ++++++++ test_regex_v3.py | 43 +++++++++++ 22 files changed, 335 insertions(+), 28 deletions(-) create mode 100644 core/migrations/0015_package.py create mode 100644 core/migrations/0016_package_classroom.py create mode 100644 core/migrations/__pycache__/0015_package.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0016_package_classroom.cpython-311.pyc create mode 100644 media/resources/Voucher-_2.pdf create mode 100644 media/resources/بسم_الله_الرحمن_الرحيم.docx create mode 100644 test_regex.py create mode 100644 test_regex_v2.py create mode 100644 test_regex_v3.py diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 066022d164beefec0648032c6e64ecbd9fa47799..76092ac30541d609659df287bcf3dcef9d2d919c 100644 GIT binary patch delta 2127 zcmbVN&2JM&6yI4pwv*VzahzZ$0haKQZulxtz5=aip#*5`Qa%ce3TCrA37f9H>DzUy zv{K_zq)I(dqB&IRwGs$X^^#LtseeF))FAbc?g8~uA<;{aIB{tE-s}nz!>Mcgw==() z_vU@g^WO*NUtM)8lW9}%`EvRT+wPcKRbUV22G8fK*|98BRAof5lGhX~#o1Kn-1XJT zoT}ikYwr6EEH}5XCYg`sk8v(8vkk0E972?9J>imTAW11-Dtg|Ak}xF`aF+K z6-j+>Br>cF(}UONL*%26E~8@?0RC>5p+rrKPC?+B-ttcO};ytHuou<0@I;Dl0bkwvyxS^-TWFEN0TFNegAI!NZGCK= zc+xiUG8v`Eg7}nc*Wduj#tg?nJ$*QaegscN(6lfH`^Am+_t<*zu)ROK(qq^zdeXbs z?I*F#1WoquA#RVjl0Fgoya#Aa2_JjI6lG z`OrC^_8%8Nb=P~VFE~gZ{;@dG6WvT#xIOo0Wsd5&T~EPzdj>|Y;530ua{@;iJ&iEeGdTaG`;54| z`ty>kA@+huKD#|{k|OB=3BqLp17Ugj0FfZREnE#F33PSYfVh`lJv-K7#m5p>%UIG% zU}sGR>9C#h*R85uLl2(b?l6}7P*>1udlkd`#ji;YKyDl|G7js)1Z!P8r8l}a; zriFO-Vt%MmqEFZmNVKf44OUrGMs3pFZi-#S>MV319ki8Pi#yV7`BxF7fKTF|00)r$ zUN&&q1!aq(b`!KpnPXJZ@{Y=ymEo3_u52_n|YW*gnt*4{|oV};*YVM%RaTb7jPVWDf|BcdM@Jc=o5@%h~{SpNZ2 CKi@|H delta 1793 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 NkM`OB;?(}je*k`px|09^ delta 1621 zcmZXUe@t6d6vy9r?JKl~Uu#PtKq(9g4}lKG#t3wp{lFi?DS|UgoOQGhR#sNHZP8)t zVD67*FxhtIoLK_M7MSQ}mTpdy>C7_Y7BZ44YH2k3mw(JZ>=F{Af2j97U{iOS_vv}( zoO|xQ-*fZkM&Y-EkUpK3W)S!ow!Z0=?8nk`;Ny#BmqF*zwh3}xELBX&fDmS8mE?uY zJS{%GaA+YKzq~MHE02E~zZgHaa99rEvspQYE-FJq#=a~&gkq<%yZ~J&8b5=h_=V9@ z=Mg>Ng9vm99?c{~#IU$U*i{qJh(Z{;#ihxD5Y~i+?v)n-9aan4Y}xpdH9Mu&1AJE! zMog7@MibuG3|R5JDPOFvz;8^OH!vfuuT5m`_lJU^4#nm3_A9Os^Cd*4By<5~H*@bv zNL|by>XjvC;n~c`fhWy7UW{;KQ205+IG<6H6hNO7H(HL?B@KdpRa8&Eq^LC+lEMn2 zsXUfrO9~pjT}AYXxT)airV~${>Y517mo?onIpS!s7%Mb6XJ1 zt0YrW_&;KgEUzCtXwLsgit>mXZ{<&Ot@{e< zU<#)Ss#=ychA?oA*G0sj#M72Xw`Ro(i~rVoic9*z{XRrAVGXl{H4|DavYAPL{d$i` z`VTbX0aG5Hv=$vW$SZAU)UGAzrG(b!-M2T9#*~0R#N3K2xW89XHD*1OwU0yAE-CLP zM=zm|BcSE5@xe05#W)5m&o7s$?oxuTm8N<Q-IdlN!ZMhNC|{Gx)pbjJxQ)%!ePQ& z6x26+eM+-e@p;%Wa)t><@YQ0uoOa6ICcHyPCu9)Ra}1J2Xph}2HbO=^_4__-cz1Or z45DLQF0Ltg58Rkv-iT?X?b0Nl62;Ea(=dXfeYatZ>JScLk9`!zu&*K)v&#}7esV{L1OfG_lI&0laC=WFe7iEpOz zc)bB@)^3+>@XSx}(Ark`5`S&7;^NwTxQbbIRS?F<>NXj!Q%OP$hwIM3buQ`OprB*@ zxXw)>WvSRM={A46fgSa&FsBy!y1oEzVzRykuAy^-Y?!5N!ZqyOa0O;#2 zLES})UBa2hyV7^ujN`jat?)fBQx8AjYG*AR#~scVzN0hFrw#LzOqh-R?%W1CGG!B< z-_qVm-<*W>jNha9SPG?;5=hyl`#4CJ+Htj6s$A*;)y=7e|9j2;Ol|7%dqO^CBm0HV z1^xu8(YN*XPCM}X_AsKIBffj&UwjaJ4ywVN6dp#jeMK;6X%N6eQrW7f)NXo_&*8FY1H&=6&CL^X8j*J@hS|cxG7y zK}7CK^^Nf|aZ@51Pzl!epZuP5Kjx}@SZ|sm-I+<%u1060d%ze6R&o)OL$h8wKXa}mlgzA0*U}Vsp5t7w4nXBD}%QaupLzW z2K*Ks7ez6FPV&3PMSPCGHD0JCA)Mum%p`4PI6{BmI)6uUxXAa&DLl=G%`_Z1W6n&^ TVhnBn#=XA&<3Ck;81DK7tqQZ( delta 570 zcmaDQv0a>RIWI340}$k0ZpysPv5_y0nTr$1Wd`EUvnJOuw@+TrqAtx=!^1Bu9CMh{8EcqpSSAOusDad2bIxH(;hMu%%TXc-Q3oQkK!$-q4QmZY4ci*-Wk91> zgNz4*TFx5I1zeNoaw@SgG60#}K&C1q&*VzhqRC=xI$}(>xQa`YvQm>v;xkijv6mL7 z76BQP9ob@lisrHDOuo#f9-<61N|URI4@li&hbq6t0#aL~02C`y1W7QL7UdMFfLJ_< zx%s7eCGkm#nZ=2>*b5RXbMg~YZgCW)78m5_6{i-10%URsdkEvZ$@N@nlh3moPY&iV z(R(VVxIlf0*3A4l`FEw|W|Yh+pJ_A4<^wCUB;N-H5Fsfb%=Li*NOZ7&RoOg)!}oBD8_TE#~ypk|JFY7wmpki2J#K_7`X7=K+0LWB`%`nNVarS(nR) z(Q0xP*LpS+kbuQxeQpPSMOOI_3~<72avirSqxR$_+`3XWAVoqTLI6no;;_lhPbtkw cwJWlp{F>Xr-hq)3s1rN+5iI@%hb-7o0PtUq>i_@% diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 02ad606aa9a1b7e1b935cf618058e120c9bc3b78..91670a38f86b2e384fba7eca879aafa9b8b72ee1 100644 GIT binary patch delta 227 zcmew&{7yuDIWI340}z;}HfMUUGcY^`abQ3a%J}THQC*RF;saJq{Z!^GX_$11!5X$@ z%nS^xffxePMWbX=41*aojW!1{b2FJM7bGTUC#I+B+hpdISV1}QnJISq#idEb$wiq- zsrt7B;p}**ip?>s4vdVlljpMSVk(lI9Ks&Jr2({n5r~V!H?L!#$S9<@f%S@k`vt$m e3oJ<&S(2`>Bu%#9T+6R5#?REi4T42#K-B;S0Xdie delta 115 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 U4N}AhQqF2-Pw#(AR|#AH8x-@H8~^|S diff --git a/core/admin.py b/core/admin.py index df378f7..1db7bb5 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, Governorate +from .models import Classroom, Teacher, Subject, Resource, Student, City, Governorate, Package class ActionsModelAdmin(admin.ModelAdmin): """ @@ -93,3 +93,11 @@ class StudentAdmin(ActionsModelAdmin): if obj and obj.classroom: form.base_fields['subscribed_subjects'].queryset = Subject.objects.filter(classroom=obj.classroom) return form + +@admin.register(Package) +class PackageAdmin(ActionsModelAdmin): + list_display = ('name_en', 'name_ar', 'classroom', 'price', 'is_active', 'actions_column') + list_filter = ('classroom', 'is_active') + list_editable = ('is_active',) + filter_horizontal = ('subjects',) + search_fields = ('name_en', 'name_ar') diff --git a/core/migrations/0015_package.py b/core/migrations/0015_package.py new file mode 100644 index 0000000..bbbc38c --- /dev/null +++ b/core/migrations/0015_package.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.7 on 2026-02-04 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0014_alter_city_options_alter_classroom_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Package', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(max_length=200, verbose_name='الاسم (إنجليزي)')), + ('name_ar', models.CharField(max_length=200, verbose_name='الاسم (عربي)')), + ('description_en', models.TextField(blank=True, verbose_name='الوصف (إنجليزي)')), + ('description_ar', models.TextField(blank=True, verbose_name='الوصف (عربي)')), + ('price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='السعر')), + ('image', models.ImageField(blank=True, null=True, upload_to='packages/', verbose_name='الصورة')), + ('is_active', models.BooleanField(default=True, verbose_name='نشط')), + ('subjects', models.ManyToManyField(related_name='packages', to='core.subject', verbose_name='المواد')), + ], + options={ + 'verbose_name': 'باقة', + 'verbose_name_plural': 'الباقات', + }, + ), + ] diff --git a/core/migrations/0016_package_classroom.py b/core/migrations/0016_package_classroom.py new file mode 100644 index 0000000..6c868df --- /dev/null +++ b/core/migrations/0016_package_classroom.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.7 on 2026-02-04 16:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0015_package'), + ] + + operations = [ + migrations.AddField( + model_name='package', + name='classroom', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='packages', to='core.classroom', verbose_name='الصف'), + ), + ] diff --git a/core/migrations/__pycache__/0015_package.cpython-311.pyc b/core/migrations/__pycache__/0015_package.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c77f8135421a2241be647f0db7ef559d200ecb8 GIT binary patch literal 2073 zcmZuxO>7fK6y9C`#OpYTO_By2$3UGz%%4GN0ad6~0a1Y(MAaxouGH0dXX0$U-gWmU z5K1N5LWMYSYYsj1(gdibmQvL8)=Q-v$I>3wd#c(~Zboq8)Hl1f32|n<&-3PeGvCbn z-p<;nwo7@I-L5-nSZtO~4zpc>%|KWUMoSMI5j9uL^*Iw~I>j zpayn84f-gtGY>e2>dBt9Qaz|$JD`SrlrI5W50Btc{01JY@9sIl9v|7ig*A-Rc)XtJ zK~DI{#1>?>zPD$8`+U?u7quV1iF5VI*X5fR$|8OXAHWBH5b9H}$3T%moCk3adE!h* zRggF=>jv>oA3uKLZ3(FsF(g^BmLk!c#bUVSyLVX4Yy_FX}81(k^>D0udQ zCtN(A4|@q@Te>701X%=o{WU|WB4b&qkY!IaiJ>4>xk0?d5;01;NhA$bVIgKEqHS)# zA?7KQ#KU;Y(UjzY2J08HoxA9B~nt6RtX4Ro4tjnJG;vhffLJQlND%cJa!-E!v;IM7uIZ5^+da->Ih4g9?TaNQ$n((ED<{jD9GpU?jL!l z7r)2^Xh+WKx=N51ctbNtTb|XKZA0}MXx87sz!Y1G!PS zrW+L#vP}vs(S=tRZUH{Yccius$7;(46U{fQV|(9DsQ3r^<9dWc>s)67C6 zv*2VFXj0i0Lo`!rWJ*q^M3b_c+D~)SKb~}Q)9aITW|8J9ja7o$;wDXg)f$eE z47PT2`wzPZPqiYU(ZLqS%y>$SKjXl>V5Sus-W9!di9N=tPR=jGrz+w_q?Pq!xa8NA zUq*o{CmGDgo`&Ab*qNX(-!t9?7lh)>IWVmd&-0)ZpQqeZ@2?fz!$(_Ouel$0l9SI^ K;KdG6pZ9-W5rZHA literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0016_package_classroom.cpython-311.pyc b/core/migrations/__pycache__/0016_package_classroom.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71020d829518b64c68e23a291f00a7f05fa4e8cb GIT binary patch literal 1051 zcmZuvy>HV%6hEIG$97W$r4mBz(9(j)5|;rcgcP+Y;;TzrBr;$*xob#j`+^+H9Cc>h(^4SGM zgItW3ndvQkD9LJt)*8udBu8|TKQMM7YJV7cV4kl4-YpOdY8c_>vn+QgM60uq^1|HQ zox4ukt*^Tc%JZ3E);%|hLdN_RZVZgk`%B_TY3Pa#$fUP-wAK50^r@)xT+MTXb*=@m z=W&w-4uC^D@J2{Iw?l~&xIX2$!+7Bh4Qni-(o$3bQ~_k_*JM;~tw}wyR2=1oBkhjE zEysbl*rOm99OrfHdLmY6gtlowXi#s`C=|`(MwyUBaYv{L)mntqn~mTJ-QYTDxj}<* zOlsVaIYKvie!jdsUtXwii?!*X!{5=d0Q>Zj{fhb2j%#tyiEa9h*5eKf?YAskj{qHN z3t;TCpJ4;4_cBzSAz-EUMkx9uiD-KC0m$ZXCOR^w6BUo^n@1 z1W8nVOy7JgW;mUt1O2<6;!+~DQu?t=893ntgmH7|mk>)}(|V@FE%K@xAe|cb(>^^2 wZ-T18j%q*>RZ$eEQJFqOQ|aoV+lLpeL@y|Mg8nDlR`>F3UpP)K@X8$i0fRppCjbBd literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 86d6d42..0bbdba6 100644 --- a/core/models.py +++ b/core/models.py @@ -65,18 +65,24 @@ class Subject(models.Model): """Extracts the video ID from a YouTube URL.""" if not self.youtube_live_url: return None + + url = self.youtube_live_url.strip() + # Handle various formats: # https://youtu.be/VIDEO_ID # https://www.youtube.com/watch?v=VIDEO_ID # https://www.youtube.com/live/VIDEO_ID - import re + # https://www.youtube.com/embed/VIDEO_ID + # https://www.youtube.com/shorts/VIDEO_ID patterns = [ - r'(?:v=|\/)([0-9A-Za-z_-]{11}).*', + r'(?:v=|\/)([0-9A-Za-z_-]{11})(?:[?&]|$)', r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})', r'(?:live\/)([0-9A-Za-z_-]{11})', + r'(?:embed\/)([0-9A-Za-z_-]{11})', + r'(?:shorts\/)([0-9A-Za-z_-]{11})', ] for pattern in patterns: - match = re.search(pattern, self.youtube_live_url) + match = re.search(pattern, url) if match: return match.group(1) return None @@ -107,14 +113,18 @@ class Resource(models.Model): """Extracts the video ID from the link if it is a YouTube URL.""" if not self.link or self.resource_type != 'VIDEO': return None + + url = self.link.strip() patterns = [ - r'(?:v=|\/)([0-9A-Za-z_-]{11}).*', + r'(?:v=|\/)([0-9A-Za-z_-]{11})(?:[?&]|$)', r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})', r'(?:live\/)([0-9A-Za-z_-]{11})', + r'(?:embed\/)([0-9A-Za-z_-]{11})', + r'(?:shorts\/)([0-9A-Za-z_-]{11})', ] for pattern in patterns: - match = re.search(pattern, self.link) + match = re.search(pattern, url) if match: return match.group(1) return None @@ -219,3 +229,21 @@ class PlatformSettings(SingletonModel): def __str__(self): return "إعدادات المنصة" + +class Package(models.Model): + name_en = models.CharField("الاسم (إنجليزي)", max_length=200) + name_ar = models.CharField("الاسم (عربي)", max_length=200) + description_en = models.TextField("الوصف (إنجليزي)", blank=True) + description_ar = models.TextField("الوصف (عربي)", blank=True) + price = models.DecimalField("السعر", max_digits=10, decimal_places=2, default=0.00) + classroom = models.ForeignKey(Classroom, on_delete=models.CASCADE, related_name='packages', verbose_name="الصف", null=True, blank=True) + image = models.ImageField("الصورة", upload_to='packages/', blank=True, null=True) + subjects = models.ManyToManyField(Subject, related_name='packages', verbose_name="المواد") + is_active = models.BooleanField("نشط", default=True) + + class Meta: + verbose_name = "باقة" + verbose_name_plural = "الباقات" + + def __str__(self): + return self.name_ar or self.name_en \ No newline at end of file diff --git a/core/templates/core/live_classroom.html b/core/templates/core/live_classroom.html index 41a40e3..cb43040 100644 --- a/core/templates/core/live_classroom.html +++ b/core/templates/core/live_classroom.html @@ -28,10 +28,11 @@ diff --git a/core/templates/core/student_dashboard.html b/core/templates/core/student_dashboard.html index 5af9f45..89d6953 100644 --- a/core/templates/core/student_dashboard.html +++ b/core/templates/core/student_dashboard.html @@ -151,6 +151,51 @@ {% endif %} + + {% if packages %} +
+

الباقات المتاحة 🎁

+
+ {% for package in packages %} +
+
+ {% if package.image %} +
+ {{ package.name_en }} +
+ {% else %} +
+ 📦 +
+ {% endif %} +
+
{{ package.name_ar }}
+

{{ package.description_ar|truncatechars:100 }}

+ +
+ تشمل المواد: +
+ {% for subject in package.subjects.all|slice:":3" %} + {{ subject.name_ar }} + {% endfor %} + {% if package.subjects.count > 3 %} + +{{ package.subjects.count|add:"-3" }} + {% endif %} +
+
+ +
+ {{ package.price }} OMR + شراء الباقة +
+
+
+
+ {% endfor %} +
+
+ {% endif %} +

المواد المتاحة

diff --git a/core/templates/core/subject_detail.html b/core/templates/core/subject_detail.html index 0782b8d..80139d5 100644 --- a/core/templates/core/subject_detail.html +++ b/core/templates/core/subject_detail.html @@ -65,13 +65,14 @@
{% endfor %} - {% if subject.youtube_live_url %} + {% if subject.get_youtube_id %}
-
@@ -255,9 +256,14 @@ document.addEventListener('DOMContentLoaded', function() { modalBody.innerHTML = ''; // Clear previous content if (type === 'VIDEO' && youtubeId) { + // Use youtube-nocookie.com and add referrerpolicy modalBody.innerHTML = `
- +
`; } else if (type === 'FILE' && fileUrl) { diff --git a/core/thawani.py b/core/thawani.py index 6c0f16e..2430cdc 100644 --- a/core/thawani.py +++ b/core/thawani.py @@ -20,7 +20,7 @@ class ThawaniClient: else: self.base_url = "https://checkout.thawani.om/api/v1" - def create_checkout_session(self, subject, user, success_url, cancel_url): + def create_checkout_session(self, product, user, success_url, cancel_url): if not self.api_key: raise Exception("Thawani API Key is not configured.") @@ -31,24 +31,32 @@ class ThawaniClient: } # Thawani expects amount in Baisa (1 OMR = 1000 Baisa) - amount_baisa = int(subject.price * 1000) + amount_baisa = int(product.price * 1000) + metadata = { + "user_id": str(user.id) + } + + # Handle Subject or Package + # Package has 'subjects' ManyToMany field + if hasattr(product, 'subjects'): + metadata["package_id"] = str(product.id) + else: + metadata["subject_id"] = str(product.id) + payload = { "client_reference_id": str(user.id), "mode": "payment", "products": [ { - "name": subject.name_en, + "name": product.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) - } + "metadata": metadata } response = httpx.post(url, json=payload, headers=headers) @@ -66,4 +74,4 @@ class ThawaniClient: response = httpx.get(url, headers=headers) response.raise_for_status() - return response.json() + return response.json() \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 0729d7d..dcf9f71 100644 --- a/core/urls.py +++ b/core/urls.py @@ -16,6 +16,7 @@ urlpatterns = [ path('profile/edit/teacher/', views.edit_teacher_profile, name='edit_teacher_profile'), path('logout/', views.custom_logout, name='logout'), path('subscribe//', views.subscribe_subject, name='subscribe_subject'), + path('package//subscribe/', views.subscribe_package, name='subscribe_package'), path('payment/success/', views.payment_success, name='payment_success'), path('payment/cancel/', views.payment_cancel, name='payment_cancel'), path('classroom//', views.live_classroom, name='live_classroom'), @@ -23,4 +24,4 @@ urlpatterns = [ path('resource//edit/', views.edit_resource, name='edit_resource'), path('resource//delete/', views.delete_resource, name='delete_resource'), path('resource//view/', views.view_resource, name='view_resource'), -] \ No newline at end of file +] diff --git a/core/views.py b/core/views.py index 7f1f356..1b11388 100644 --- a/core/views.py +++ b/core/views.py @@ -7,7 +7,8 @@ from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required from django.contrib.auth import logout, login from django.urls import reverse -from .models import Classroom, Subject, Teacher, Student, City, Resource +from django.db.models import Q +from .models import Classroom, Subject, Teacher, Student, City, Resource, Package from .forms import StudentRegistrationForm, TeacherProfileForm, ResourceForm from .wablas import send_whatsapp_message from .thawani import ThawaniClient @@ -175,10 +176,20 @@ def profile(request): id__in=subscribed_subjects.values_list('id', flat=True) ) + # Get active packages + # Filter by student's classroom or global packages (classroom is None) + packages = Package.objects.filter(is_active=True) + if student_profile.classroom: + packages = packages.filter(Q(classroom=student_profile.classroom) | Q(classroom__isnull=True)) + else: + # If student has no classroom, show only global packages + packages = packages.filter(classroom__isnull=True) + return render(request, 'core/student_dashboard.html', { 'student_profile': student_profile, 'subscribed_subjects': subscribed_subjects, - 'available_subjects': available_subjects + 'available_subjects': available_subjects, + 'packages': packages }) # Fallback (Superuser or Admin without profile) @@ -237,6 +248,32 @@ def subscribe_subject(request, subject_id): print(f"Payment Error: {e}") return render(request, 'core/error.html', {'message': f'فشل بدء عملية الدفع: {str(e)}'}) +@login_required +def subscribe_package(request, package_id): + try: + student = request.user.student_profile + except Student.DoesNotExist: + return redirect('index') + + package = get_object_or_404(Package, pk=package_id) + + 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(package, 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': 'تعذر إنشاء جلسة الدفع.'}) + + 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'فشل بدء عملية الدفع: {str(e)}'}) + @login_required def payment_success(request): session_id = request.GET.get('session_id') @@ -250,14 +287,26 @@ def payment_success(request): payment_status = data.get('payment_status') metadata = data.get('metadata', {}) subject_id = metadata.get('subject_id') + package_id = metadata.get('package_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? + if payment_status == 'paid': + student = request.user.student_profile + + if subject_id: + try: + subject = get_object_or_404(Subject, pk=subject_id) + student.subscribed_subjects.add(subject) + except Exception: + pass + + if package_id: + try: + package = get_object_or_404(Package, pk=package_id) + for subject in package.subjects.all(): + student.subscribed_subjects.add(subject) + except Exception: + pass + return redirect('profile') else: return render(request, 'core/error.html', {'message': f'لم تتم عملية الدفع بنجاح. الحالة: {payment_status}'}) diff --git a/media/resources/Voucher-_2.pdf b/media/resources/Voucher-_2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..32f9d659a598511309fb29813ae8e20d05e94aec GIT binary patch literal 9129 zcmbW71yodB`{)Vjk{(cCq+?)a=n^Caqy*_^n4uYB2x&o4VCZfT!JF-wEf`Sz8X7|Th;vY9zS+FMD%^u|dl*EekC`iGojxH#; zClIXaVuOM!!(m7}I8aWGg47G;3Ab^jAT=f_H8w*osnP|{DakxQ;=O_EYnvA4XtAHUhI9 z8%=Zza)?vg1FLdHR@6*;>qj_mdleg2Wa+Ku+__^+{A8HqqC33Ys8n%u&f)Vb`sBGp zRHg}Qku-P8tE1dtwX~_gl$eQ;&)E^Dt8%ITQ;3{CKY1|N)9!RDq0a#aZ9{?cvKe z#)|QZdd6LT1T+1sr*_i0h+duz#36Y5?W-^agENs=G^?7xG>!{M9=sR){+H~#hdSDv zLQ$kqZ@z!FOI$^z-R(a(^&i{VpVaJ>l64#03^J&bVcq4V8gWti?c$T4_>v}AqA>1f zr~;38F{nsBkxJ>6I}!rmN}j1pOHzE7jc;DqFJsbZ<^C#k6qgJ03_0LyP39eengGNb zY=#~3F~u>)9g6j{*@vw53OE_HZ4RF#iS6YQlpYhB3P(rXc6e!-!*kRKSS>mpLUb42 zkXjuGm8urfMo>*d-R=4-WlA^4^LN&t0JWwS-$<_YO~Wo&EsK|{nwPW-G`+Q z(31fha$>JEDA0ID3x==Rzb{7^xFirA{-zruR=^RN#vywqlCz&0a=tpA4D~1Z^mFE>|BBH zdl7w(AmK!{?|`KiD~p&%Rk@#3vm>PoyNg^k#Hcg%T$S#T@0jsue@_erglGS}g(v(a z?KBX*05(qAcTnrzWj^=&epkrqrrjWefQwLlGDDED8==<+dM)yUYazMsEu(*~=zne`2v{FI))31%KcWcF z9Mbwz2LCB%|5U{5^7p4SLWQA{|0XqLM-opoEmL5X1rqRzkX=u%=$X>!ORb)q{H>y**)YFW_}dT~8!T zAC3ZE7jmr9SjW~04nzG>sk@2-MgNH9FSPj zyUtY-JBk0YIe@inyqy0^{5p9k5Q#Y!YK)2KuYzSOwTCPE> zJ2Fv&b(&@nRV_aA6Z5wTG`5tLIIKZZVzW-wD$S3!J*T_B&JxMZ=zUkobDk*Y8OS7p zIvc^aX}I5(mk;onc%5yvHqWe<770LM_p(%){kA=Cbz@4WXH{6s7rgd|SLphTP0yAy z%4<#3QHIaIZVhK-pS1c{D>?fJrzY;rag8qzXR1Y-t~)F z&g*DxyOs2C7ZvNMJ9D^l+mheSiDpWn{n3O&(`M1PC&Y}3&NtBFFh!ZfOntes^>}&f zZA(2Rz20+DK2Tscy+8?3rrCF2U1!vDcW0oC({W46%1m=mn$dyBaw$R(VTuu`{`N_t z8qu!$L3&dB%mhkE|7?oH(&r#rj+6C5tVR!0!yw^T26*MO%v4yfZ45Yf=;8^jOpKz? z!vvqLzwRsYp{zbiu6exZy17t$RBfxtpJ3hBo9_@xh)dNk@#bEZRx0$&?D?}wUn9Hu z`XTnuTjQl>yC7fcD|sog>~ESJ9u~GBN|seW2tO14*YMA!Q0bq=E*>opMT())H)^*P z>35U~hj7jGQ57V09D8ZqU>Zy_^RmrGzS7z&q=zY{q-ZcmWsKmr#D(rS2vc54m!^PJ z9zQf`S{vtQn*Mk$(gujv`4z$qCwH!WR&I(M(8*&s zupLmHsu;5$m|LIQ?_>V3Tk{@F!FnW%iTeD<5yMp=aqCR{sPht6q4TEo&l zVTACr8aWf$qI)M`eKRV#0`oz;RMqO(L4F& zA+0uwWpoq{v~{trE|9-$sjK^Hq8kM@OSK?2K6pM=yE(y#2nMQV`#+vE90Ehv^H!JYDHYVbC;ojJBCxtgn`fB1(*DuQifq zm+9$MBev%B%!~m<7P5j)e@a|J@LNL1ocV9;nE3aAKBqgbFsJ=AZ3w1NRXMTU*oTpB zL>)Q>mKEC2bGSeK7wI3Pa_3pgKQIO4IN5T z22beBH25O5zc8bj;ss@#kaNu|aWy)dAJuYG6kghY5lK-QP`l+=km#{4l?LGB`ml#v z`=;(^Mf4|D;3M#|v@~r1AOLc_Lsp*ufjHaAxgxkIe-|g({)utbFL*u6BN;Pm+F;U; ztt7FWI?G;INy8ejgddj`J{*KaV5lrsQS{scI4naK_yMEgS55nr9EuA z!RAJca*W5s+DC;-T@48pb%Kx3Ij6f&oMC4e8V-v0GK zb`I1RcL>qb!Q8nm9Q62CsV~_)N(#5NX_3A!@H;+}@r#c9B+v#M8E4roafkJ#HXg+0 zI1n%73Z8VGHwO@^1^hJdcP$p1R@Wgt+1T3-Z;@ZR74UP7sliS15%qBKDEruJ_iMv$fzgcL+iepGl*~_@yaLMELF@b_Zv@IGg<2WOv%oIG(YBUN8SKOKumk}Tvwbr z`}ovFZbvW>s=J#jes3-CHctq=@sk^c7?)F=#x2_mWF=2eH@2Wy#C>V&F+|PI0(aJ8 zoA~o+reDGD=gMLT;v?o+)`?Y;do#LnFHEH*fLj z&r_VPwGWB128H^J@_i}0A4PB!kuRLk{A~!x{D$X=OpPA+bzp}NV8kJ&5^gG# zpzj>nLF>CS^#Rm`150g~u`s$yk?#|-Y|KvGCqzoFBTbx)-;oecWfMW ztqkT#`Ccd=?Vat6cYD4H(wm{yeWys4Bu%G%VK7Ix_j~43!~DgsNTr-}7Lr%}#!V#B zS&GLlSM6sPdUI}u6M&`ApKZ(Pb$x<4e#(Ghu^W_0O%M@)EU| z@@!Y2#YM36v2%PUxv;8qR>Ttu-$R?2;^N|-4c8mhW6tl(=Ia;B8RIQ<@tCqw5Wv?UG zoR;IAMNcOCu?ufzYxNB3Mei?U(Xce&RyJ>54fJwMZOXUSGVs=N4GL<}AyF{;M$7`? zue1~4FfhL9oJu(fY!DEE9egG-(?IbuN}zBpg43->>V7ji$6|;(M@vGY&nYE&+k-v! zpV%%Wsp%Oe_qb`nel`eLs zIhu@gSWWZ#vPvGuf@fC9kWS4T6?=3R&(0%*z4s$M)hi( zgy^ip>EhNYnfLRLKeC3AuXbBhTekC5%hR!VtxN%S6{Pp~YA09VT**_=h}g1C23k zHhpuxKL{b2GRL>O1xvfle+0WOPQL?)NWt#&p@Z*p!MfG>0<8Azo{RGWj@Vwgy;CO7 zT}KCil4P`>W-vI5=$PK8iknKX>Gb)CbC7*MkcSZHE5}x0Yee`4w;H;?%=*F4&bfbLMg#eO6o4+!S4CbLYrh!U&j>|Fi+QlRwMwa$hN11d+c zRG_QU=~EM+{U|YXJ97mu35}{C3~~D*;{cK-TC}||riZY47mu#b6uJ2*y`$uIXGx%C zR7SCb9-4<tAyCs2!rXHC4cf}ezvw6OU0TTkRQ&C45H3#$M5WN z<=7L}W%&<)JX8A-)03N#iS?}aKJJZ>)k6ZtfA+)WZ-4VTWZ%-Ra&jM#ll6BS5WIZ& z)pmT0jT>K+pDDallWEM+3j+wL=KKAkH9G#(*vV~6=Fi8xA}wHoNVyPS)HdR zgGb*l))Vs{<6bKHcn{4K^m^Fa{cd5^$1JJPDe5QGJylLNu7h!8W)D#Oa$7nY?BBM6vcJH7?IpdJ`3X9B-M{mA+0=Vw>|o{r8%Q!w zoH|eMcqM@=)jkS+%?9Q^A0e$-4sCp|wnF1Z(4cH*F08^M$-+O(n?ji>!7=hJHDT zedOy1axR5xQXyNPlWzGUbbq(;O`*FOTZU3Jl*2KPU3FM1;;@Hrvul~II97oEt_($d zlwGDmDv{T)ErWc2vs{!3B&ss{Jd5SPa-74JP~EK4xjH83l1IKQz{8ws>H_maA;fLt zR1%H-yelF~Im*(3F12Vi>6t{ik-9@LZ=5C9fQVOsLh-cWjM*sm(>wSW`b5)|A%d6y z8N?O;Apu4_EJGg8M@nVKL{6Szur$g_rsdr^+u`npCDZ%^aoGny1{1zpIDEPd&|0U% zaT5y%%E_O7|@p zw!Bwjf3~pEx8CGgwITQEb!aYPDk$=?uXnlD8i#ywBtXWOF`8-r0&MZ?%Y{_PK26a8 zRpT^EOwC-_0O$T28*(r`{e|8z-v+k?7tEiOEmGAXLA*LikFRK&sC)(*A}Mmrhp1E+ zUOrmV&zj?xZQf48fCqv@SlL?N&hK4lw~+2^?8qD3B9@uA8xi+ONR24gSH#rq%%~V% zlJo#N2e@826yvnys-0dWmvWOF4dptJ4o?)O0SGenO_+b-_j0eV#@Sj1M48wn+ahEt zesq~hP-V+iEdk&6^EX{lzo{zCVLfmbYG`PicJl2dgfYSq35%S}nDo^{-n_f<%yBnh z%v0-?!pP@OX|rO66<$)z4w7s;gdMDN&ZcTbRmLDJgAyOZYIutazJz>v*t-6+<#Whn zawOH9sDr~DJ^1jDrdxe>ZHmnCRwr64cZmlY6!Me*!&ZpDo*oT7tm3)K=Eb=dy~DX= zYX&JKxr$5NNBS%__e@rFF^DgKDL*%fZuNLBFXgt({HMOhG#Jr9gDEf>H+S*?kp)g7 z*`xieu26oN$y1wkN$z172sZ6g<-c{1vP%prTG{+!$HhIFE-CLJU&V`vF z$sz20eZNOrmxZxLb5Nb|TP8Ap$h=RLkcKEUV&W0`b2*YjkY{r@Ys%BicCzIiUrSpA z70i;2k9F`U|A?IGR*J>6tFA#H(s2uqxFK1j8~{-@w3Q0_p($)x0us9`CMI?Rb5!iI z0f_x%_JS0n>&f@*N5Lp8S;3mXmO}5rMd=uUW^l_M(~GGn=*J&46q#wBLQ}h}EnnDd8XXkF>#o6N^W}{-c`c=7csGrMZiFH1 zc}}HuEvFx;!H3H@E?Sws;KauxYrTNnkx#;gbB82scM*G$rZ{|-L}{saJ&(-#j4AE8 zRGPZ^;N)c{%JavO8knbCHz-3&@(8ApNKEL=PfRt$mfNnxE^2VQd`(VyIquF~O!4uw zOI^_pp3aIY`Q#;j9<79CjZ29wX5q}JW}}1WIX@}W_lMNww6~yUrzJUceeZ}EcY*_C zMbjD-LtG*tft5S0tSjd!YLQ}O$&(vmDGw}8{eE;;CQ`1WcGd zgSxet%zH0SddtukClh}N$lo@K2w36$?Qm9~{mpFtSRpaKlJYV5yC!+!FDrkmmk{TN z4qQ))O%u$XTKpcd|5~-F`~{bR9R`V2FrtT0KjYw`@d(aIvB-!&bF$po8U5V@q2T0$ zWtoK-$IP7X+~`hcvoASomuJKr13iLkETMVQWSYzEO3!hv8HFq}rHSxU6GZJ}w_+4w z=mKqz)vipAwCTdvmUmM7YF@@Y)2RMBO1z@w9XyUO;yi9myKx9TSpeid>OcsVg*dE< zi&{>-czM=glDcy1L$LoI8Q45h4MQ|WC~-t2z|jvy+hW*nF>{pm zllFcrldIJ*(O!Y6vqM3wWFl84$dGq~(?nG}bGTQcf^vgQ&{q;fb5uG3krSzFIzk_z21{)0cSWA6Opa zC}lV*^r}}H>RDkTj-SZ4zIP6B3!;ZwFX$Vz(CauA&Uh))!$!r4>m^viSH?bw!;)R^ zKYAKwCn$@gWBqa8N#&1OCi29C_oaUD#eSFf=6jxjx*4M?H_WSOrtqP~xNitS$K8HLPwkCF$> zlHYnZtyVxVy!%ER9mZ`rb;=WTMSTs)E1&quzc{Ul0fOfi3nD%rU@9M|EJB{89Z%j$ zpyHPP*c^EHj(q^lB%;`CApjwi7}I-hA|Q;}meuC59nhExU)btuBYS7PbzfixL}fn3 zoS1S$oDxsm(nwat>IaMs*P-A2{;kR&^75zA(9f)L0`;!eZpl8OscCa1v?UWfDKh+ zkhhD6*T%t|f-gjnrFW{D8vI?`uxaTov{SI42HEacf2y``x3GBi()Qh2x#gLbcHwiV z_d8Icvp0<`zHH9eE~3g+53=ISQo>9%6JYU0w7>)X;%OSRCg1P{1+@nz?3lEDLprx1!qXczrc3PIuZlqfV6imtmH+=F$ zaVPXBfi+C%jvv2R*c~YZ+xHj9rMSTy;1Skk{Z&^6bBVu6p17yK`c z8r;PPj&g+A2r437?0{euH|#M0M>qRxuH&ZQ=H>W5f`4aD*DUJ)ccA~*F|K*re~fX> z*L_!z>5tqW=aBN=`0uhB``y$xe_Ai+@)=okK+lOIqtb;h#M*RBu zx^MS)`?s%{IQIG4{#th(L-H@ZgfN!uffc+_4oFX+>~$ZD@czPt*AGhAgBd8ShF8lG z=85z|B2Yj*xI5Alb-iNxaL;R^=Y?f)!oosOA&3x!59@=)j&|NK>^YDByVLcV5i_my z|I11m>%kE~?Bvw31O7p&P+{zu6|fP|42lJzBG>)@3Ly0z{oz2U@c-~dF)=J-EM$RdqK668L8w_5K4p!mM$gTwO6S0qBBpP3~nrAyceIC^(375trtZ6}?s%lTsWb vP;yWq|iF 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 Co`f<0 literal 0 HcmV?d00001 diff --git a/test_regex.py b/test_regex.py new file mode 100644 index 0000000..d9345d8 --- /dev/null +++ b/test_regex.py @@ -0,0 +1,39 @@ +import re + +def get_youtube_id(url): + """Extracts the video ID from a YouTube URL.""" + if not url: + return None + + # regex for extracting youtube id + # Supports: + # - https://www.youtube.com/watch?v=VIDEO_ID + # - https://youtu.be/VIDEO_ID + # - https://www.youtube.com/embed/VIDEO_ID + # - https://www.youtube.com/live/VIDEO_ID + # - https://m.youtube.com/watch?v=VIDEO_ID + + patterns = [ + r'(?:v=|\/)([0-9A-Za-z_-]{11}).*', + r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})', + r'(?:embed\/)([0-9A-Za-z_-]{11})', + r'(?:live\/)([0-9A-Za-z_-]{11})', + ] + + for pattern in patterns: + match = re.search(pattern, url) + if match: + return match.group(1) + return None + +test_urls = [ + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "https://youtu.be/dQw4w9WgXcQ", + "https://www.youtube.com/embed/dQw4w9WgXcQ", + "https://www.youtube.com/live/dQw4w9WgXcQ?feature=share", + "https://m.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ", + "invalid-url" +] + +for url in test_urls: + print(f"URL: {url} -> ID: {get_youtube_id(url)}") diff --git a/test_regex_v2.py b/test_regex_v2.py new file mode 100644 index 0000000..94cc9e0 --- /dev/null +++ b/test_regex_v2.py @@ -0,0 +1,29 @@ +import re + +def get_youtube_id(url): + if not url: + return None + patterns = [ + r'(?:v=|\/)([0-9A-Za-z_-]{11}).*', + r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})', + r'(?:live\/)([0-9A-Za-z_-]{11})', + r'(?:embed\/)([0-9A-Za-z_-]{11})', + r'(?:shorts\/)([0-9A-Za-z_-]{11})', # Added shorts + ] + for pattern in patterns: + match = re.search(pattern, url) + if match: + return match.group(1) + return None + +urls = [ + "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "https://youtu.be/dQw4w9WgXcQ", + "https://www.youtube.com/embed/dQw4w9WgXcQ", + "https://www.youtube.com/live/dQw4w9WgXcQ", + "https://www.youtube.com/shorts/dQw4w9WgXcQ", + "https://m.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be" +] + +for url in urls: + print(f"{url} -> {get_youtube_id(url)}") diff --git a/test_regex_v3.py b/test_regex_v3.py new file mode 100644 index 0000000..dca8160 --- /dev/null +++ b/test_regex_v3.py @@ -0,0 +1,43 @@ +import re + +def get_youtube_id(url): + """Extracts the video ID from a YouTube URL.""" + if not url: + return None + + url = url.strip() + + # Handle various formats: + # https://youtu.be/VIDEO_ID + # https://www.youtube.com/watch?v=VIDEO_ID + # https://www.youtube.com/live/VIDEO_ID + # https://www.youtube.com/embed/VIDEO_ID + # https://www.youtube.com/shorts/VIDEO_ID + patterns = [ + r'(?:v=|\/)([0-9A-Za-z_-]{11})(?:[?&]|$)', + r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})', + r'(?:live\/)([0-9A-Za-z_-]{11})', + r'(?:embed\/)([0-9A-Za-z_-]{11})', + r'(?:shorts\/)([0-9A-Za-z_-]{11})', + ] + for pattern in patterns: + match = re.search(pattern, url) + if match: + return match.group(1) + return None + +urls = [ + ("https://www.youtube.com/watch?v=dQw4w9WgXcQ", "dQw4w9WgXcQ"), + ("https://youtu.be/dQw4w9WgXcQ", "dQw4w9WgXcQ"), + ("https://www.youtube.com/embed/dQw4w9WgXcQ", "dQw4w9WgXcQ"), + ("https://www.youtube.com/shorts/dQw4w9WgXcQ", "dQw4w9WgXcQ"), + ("https://www.youtube.com/live/dQw4w9WgXcQ", "dQw4w9WgXcQ"), + (" https://www.youtube.com/watch?v=dQw4w9WgXcQ ", "dQw4w9WgXcQ"), # Whitespace + ("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1s", "dQw4w9WgXcQ"), + ("https://m.youtube.com/watch?v=dQw4w9WgXcQ", "dQw4w9WgXcQ"), +] + +print("Testing YouTube ID extraction...") +for url, expected in urls: + result = get_youtube_id(url) + print(f"URL: {url.strip()} -> Expected: {expected}, Got: {result} -> {'PASS' if result == expected else 'FAIL'}")