diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index b27f091..f34b19d 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 3554283..ebebf1c 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 17137ff..bd12b3a 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 3402ea9..7e25da6 100644 --- a/core/admin.py +++ b/core/admin.py @@ -35,9 +35,14 @@ class TeacherAdmin(ActionsModelAdmin): @admin.register(Subject) class SubjectAdmin(ActionsModelAdmin): - list_display = ('name_en', 'name_ar', 'classroom', 'price', 'teacher', 'actions_column') - list_filter = ('classroom', 'teacher') + list_display = ('name_en', 'name_ar', 'classroom', 'price', 'get_teachers', 'actions_column') + list_filter = ('classroom', 'teachers') search_fields = ('name_en', 'name_ar') + filter_horizontal = ('teachers',) + + def get_teachers(self, obj): + return ", ".join([str(t) for t in obj.teachers.all()]) + get_teachers.short_description = 'Teachers' @admin.register(City) class CityAdmin(ActionsModelAdmin): @@ -67,9 +72,9 @@ class ResourceAdminForm(forms.ModelForm): @admin.register(Resource) class ResourceAdmin(ActionsModelAdmin): form = ResourceAdminForm - list_display = ('title_en', 'subject', 'created_at', 'actions_column') - list_filter = ('subject__classroom', 'subject') - fields = ('classroom', 'subject', 'title_en', 'title_ar', 'file', 'link') + list_display = ('title_en', 'subject', 'resource_type', 'created_at', 'actions_column') + list_filter = ('subject__classroom', 'subject', 'resource_type') + fields = ('classroom', 'subject', 'title_en', 'title_ar', 'resource_type', 'file', 'link') class Media: js = ('js/admin_resource.js',) diff --git a/core/migrations/0011_remove_subject_teacher_subject_teachers.py b/core/migrations/0011_remove_subject_teacher_subject_teachers.py new file mode 100644 index 0000000..7d5c73a --- /dev/null +++ b/core/migrations/0011_remove_subject_teacher_subject_teachers.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.7 on 2026-02-04 06:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_governorate_remove_student_moderate_remove_city_name_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='subject', + name='teacher', + ), + migrations.AddField( + model_name='subject', + name='teachers', + field=models.ManyToManyField(blank=True, related_name='subjects', to='core.teacher'), + ), + ] diff --git a/core/migrations/0012_subject_youtube_live_url.py b/core/migrations/0012_subject_youtube_live_url.py new file mode 100644 index 0000000..39439ae --- /dev/null +++ b/core/migrations/0012_subject_youtube_live_url.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-04 06:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0011_remove_subject_teacher_subject_teachers'), + ] + + operations = [ + migrations.AddField( + model_name='subject', + name='youtube_live_url', + field=models.URLField(blank=True, help_text='Paste the YouTube Live link here for large broadcasts (500+ students).', verbose_name='YouTube Live URL'), + ), + ] diff --git a/core/migrations/0013_resource_resource_type_alter_resource_link.py b/core/migrations/0013_resource_resource_type_alter_resource_link.py new file mode 100644 index 0000000..3cbec22 --- /dev/null +++ b/core/migrations/0013_resource_resource_type_alter_resource_link.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-02-04 07:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_subject_youtube_live_url'), + ] + + operations = [ + migrations.AddField( + model_name='resource', + name='resource_type', + field=models.CharField(choices=[('FILE', 'File (PDF, Doc, etc.)'), ('VIDEO', 'Video Link (YouTube)'), ('LINK', 'External Link')], default='FILE', max_length=10, verbose_name='Resource Type'), + ), + migrations.AlterField( + model_name='resource', + name='link', + field=models.URLField(blank=True, help_text='YouTube URL for Video type, or external URL for Link type.', verbose_name='Link'), + ), + ] diff --git a/core/migrations/__pycache__/0011_remove_subject_teacher_subject_teachers.cpython-311.pyc b/core/migrations/__pycache__/0011_remove_subject_teacher_subject_teachers.cpython-311.pyc new file mode 100644 index 0000000..81b9361 Binary files /dev/null and b/core/migrations/__pycache__/0011_remove_subject_teacher_subject_teachers.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0012_subject_youtube_live_url.cpython-311.pyc b/core/migrations/__pycache__/0012_subject_youtube_live_url.cpython-311.pyc new file mode 100644 index 0000000..c41a52c Binary files /dev/null and b/core/migrations/__pycache__/0012_subject_youtube_live_url.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0013_resource_resource_type_alter_resource_link.cpython-311.pyc b/core/migrations/__pycache__/0013_resource_resource_type_alter_resource_link.cpython-311.pyc new file mode 100644 index 0000000..2d4b063 Binary files /dev/null and b/core/migrations/__pycache__/0013_resource_resource_type_alter_resource_link.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 4fb8391..989c244 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _ +import re class SingletonModel(models.Model): class Meta: @@ -38,7 +39,7 @@ class Classroom(models.Model): class Subject(models.Model): classroom = models.ForeignKey(Classroom, on_delete=models.CASCADE, related_name='subjects') - teacher = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True, blank=True, related_name='subjects') + teachers = models.ManyToManyField(Teacher, blank=True, related_name='subjects') name_en = models.CharField(_("Name (English)"), max_length=200) name_ar = models.CharField(_("Name (Arabic)"), max_length=200) description_en = models.TextField(_("Description (English)"), blank=True) @@ -47,21 +48,75 @@ class Subject(models.Model): image = models.ImageField(_("Image"), upload_to='subjects/', blank=True, null=True) google_drive_link = models.URLField(_("Google Drive Link"), blank=True) google_meet_link = models.URLField(_("Google Meet Link"), blank=True) + youtube_live_url = models.URLField(_("YouTube Live URL"), blank=True, help_text=_("Paste the YouTube Live link here for large broadcasts (500+ students).")) def __str__(self): return self.name_en + def get_youtube_id(self): + """Extracts the video ID from a YouTube URL.""" + if not self.youtube_live_url: + return None + # 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 + patterns = [ + r'(?:v=|\/)([0-9A-Za-z_-]{11}).*', + r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})', + r'(?:live\/)([0-9A-Za-z_-]{11})', + ] + for pattern in patterns: + match = re.search(pattern, self.youtube_live_url) + if match: + return match.group(1) + return None + class Resource(models.Model): + RESOURCE_TYPES = ( + ('FILE', _('File (PDF, Doc, etc.)')), + ('VIDEO', _('Video Link (YouTube)')), + ('LINK', _('External Link')), + ) + subject = models.ForeignKey(Subject, on_delete=models.CASCADE, related_name='resources') title_en = models.CharField(_("Title (English)"), max_length=200) title_ar = models.CharField(_("Title (Arabic)"), max_length=200) + resource_type = models.CharField(_("Resource Type"), max_length=10, choices=RESOURCE_TYPES, default='FILE') file = models.FileField(_("File"), upload_to='resources/', blank=True, null=True) - link = models.URLField(_("External Link"), blank=True) + link = models.URLField(_("Link"), blank=True, help_text=_("YouTube URL for Video type, or external URL for Link type.")) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.title_en + def get_youtube_id(self): + """Extracts the video ID from the link if it is a YouTube URL.""" + if not self.link or self.resource_type != 'VIDEO': + 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})', + ] + for pattern in patterns: + match = re.search(pattern, self.link) + if match: + return match.group(1) + return None + + def is_image(self): + if self.file: + return self.file.name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')) + return False + + def is_pdf(self): + if self.file: + return self.file.name.lower().endswith('.pdf') + return False + class Governorate(models.Model): name_en = models.CharField(_("Name (English)"), max_length=100, default="") name_ar = models.CharField(_("Name (Arabic)"), max_length=100, default="") @@ -147,4 +202,4 @@ class PlatformSettings(SingletonModel): verbose_name_plural = "Platform Profile" def __str__(self): - return "Platform Profile" + return "Platform Profile" \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index a5f5d2e..ed0ad04 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -103,6 +103,50 @@ + +
+
+
+

