Autosave: 20260204-035622

This commit is contained in:
Flatlogic Bot 2026-02-04 03:56:23 +00:00
parent b9bad07848
commit 776c322148
29 changed files with 688 additions and 54 deletions

View File

@ -276,7 +276,7 @@ JAZZMIN_SETTINGS = {
"core.Teacher": "fas fa-chalkboard-teacher",
"core.Subject": "fas fa-book",
"core.Resource": "fas fa-file-alt",
"core.EducationalLevel": "fas fa-layer-group",
"core.Classroom": "fas fa-layer-group",
},
# Icons that are used when one is not manually specified
"default_icon_parents": "fas fa-chevron-circle-right",
@ -298,4 +298,4 @@ JAZZMIN_SETTINGS = {
"use_google_fonts_cdn": True,
# Whether to show the UI customizer on the sidebar
"show_ui_builder": True,
}
}

Binary file not shown.

View File

@ -1,26 +1,57 @@
from django.contrib import admin
from django import forms
from .models import EducationalLevel, Teacher, Subject, Resource, Student
from django.utils.html import format_html
from django.urls import reverse
from .models import Classroom, Teacher, Subject, Resource, Student, City, Moderate
@admin.register(EducationalLevel)
class EducationalLevelAdmin(admin.ModelAdmin):
list_display = ('name_en', 'name_ar')
class ActionsModelAdmin(admin.ModelAdmin):
"""
Mixin to add an actions column to the admin list view.
"""
def actions_column(self, obj):
app_label = obj._meta.app_label
model_name = obj._meta.model_name
edit_url = reverse(f'admin:{app_label}_{model_name}_change', args=[obj.pk])
delete_url = reverse(f'admin:{app_label}_{model_name}_delete', args=[obj.pk])
return format_html(
'<div style="white-space: nowrap;">'
'<a class="btn btn-sm btn-primary" href="{}" title="Edit" style="margin-right:5px;"><i class="fas fa-edit"></i></a>'
'<a class="btn btn-sm btn-danger" href="{}" title="Delete"><i class="fas fa-trash-alt"></i></a>'
'</div>',
edit_url,
delete_url
)
actions_column.short_description = 'Actions'
@admin.register(Classroom)
class ClassroomAdmin(ActionsModelAdmin):
list_display = ('name_en', 'name_ar', 'actions_column')
@admin.register(Teacher)
class TeacherAdmin(admin.ModelAdmin):
list_display = ('user', 'specialization')
class TeacherAdmin(ActionsModelAdmin):
list_display = ('user', 'specialization', 'actions_column')
@admin.register(Subject)
class SubjectAdmin(admin.ModelAdmin):
list_display = ('name_en', 'name_ar', 'level', 'price', 'teacher')
list_filter = ('level', 'teacher')
class SubjectAdmin(ActionsModelAdmin):
list_display = ('name_en', 'name_ar', 'classroom', 'price', 'teacher', 'actions_column')
list_filter = ('classroom', 'teacher')
search_fields = ('name_en', 'name_ar')
@admin.register(City)
class CityAdmin(ActionsModelAdmin):
list_display = ('name', 'actions_column')
@admin.register(Moderate)
class ModerateAdmin(ActionsModelAdmin):
list_display = ('name', 'actions_column')
class ResourceAdminForm(forms.ModelForm):
educational_level = forms.ModelChoiceField(
queryset=EducationalLevel.objects.all(),
classroom = forms.ModelChoiceField(
queryset=Classroom.objects.all(),
required=True,
label="Filter by Educational Level"
label="Filter by Classroom"
)
class Meta:
@ -30,22 +61,22 @@ class ResourceAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk and self.instance.subject:
self.fields['educational_level'].initial = self.instance.subject.level
self.fields['classroom'].initial = self.instance.subject.classroom
@admin.register(Resource)
class ResourceAdmin(admin.ModelAdmin):
class ResourceAdmin(ActionsModelAdmin):
form = ResourceAdminForm
list_display = ('title_en', 'subject', 'created_at')
list_filter = ('subject__level', 'subject')
fields = ('educational_level', 'subject', 'title_en', 'title_ar', 'file', 'link')
list_display = ('title_en', 'subject', 'created_at', 'actions_column')
list_filter = ('subject__classroom', 'subject')
fields = ('classroom', 'subject', 'title_en', 'title_ar', 'file', 'link')
class Media:
js = ('js/admin_resource.js',)
@admin.register(Student)
class StudentAdmin(admin.ModelAdmin):
list_display = ('user', 'level', 'phone_number')
list_filter = ('level',)
class StudentAdmin(ActionsModelAdmin):
list_display = ('user', 'classroom', 'mobile_number', 'city', 'moderate', 'actions_column')
list_filter = ('classroom', 'city', 'moderate')
filter_horizontal = ('subscribed_subjects',)
class Media:
@ -53,6 +84,6 @@ class StudentAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if obj and obj.level:
form.base_fields['subscribed_subjects'].queryset = Subject.objects.filter(level=obj.level)
if obj and obj.classroom:
form.base_fields['subscribed_subjects'].queryset = Subject.objects.filter(classroom=obj.classroom)
return form

57
core/forms.py Normal file
View File

@ -0,0 +1,57 @@
from django import forms
from django.contrib.auth.models import User
from .models import Student, Subject, Classroom
class StudentRegistrationForm(forms.ModelForm):
first_name = forms.CharField(max_length=30, required=True, label="Full Name (First)")
last_name = forms.CharField(max_length=30, required=True, label="Full Name (Last)")
username = forms.CharField(max_length=150, required=True)
email = forms.EmailField(required=True)
password = forms.CharField(widget=forms.PasswordInput, required=True)
classroom = forms.ModelChoiceField(queryset=Classroom.objects.all(), required=True, empty_label="Select Classroom")
subjects = forms.ModelMultipleChoiceField(
queryset=Subject.objects.all(),
required=False,
widget=forms.CheckboxSelectMultiple,
label="Select Subjects"
)
class Meta:
model = Student
fields = ['mobile_number', 'moderate', 'city', 'avatar']
widgets = {
'avatar': forms.FileInput(attrs={'accept': 'image/*', 'capture': 'camera'}), # generic hint, but we'll use custom JS
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# optimize subjects loading
self.fields['subjects'].queryset = Subject.objects.none()
if 'classroom' in self.data:
try:
classroom_id = int(self.data.get('classroom'))
self.fields['subjects'].queryset = Subject.objects.filter(classroom_id=classroom_id)
except (ValueError, TypeError):
pass
elif self.instance.pk and self.instance.classroom:
self.fields['subjects'].queryset = Subject.objects.filter(classroom=self.instance.classroom)
def save(self, commit=True):
# We need to save the User first, then the Student
user = User.objects.create_user(
username=self.cleaned_data['username'],
email=self.cleaned_data['email'],
password=self.cleaned_data['password'],
first_name=self.cleaned_data['first_name'],
last_name=self.cleaned_data['last_name']
)
student = super().save(commit=False)
student.user = user
student.classroom = self.cleaned_data['classroom']
if commit:
student.save()
student.subscribed_subjects.set(self.cleaned_data['subjects'])
return student

View File

@ -1,6 +1,6 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from core.models import EducationalLevel, Subject, Teacher
from core.models import Classroom, Subject, Teacher
class Command(BaseCommand):
help = 'Seeds the database with sample data'
@ -14,13 +14,13 @@ class Command(BaseCommand):
teacher, _ = Teacher.objects.get_or_create(user=user, specialization='Mathematics')
# Create Educational Levels
l1, _ = EducationalLevel.objects.get_or_create(
# Create Classrooms
l1, _ = Classroom.objects.get_or_create(
name_en='Primary School',
name_ar='المرحلة الابتدائية',
description='Grades 1-6'
)
l2, _ = EducationalLevel.objects.get_or_create(
l2, _ = Classroom.objects.get_or_create(
name_en='High School',
name_ar='المرحلة الثانوية',
description='Grades 10-12'
@ -28,7 +28,7 @@ class Command(BaseCommand):
# Create Subjects
Subject.objects.get_or_create(
level=l1,
classroom=l1,
teacher=teacher,
name_en='Basic Math',
name_ar='الرياضيات الأساسية',
@ -37,7 +37,7 @@ class Command(BaseCommand):
price=20.00
)
Subject.objects.get_or_create(
level=l2,
classroom=l2,
teacher=teacher,
name_en='Physics',
name_ar='الفيزياء',
@ -46,7 +46,7 @@ class Command(BaseCommand):
price=50.00
)
Subject.objects.get_or_create(
level=l2,
classroom=l2,
teacher=teacher,
name_en='Chemistry',
name_ar='الكيمياء',
@ -55,4 +55,4 @@ class Command(BaseCommand):
price=45.00
)
self.stdout.write(self.style.SUCCESS('Successfully seeded sample data'))
self.stdout.write(self.style.SUCCESS('Successfully seeded sample data'))

View File

@ -0,0 +1,21 @@
# Generated by Django 5.2.7 on 2026-02-03 19:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='EducationalLevel',
new_name='Classroom',
),
migrations.AlterModelOptions(
name='classroom',
options={'verbose_name': 'Classroom', 'verbose_name_plural': 'Classrooms'},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2026-02-03 19:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0002_rename_educationallevel_classroom_and_more'),
]
operations = [
migrations.RenameField(
model_name='student',
old_name='level',
new_name='classroom',
),
migrations.RenameField(
model_name='subject',
old_name='level',
new_name='classroom',
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-02-04 02:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_rename_level_student_classroom_and_more'),
]
operations = [
migrations.AddField(
model_name='student',
name='avatar',
field=models.ImageField(blank=True, null=True, upload_to='students/', verbose_name='Picture'),
),
migrations.AddField(
model_name='student',
name='city',
field=models.CharField(blank=True, choices=[('cairo', 'Cairo'), ('giza', 'Giza'), ('alexandria', 'Alexandria')], max_length=50, verbose_name='City'),
),
migrations.AddField(
model_name='student',
name='moderate',
field=models.CharField(blank=True, choices=[('morning', 'Morning'), ('evening', 'Evening')], max_length=50, verbose_name='Moderate'),
),
]

View File

@ -0,0 +1,51 @@
# Generated by Django 5.2.7 on 2026-02-04 03:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_student_avatar_student_city_student_moderate'),
]
operations = [
migrations.CreateModel(
name='City',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Name')),
],
options={
'verbose_name': 'City',
'verbose_name_plural': 'Cities',
},
),
migrations.CreateModel(
name='Moderate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Name')),
],
options={
'verbose_name': 'Moderate',
'verbose_name_plural': 'Moderates',
},
),
migrations.AddField(
model_name='student',
name='mobile_number',
field=models.CharField(blank=True, max_length=20, verbose_name='Mobile Number'),
),
migrations.AlterField(
model_name='student',
name='city',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.city', verbose_name='City'),
),
migrations.AlterField(
model_name='student',
name='moderate',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.moderate', verbose_name='Moderate'),
),
]

