add packeges and youtube
This commit is contained in:
parent
c673d9b2b1
commit
f4cb2d0af8
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,7 +2,7 @@ from django.contrib import admin
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.urls import reverse
|
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):
|
class ActionsModelAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
@ -93,3 +93,11 @@ class StudentAdmin(ActionsModelAdmin):
|
|||||||
if obj and obj.classroom:
|
if obj and obj.classroom:
|
||||||
form.base_fields['subscribed_subjects'].queryset = Subject.objects.filter(classroom=obj.classroom)
|
form.base_fields['subscribed_subjects'].queryset = Subject.objects.filter(classroom=obj.classroom)
|
||||||
return form
|
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')
|
||||||
|
|||||||
31
core/migrations/0015_package.py
Normal file
31
core/migrations/0015_package.py
Normal file
@ -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': 'الباقات',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
19
core/migrations/0016_package_classroom.py
Normal file
19
core/migrations/0016_package_classroom.py
Normal file
@ -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='الصف'),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0015_package.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0015_package.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -65,18 +65,24 @@ class Subject(models.Model):
|
|||||||
"""Extracts the video ID from a YouTube URL."""
|
"""Extracts the video ID from a YouTube URL."""
|
||||||
if not self.youtube_live_url:
|
if not self.youtube_live_url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
url = self.youtube_live_url.strip()
|
||||||
|
|
||||||
# Handle various formats:
|
# Handle various formats:
|
||||||
# https://youtu.be/VIDEO_ID
|
# https://youtu.be/VIDEO_ID
|
||||||
# https://www.youtube.com/watch?v=VIDEO_ID
|
# https://www.youtube.com/watch?v=VIDEO_ID
|
||||||
# https://www.youtube.com/live/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 = [
|
patterns = [
|
||||||
r'(?:v=|\/)([0-9A-Za-z_-]{11}).*',
|
r'(?:v=|\/)([0-9A-Za-z_-]{11})(?:[?&]|$)',
|
||||||
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
||||||
r'(?:live\/)([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:
|
for pattern in patterns:
|
||||||
match = re.search(pattern, self.youtube_live_url)
|
match = re.search(pattern, url)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
return None
|
return None
|
||||||
@ -107,14 +113,18 @@ class Resource(models.Model):
|
|||||||
"""Extracts the video ID from the link if it is a YouTube URL."""
|
"""Extracts the video ID from the link if it is a YouTube URL."""
|
||||||
if not self.link or self.resource_type != 'VIDEO':
|
if not self.link or self.resource_type != 'VIDEO':
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
url = self.link.strip()
|
||||||
|
|
||||||
patterns = [
|
patterns = [
|
||||||
r'(?:v=|\/)([0-9A-Za-z_-]{11}).*',
|
r'(?:v=|\/)([0-9A-Za-z_-]{11})(?:[?&]|$)',
|
||||||
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
||||||
r'(?:live\/)([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:
|
for pattern in patterns:
|
||||||
match = re.search(pattern, self.link)
|
match = re.search(pattern, url)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
return None
|
return None
|
||||||
@ -219,3 +229,21 @@ class PlatformSettings(SingletonModel):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "إعدادات المنصة"
|
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
|
||||||
@ -28,10 +28,11 @@
|
|||||||
<iframe
|
<iframe
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
src="https://www.youtube.com/embed/{{ youtube_id }}?autoplay=1"
|
src="https://www.youtube-nocookie.com/embed/{{ youtube_id }}?autoplay=1&playsinline=1"
|
||||||
title="YouTube video player"
|
title="YouTube video player"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
allowfullscreen>
|
allowfullscreen>
|
||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -151,6 +151,51 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Available Packages Section -->
|
||||||
|
{% if packages %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="fw-bold mb-3">الباقات المتاحة 🎁</h4>
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for package in packages %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="glass-card p-0 overflow-hidden h-100" style="border: 2px solid #ffc107;">
|
||||||
|
{% if package.image %}
|
||||||
|
<div style="height: 150px; overflow: hidden;">
|
||||||
|
<img src="{{ package.image.url }}" class="w-100 h-100 object-fit-cover" alt="{{ package.name_en }}">
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-warning bg-opacity-25 d-flex align-items-center justify-content-center" style="height: 150px;">
|
||||||
|
<span class="text-muted display-4">📦</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="p-3">
|
||||||
|
<h5 class="fw-bold mb-2">{{ package.name_ar }}</h5>
|
||||||
|
<p class="text-muted small mb-3">{{ package.description_ar|truncatechars:100 }}</p>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<small class="text-muted">تشمل المواد:</small>
|
||||||
|
<div class="d-flex flex-wrap gap-1 mt-1">
|
||||||
|
{% for subject in package.subjects.all|slice:":3" %}
|
||||||
|
<span class="badge bg-secondary bg-opacity-10 text-dark">{{ subject.name_ar }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% if package.subjects.count > 3 %}
|
||||||
|
<span class="badge bg-secondary bg-opacity-10 text-dark">+{{ package.subjects.count|add:"-3" }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<span class="fw-bold text-success fs-5">{{ package.price }} OMR</span>
|
||||||
|
<a href="{% url 'subscribe_package' package.id %}" class="btn btn-sm btn-warning text-dark fw-bold rounded-pill">شراء الباقة</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Available Subjects Section -->
|
<!-- Available Subjects Section -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="fw-bold mb-3">المواد المتاحة</h4>
|
<h4 class="fw-bold mb-3">المواد المتاحة</h4>
|
||||||
|
|||||||
@ -65,13 +65,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if subject.youtube_live_url %}
|
{% if subject.get_youtube_id %}
|
||||||
<div class="mt-5 mb-5">
|
<div class="mt-5 mb-5">
|
||||||
<div class="ratio ratio-16x9 rounded-4 overflow-hidden shadow-sm">
|
<div class="ratio ratio-16x9 rounded-4 overflow-hidden shadow-sm">
|
||||||
<iframe src="https://www.youtube.com/embed/{{ subject.get_youtube_id }}"
|
<iframe src="https://www.youtube-nocookie.com/embed/{{ subject.get_youtube_id }}?rel=0&playsinline=1"
|
||||||
title="YouTube video player"
|
title="YouTube video player"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
allowfullscreen>
|
allowfullscreen>
|
||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
@ -255,9 +256,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
modalBody.innerHTML = ''; // Clear previous content
|
modalBody.innerHTML = ''; // Clear previous content
|
||||||
|
|
||||||
if (type === 'VIDEO' && youtubeId) {
|
if (type === 'VIDEO' && youtubeId) {
|
||||||
|
// Use youtube-nocookie.com and add referrerpolicy
|
||||||
modalBody.innerHTML = `
|
modalBody.innerHTML = `
|
||||||
<div class="ratio ratio-16x9 h-100">
|
<div class="ratio ratio-16x9 h-100">
|
||||||
<iframe src="https://www.youtube.com/embed/${youtubeId}?autoplay=1" title="${title}" allowfullscreen></iframe>
|
<iframe src="https://www.youtube-nocookie.com/embed/${youtubeId}?autoplay=1&rel=0&playsinline=1"
|
||||||
|
title="${title}"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
|
allowfullscreen></iframe>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (type === 'FILE' && fileUrl) {
|
} else if (type === 'FILE' && fileUrl) {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ class ThawaniClient:
|
|||||||
else:
|
else:
|
||||||
self.base_url = "https://checkout.thawani.om/api/v1"
|
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:
|
if not self.api_key:
|
||||||
raise Exception("Thawani API Key is not configured.")
|
raise Exception("Thawani API Key is not configured.")
|
||||||
|
|
||||||
@ -31,24 +31,32 @@ class ThawaniClient:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Thawani expects amount in Baisa (1 OMR = 1000 Baisa)
|
# 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 = {
|
payload = {
|
||||||
"client_reference_id": str(user.id),
|
"client_reference_id": str(user.id),
|
||||||
"mode": "payment",
|
"mode": "payment",
|
||||||
"products": [
|
"products": [
|
||||||
{
|
{
|
||||||
"name": subject.name_en,
|
"name": product.name_en,
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"unit_amount": amount_baisa
|
"unit_amount": amount_baisa
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"success_url": success_url,
|
"success_url": success_url,
|
||||||
"cancel_url": cancel_url,
|
"cancel_url": cancel_url,
|
||||||
"metadata": {
|
"metadata": metadata
|
||||||
"subject_id": str(subject.id),
|
|
||||||
"user_id": str(user.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = httpx.post(url, json=payload, headers=headers)
|
response = httpx.post(url, json=payload, headers=headers)
|
||||||
@ -66,4 +74,4 @@ class ThawaniClient:
|
|||||||
|
|
||||||
response = httpx.get(url, headers=headers)
|
response = httpx.get(url, headers=headers)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
@ -16,6 +16,7 @@ urlpatterns = [
|
|||||||
path('profile/edit/teacher/', views.edit_teacher_profile, name='edit_teacher_profile'),
|
path('profile/edit/teacher/', views.edit_teacher_profile, name='edit_teacher_profile'),
|
||||||
path('logout/', views.custom_logout, name='logout'),
|
path('logout/', views.custom_logout, name='logout'),
|
||||||
path('subscribe/<int:subject_id>/', views.subscribe_subject, name='subscribe_subject'),
|
path('subscribe/<int:subject_id>/', views.subscribe_subject, name='subscribe_subject'),
|
||||||
|
path('package/<int:package_id>/subscribe/', views.subscribe_package, name='subscribe_package'),
|
||||||
path('payment/success/', views.payment_success, name='payment_success'),
|
path('payment/success/', views.payment_success, name='payment_success'),
|
||||||
path('payment/cancel/', views.payment_cancel, name='payment_cancel'),
|
path('payment/cancel/', views.payment_cancel, name='payment_cancel'),
|
||||||
path('classroom/<int:subject_id>/', views.live_classroom, name='live_classroom'),
|
path('classroom/<int:subject_id>/', views.live_classroom, name='live_classroom'),
|
||||||
@ -23,4 +24,4 @@ urlpatterns = [
|
|||||||
path('resource/<int:resource_id>/edit/', views.edit_resource, name='edit_resource'),
|
path('resource/<int:resource_id>/edit/', views.edit_resource, name='edit_resource'),
|
||||||
path('resource/<int:resource_id>/delete/', views.delete_resource, name='delete_resource'),
|
path('resource/<int:resource_id>/delete/', views.delete_resource, name='delete_resource'),
|
||||||
path('resource/<int:resource_id>/view/', views.view_resource, name='view_resource'),
|
path('resource/<int:resource_id>/view/', views.view_resource, name='view_resource'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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.decorators import login_required
|
||||||
from django.contrib.auth import logout, login
|
from django.contrib.auth import logout, login
|
||||||
from django.urls import reverse
|
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 .forms import StudentRegistrationForm, TeacherProfileForm, ResourceForm
|
||||||
from .wablas import send_whatsapp_message
|
from .wablas import send_whatsapp_message
|
||||||
from .thawani import ThawaniClient
|
from .thawani import ThawaniClient
|
||||||
@ -175,10 +176,20 @@ def profile(request):
|
|||||||
id__in=subscribed_subjects.values_list('id', flat=True)
|
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', {
|
return render(request, 'core/student_dashboard.html', {
|
||||||
'student_profile': student_profile,
|
'student_profile': student_profile,
|
||||||
'subscribed_subjects': subscribed_subjects,
|
'subscribed_subjects': subscribed_subjects,
|
||||||
'available_subjects': available_subjects
|
'available_subjects': available_subjects,
|
||||||
|
'packages': packages
|
||||||
})
|
})
|
||||||
|
|
||||||
# Fallback (Superuser or Admin without profile)
|
# Fallback (Superuser or Admin without profile)
|
||||||
@ -237,6 +248,32 @@ def subscribe_subject(request, subject_id):
|
|||||||
print(f"Payment Error: {e}")
|
print(f"Payment Error: {e}")
|
||||||
return render(request, 'core/error.html', {'message': f'فشل بدء عملية الدفع: {str(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
|
@login_required
|
||||||
def payment_success(request):
|
def payment_success(request):
|
||||||
session_id = request.GET.get('session_id')
|
session_id = request.GET.get('session_id')
|
||||||
@ -250,14 +287,26 @@ def payment_success(request):
|
|||||||
payment_status = data.get('payment_status')
|
payment_status = data.get('payment_status')
|
||||||
metadata = data.get('metadata', {})
|
metadata = data.get('metadata', {})
|
||||||
subject_id = metadata.get('subject_id')
|
subject_id = metadata.get('subject_id')
|
||||||
|
package_id = metadata.get('package_id')
|
||||||
|
|
||||||
if payment_status == 'paid' and subject_id:
|
if payment_status == 'paid':
|
||||||
try:
|
student = request.user.student_profile
|
||||||
student = request.user.student_profile
|
|
||||||
subject = get_object_or_404(Subject, pk=subject_id)
|
if subject_id:
|
||||||
student.subscribed_subjects.add(subject)
|
try:
|
||||||
except Exception:
|
subject = get_object_or_404(Subject, pk=subject_id)
|
||||||
pass # Already handled or user mismatch?
|
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')
|
return redirect('profile')
|
||||||
else:
|
else:
|
||||||
return render(request, 'core/error.html', {'message': f'لم تتم عملية الدفع بنجاح. الحالة: {payment_status}'})
|
return render(request, 'core/error.html', {'message': f'لم تتم عملية الدفع بنجاح. الحالة: {payment_status}'})
|
||||||
|
|||||||
BIN
media/resources/Voucher-_2.pdf
Normal file
BIN
media/resources/Voucher-_2.pdf
Normal file
Binary file not shown.
BIN
media/resources/بسم_الله_الرحمن_الرحيم.docx
Normal file
BIN
media/resources/بسم_الله_الرحمن_الرحيم.docx
Normal file
Binary file not shown.
39
test_regex.py
Normal file
39
test_regex.py
Normal file
@ -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)}")
|
||||||
29
test_regex_v2.py
Normal file
29
test_regex_v2.py
Normal file
@ -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)}")
|
||||||
43
test_regex_v3.py
Normal file
43
test_regex_v3.py
Normal file
@ -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'}")
|
||||||
Loading…
x
Reference in New Issue
Block a user