{% trans "Meet Our Teachers" %}

+

{% trans "Learn from our expert instructors." %}

+
+ +
+ {% for teacher in teachers %} +
+
+
+ {% if teacher.avatar %} + {{ teacher.user.get_full_name }} + {% else %} +
+ {{ teacher.user.first_name|first|default:teacher.user.username|first|upper }} +
+ {% endif %} +
+
+ +
{{ teacher.user.get_full_name|default:teacher.user.username }}
+

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

+ +

+ {{ teacher.bio|default:"No bio available."|truncatewords:20 }} +

+ +
+ +
+
+
+ {% empty %} +
+

{% trans "No teachers found at the moment." %}

+
+ {% endfor %} +
+
+
+
diff --git a/core/templates/core/live_classroom.html b/core/templates/core/live_classroom.html index 35005d8..7cff1d0 100644 --- a/core/templates/core/live_classroom.html +++ b/core/templates/core/live_classroom.html @@ -5,53 +5,107 @@ {% block content %}
+ + {% with youtube_id=subject.get_youtube_id %} -
+ {% if subject.google_meet_link %} + +
+
+

{% trans "Live Class via Google Meet" %}

+

{% trans "This class is being held on Google Meet. Click the button below to join." %}

+ + {% trans "Join Google Meet" %} + +
+
+ + {% elif not is_teacher and youtube_id %} + +
+ +
+ +
+ +
+ +
+
+ + {% else %} + +
+ {% endif %} + + {% endwith %}
- - -{% endblock %} +{% with youtube_id=subject.get_youtube_id %} + + +{% if not subject.google_meet_link %} + {% if is_teacher or not youtube_id %} + + + {% endif %} +{% endif %} +{% endwith %} + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/student_dashboard.html b/core/templates/core/student_dashboard.html index 876f2b3..ef11fbd 100644 --- a/core/templates/core/student_dashboard.html +++ b/core/templates/core/student_dashboard.html @@ -124,7 +124,11 @@

