diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 8c9c2f7..7ccceb4 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 8b4d2d5..04a4eca 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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, -} +} \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 4069c42..e3f5397 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..aa5bbba Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 3cf4a1d..d0c89de 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 31d12f8..544cde0 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 701adce..fdde019 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 287c66a..5832a93 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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( + '
', + 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 \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..d2bdeb4 --- /dev/null +++ b/core/forms.py @@ -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 \ No newline at end of file diff --git a/core/management/commands/seed_data.py b/core/management/commands/seed_data.py index c143bb3..3918e96 100644 --- a/core/management/commands/seed_data.py +++ b/core/management/commands/seed_data.py @@ -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')) \ No newline at end of file diff --git a/core/migrations/0002_rename_educationallevel_classroom_and_more.py b/core/migrations/0002_rename_educationallevel_classroom_and_more.py new file mode 100644 index 0000000..74c3896 --- /dev/null +++ b/core/migrations/0002_rename_educationallevel_classroom_and_more.py @@ -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'}, + ), + ] diff --git a/core/migrations/0003_rename_level_student_classroom_and_more.py b/core/migrations/0003_rename_level_student_classroom_and_more.py new file mode 100644 index 0000000..b9026dc --- /dev/null +++ b/core/migrations/0003_rename_level_student_classroom_and_more.py @@ -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', + ), + ] diff --git a/core/migrations/0004_student_avatar_student_city_student_moderate.py b/core/migrations/0004_student_avatar_student_city_student_moderate.py new file mode 100644 index 0000000..89992c2 --- /dev/null +++ b/core/migrations/0004_student_avatar_student_city_student_moderate.py @@ -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'), + ), + ] diff --git a/core/migrations/0005_city_moderate_student_mobile_number_and_more.py b/core/migrations/0005_city_moderate_student_mobile_number_and_more.py new file mode 100644 index 0000000..e5b8534 --- /dev/null +++ b/core/migrations/0005_city_moderate_student_mobile_number_and_more.py @@ -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'), + ), + ] diff --git a/core/migrations/__pycache__/0002_rename_educationallevel_classroom_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_rename_educationallevel_classroom_and_more.cpython-311.pyc new file mode 100644 index 0000000..8436b8e Binary files /dev/null and b/core/migrations/__pycache__/0002_rename_educationallevel_classroom_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_rename_level_student_classroom_and_more.cpython-311.pyc b/core/migrations/__pycache__/0003_rename_level_student_classroom_and_more.cpython-311.pyc new file mode 100644 index 0000000..676f49c Binary files /dev/null and b/core/migrations/__pycache__/0003_rename_level_student_classroom_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0004_student_avatar_student_city_student_moderate.cpython-311.pyc b/core/migrations/__pycache__/0004_student_avatar_student_city_student_moderate.cpython-311.pyc new file mode 100644 index 0000000..da1f27a Binary files /dev/null and b/core/migrations/__pycache__/0004_student_avatar_student_city_student_moderate.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0005_city_moderate_student_mobile_number_and_more.cpython-311.pyc b/core/migrations/__pycache__/0005_city_moderate_student_mobile_number_and_more.cpython-311.pyc new file mode 100644 index 0000000..646016c Binary files /dev/null and b/core/migrations/__pycache__/0005_city_moderate_student_mobile_number_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index ed0971a..5efebf0 100644 --- a/core/models.py +++ b/core/models.py @@ -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 \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index f9b5bbd..000cc1a 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -130,9 +130,11 @@{{ user.email }}
+ {% if student_profile and student_profile.classroom %} + {{ student_profile.classroom.name_en }} + {% elif user.is_staff %} + {% trans "Staff / Admin" %} + {% endif %} +{{ user.username }}
+{{ student_profile.city|default:"-" }}
+{{ student_profile.moderate|default:"-" }}
+{{ student_profile.mobile_number|default:"-" }}
+{% trans "No subjects subscribed yet." %}
+ {% endif %} + {% else %} +