View File

@ -11,16 +11,20 @@ class Teacher(models.Model):
def __str__(self):
return self.user.get_full_name() or self.user.username
class EducationalLevel(models.Model):
class Classroom(models.Model):
name_en = models.CharField(_("Name (English)"), max_length=100)
name_ar = models.CharField(_("Name (Arabic)"), max_length=100)
description = models.TextField(_("Description"), blank=True)
class Meta:
verbose_name = _("Classroom")
verbose_name_plural = _("Classrooms")
def __str__(self):
return self.name_en
class Subject(models.Model):
level = models.ForeignKey(EducationalLevel, on_delete=models.CASCADE, related_name='subjects')
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')
name_en = models.CharField(_("Name (English)"), max_length=200)
name_ar = models.CharField(_("Name (Arabic)"), max_length=200)
@ -45,11 +49,36 @@ class Resource(models.Model):
def __str__(self):
return self.title_en
class City(models.Model):
name = models.CharField(_("Name"), max_length=100)
class Meta:
verbose_name = _("City")
verbose_name_plural = _("Cities")
def __str__(self):
return self.name
class Moderate(models.Model):
name = models.CharField(_("Name"), max_length=100)
class Meta:
verbose_name = _("Moderate")
verbose_name_plural = _("Moderates")
def __str__(self):
return self.name
class Student(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='student_profile')
level = models.ForeignKey(EducationalLevel, on_delete=models.SET_NULL, null=True, blank=True, related_name='students')
classroom = models.ForeignKey(Classroom, on_delete=models.SET_NULL, null=True, blank=True, related_name='students')
phone_number = models.CharField(_("Phone Number"), max_length=20, blank=True)
mobile_number = models.CharField(_("Mobile Number"), max_length=20, blank=True)
subscribed_subjects = models.ManyToManyField(Subject, blank=True, related_name='subscribers')
moderate = models.ForeignKey(Moderate, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Moderate"))
city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("City"))
avatar = models.ImageField(_("Picture"), upload_to='students/', blank=True, null=True)
def __str__(self):
return self.user.get_full_name() or self.user.username

