Autosave: 20260204-093027
This commit is contained in:
parent
668d2596e7
commit
05987f69ac
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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',)
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0012_subject_youtube_live_url.py
Normal file
18
core/migrations/0012_subject_youtube_live_url.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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"
|
||||
@ -103,6 +103,50 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- MEET OUR TEACHERS SECTION -->
|
||||
<section id="teachers-section" class="py-5 bg-light">
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="fw-bold">{% trans "Meet Our Teachers" %}</h2>
|
||||
<p class="text-muted">{% trans "Learn from our expert instructors." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 justify-content-center">
|
||||
{% for teacher in teachers %}
|
||||
<div class="col-md-6 col-lg-4 col-xl-3">
|
||||
<div class="card h-100 border-0 shadow-sm text-center p-4 rounded-4 hover-lift">
|
||||
<div class="mb-4 position-relative mx-auto" style="width: 120px; height: 120px;">
|
||||
{% if teacher.avatar %}
|
||||
<img src="{{ teacher.avatar.url }}" alt="{{ teacher.user.get_full_name }}" class="rounded-circle img-thumbnail p-1 border-0" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center text-primary" style="width: 100%; height: 100%; font-size: 2.5rem; border: 4px solid white; box-shadow: 0 5px 15px rgba(0,0,0,0.05);">
|
||||
{{ teacher.user.first_name|first|default:teacher.user.username|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="position-absolute bottom-0 end-0 bg-success border border-white rounded-circle p-2"></div>
|
||||
</div>
|
||||
|
||||
<h5 class="fw-bold mb-1">{{ teacher.user.get_full_name|default:teacher.user.username }}</h5>
|
||||
<p class="text-primary small fw-bold mb-3">{{ teacher.specialization|default:"Teacher" }}</p>
|
||||
|
||||
<p class="text-muted small mb-4">
|
||||
{{ teacher.bio|default:"No bio available."|truncatewords:20 }}
|
||||
</p>
|
||||
|
||||
<div class="mt-auto">
|
||||
<!-- Optional: Add social links or 'View Profile' button here if needed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center">
|
||||
<p class="text-muted">{% trans "No teachers found at the moment." %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section py-5 bg-white">
|
||||
<div class="container">
|
||||
<div class="row text-center g-4">
|
||||
|
||||
@ -5,53 +5,107 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid p-0" style="height: calc(100vh - 76px); margin-top: 76px;">
|
||||
|
||||
{% with youtube_id=subject.get_youtube_id %}
|
||||
|
||||
<div id="meet" style="width: 100%; height: 100%;"></div>
|
||||
{% if subject.google_meet_link %}
|
||||
<!-- Google Meet Mode (Highest Priority) -->
|
||||
<div class="d-flex flex-column align-items-center justify-content-center h-100 bg-light">
|
||||
<div class="text-center p-5 bg-white shadow rounded">
|
||||
<h2 class="mb-4">{% trans "Live Class via Google Meet" %}</h2>
|
||||
<p class="mb-4 text-muted">{% trans "This class is being held on Google Meet. Click the button below to join." %}</p>
|
||||
<a href="{{ subject.google_meet_link }}" target="_blank" class="btn btn-primary btn-lg">
|
||||
<i class="fas fa-video me-2"></i> {% trans "Join Google Meet" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif not is_teacher and youtube_id %}
|
||||
<!-- Broadcast Mode for Students (YouTube) -->
|
||||
<div class="row h-100 m-0">
|
||||
<!-- Video Player -->
|
||||
<div class="col-md-9 p-0 h-100 bg-black d-flex align-items-center justify-content-center">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src="https://www.youtube.com/embed/{{ youtube_id }}?autoplay=1"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
<!-- Chat Window -->
|
||||
<div class="col-md-3 p-0 h-100 border-start">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src="https://www.youtube.com/live_chat?v={{ youtube_id }}&embed_domain={{ request.get_host|cut:':8000'|cut:':80' }}"
|
||||
frameborder="0">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Jitsi Meeting (Teacher Host or Regular Class) -->
|
||||
<div id="meet" style="width: 100%; height: 100%;"></div>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
|
||||
<script src='https://meet.jit.si/external_api.js'></script>
|
||||
<script>
|
||||
const domain = 'meet.jit.si';
|
||||
const options = {
|
||||
roomName: '{{ room_name }}',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
parentNode: document.querySelector('#meet'),
|
||||
userInfo: {
|
||||
displayName: '{{ user_display_name }}'
|
||||
},
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: true,
|
||||
{% if not is_teacher %}
|
||||
// Hide some controls for students
|
||||
disableRemoteMute: true,
|
||||
{% endif %}
|
||||
prejoinPageEnabled: false
|
||||
},
|
||||
interfaceConfigOverwrite: {
|
||||
// Customize toolbar if needed
|
||||
SHOW_JITSI_WATERMARK: false,
|
||||
DEFAULT_BACKGROUND: 'white',
|
||||
DEFAULT_LOCAL_DISPLAY_NAME: '{{ user_display_name }}',
|
||||
TOOLBAR_BUTTONS: [
|
||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
|
||||
'security'
|
||||
],
|
||||
}
|
||||
};
|
||||
const api = new JitsiMeetExternalAPI(domain, options);
|
||||
|
||||
// Redirect when hanging up
|
||||
api.addEventListeners({
|
||||
videoConferenceLeft: function () {
|
||||
window.location.href = "{% url 'profile' %}";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% with youtube_id=subject.get_youtube_id %}
|
||||
<!-- Only load Jitsi if we are in Jitsi mode -->
|
||||
<!-- Jitsi Mode conditions: NO Google Meet Link AND (Teacher OR No YouTube ID) -->
|
||||
{% if not subject.google_meet_link %}
|
||||
{% if is_teacher or not youtube_id %}
|
||||
<script src='https://meet.jit.si/external_api.js'></script>
|
||||
<script>
|
||||
const domain = 'meet.jit.si';
|
||||
const options = {
|
||||
roomName: '{{ room_name }}',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
parentNode: document.querySelector('#meet'),
|
||||
userInfo: {
|
||||
displayName: '{{ user_display_name }}'
|
||||
},
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: true,
|
||||
{% if not is_teacher %}
|
||||
// Hide some controls for students
|
||||
disableRemoteMute: true,
|
||||
{% endif %}
|
||||
prejoinPageEnabled: false
|
||||
},
|
||||
interfaceConfigOverwrite: {
|
||||
// Customize toolbar if needed
|
||||
SHOW_JITSI_WATERMARK: false,
|
||||
DEFAULT_BACKGROUND: 'white',
|
||||
DEFAULT_LOCAL_DISPLAY_NAME: '{{ user_display_name }}',
|
||||
TOOLBAR_BUTTONS: [
|
||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
|
||||
'security'
|
||||
],
|
||||
}
|
||||
};
|
||||
const api = new JitsiMeetExternalAPI(domain, options);
|
||||
|
||||
// Redirect when hanging up
|
||||
api.addEventListeners({
|
||||
videoConferenceLeft: function () {
|
||||
window.location.href = "{% url 'profile' %}";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
@ -124,7 +124,11 @@
|
||||
<p class="text-muted small mb-3 text-truncate">{{ subject.description_en }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary">
|
||||
{{ 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 %}
|
||||
</span>
|
||||
<div>
|
||||
<a href="{% url 'live_classroom' subject.id %}" class="btn btn-sm btn-danger rounded-pill me-1" title="{% trans 'Join Live Class' %}">
|
||||
|
||||
@ -36,17 +36,26 @@
|
||||
{% if LANGUAGE_CODE == 'ar' %}{{ subject.name_ar }}{% else %}{{ subject.name_en }}{% endif %}
|
||||
</h1>
|
||||
|
||||
<div class="d-flex align-items-center mb-5 p-3 rounded-3 bg-light">
|
||||
{% for teacher in subject.teachers.all %}
|
||||
<div class="d-flex align-items-center mb-3 p-3 rounded-3 bg-light">
|
||||
<div class="avatar-placeholder me-3" style="width: 50px; height: 50px; background: var(--primary-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white;">
|
||||
{{ subject.teacher.user.username|first|upper }}
|
||||
{{ teacher.user.username|first|upper }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">{% trans "Teacher" %}</p>
|
||||
<h6 class="fw-bold mb-0">{{ subject.teacher }}</h6>
|
||||
<h6 class="fw-bold mb-0">{{ teacher }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="d-flex align-items-center mb-3 p-3 rounded-3 bg-light">
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">{% trans "Teacher" %}</p>
|
||||
<h6 class="fw-bold mb-0">{% trans "Not Assigned" %}</h6>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h4 class="fw-bold mb-3">{% trans "About this Subject" %}</h4>
|
||||
<h4 class="fw-bold mb-3 mt-4">{% trans "About this Subject" %}</h4>
|
||||
<div class="lead text-muted mb-5">
|
||||
{% if LANGUAGE_CODE == 'ar' %}
|
||||
{{ subject.description_ar|linebreaks }}
|
||||
@ -55,22 +64,65 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-6">
|
||||
<div class="p-4 border rounded-4 h-100">
|
||||
<div class="h3 mb-3 text-primary">🎥</div>
|
||||
<h5 class="fw-bold">{% trans "Live Sessions" %}</h5>
|
||||
<p class="text-muted small mb-0">{% trans "Interactive online classes and group discussions." %}</p>
|
||||
</div>
|
||||
<!-- Resources Section -->
|
||||
<div class="mb-5">
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
<h4 class="fw-bold mb-0">{% trans "Course Resources" %}</h4>
|
||||
<span class="badge bg-light text-primary ms-3 border rounded-pill px-3">{{ subject.resources.count }}</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-4 border rounded-4 h-100">
|
||||
<div class="h3 mb-3 text-primary">📁</div>
|
||||
<h5 class="fw-bold">{% trans "Study Materials" %}</h5>
|
||||
<p class="text-muted small mb-0">{% trans "Comprehensive guides, notes, and practice exams." %}</p>
|
||||
|
||||
{% if subject.resources.all %}
|
||||
<div class="list-group list-group-flush border rounded-4 overflow-hidden">
|
||||
{% for resource in subject.resources.all %}
|
||||
<div class="list-group-item p-3 d-flex align-items-center justify-content-between hover-bg-light transition-all">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3 p-2 rounded-3
|
||||
{% if resource.resource_type == 'VIDEO' %}bg-danger-subtle text-danger
|
||||
{% elif resource.resource_type == 'FILE' %}bg-primary-subtle text-primary
|
||||
{% else %}bg-success-subtle text-success{% endif %}">
|
||||
{% if resource.resource_type == 'VIDEO' %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-play-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z"/>
|
||||
</svg>
|
||||
{% elif resource.resource_type == 'FILE' %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-text-fill" viewBox="0 0 16 16">
|
||||
<path d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.793 4L10 0.207A1.5 1.5 0 0 0 9.293 0zM9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1zM4.5 9a.5.5 0 0 1 0-1h7a.5.5 0 0 1 0 1h-7zM4 10.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm.5 2.5a.5.5 0 0 1 0-1h4a.5.5 0 0 1 0 1h-4z"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-link-45deg" viewBox="0 0 16 16">
|
||||
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"/>
|
||||
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243L6.586 4.672z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 fw-semibold">
|
||||
{% if LANGUAGE_CODE == 'ar' %}{{ resource.title_ar }}{% else %}{{ resource.title_en }}{% endif %}
|
||||
</h6>
|
||||
<small class="text-muted">
|
||||
{{ resource.get_resource_type_display }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary rounded-pill px-3 view-resource-btn"
|
||||
data-type="{{ resource.resource_type }}"
|
||||
data-title="{% if LANGUAGE_CODE == 'ar' %}{{ resource.title_ar }}{% else %}{{ resource.title_en }}{% endif %}"
|
||||
data-file-url="{% if resource.file %}{{ resource.file.url }}{% endif %}"
|
||||
data-link="{% if resource.link %}{{ resource.link }}{% endif %}"
|
||||
data-youtube-id="{{ resource.get_youtube_id|default:'' }}">
|
||||
{% trans "View" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center p-5 bg-light rounded-4">
|
||||
<div class="display-6 mb-3">📂</div>
|
||||
<h6 class="text-muted">{% trans "No resources available yet." %}</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -101,19 +153,102 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card mt-4">
|
||||
<div class="p-4 text-center">
|
||||
<h6 class="fw-bold mb-3">{% trans "Share this course" %}</h6>
|
||||
<div class="d-flex justify-content-center gap-2">
|
||||
<button class="btn btn-light btn-sm rounded-circle" style="width: 40px; height: 40px;">f</button>
|
||||
<button class="btn btn-light btn-sm rounded-circle" style="width: 40px; height: 40px;">t</button>
|
||||
<button class="btn btn-light btn-sm rounded-circle" style="width: 40px; height: 40px;">in</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resource Modal -->
|
||||
<div class="modal fade" id="resourceModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content rounded-4 border-0 shadow-lg">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title fw-bold" id="resourceModalTitle">{% trans "Resource" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4" id="resourceModalBody">
|
||||
<!-- Content injected via JS -->
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<a href="#" target="_blank" id="openNewTabBtn" class="btn btn-primary rounded-pill">
|
||||
{% trans "Open in New Tab" %} <i class="bi bi-box-arrow-up-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const resourceModal = new bootstrap.Modal(document.getElementById('resourceModal'));
|
||||
const modalTitle = document.getElementById('resourceModalTitle');
|
||||
const modalBody = document.getElementById('resourceModalBody');
|
||||
const openNewTabBtn = document.getElementById('openNewTabBtn');
|
||||
|
||||
document.querySelectorAll('.view-resource-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const type = this.dataset.type;
|
||||
const title = this.dataset.title;
|
||||
const fileUrl = this.dataset.fileUrl;
|
||||
const link = this.dataset.link;
|
||||
const youtubeId = this.dataset.youtubeId;
|
||||
|
||||
modalTitle.textContent = title;
|
||||
openNewTabBtn.style.display = 'inline-block'; // Default to showing it
|
||||
|
||||
if (type === 'VIDEO' && youtubeId) {
|
||||
modalBody.innerHTML = `
|
||||
<div class="ratio ratio-16x9">
|
||||
<iframe src="https://www.youtube.com/embed/${youtubeId}?autoplay=1" title="${title}" allowfullscreen></iframe>
|
||||
</div>
|
||||
`;
|
||||
openNewTabBtn.href = link;
|
||||
} else if (type === 'FILE' && fileUrl) {
|
||||
// Check if it's an image or PDF for preview
|
||||
const ext = fileUrl.split('.').pop().toLowerCase();
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
|
||||
modalBody.innerHTML = `<img src="${fileUrl}" class="img-fluid rounded" alt="${title}">`;
|
||||
} else if (ext === 'pdf') {
|
||||
modalBody.innerHTML = `
|
||||
<div class="ratio ratio-4x3">
|
||||
<iframe src="${fileUrl}" title="${title}"></iframe>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Other files: no preview, just download button
|
||||
modalBody.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="mb-3 display-4">📄</div>
|
||||
<p class="mb-3">{% trans "This file type cannot be previewed directly." %}</p>
|
||||
<a href="${fileUrl}" class="btn btn-primary rounded-pill" download>{% trans "Download File" %}</a>
|
||||
</div>
|
||||
`;
|
||||
openNewTabBtn.style.display = 'none'; // Use the internal download button instead
|
||||
}
|
||||
openNewTabBtn.href = fileUrl;
|
||||
|
||||
} else if (type === 'LINK' && link) {
|
||||
modalBody.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="mb-3 display-4">🔗</div>
|
||||
<p class="mb-3">{% trans "You are about to visit an external link." %}</p>
|
||||
<p class="text-primary fw-bold text-break">${link}</p>
|
||||
</div>
|
||||
`;
|
||||
openNewTabBtn.href = link;
|
||||
}
|
||||
|
||||
resourceModal.show();
|
||||
});
|
||||
});
|
||||
|
||||
// Stop video when modal closes
|
||||
document.getElementById('resourceModal').addEventListener('hidden.bs.modal', function () {
|
||||
modalBody.innerHTML = '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -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
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user