Autosave: 20260204-035622
This commit is contained in:
parent
b9bad07848
commit
776c322148
Binary file not shown.
@ -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.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
57
core/forms.py
Normal 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
|
||||
@ -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'))
|
||||
@ -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'},
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
@ -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>
|
||||
|
||||
77
core/templates/core/profile.html
Normal file
77
core/templates/core/profile.html
Normal 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 %}
|
||||
262
core/templates/core/registration.html
Normal file
262
core/templates/core/registration.html
Normal 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 %}
|
||||
@ -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>
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
@ -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')
|
||||
|
||||
@ -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>');
|
||||
|
||||
@ -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);
|
||||
@ -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>');
|
||||
|
||||
@ -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);
|
||||
Loading…
x
Reference in New Issue
Block a user