View File

@ -130,9 +130,11 @@
<li class="nav-item">
<a class="nav-link" href="#">{% trans "Teachers" %}</a>
</li>
{% if user.is_staff %}
<li class="nav-item">
<a class="nav-link" href="/admin/">{% trans "Admin" %}</a>
</li>
{% endif %}
</ul>
<div class="d-flex align-items-center">
<div class="dropdown me-3">
@ -145,8 +147,21 @@
</ul>
</div>
{% if user.is_authenticated %}
<span class="me-3">{% trans "Hello" %}, {{ user.username }}</span>
<div class="dropdown ms-2">
<button class="btn btn-link text-decoration-none dropdown-toggle text-dark d-flex align-items-center" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<span class="fw-semibold">{{ user.username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0" aria-labelledby="userDropdown" style="border-radius: 12px;">
<li><a class="dropdown-item py-2" href="{% url 'profile' %}">{% trans "Profile" %}</a></li>
{% if user.is_staff %}
<li><a class="dropdown-item py-2" href="/admin/">{% trans "Admin Panel" %}</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2 text-danger" href="{% url 'logout' %}">{% trans "Logout" %}</a></li>
</ul>
</div>
{% else %}
<a href="{% url 'register_student' %}" class="btn btn-outline-primary me-2">{% trans "Register" %}</a>
<a href="/admin/login/" class="btn btn-primary">{% trans "Login" %}</a>
{% endif %}
</div>

View File

@ -0,0 +1,77 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block title %}{% trans "My Profile" %} | EduPlatform{% endblock %}
{% block content %}
<section class="hero-section text-center pt-5 pb-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="glass-card text-start">
<div class="d-flex align-items-center mb-4">
{% if student_profile and student_profile.avatar %}
<img src="{{ student_profile.avatar.url }}" alt="Profile" class="rounded-circle me-3" width="100" height="100" style="object-fit: cover; border: 3px solid var(--primary-color);">
{% else %}
<div class="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-3" style="width: 100px; height: 100px; color: white; font-size: 2rem;">
{{ user.username|make_list|first|upper }}
</div>
{% endif %}
<div>
<h2 class="mb-1">{{ user.get_full_name|default:user.username }}</h2>
<p class="text-muted mb-0">{{ user.email }}</p>
{% if student_profile and student_profile.classroom %}
<span class="badge bg-primary">{{ student_profile.classroom.name_en }}</span>
{% elif user.is_staff %}
<span class="badge bg-success">{% trans "Staff / Admin" %}</span>
{% endif %}
</div>
</div>
<hr class="my-4">
{% if student_profile %}
<h4 class="mb-3">{% trans "Student Details" %}</h4>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label text-muted">{% trans "Username" %}</label>
<p class="fw-bold">{{ user.username }}</p>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-muted">{% trans "City" %}</label>
<p class="fw-bold">{{ student_profile.city|default:"-" }}</p>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-muted">{% trans "Shift (Moderate)" %}</label>
<p class="fw-bold">{{ student_profile.moderate|default:"-" }}</p>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-muted">{% trans "Mobile Number" %}</label>
<p class="fw-bold">{{ student_profile.mobile_number|default:"-" }}</p>
</div>
</div>
<h4 class="mb-3 mt-4">{% trans "My Subjects" %}</h4>
{% if student_profile.subscribed_subjects.all %}
<div class="list-group">
{% for subject in student_profile.subscribed_subjects.all %}
<a href="{% url 'subject_detail' subject.pk %}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
{{ subject.name_en }}
<span class="badge bg-secondary rounded-pill">{% trans "View" %}</span>
</a>
{% endfor %}
</div>
{% else %}
<p class="text-muted">{% trans "No subjects subscribed yet." %}</p>
{% endif %}
{% else %}
<div class="alert alert-info">
{% trans "You are logged in as a staff member or user without a student profile." %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,262 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block title %}{% trans "Student Registration" %}{% endblock %}
{% block content %}
<div class="container hero-section">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="glass-card">
<h2 class="text-center mb-4 text-primary">{% trans "Student Registration" %}</h2>
<form method="post" enctype="multipart/form-data" id="registrationForm">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger">
{{ form.errors }}
</div>
{% endif %}
<!-- User Details -->
<h5 class="mb-3 border-bottom pb-2">{% trans "Personal Information" %}</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">{{ form.first_name.label }}</label>
{{ form.first_name }}
</div>
<div class="col-md-6 mb-3">
<label class="form-label">{{ form.last_name.label }}</label>
{{ form.last_name }}
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ form.username.label }}</label>
{{ form.username }}
</div>
<div class="mb-3">
<label class="form-label">{{ form.email.label }}</label>
{{ form.email }}
</div>
<div class="mb-3">
<label class="form-label">{{ form.password.label }}</label>
{{ form.password }}
</div>
<!-- Student Details -->
<h5 class="mb-3 border-bottom pb-2 mt-4">{% trans "Student Details" %}</h5>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">{{ form.mobile_number.label }}</label>
{{ form.mobile_number }}
</div>
<div class="col-md-4 mb-3">
<label class="form-label">{{ form.moderate.label }}</label>
{{ form.moderate }}
</div>
<div class="col-md-4 mb-3">
<label class="form-label">{{ form.city.label }}</label>
{{ form.city }}
</div>
</div>
<!-- Classroom & Subjects -->
<div class="mb-3">
<label class="form-label">{{ form.classroom.label }}</label>
{{ form.classroom }}
</div>
<div class="mb-3">
<label class="form-label">{% trans "Subjects" %}</label>
<div id="subjects-container" class="border p-3 rounded bg-white" style="min-height: 100px;">
<p class="text-muted text-center mt-3">{% trans "Select a classroom to see subjects." %}</p>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-4 p-3 bg-primary text-white rounded">
<h4 class="m-0">{% trans "Total Amount:" %}</h4>
<h4 class="m-0"><span id="total-amount">0.00</span></h4>
</div>
<!-- Webcam -->
<h5 class="mb-3 border-bottom pb-2">{% trans "Profile Picture" %}</h5>
<div class="mb-4">
<div class="row">
<div class="col-md-6 d-flex flex-column align-items-center">
<label class="mb-2">{% trans "Webcam" %}</label>
<div id="video-container" style="position: relative;">
<video id="video" width="240" height="180" autoplay style="border: 2px solid #ddd; border-radius: 8px; background: #000;"></video>
<button type="button" id="snap" class="btn btn-sm btn-light position-absolute bottom-0 start-50 translate-middle-x mb-2">
📸 {% trans "Snap" %}
</button>
</div>
</div>
<div class="col-md-6 d-flex flex-column align-items-center">
<label class="mb-2">{% trans "Preview" %}</label>
<canvas id="canvas" width="240" height="180" style="display:none;"></canvas>
<div id="preview-placeholder" style="width: 240px; height: 180px; border: 2px dashed #ddd; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #aaa;">
{% trans "No photo taken" %}
</div>
<img id="photo-preview" width="240" height="180" class="rounded" style="display:none; border: 2px solid #4ECDC4;">
</div>
</div>
<div class="mt-3">
<label class="form-label small">{% trans "Or upload file:" %}</label>
{{ form.avatar }}
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 fs-5">{% trans "Register Now" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 1. Add Bootstrap classes to inputs
const inputs = document.querySelectorAll('input, select');
inputs.forEach(input => {
if (input.type !== 'checkbox' && input.type !== 'radio' && input.type !== 'file') {
input.classList.add('form-control');
} else if (input.type === 'file') {
input.classList.add('form-control');
} else if (input.type === 'checkbox') {
input.classList.add('form-check-input');
}
});
// 2. Classroom & Subjects Logic
const classroomSelect = document.getElementById('id_classroom');
const subjectsContainer = document.getElementById('subjects-container');
const totalAmountSpan = document.getElementById('total-amount');
classroomSelect.addEventListener('change', function() {
const classroomId = this.value;
if (!classroomId) {
subjectsContainer.innerHTML = '<p class="text-muted text-center mt-3">{% trans "Select a classroom to see subjects." %}</p>';
updateTotal();
return;
}
fetch(`{% url 'get_classroom_subjects' %}?classroom_id=${classroomId}`)
.then(response => response.json())
.then(data => {
subjectsContainer.innerHTML = '';
if (data.length === 0) {
subjectsContainer.innerHTML = '<p class="text-warning text-center">{% trans "No subjects found for this classroom." %}</p>';
} else {
data.forEach(subject => {
// Default price 0 if null
const price = parseFloat(subject.price) || 0;
const div = document.createElement('div');
div.className = 'form-check mb-2';
div.innerHTML = `
<input class="form-check-input subject-checkbox" type="checkbox" name="subjects" value="${subject.id}" id="subject_${subject.id}" data-price="${price}" checked>
<label class="form-check-label d-flex justify-content-between" for="subject_${subject.id}">
<span>${subject.name_en}</span>
<span class="badge bg-secondary rounded-pill">${price.toFixed(2)}</span>
</label>
`;
subjectsContainer.appendChild(div);
});
updateTotal();
}
})
.catch(err => {
console.error('Error fetching subjects:', err);
subjectsContainer.innerHTML = '<p class="text-danger">{% trans "Error loading subjects." %}</p>';
});
});
// Event delegation for dynamically created checkboxes
subjectsContainer.addEventListener('change', function(e) {
if (e.target.classList.contains('subject-checkbox')) {
updateTotal();
}
});
function updateTotal() {
let total = 0;
document.querySelectorAll('.subject-checkbox:checked').forEach(cb => {
total += parseFloat(cb.dataset.price) || 0;
});
totalAmountSpan.textContent = total.toFixed(2);
}
// 3. Webcam Logic
const video = document.getElementById('video');
const videoContainer = document.getElementById('video-container');
const canvas = document.getElementById('canvas');
const snap = document.getElementById('snap');
const photoPreview = document.getElementById('photo-preview');
const previewPlaceholder = document.getElementById('preview-placeholder');
const avatarInput = document.getElementById('id_avatar');
function showCameraError(message) {
videoContainer.innerHTML = `
<div class="alert alert-warning p-2 small text-center" style="height: 180px; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<i class="fas fa-exclamation-triangle mb-2" style="font-size: 1.5rem;"></i>
<div>${message}</div>
<div class="mt-1 text-muted small">{% trans "Please use the file upload below." %}</div>
</div>
`;
}
// Request camera access
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: true })
.then(function(stream) {
video.srcObject = stream;
video.play();
})
.catch(function(err) {
console.log("An error occurred: " + err);
let msg = "{% trans 'Camera access denied.' %}";
if (err.name === 'NotAllowedError') {
msg = "{% trans 'Permission denied. Allow camera access.' %}";
} else if (err.name === 'NotFoundError') {
msg = "{% trans 'No camera found.' %}";
} else if (window.isSecureContext === false) {
msg = "{% trans 'Camera requires HTTPS.' %}";
}
showCameraError(msg);
});
} else {
showCameraError("{% trans 'Browser does not support camera API or not secure.' %}");
}
if (snap) {
snap.addEventListener('click', function() {
if (video.srcObject) {
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0, 240, 180);
canvas.toBlob(function(blob) {
// Create a File object
const file = new File([blob], "webcam_capture.jpg", { type: "image/jpeg" });
// Create a DataTransfer to manipulate the file input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
avatarInput.files = dataTransfer.files;
// Show preview
const url = URL.createObjectURL(blob);
photoPreview.src = url;
photoPreview.style.display = 'block';
previewPlaceholder.style.display = 'none';
}, 'image/jpeg', 0.95);
}
});
}
});
</script>
{% endblock %}