{{ subject.description_en }}

- {{ subject.teacher.user.get_full_name|default:"No Teacher" }} + {% for teacher in subject.teachers.all %} + {{ teacher.user.get_full_name|default:teacher.user.username }}{% if not forloop.last %}, {% endif %} + {% empty %} + {% trans "No Teacher" %} + {% endfor %}
diff --git a/core/templates/core/subject_detail.html b/core/templates/core/subject_detail.html index 49302d7..5469111 100644 --- a/core/templates/core/subject_detail.html +++ b/core/templates/core/subject_detail.html @@ -36,17 +36,26 @@ {% if LANGUAGE_CODE == 'ar' %}{{ subject.name_ar }}{% else %}{{ subject.name_en }}{% endif %} -
+ {% for teacher in subject.teachers.all %} +
- {{ subject.teacher.user.username|first|upper }} + {{ teacher.user.username|first|upper }}

{% trans "Teacher" %}

-
{{ subject.teacher }}
+
{{ teacher }}
+ {% empty %} +
+
+

{% trans "Teacher" %}

+
{% trans "Not Assigned" %}
+
+
+ {% endfor %} -

{% trans "About this Subject" %}

+

{% trans "About this Subject" %}

{% if LANGUAGE_CODE == 'ar' %} {{ subject.description_ar|linebreaks }} @@ -55,22 +64,65 @@ {% endif %}
-
-
-
-
🎥
-
{% trans "Live Sessions" %}
-

{% trans "Interactive online classes and group discussions." %}

-
+ +
+
+

{% trans "Course Resources" %}

+ {{ subject.resources.count }}
-
-
-
📁
-
{% trans "Study Materials" %}
-

{% trans "Comprehensive guides, notes, and practice exams." %}

+ + {% if subject.resources.all %} +
+ {% for resource in subject.resources.all %} +
+
+
+ {% if resource.resource_type == 'VIDEO' %} + + + + {% elif resource.resource_type == 'FILE' %} + + + + {% else %} + + + + + {% endif %} +
+
+
+ {% if LANGUAGE_CODE == 'ar' %}{{ resource.title_ar }}{% else %}{{ resource.title_en }}{% endif %} +
+ + {{ resource.get_resource_type_display }} + +
+
+
+ {% endfor %}
+ {% else %} +
+
📂
+
{% trans "No resources available yet." %}
+
+ {% endif %}
+
@@ -101,19 +153,102 @@

- -
-
-
{% trans "Share this course" %}
-
- - - -
-
-
+ + + + +{% endblock %} + +{% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/core/views.py b/core/views.py index b256573..0b5a69d 100644 --- a/core/views.py +++ b/core/views.py @@ -16,7 +16,8 @@ import string def index(request): levels = Classroom.objects.prefetch_related('subjects').all() - context = {'levels': levels} + teachers = Teacher.objects.select_related('user').all() + context = {'levels': levels, 'teachers': teachers} return render(request, 'core/index.html', context) def set_language(request, lang_code): @@ -253,7 +254,7 @@ def live_classroom(request, subject_id): is_teacher = False is_student = False - if hasattr(request.user, 'teacher_profile') and subject.teacher and subject.teacher.user == request.user: + if hasattr(request.user, 'teacher_profile') and request.user.teacher_profile in subject.teachers.all(): is_teacher = True if hasattr(request.user, 'student_profile') and request.user.student_profile.subscribed_subjects.filter(pk=subject_id).exists(): @@ -271,4 +272,4 @@ def live_classroom(request, subject_id): 'room_name': room_name, 'user_display_name': request.user.get_full_name() or request.user.username, 'is_teacher': is_teacher - }) + }) \ No newline at end of file