diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 7ccceb4..fa65535 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 04a4eca..7c015e4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -58,6 +58,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'core', + 'configuration', ] MIDDLEWARE = [ @@ -196,6 +197,11 @@ if EMAIL_USE_SSL: DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# Authentication Redirects +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'profile' +LOGOUT_REDIRECT_URL = 'index' + JAZZMIN_SETTINGS = { # title of the window (Will default to current_admin_site.site_title if absent or None) "site_title": "School Admin", @@ -265,7 +271,7 @@ JAZZMIN_SETTINGS = { "hide_models": [], # List of apps (and/or models) to base side menu ordering off of (does not need to contain all apps/models) - "order_with_respect_to": ["core", "auth"], + "order_with_respect_to": ["core", "configuration", "auth"], # Custom icons for side menu apps/models See https://fontawesome.com/icons?d=gallery&m=free&v=5.0.0,5.0.1,5.0.10,5.0.11,5.0.12,5.0.13,5.1.0,5.1.1,5.2.0,5.3.0,5.3.1,5.4.0,5.4.1,5.4.2,5.13.0,5.12.0,5.11.2,5.11.1,5.10.0,5.5.0,5.6.0,5.6.1,5.6.3,5.7.0,5.7.1,5.7.2,5.8.0,5.8.1,5.8.2,5.9.0 "icons": { @@ -277,6 +283,9 @@ JAZZMIN_SETTINGS = { "core.Subject": "fas fa-book", "core.Resource": "fas fa-file-alt", "core.Classroom": "fas fa-layer-group", + "configuration.ThawaniSettings": "fas fa-credit-card", + "configuration.WablasSettings": "fas fa-comment-alt", + "configuration.PlatformProfile": "fas fa-cogs", }, # Icons that are used when one is not manually specified "default_icon_parents": "fas fa-chevron-circle-right", @@ -298,4 +307,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/configuration/__init__.py b/configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/configuration/__pycache__/__init__.cpython-311.pyc b/configuration/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..336d306 Binary files /dev/null and b/configuration/__pycache__/__init__.cpython-311.pyc differ diff --git a/configuration/__pycache__/admin.cpython-311.pyc b/configuration/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000..72a8709 Binary files /dev/null and b/configuration/__pycache__/admin.cpython-311.pyc differ diff --git a/configuration/__pycache__/apps.cpython-311.pyc b/configuration/__pycache__/apps.cpython-311.pyc new file mode 100644 index 0000000..00363d7 Binary files /dev/null and b/configuration/__pycache__/apps.cpython-311.pyc differ diff --git a/configuration/__pycache__/models.cpython-311.pyc b/configuration/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..e06df4b Binary files /dev/null and b/configuration/__pycache__/models.cpython-311.pyc differ diff --git a/configuration/admin.py b/configuration/admin.py new file mode 100644 index 0000000..875785c --- /dev/null +++ b/configuration/admin.py @@ -0,0 +1,71 @@ +from django.contrib import admin +from django.shortcuts import redirect +from django.urls import reverse +from .models import ThawaniSettings, WablasSettings, PlatformProfile + +class SingletonAdmin(admin.ModelAdmin): + def changelist_view(self, request, extra_context=None): + model = self.model + if model.objects.exists(): + obj = model.objects.first() + return redirect(reverse(f'admin:{model._meta.app_label}_{model._meta.model_name}_change', args=[obj.pk])) + else: + return redirect(reverse(f'admin:{model._meta.app_label}_{model._meta.model_name}_add')) + + def has_add_permission(self, request): + if self.model.objects.exists(): + return False + return super().has_add_permission(request) + + def change_view(self, request, object_id, form_url='', extra_context=None): + extra_context = extra_context or {} + extra_context['show_save_and_add_another'] = False + # extra_context['show_save_and_continue'] = False # Optional: keep if user wants to save and stay + return super().change_view(request, object_id, form_url, extra_context=extra_context) + + def add_view(self, request, form_url='', extra_context=None): + extra_context = extra_context or {} + extra_context['show_save_and_add_another'] = False + return super().add_view(request, form_url, extra_context=extra_context) + +@admin.register(ThawaniSettings) +class ThawaniSettingsAdmin(SingletonAdmin): + fieldsets = ( + ('API Credentials', { + 'fields': ('api_key', 'publishable_key'), + 'description': 'Enter your Thawani Pay API credentials.' + }), + ('Environment', { + 'fields': ('is_sandbox',), + 'description': 'Toggle Sandbox mode for testing.' + }), + ) + +@admin.register(WablasSettings) +class WablasSettingsAdmin(SingletonAdmin): + fieldsets = ( + ('Connection Details', { + 'fields': ('api_url', 'api_token'), + 'description': 'Configuration for WhatsApp integration via Wablas.' + }), + ('Security', { + 'fields': ('secret_key',), + }), + ) + +@admin.register(PlatformProfile) +class PlatformProfileAdmin(SingletonAdmin): + fieldsets = ( + ('Identity', { + 'fields': ('name', 'logo', 'description'), + 'classes': ('wide',), + }), + ('Contact Information', { + 'fields': ('contact_email', 'contact_phone', 'address'), + 'classes': ('wide',), + }), + ('Social Presence', { + 'fields': ('facebook_link', 'twitter_link', 'instagram_link'), + 'classes': ('collapse',), + }), + ) \ No newline at end of file diff --git a/configuration/apps.py b/configuration/apps.py new file mode 100644 index 0000000..d440a2b --- /dev/null +++ b/configuration/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + +class ConfigurationConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'configuration' + verbose_name = 'Settings' diff --git a/configuration/migrations/0001_initial.py b/configuration/migrations/0001_initial.py new file mode 100644 index 0000000..b8b06b2 --- /dev/null +++ b/configuration/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.7 on 2026-02-04 05:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('core', '0009_platformsettings_remove_student_is_mobile_verified_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PlatformProfile', + fields=[ + ], + options={ + 'verbose_name': 'Platform Profile', + 'verbose_name_plural': 'Platform Profile', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('core.platformsettings',), + ), + migrations.CreateModel( + name='ThawaniSettings', + fields=[ + ], + options={ + 'verbose_name': 'Thawani API', + 'verbose_name_plural': 'Thawani API', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('core.thawaniconfiguration',), + ), + migrations.CreateModel( + name='WablasSettings', + fields=[ + ], + options={ + 'verbose_name': 'Wablas API', + 'verbose_name_plural': 'Wablas API', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('core.wablasconfiguration',), + ), + ] diff --git a/configuration/migrations/__init__.py b/configuration/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/configuration/migrations/__pycache__/0001_initial.cpython-311.pyc b/configuration/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..9e277e0 Binary files /dev/null and b/configuration/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/configuration/migrations/__pycache__/__init__.cpython-311.pyc b/configuration/migrations/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..9fa991c Binary files /dev/null and b/configuration/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/configuration/models.py b/configuration/models.py new file mode 100644 index 0000000..55ced39 --- /dev/null +++ b/configuration/models.py @@ -0,0 +1,20 @@ +from django.db import models +from core.models import ThawaniConfiguration, WablasConfiguration, PlatformSettings + +class ThawaniSettings(ThawaniConfiguration): + class Meta: + proxy = True + verbose_name = "Thawani API" + verbose_name_plural = "Thawani API" + +class WablasSettings(WablasConfiguration): + class Meta: + proxy = True + verbose_name = "Wablas API" + verbose_name_plural = "Wablas API" + +class PlatformProfile(PlatformSettings): + class Meta: + proxy = True + verbose_name = "Platform Profile" + verbose_name_plural = "Platform Profile" diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index e3f5397..b27f091 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 index aa5bbba..7abaee6 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc 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 d0c89de..3554283 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/thawani.cpython-311.pyc b/core/__pycache__/thawani.cpython-311.pyc new file mode 100644 index 0000000..03b715c Binary files /dev/null and b/core/__pycache__/thawani.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 544cde0..bf0527e 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 fdde019..dc24be3 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/__pycache__/wablas.cpython-311.pyc b/core/__pycache__/wablas.cpython-311.pyc new file mode 100644 index 0000000..8d34e2f Binary files /dev/null and b/core/__pycache__/wablas.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 5832a93..3402ea9 100644 --- a/core/admin.py +++ b/core/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django import forms from django.utils.html import format_html from django.urls import reverse -from .models import Classroom, Teacher, Subject, Resource, Student, City, Moderate +from .models import Classroom, Teacher, Subject, Resource, Student, City, Governorate class ActionsModelAdmin(admin.ModelAdmin): """ @@ -41,11 +41,12 @@ class SubjectAdmin(ActionsModelAdmin): @admin.register(City) class CityAdmin(ActionsModelAdmin): - list_display = ('name', 'actions_column') + list_display = ('name_en', 'name_ar', 'governorate', 'actions_column') + list_filter = ('governorate',) -@admin.register(Moderate) -class ModerateAdmin(ActionsModelAdmin): - list_display = ('name', 'actions_column') +@admin.register(Governorate) +class GovernorateAdmin(ActionsModelAdmin): + list_display = ('name_en', 'name_ar', 'actions_column') class ResourceAdminForm(forms.ModelForm): classroom = forms.ModelChoiceField( @@ -75,8 +76,8 @@ class ResourceAdmin(ActionsModelAdmin): @admin.register(Student) class StudentAdmin(ActionsModelAdmin): - list_display = ('user', 'classroom', 'mobile_number', 'city', 'moderate', 'actions_column') - list_filter = ('classroom', 'city', 'moderate') + list_display = ('user', 'classroom', 'mobile_number', 'city', 'governorate', 'actions_column') + list_filter = ('classroom', 'city', 'governorate') filter_horizontal = ('subscribed_subjects',) class Media: diff --git a/core/forms.py b/core/forms.py index d2bdeb4..813bec2 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,13 +1,13 @@ from django import forms from django.contrib.auth.models import User -from .models import Student, Subject, Classroom +from .models import Student, Subject, Classroom, City, Governorate 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)") + full_name = forms.CharField(max_length=150, required=True, label="Full Name") username = forms.CharField(max_length=150, required=True) email = forms.EmailField(required=True) password = forms.CharField(widget=forms.PasswordInput, required=True) + password_confirm = forms.CharField(widget=forms.PasswordInput, required=True, label="Confirm Password") classroom = forms.ModelChoiceField(queryset=Classroom.objects.all(), required=True, empty_label="Select Classroom") subjects = forms.ModelMultipleChoiceField( @@ -19,13 +19,22 @@ class StudentRegistrationForm(forms.ModelForm): class Meta: model = Student - fields = ['mobile_number', 'moderate', 'city', 'avatar'] + fields = ['mobile_number', 'governorate', 'city', 'avatar'] widgets = { - 'avatar': forms.FileInput(attrs={'accept': 'image/*', 'capture': 'camera'}), # generic hint, but we'll use custom JS + 'avatar': forms.FileInput(attrs={'accept': 'image/*', 'capture': 'camera'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + + # Add Bootstrap classes + for field_name, field in self.fields.items(): + if field.widget.__class__.__name__ == 'CheckboxInput': + field.widget.attrs['class'] = 'form-check-input' + elif field.widget.__class__.__name__ != 'CheckboxSelectMultiple': + field.widget.attrs['class'] = 'form-control' + field.widget.attrs['placeholder'] = field.label # Important for Floating Labels! + # optimize subjects loading self.fields['subjects'].queryset = Subject.objects.none() @@ -38,14 +47,42 @@ class StudentRegistrationForm(forms.ModelForm): elif self.instance.pk and self.instance.classroom: self.fields['subjects'].queryset = Subject.objects.filter(classroom=self.instance.classroom) + # optimize city loading + self.fields['city'].queryset = City.objects.none() + + if 'governorate' in self.data: + try: + governorate_id = int(self.data.get('governorate')) + self.fields['city'].queryset = City.objects.filter(governorate_id=governorate_id) + except (ValueError, TypeError): + pass + elif self.instance.pk and self.instance.governorate: + self.fields['city'].queryset = City.objects.filter(governorate=self.instance.governorate) + + def clean(self): + cleaned_data = super().clean() + password = cleaned_data.get("password") + password_confirm = cleaned_data.get("password_confirm") + + if password and password_confirm and password != password_confirm: + self.add_error('password_confirm', "Passwords do not match") + + return cleaned_data + def save(self, commit=True): - # We need to save the User first, then the Student + full_name = self.cleaned_data['full_name'] + if " " in full_name: + first_name, last_name = full_name.split(" ", 1) + else: + first_name = full_name + last_name = "" + 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'] + first_name=first_name, + last_name=last_name ) student = super().save(commit=False) student.user = user diff --git a/core/management/commands/__pycache__/create_test_users.cpython-311.pyc b/core/management/commands/__pycache__/create_test_users.cpython-311.pyc new file mode 100644 index 0000000..2bfe5d1 Binary files /dev/null and b/core/management/commands/__pycache__/create_test_users.cpython-311.pyc differ diff --git a/core/management/commands/create_test_users.py b/core/management/commands/create_test_users.py new file mode 100644 index 0000000..b9dbcc3 --- /dev/null +++ b/core/management/commands/create_test_users.py @@ -0,0 +1,60 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User +from core.models import Teacher, Student, Classroom, City, Governorate + +class Command(BaseCommand): + help = 'Creates test users: one teacher and one student' + + def handle(self, *args, **options): + # 1. Setup Dependencies + gov, _ = Governorate.objects.get_or_create( + name_en="Muscat", + defaults={'name_ar': "مسقط"} + ) + city, _ = City.objects.get_or_create( + name_en="Seeb", + defaults={'name_ar': "السيب", 'governorate': gov} + ) + classroom, _ = Classroom.objects.get_or_create( + name_en="Grade 10", + defaults={'name_ar': "الصف 10", 'description': "Test Classroom"} + ) + + # 2. Create Teacher + t_user, created = User.objects.get_or_create(username='teacher_test') + if created: + t_user.set_password('password123') + t_user.email = 'teacher@example.com' + t_user.first_name = 'John' + t_user.last_name = 'Doe (Teacher)' + t_user.save() + + Teacher.objects.create( + user=t_user, + bio="I am a test teacher specializing in Mathematics.", + specialization="Mathematics" + ) + self.stdout.write(self.style.SUCCESS(f"Created teacher: {t_user.username} (password123)")) + else: + self.stdout.write(self.style.WARNING(f"User {t_user.username} already exists")) + + # 3. Create Student + s_user, created = User.objects.get_or_create(username='student_test') + if created: + s_user.set_password('password123') + s_user.email = 'student@example.com' + s_user.first_name = 'Jane' + s_user.last_name = 'Smith (Student)' + s_user.save() + + Student.objects.create( + user=s_user, + classroom=classroom, + city=city, + governorate=gov, + mobile_number="1234567890", + is_email_verified=True + ) + self.stdout.write(self.style.SUCCESS(f"Created student: {s_user.username} (password123)")) + else: + self.stdout.write(self.style.WARNING(f"User {s_user.username} already exists")) \ No newline at end of file diff --git a/core/migrations/0006_student_email_otp_code_student_is_email_verified_and_more.py b/core/migrations/0006_student_email_otp_code_student_is_email_verified_and_more.py new file mode 100644 index 0000000..14357b1 --- /dev/null +++ b/core/migrations/0006_student_email_otp_code_student_is_email_verified_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-02-04 04:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_city_moderate_student_mobile_number_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='student', + name='email_otp_code', + field=models.CharField(blank=True, max_length=6, null=True), + ), + migrations.AddField( + model_name='student', + name='is_email_verified', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='student', + name='is_mobile_verified', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='student', + name='mobile_otp_code', + field=models.CharField(blank=True, max_length=6, null=True), + ), + ] diff --git a/core/migrations/0007_wablasconfiguration.py b/core/migrations/0007_wablasconfiguration.py new file mode 100644 index 0000000..43dd6d0 --- /dev/null +++ b/core/migrations/0007_wablasconfiguration.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2026-02-04 04:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_student_email_otp_code_student_is_email_verified_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='WablasConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('api_url', models.URLField(default='https://kudus.wablas.com/api/send-message', verbose_name='API URL')), + ('api_token', models.CharField(max_length=500, verbose_name='API Token')), + ('secret_key', models.CharField(blank=True, max_length=500, null=True, verbose_name='Secret Key')), + ], + options={ + 'verbose_name': 'Wablas Configuration', + 'verbose_name_plural': 'Wablas Configuration', + }, + ), + ] diff --git a/core/migrations/0008_thawaniconfiguration.py b/core/migrations/0008_thawaniconfiguration.py new file mode 100644 index 0000000..7907936 --- /dev/null +++ b/core/migrations/0008_thawaniconfiguration.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2026-02-04 05:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_wablasconfiguration'), + ] + + operations = [ + migrations.CreateModel( + name='ThawaniConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('api_key', models.CharField(max_length=500, verbose_name='Secret Key')), + ('publishable_key', models.CharField(max_length=500, verbose_name='Publishable Key')), + ('is_sandbox', models.BooleanField(default=True, verbose_name='Sandbox Mode')), + ], + options={ + 'verbose_name': 'Thawani Configuration', + 'verbose_name_plural': 'Thawani Configuration', + }, + ), + ] diff --git a/core/migrations/0009_platformsettings_remove_student_is_mobile_verified_and_more.py b/core/migrations/0009_platformsettings_remove_student_is_mobile_verified_and_more.py new file mode 100644 index 0000000..ae9d621 --- /dev/null +++ b/core/migrations/0009_platformsettings_remove_student_is_mobile_verified_and_more.py @@ -0,0 +1,70 @@ +# Generated by Django 5.2.7 on 2026-02-04 05:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_thawaniconfiguration'), + ] + + operations = [ + migrations.CreateModel( + name='PlatformSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='My School Platform', max_length=100)), + ('logo', models.ImageField(blank=True, null=True, upload_to='platform/')), + ('description', models.TextField(blank=True, null=True)), + ('contact_email', models.EmailField(blank=True, max_length=254, null=True)), + ('contact_phone', models.CharField(blank=True, max_length=20, null=True)), + ('address', models.TextField(blank=True, null=True)), + ('facebook_link', models.URLField(blank=True, null=True)), + ('twitter_link', models.URLField(blank=True, null=True)), + ('instagram_link', models.URLField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Platform Profile', + 'verbose_name_plural': 'Platform Profile', + }, + ), + migrations.RemoveField( + model_name='student', + name='is_mobile_verified', + ), + migrations.RemoveField( + model_name='student', + name='mobile_otp_code', + ), + migrations.AlterField( + model_name='thawaniconfiguration', + name='api_key', + field=models.CharField(help_text='Thawani Secret Key', max_length=255), + ), + migrations.AlterField( + model_name='thawaniconfiguration', + name='is_sandbox', + field=models.BooleanField(default=True, help_text='Check to use Sandbox environment'), + ), + migrations.AlterField( + model_name='thawaniconfiguration', + name='publishable_key', + field=models.CharField(help_text='Thawani Publishable Key', max_length=255), + ), + migrations.AlterField( + model_name='wablasconfiguration', + name='api_token', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='wablasconfiguration', + name='api_url', + field=models.URLField(default='https://texas.wablas.com'), + ), + migrations.AlterField( + model_name='wablasconfiguration', + name='secret_key', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/core/migrations/0010_governorate_remove_student_moderate_remove_city_name_and_more.py b/core/migrations/0010_governorate_remove_student_moderate_remove_city_name_and_more.py new file mode 100644 index 0000000..af87c2e --- /dev/null +++ b/core/migrations/0010_governorate_remove_student_moderate_remove_city_name_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 5.2.7 on 2026-02-04 05:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_platformsettings_remove_student_is_mobile_verified_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Governorate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(default='', max_length=100, verbose_name='Name (English)')), + ('name_ar', models.CharField(default='', max_length=100, verbose_name='Name (Arabic)')), + ], + options={ + 'verbose_name': 'Governorate', + 'verbose_name_plural': 'Governorates', + }, + ), + migrations.RemoveField( + model_name='student', + name='moderate', + ), + migrations.RemoveField( + model_name='city', + name='name', + ), + migrations.AddField( + model_name='city', + name='name_ar', + field=models.CharField(default='', max_length=100, verbose_name='Name (Arabic)'), + ), + migrations.AddField( + model_name='city', + name='name_en', + field=models.CharField(default='', max_length=100, verbose_name='Name (English)'), + ), + migrations.AddField( + model_name='city', + name='governorate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cities', to='core.governorate', verbose_name='Governorate'), + ), + migrations.AddField( + model_name='student', + name='governorate', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.governorate', verbose_name='Governorate'), + ), + migrations.DeleteModel( + name='Moderate', + ), + ] diff --git a/core/migrations/__pycache__/0006_student_email_otp_code_student_is_email_verified_and_more.cpython-311.pyc b/core/migrations/__pycache__/0006_student_email_otp_code_student_is_email_verified_and_more.cpython-311.pyc new file mode 100644 index 0000000..7145eb7 Binary files /dev/null and b/core/migrations/__pycache__/0006_student_email_otp_code_student_is_email_verified_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0007_wablasconfiguration.cpython-311.pyc b/core/migrations/__pycache__/0007_wablasconfiguration.cpython-311.pyc new file mode 100644 index 0000000..4757dbc Binary files /dev/null and b/core/migrations/__pycache__/0007_wablasconfiguration.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0008_thawaniconfiguration.cpython-311.pyc b/core/migrations/__pycache__/0008_thawaniconfiguration.cpython-311.pyc new file mode 100644 index 0000000..881ab21 Binary files /dev/null and b/core/migrations/__pycache__/0008_thawaniconfiguration.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0009_platformsettings_alter_city_options_and_more.cpython-311.pyc b/core/migrations/__pycache__/0009_platformsettings_alter_city_options_and_more.cpython-311.pyc new file mode 100644 index 0000000..19891b6 Binary files /dev/null and b/core/migrations/__pycache__/0009_platformsettings_alter_city_options_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0009_platformsettings_remove_student_is_mobile_verified_and_more.cpython-311.pyc b/core/migrations/__pycache__/0009_platformsettings_remove_student_is_mobile_verified_and_more.cpython-311.pyc new file mode 100644 index 0000000..0b4563b Binary files /dev/null and b/core/migrations/__pycache__/0009_platformsettings_remove_student_is_mobile_verified_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0010_governorate_remove_student_moderate_remove_city_name_and_more.cpython-311.pyc b/core/migrations/__pycache__/0010_governorate_remove_student_moderate_remove_city_name_and_more.cpython-311.pyc new file mode 100644 index 0000000..245a1c5 Binary files /dev/null and b/core/migrations/__pycache__/0010_governorate_remove_student_moderate_remove_city_name_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 5efebf0..4fb8391 100644 --- a/core/models.py +++ b/core/models.py @@ -2,6 +2,19 @@ from django.db import models from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _ +class SingletonModel(models.Model): + class Meta: + abstract = True + + def save(self, *args, **kwargs): + self.pk = 1 + super(SingletonModel, self).save(*args, **kwargs) + + @classmethod + def load(cls): + obj, created = cls.objects.get_or_create(pk=1) + return obj + class Teacher(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='teacher_profile') bio = models.TextField(_("Bio"), blank=True) @@ -49,25 +62,28 @@ class Resource(models.Model): def __str__(self): return self.title_en +class Governorate(models.Model): + name_en = models.CharField(_("Name (English)"), max_length=100, default="") + name_ar = models.CharField(_("Name (Arabic)"), max_length=100, default="") + + class Meta: + verbose_name = _("Governorate") + verbose_name_plural = _("Governorates") + + def __str__(self): + return f"{self.name_en}" + class City(models.Model): - name = models.CharField(_("Name"), max_length=100) + governorate = models.ForeignKey(Governorate, on_delete=models.CASCADE, related_name='cities', verbose_name=_("Governorate"), null=True, blank=True) + name_en = models.CharField(_("Name (English)"), max_length=100, default="") + name_ar = models.CharField(_("Name (Arabic)"), max_length=100, default="") 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 + return f"{self.name_en}" class Student(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='student_profile') @@ -76,9 +92,59 @@ class Student(models.Model): 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")) + governorate = models.ForeignKey(Governorate, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Governorate")) 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) + + # Email Verification + email_otp_code = models.CharField(max_length=6, blank=True, null=True) + is_email_verified = models.BooleanField(default=False) def __str__(self): - return self.user.get_full_name() or self.user.username \ No newline at end of file + return self.user.get_full_name() or self.user.username + +# --- Configuration Models --- + +class WablasConfiguration(SingletonModel): + api_url = models.URLField(default="https://texas.wablas.com") + api_token = models.CharField(max_length=255) + secret_key = models.CharField(max_length=255, blank=True, null=True) + + class Meta: + verbose_name = "Wablas Configuration" + verbose_name_plural = "Wablas Configuration" + + def __str__(self): + return "Wablas Configuration" + +class ThawaniConfiguration(SingletonModel): + api_key = models.CharField(max_length=255, help_text="Thawani Secret Key") + publishable_key = models.CharField(max_length=255, help_text="Thawani Publishable Key") + is_sandbox = models.BooleanField(default=True, help_text="Check to use Sandbox environment") + + class Meta: + verbose_name = "Thawani Configuration" + verbose_name_plural = "Thawani Configuration" + + def __str__(self): + return "Thawani Configuration" + +class PlatformSettings(SingletonModel): + name = models.CharField(max_length=100, default="My School Platform") + logo = models.ImageField(upload_to='platform/', blank=True, null=True) + description = models.TextField(blank=True, null=True) + contact_email = models.EmailField(blank=True, null=True) + contact_phone = models.CharField(max_length=20, blank=True, null=True) + address = models.TextField(blank=True, null=True) + + # Social Media + facebook_link = models.URLField(blank=True, null=True) + twitter_link = models.URLField(blank=True, null=True) + instagram_link = models.URLField(blank=True, null=True) + + class Meta: + verbose_name = "Platform Profile" + verbose_name_plural = "Platform Profile" + + def __str__(self): + return "Platform Profile" diff --git a/core/templates/base.html b/core/templates/base.html index 000cc1a..0cc6e8a 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -162,7 +162,7 @@ {% else %} {% trans "Register" %} - {% trans "Login" %} + {% trans "Login" %} {% endif %} diff --git a/core/templates/core/error.html b/core/templates/core/error.html new file mode 100644 index 0000000..34dd7a2 --- /dev/null +++ b/core/templates/core/error.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block content %} +
{% trans "Sign in to continue your learning journey" %}
+{{ student_profile.city|default:"-" }}
{{ student_profile.moderate|default:"-" }}
+ +{{ student_profile.governorate|default:"-" }}
{% trans "Join our learning platform today" %}
+