View File

@ -11,7 +11,7 @@
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'index' %}">{% trans "Home" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">
{% if LANGUAGE_CODE == 'ar' %}{{ subject.level.name_ar }}{% else %}{{ subject.level.name_en }}{% endif %}
{% if LANGUAGE_CODE == 'ar' %}{{ subject.classroom.name_ar }}{% else %}{{ subject.classroom.name_en }}{% endif %}
</li>
</ol>
</nav>
@ -27,7 +27,7 @@
{% endif %}
<div class="position-absolute top-0 end-0 m-4">
<span class="badge bg-primary fs-6 px-3 py-2 shadow-sm">
{% if LANGUAGE_CODE == 'ar' %}{{ subject.level.name_ar }}{% else %}{{ subject.level.name_en }}{% endif %}
{% if LANGUAGE_CODE == 'ar' %}{{ subject.classroom.name_ar }}{% else %}{{ subject.classroom.name_en }}{% endif %}
</span>
</div>
</div>

View File

@ -6,4 +6,8 @@ urlpatterns = [
path('set-language/<str:lang_code>/', views.set_language, name='set_language'),
path('subject/<int:pk>/', views.subject_detail, name='subject_detail'),
path('ajax/get-subjects-by-level/', views.get_subjects_by_level, name='get_subjects_by_level'),
]
path('register/', views.register_student, name='register_student'),
path('ajax/get-classroom-subjects/', views.get_classroom_subjects, name='get_classroom_subjects'),
path('profile/', views.profile, name='profile'),
path('logout/', views.custom_logout, name='logout'),
]

View File

@ -3,11 +3,14 @@ from django.utils import translation
from django.conf import settings
from django.http import JsonResponse
from django.contrib.admin.views.decorators import staff_member_required
from .models import EducationalLevel, Subject, Teacher
from django.contrib.auth.decorators import login_required
from django.contrib.auth import logout
from .models import Classroom, Subject, Teacher, Student
from .forms import StudentRegistrationForm
def index(request):
# Fetch levels with their related subjects using prefetch_related for efficiency
levels = EducationalLevel.objects.prefetch_related('subjects').all()
levels = Classroom.objects.prefetch_related('subjects').all()
context = {
'levels': levels,
@ -32,8 +35,41 @@ def subject_detail(request, pk):
@staff_member_required
def get_subjects_by_level(request):
level_id = request.GET.get('level_id')
level_id = request.GET.get('level_id') or request.GET.get('classroom_id')
if not level_id:
return JsonResponse([], safe=False)
subjects = Subject.objects.filter(level_id=level_id).values('id', 'name_en', 'name_ar')
return JsonResponse(list(subjects), safe=False)
subjects = Subject.objects.filter(classroom_id=level_id).values('id', 'name_en', 'name_ar')
return JsonResponse(list(subjects), safe=False)
def get_classroom_subjects(request):
"""Public endpoint for registration form"""
classroom_id = request.GET.get('classroom_id')
if not classroom_id:
return JsonResponse([], safe=False)
subjects = Subject.objects.filter(classroom_id=classroom_id).values('id', 'name_en', 'price')
return JsonResponse(list(subjects), safe=False)
def register_student(request):
if request.method == 'POST':
form = StudentRegistrationForm(request.POST, request.FILES)
if form.is_valid():
student = form.save()
# Redirect to success page or login
return redirect('index') # Placeholder
else:
form = StudentRegistrationForm()
return render(request, 'core/registration.html', {'form': form})
@login_required
def profile(request):
try:
student_profile = request.user.student_profile
except Student.DoesNotExist:
student_profile = None
return render(request, 'core/profile.html', {'student_profile': student_profile})
def custom_logout(request):
logout(request)
return redirect('index')

View File

@ -2,12 +2,12 @@
'use strict';
$(function() {
console.log("Admin Resource JS loaded");
var $levelSelect = $('#id_educational_level');
var $levelSelect = $('#id_classroom');
var $subjectSelect = $('#id_subject');
// Check if elements exist
if (!$levelSelect.length) {
console.warn("Educational Level field #id_educational_level not found");
console.warn("Classroom field #id_classroom not found");
}
if (!$subjectSelect.length) {
console.warn("Subject field #id_subject not found");
@ -17,7 +17,7 @@
var isArabic = $('html').attr('lang') === 'ar' || $('body').hasClass('rtl');
function updateSubjects(levelId, currentSubjectId) {
console.log("Updating subjects for level:", levelId);
console.log("Updating subjects for classroom:", levelId);
// If no level selected, clear subjects
if (!levelId) {
$subjectSelect.html('<option value="">---------</option>');

View File

@ -1,7 +1,7 @@
(function($) {
'use strict';
$(function() {
var $levelSelect = $('#id_level');
var $levelSelect = $('#id_classroom');
var $subjectSelect = $('#id_subscribed_subjects');
// Check if we are in the admin change form
if (!$levelSelect.length || !$subjectSelect.length) return;
@ -75,4 +75,4 @@
updateSubjects($levelSelect.val(), selectedValues);
}
});
})(django.jQuery);
})(django.jQuery);

View File

@ -2,12 +2,12 @@
'use strict';
$(function() {
console.log("Admin Resource JS loaded");
var $levelSelect = $('#id_educational_level');
var $levelSelect = $('#id_classroom');
var $subjectSelect = $('#id_subject');
// Check if elements exist
if (!$levelSelect.length) {
console.warn("Educational Level field #id_educational_level not found");
console.warn("Classroom field #id_classroom not found");
}
if (!$subjectSelect.length) {
console.warn("Subject field #id_subject not found");
@ -17,7 +17,7 @@
var isArabic = $('html').attr('lang') === 'ar' || $('body').hasClass('rtl');
function updateSubjects(levelId, currentSubjectId) {
console.log("Updating subjects for level:", levelId);
console.log("Updating subjects for classroom:", levelId);
// If no level selected, clear subjects
if (!levelId) {
$subjectSelect.html('<option value="">---------</option>');

View File

@ -1,7 +1,7 @@
(function($) {
'use strict';
$(function() {
var $levelSelect = $('#id_level');
var $levelSelect = $('#id_classroom');
var $subjectSelect = $('#id_subscribed_subjects');
// Check if we are in the admin change form
if (!$levelSelect.length || !$subjectSelect.length) return;
@ -75,4 +75,4 @@
updateSubjects($levelSelect.val(), selectedValues);
}
});
})(django.jQuery);
})(django.jQuery);