├── .gitignore ├── .vscode └── settings.json ├── Procfile ├── README.md ├── academic ├── __init__.py ├── admin.py ├── apps.py ├── management │ └── commands │ │ ├── __init__.py │ │ ├── update_student_debt.py │ │ └── update_unpaid_salaries.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_initial.py │ ├── 0003_allocatedsubject_delete_subjectallocation.py │ ├── 0004_remove_classroom_grade_level_and_more.py │ ├── 0005_alter_subject_options_remove_topic_class_room_and_more.py │ ├── 0006_remove_studentclass_student_id_studentclass_student_and_more.py │ ├── 0007_alter_parent_options_alter_student_options_and_more.py │ ├── 0008_alter_subject_options_alter_teacher_options.py │ ├── 0009_alter_student_options_remove_teacher_isteacher.py │ ├── 0010_student_std_vii_number.py │ ├── 0011_rename_prem_number_student_prems_number.py │ ├── 0012_alter_student_parent_guardian.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── validators.py └── views.py ├── administration ├── __init__.py ├── admin.py ├── apps.py ├── common_objs.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_initial.py │ ├── 0003_alter_term_default_term_fee.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py ├── api ├── __init__.py ├── academic │ └── urls.py ├── admin.py ├── administration │ └── urls.py ├── apps.py ├── assignments │ └── urls.py ├── attendance │ └── urls.py ├── blog │ └── urls.py ├── finance │ └── urls.py ├── journals │ └── urls.py ├── migrations │ └── __init__.py ├── models.py ├── notes │ └── urls.py ├── schedule │ └── urls.py ├── serializers.py ├── sis │ └── urls.py ├── tests.py ├── users │ └── urls.py └── views.py ├── attendance ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py ├── db.sqlite3 ├── examination ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py ├── finance ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_initial.py │ ├── 0003_alter_payment_user.py │ ├── 0004_remove_payment_payment_no_remove_receipt_receipt_no_and_more.py │ ├── 0005_receipt_paid_through_alter_receipt_payer_debtrecord.py │ ├── 0006_receipt_payment_date.py │ ├── 0007_payment_paid_through_alter_receipt_paid_through.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py ├── manage.py ├── media └── articles │ ├── Copy_of_HIC_logo.jpg │ ├── Copy_of_HIC_logo_KnCOfnt.jpg │ └── Screenshot_from_2020-12-21_09-39-21.png ├── notes ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py ├── requirements.txt ├── schedule ├── __init__.py ├── admin.py ├── apps.py ├── management │ └── commands │ │ └── generate_timetable.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py ├── school ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── sis ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py ├── static └── images │ ├── articles │ └── Copy_of_HIC_logo.jpg │ ├── carousel │ ├── 20200918_100730.jpg │ ├── 20200921_093334.jpg │ └── 20200921_093729.jpg │ ├── nursery │ ├── 20201128_091608.jpg │ └── 20201128_091634.jpg │ └── primary │ ├── 20201128_092423.jpg │ └── 20201128_092541.jpg ├── templates └── index.html └── users ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── managers.py ├── migrations ├── 0001_initial.py ├── 0002_alter_customuser_options_and_more.py ├── 0003_customuser_phone_number.py └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | # Virtualenv related 4 | bin/ 5 | include/ 6 | pip-selfcheck.json 7 | 8 | # Django related 9 | # src//settings/local.py 10 | # static-cdn/ # any collected static files 11 | 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # Environments 94 | .env 95 | .venv 96 | env/ 97 | venv/ 98 | ENV/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | 113 | db.sqlite3 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/*.pyc": true 4 | }, 5 | "[python]": { 6 | "editor.defaultFormatter": "ms-python.black-formatter", 7 | "editor.formatOnSave": true 8 | } 9 | } -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python manage.py migrate 2 | web: gunicorn school.wsgi 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django School Management System 2 | This an open source school management system API built with Django and Django-rest framework for managing school or college. It provides an API for administration, admission, attendance, schedule and results. It also provide users with different permissions to access various apps depending on their access level. 3 | 4 | Techdometz is a tech startup helping schools and education centers to provide solutions to their tech problems. 5 | [Contact us](http://techdometz.com/contact-us/) for details. 6 | 7 | # Quick Install 8 | You should have at least basic django and django-rest framework experience to run django-scms. We test only in PostgreSQL database. 9 | 10 | ### Fork the repo 11 | You first need to fork the repo from [Techdometz](https://github.com/TechDometz/django-scms). 12 | ### Clone the repo 13 | Clone the forked repo 14 | 15 | `git clone https://github.com/[username]/django-scms.git` 16 | 17 | ### Create a virtual environment 18 | 19 | There are several ways depending on the OS and package you choose. Here's my favorite 20 | `sudo apt-get install python3-pip` 21 | `pip3 install virtualenv` 22 | Then either 23 | `python3 -m venv venv` 24 | or 25 | `python -m venv venv` 26 | or 27 | `virtualenv venv` (you can call it venv or anything you like) 28 | 29 | #### Activate the virtual environment 30 | 31 | in Mac or Linux 32 | `source venv/bin/activate` 33 | in windows 34 | `venv/Scripts/activate.bat` 35 | 36 | 37 | ## 🚀 Key Features 38 | 39 | - 🔐 **Authentication & Role-Based Access Control**: 40 | Supports authentication and permission control for: 41 | - **Admins** 42 | - **Teachers** 43 | - **Accountants** 44 | - **Parents** 45 | 46 | - 💸 **Finance Module** *(NEW)*: 47 | - Manage **Receipts** 48 | - Track **Payments** 49 | - Generate **Financial Reports** 50 | 51 | - 🧾 **School Information System (SIS)**: 52 | - Tracks student records and their associated parent/guardian contacts. 53 | - Manages class and academic year data. 54 | - Required module for all other apps. 55 | 56 | - 📝 **Admissions**: 57 | - Manages student admission pipeline and levels. 58 | - Tracks marketing channels and open house participation. 59 | 60 | --- 61 | 62 | ## 🔧 Upcoming Features 63 | 64 | - 📅 **Schedule Management** 65 | - 🧠 **Examinations and Grading** 66 | - 📚 **Digital Notes and Materials** 67 | - 📊 **Attendance Tracking** 68 | 69 | --- 70 | 71 | ## Contributors 72 | 73 | - [Mwinamijr](https://github.com/mwinamijr) 74 | 75 | ## License 76 | 77 | The project is licensed under the MIT License 78 | -------------------------------------------------------------------------------- /academic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/academic/__init__.py -------------------------------------------------------------------------------- /academic/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import * 4 | 5 | admin.site.register(Department) 6 | admin.site.register(Subject) 7 | admin.site.register(GradeLevel) 8 | admin.site.register(ClassLevel) 9 | admin.site.register(ClassYear) 10 | admin.site.register(Stream) 11 | admin.site.register(ClassRoom) 12 | admin.site.register(AllocatedSubject) 13 | admin.site.register(StudentClass) 14 | admin.site.register(Topic) 15 | admin.site.register(SubTopic) 16 | admin.site.register(Dormitory) 17 | admin.site.register(DormitoryAllocation) 18 | admin.site.register(MessageToParent) 19 | admin.site.register(MessageToTeacher) 20 | admin.site.register(Teacher) 21 | -------------------------------------------------------------------------------- /academic/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AcademicConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'academic' 7 | -------------------------------------------------------------------------------- /academic/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/academic/management/commands/__init__.py -------------------------------------------------------------------------------- /academic/management/commands/update_student_debt.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from academic.models import Student 3 | from administration.models import Term, AcademicYear 4 | from django.utils.timezone import now 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Update student debt at the start of each term and carry forward unpaid debt to the new academic year." 9 | 10 | def handle(self, *args, **kwargs): 11 | # Get the current date 12 | today = now().date() 13 | 14 | # Get the current academic year and term 15 | current_term = Term.objects.filter( 16 | start_date__lte=today, end_date__gte=today 17 | ).first() 18 | current_year = AcademicYear.objects.filter(current=True).first() 19 | 20 | if not current_term: 21 | self.stdout.write("No active term found for today.") 22 | return 23 | 24 | # Update debts for the current term 25 | students = Student.objects.all() 26 | for student in students: 27 | if current_term.academic_year == current_year: 28 | student.update_debt_for_term(current_term) 29 | else: 30 | student.carry_forward_debt_to_new_academic_year() 31 | 32 | self.stdout.write(f"Debts updated for term: {current_term.name}.") 33 | -------------------------------------------------------------------------------- /academic/management/commands/update_unpaid_salaries.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from users.models import Accountant 3 | from academic.models import Teacher 4 | from django.utils.timezone import now 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Update unpaid salaries for all teachers and accountants." 9 | 10 | def handle(self, *args, **kwargs): 11 | # Get the current date 12 | today = now().date() 13 | 14 | # Check if it's the start of a new month 15 | if today.day == 1: 16 | # Update unpaid salary for all teachers and accountants 17 | teachers = Teacher.objects.all() 18 | accountants = Accountant.objects.all() 19 | 20 | for teacher in teachers: 21 | teacher.update_unpaid_salary() 22 | self.stdout.write( 23 | self.style.SUCCESS( 24 | f"Updated unpaid salary for {teacher.first_name} {teacher.last_name}." 25 | ) 26 | ) 27 | 28 | for accountant in accountants: 29 | accountant.update_unpaid_salary() 30 | self.stdout.write( 31 | self.style.SUCCESS( 32 | f"Updated unpaid salary for {accountant.first_name} {accountant.last_name}." 33 | ) 34 | ) 35 | 36 | self.stdout.write( 37 | self.style.SUCCESS("All unpaid salaries have been updated.") 38 | ) 39 | else: 40 | self.stdout.write( 41 | self.style.WARNING( 42 | "It's not the start of the month, no updates were made." 43 | ) 44 | ) 45 | -------------------------------------------------------------------------------- /academic/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('academic', '0001_initial'), 13 | ('administration', '0001_initial'), 14 | ('users', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='FamilyAccessUser', 20 | fields=[ 21 | ], 22 | options={ 23 | 'proxy': True, 24 | 'indexes': [], 25 | 'constraints': [], 26 | }, 27 | bases=('users.customuser',), 28 | ), 29 | migrations.AddField( 30 | model_name='classroom', 31 | name='name', 32 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='class_level', to='academic.classlevel'), 33 | ), 34 | migrations.AddField( 35 | model_name='dormitoryallocation', 36 | name='dormitory', 37 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.dormitory'), 38 | ), 39 | migrations.AddField( 40 | model_name='classroom', 41 | name='grade_level', 42 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='academic.gradelevel'), 43 | ), 44 | migrations.AddField( 45 | model_name='classroom', 46 | name='stream', 47 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='class_stream', to='academic.stream'), 48 | ), 49 | migrations.AddField( 50 | model_name='student', 51 | name='class_of_year', 52 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='academic.classyear'), 53 | ), 54 | migrations.AddField( 55 | model_name='student', 56 | name='grade_level', 57 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='academic.gradelevel'), 58 | ), 59 | migrations.AddField( 60 | model_name='student', 61 | name='parent_guardian', 62 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='academic.parent'), 63 | ), 64 | migrations.AddField( 65 | model_name='student', 66 | name='reason_left', 67 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='academic.reasonleft'), 68 | ), 69 | migrations.AddField( 70 | model_name='student', 71 | name='siblings', 72 | field=models.ManyToManyField(blank=True, to='academic.student'), 73 | ), 74 | migrations.AddField( 75 | model_name='dormitoryallocation', 76 | name='student', 77 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.student'), 78 | ), 79 | migrations.AddField( 80 | model_name='dormitory', 81 | name='captain', 82 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='academic.student'), 83 | ), 84 | migrations.AddField( 85 | model_name='studentclass', 86 | name='academic_year', 87 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='administration.academicyear'), 88 | ), 89 | migrations.AddField( 90 | model_name='studentclass', 91 | name='classroom', 92 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='class_student', to='academic.classroom'), 93 | ), 94 | migrations.AddField( 95 | model_name='studentclass', 96 | name='student_id', 97 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_class', to='academic.student'), 98 | ), 99 | migrations.AddField( 100 | model_name='studentfile', 101 | name='student', 102 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.student'), 103 | ), 104 | migrations.AddField( 105 | model_name='studenthealthrecord', 106 | name='student', 107 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.student'), 108 | ), 109 | migrations.AddField( 110 | model_name='studentsmedicalhistory', 111 | name='student', 112 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.student'), 113 | ), 114 | migrations.AddField( 115 | model_name='studentspreviousacademichistory', 116 | name='student', 117 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.student'), 118 | ), 119 | migrations.AddField( 120 | model_name='subject', 121 | name='department', 122 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.department'), 123 | ), 124 | migrations.AddField( 125 | model_name='subjectallocation', 126 | name='academic_year', 127 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='administration.academicyear'), 128 | ), 129 | migrations.AddField( 130 | model_name='subjectallocation', 131 | name='class_room', 132 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subjects', to='academic.classroom'), 133 | ), 134 | migrations.AddField( 135 | model_name='subjectallocation', 136 | name='subject', 137 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocated_subjects', to='academic.subject'), 138 | ), 139 | migrations.AddField( 140 | model_name='subjectallocation', 141 | name='term', 142 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='administration.term'), 143 | ), 144 | migrations.AddField( 145 | model_name='teacher', 146 | name='subject_specialization', 147 | field=models.ManyToManyField(blank=True, to='academic.subject'), 148 | ), 149 | migrations.AddField( 150 | model_name='subjectallocation', 151 | name='teacher_name', 152 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.teacher'), 153 | ), 154 | migrations.AddField( 155 | model_name='classroom', 156 | name='class_teacher', 157 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='academic.teacher'), 158 | ), 159 | migrations.AddField( 160 | model_name='topic', 161 | name='class_room', 162 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.classroom'), 163 | ), 164 | migrations.AddField( 165 | model_name='topic', 166 | name='subject', 167 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.subject'), 168 | ), 169 | migrations.AddField( 170 | model_name='subtopic', 171 | name='topic', 172 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.topic'), 173 | ), 174 | migrations.AddConstraint( 175 | model_name='classroom', 176 | constraint=models.UniqueConstraint(fields=('name', 'stream'), name='unique_classroom'), 177 | ), 178 | ] 179 | -------------------------------------------------------------------------------- /academic/migrations/0003_allocatedsubject_delete_subjectallocation.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 15:16 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('academic', '0002_initial'), 11 | ('administration', '0002_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='AllocatedSubject', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('weekly_periods', models.IntegerField(help_text='Total number of periods per week.')), 20 | ('max_daily_periods', models.IntegerField(default=2, help_text='Maximum number of periods allowed per day for this subject.')), 21 | ('academic_year', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='administration.academicyear')), 22 | ('class_room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subjects', to='academic.classroom')), 23 | ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocated_subjects', to='academic.subject')), 24 | ('teacher_name', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.teacher')), 25 | ('term', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='administration.term')), 26 | ], 27 | ), 28 | migrations.DeleteModel( 29 | name='SubjectAllocation', 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /academic/migrations/0004_remove_classroom_grade_level_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-23 08:19 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('academic', '0003_allocatedsubject_delete_subjectallocation'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='classroom', 16 | name='grade_level', 17 | ), 18 | migrations.RemoveField( 19 | model_name='student', 20 | name='grade_level', 21 | ), 22 | migrations.AddField( 23 | model_name='classlevel', 24 | name='grade_level', 25 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='academic.gradelevel'), 26 | ), 27 | migrations.AddField( 28 | model_name='student', 29 | name='class_level', 30 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='academic.classlevel'), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /academic/migrations/0005_alter_subject_options_remove_topic_class_room_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-23 16:59 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('academic', '0004_remove_classroom_grade_level_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='subject', 16 | options={'ordering': ['name'], 'verbose_name': 'Subject', 'verbose_name_plural': 'Subjects'}, 17 | ), 18 | migrations.RemoveField( 19 | model_name='topic', 20 | name='class_room', 21 | ), 22 | migrations.AddField( 23 | model_name='topic', 24 | name='class_level', 25 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.classlevel'), 26 | ), 27 | migrations.AlterField( 28 | model_name='department', 29 | name='order_rank', 30 | field=models.IntegerField(blank=True, help_text='Rank for subject reports', null=True), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /academic/migrations/0006_remove_studentclass_student_id_studentclass_student_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-23 19:41 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('academic', '0005_alter_subject_options_remove_topic_class_room_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='studentclass', 16 | name='student_id', 17 | ), 18 | migrations.AddField( 19 | model_name='studentclass', 20 | name='student', 21 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='student_classes', to='academic.student'), 22 | ), 23 | migrations.AlterField( 24 | model_name='studentclass', 25 | name='classroom', 26 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='class_students', to='academic.classroom'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /academic/migrations/0007_alter_parent_options_alter_student_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-29 20:38 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('academic', '0006_remove_studentclass_student_id_studentclass_student_and_more'), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='parent', 18 | options={'ordering': ['email', 'first_name', 'last_name']}, 19 | ), 20 | migrations.AlterModelOptions( 21 | name='student', 22 | options={'ordering': ['last_name', 'first_name', 'admission_number']}, 23 | ), 24 | migrations.AddField( 25 | model_name='parent', 26 | name='user', 27 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent', to=settings.AUTH_USER_MODEL), 28 | ), 29 | migrations.AddField( 30 | model_name='teacher', 31 | name='user', 32 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='teacher', to=settings.AUTH_USER_MODEL), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /academic/migrations/0008_alter_subject_options_alter_teacher_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-02-04 15:25 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('academic', '0007_alter_parent_options_alter_student_options_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='subject', 15 | options={'ordering': ['subject_code'], 'verbose_name': 'Subject', 'verbose_name_plural': 'Subjects'}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='teacher', 19 | options={'ordering': ('id', 'first_name', 'last_name')}, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /academic/migrations/0009_alter_student_options_remove_teacher_isteacher.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-02-27 11:50 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('academic', '0008_alter_subject_options_alter_teacher_options'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='student', 15 | options={'ordering': ['admission_number', 'last_name', 'first_name']}, 16 | ), 17 | migrations.RemoveField( 18 | model_name='teacher', 19 | name='isTeacher', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /academic/migrations/0010_student_std_vii_number.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-05-01 17:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('academic', '0009_alter_student_options_remove_teacher_isteacher'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='student', 15 | name='std_vii_number', 16 | field=models.CharField(blank=True, max_length=50), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /academic/migrations/0011_rename_prem_number_student_prems_number.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-05-01 17:12 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('academic', '0010_student_std_vii_number'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='student', 15 | old_name='prem_number', 16 | new_name='prems_number', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /academic/migrations/0012_alter_student_parent_guardian.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-05-04 11:47 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('academic', '0011_rename_prem_number_student_prems_number'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='student', 16 | name='parent_guardian', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='academic.parent'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /academic/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/academic/migrations/__init__.py -------------------------------------------------------------------------------- /academic/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | 4 | from .models import ( 5 | ClassYear, 6 | ClassRoom, 7 | GradeLevel, 8 | ClassLevel, 9 | Subject, 10 | Department, 11 | Stream, 12 | ReasonLeft, 13 | StudentClass, 14 | ) 15 | 16 | 17 | class ClassYearSerializer(serializers.ModelSerializer): 18 | class Meta: 19 | model = ClassYear 20 | fields = "__all__" 21 | 22 | 23 | class DepartmentSerializer(serializers.ModelSerializer): 24 | class Meta: 25 | model = Department 26 | fields = "__all__" 27 | 28 | 29 | class ClassLevelSerializer(serializers.ModelSerializer): 30 | class Meta: 31 | model = ClassLevel 32 | fields = "__all__" 33 | 34 | 35 | class StreamSerializer(serializers.ModelSerializer): 36 | class Meta: 37 | model = Stream 38 | fields = "__all__" 39 | 40 | 41 | class SubjectSerializer(serializers.ModelSerializer): 42 | department = serializers.PrimaryKeyRelatedField(queryset=Department.objects.all()) 43 | 44 | class Meta: 45 | model = Subject 46 | fields = "__all__" 47 | 48 | def validate_subject_code(self, value): 49 | # Add custom validation if needed (e.g., regex validation) 50 | if len(value) < 3: 51 | raise serializers.ValidationError( 52 | "Subject code must be at least 3 characters." 53 | ) 54 | return value 55 | 56 | 57 | class GradeLevelSerializer(serializers.ModelSerializer): 58 | class Meta: 59 | model = GradeLevel 60 | fields = "__all__" 61 | 62 | 63 | class ClassRoomSerializer(serializers.ModelSerializer): 64 | name = serializers.SerializerMethodField() 65 | stream = serializers.SerializerMethodField() 66 | class_teacher = serializers.SerializerMethodField() 67 | available_sits = serializers.ReadOnlyField() 68 | class_status = serializers.ReadOnlyField() 69 | 70 | class Meta: 71 | model = ClassRoom 72 | fields = "__all__" 73 | 74 | def get_name(self, obj): 75 | return obj.name.name # Access the name field of the related ClassLevel object 76 | 77 | def get_stream(self, obj): 78 | return obj.stream.name # Access the name field of the related Stream object 79 | 80 | def get_class_teacher(self, obj): 81 | return ( 82 | f"{obj.class_teacher.first_name} {obj.class_teacher.last_name}" 83 | if obj.class_teacher 84 | else None 85 | ) 86 | 87 | 88 | class SchoolYearSerializer(serializers.ModelSerializer): 89 | class Meta: 90 | model = ClassYear 91 | fields = "__all__" 92 | 93 | 94 | class ReasonLeftSerializer(serializers.ModelSerializer): 95 | class Meta: 96 | model = ReasonLeft 97 | fields = "__all__" 98 | 99 | 100 | class StudentClassSerializer(serializers.ModelSerializer): 101 | class Meta: 102 | model = StudentClass 103 | fields = "__all__" 104 | 105 | def validate(self, data): 106 | classroom = data.get("classroom") 107 | if classroom.occupied_sits >= classroom.capacity: 108 | raise serializers.ValidationError("This class is already full.") 109 | return data 110 | -------------------------------------------------------------------------------- /academic/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /academic/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.utils.translation import gettext_lazy as _ 3 | from django.utils.deconstruct import deconstructible 4 | from django.core import validators 5 | import re 6 | from datetime import date 7 | 8 | 9 | def class_room_validator(value): 10 | """Ensure the classroom name is unique.""" 11 | from .models import ClassRoom 12 | 13 | if ClassRoom.objects.filter(name=value).exists(): 14 | raise ValidationError(_('"{}" already exists.'.format(value))) 15 | 16 | 17 | def subject_validator(value): 18 | """Ensure the subject name is unique.""" 19 | from .models import Subject 20 | 21 | if Subject.objects.filter(name=value).exists(): 22 | raise ValidationError(_('"{}" subject already exists.'.format(value))) 23 | 24 | 25 | def stream_validator(value): 26 | """Ensure the stream name is unique.""" 27 | from .models import Stream 28 | 29 | if Stream.objects.filter(name=value).exists(): 30 | raise ValidationError(_('"{}" stream already exists.'.format(value))) 31 | 32 | 33 | def students_date_of_birth_validator(value): 34 | """ 35 | Validate the student's date of birth to ensure the age is at least 13 years. 36 | """ 37 | required_age = 13 38 | least_year_of_birth = date.today().year - required_age 39 | 40 | if value.year > least_year_of_birth: 41 | raise ValidationError( 42 | _( 43 | "Invalid date. The student must be at least {} years old.".format( 44 | required_age 45 | ) 46 | ) 47 | ) 48 | 49 | 50 | @deconstructible 51 | class ASCIIUsernameValidator(validators.RegexValidator): 52 | """ 53 | Validator for ASCII-based usernames with a specific pattern. 54 | """ 55 | 56 | regex = r"^[a-zA-Z]+\/[a-zA-Z0-9]{3}\/\d{4}$" 57 | message = _( 58 | "Enter a valid username. This value must follow the pattern: " 59 | "letters/three alphanumeric characters/four digits." 60 | ) 61 | flags = re.ASCII 62 | -------------------------------------------------------------------------------- /administration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/administration/__init__.py -------------------------------------------------------------------------------- /administration/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import * 4 | 5 | admin.site.register(Article) 6 | admin.site.register(CarouselImage) 7 | admin.site.register(School) 8 | admin.site.register(Day) 9 | admin.site.register(AcademicYear) 10 | admin.site.register(Term) 11 | -------------------------------------------------------------------------------- /administration/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AdministrationConfig(AppConfig): 5 | name = 'administration' 6 | -------------------------------------------------------------------------------- /administration/common_objs.py: -------------------------------------------------------------------------------- 1 | A = "A" 2 | B = "B" 3 | C = "C" 4 | D = "D" 5 | F = "F" 6 | PASS = "PASS" 7 | FAIL = "FAIL" 8 | 9 | GRADE = ( 10 | (A, "A"), 11 | (B, "B"), 12 | (C, "C"), 13 | (D, "D"), 14 | (F, "F"), 15 | ) 16 | 17 | COMMENT = ( 18 | (PASS, "PASS"), 19 | (FAIL, "FAIL"), 20 | ) 21 | 22 | ACADEMIC_TERM = ( 23 | ("One", "One"), 24 | ("Two", "Two"), 25 | ("Three", "Three"), 26 | ("Four", "Four"), 27 | ) 28 | 29 | GENDER_CHOICE = (("Male", "Male"), ("Female", "Female"), ("Other", "Other")) 30 | RELIGION_CHOICE = (("Islam", "Islam"), ("Christian", "Christian"), ("Other", "Other")) 31 | 32 | PARENT_CHOICE = ( 33 | ("Father", "Father"), 34 | ("Mother", "Mother"), 35 | ("Guardian", "Guardian"), 36 | ) 37 | 38 | SCHOOL_TYPE_CHOICE = ( 39 | ("boarding school", "boarding school"), 40 | ("day school", "day school"), 41 | ("boarding-day school", "boarding-day school"), 42 | ) 43 | 44 | SCHOOL_STUDENTS_GENDER = ( 45 | ("Boys School", "Boys School"), 46 | ("Girl School", "Girl School"), 47 | ("Mixed", "Mixed"), 48 | ) 49 | 50 | SCHOOL_OWNERSHIP = ( 51 | ("Government", "Government"), 52 | ("Private", "Private"), 53 | ) 54 | 55 | 56 | ATTENDANCE_CHOICES = ( 57 | ("Present", "Present"), 58 | ("Absent", "Absent"), 59 | ("Holiday", "Holiday"), 60 | ("Sick", "Sick"), 61 | ) 62 | -------------------------------------------------------------------------------- /administration/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='AcademicYear', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=255, unique=True)), 19 | ('start_date', models.DateField()), 20 | ('end_date', models.DateField(blank=True, null=True)), 21 | ('graduation_date', models.DateField(blank=True, help_text='The date when students graduate', null=True)), 22 | ('active_year', models.BooleanField(help_text='DANGER!! This is the current school year. There can only be one and setting this will remove it from other years. If you want to change the active year, click Admin, Change School Year.')), 23 | ], 24 | options={ 25 | 'ordering': ('-start_date',), 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='AccessLog', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('ua', models.CharField(help_text='User agent. We can use this to determine operating system and browser in use.', max_length=2000)), 33 | ('date', models.DateTimeField(auto_now_add=True)), 34 | ('ip', models.GenericIPAddressField()), 35 | ('usage', models.CharField(max_length=255)), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='Article', 40 | fields=[ 41 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('title', models.CharField(blank=True, max_length=150, null=True)), 43 | ('content', models.TextField(blank=True, null=True)), 44 | ('picture', models.ImageField(blank=True, null=True, upload_to='articles')), 45 | ('created_at', models.DateTimeField(auto_now=True)), 46 | ], 47 | ), 48 | migrations.CreateModel( 49 | name='CarouselImage', 50 | fields=[ 51 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 52 | ('title', models.CharField(blank=True, max_length=150, null=True)), 53 | ('description', models.TextField(blank=True, null=True)), 54 | ('picture', models.ImageField(upload_to='carousel')), 55 | ], 56 | ), 57 | migrations.CreateModel( 58 | name='Day', 59 | fields=[ 60 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 61 | ('day', models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')], unique=True)), 62 | ], 63 | options={ 64 | 'ordering': ('day',), 65 | }, 66 | ), 67 | migrations.CreateModel( 68 | name='School', 69 | fields=[ 70 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('active', models.BooleanField(default=False, help_text='DANGER..!!!! If marked, this will be the default School Information System Wide...')), 72 | ('name', models.CharField(max_length=100)), 73 | ('address', models.CharField(max_length=250)), 74 | ('school_type', models.CharField(blank=True, choices=[('boarding school', 'boarding school'), ('day school', 'day school'), ('boarding-day school', 'boarding-day school')], max_length=25, null=True)), 75 | ('students_gender', models.CharField(blank=True, choices=[('Boys School', 'Boys School'), ('Girl School', 'Girl School'), ('Mixed', 'Mixed')], max_length=25, null=True)), 76 | ('ownership', models.CharField(blank=True, choices=[('Government', 'Government'), ('Private', 'Private')], max_length=25, null=True)), 77 | ('mission', models.TextField(blank=True, null=True)), 78 | ('vision', models.TextField(blank=True, null=True)), 79 | ('telephone', models.CharField(blank=True, max_length=20)), 80 | ('school_email', models.EmailField(blank=True, max_length=254, null=True)), 81 | ('school_logo', models.ImageField(blank=True, null=True, upload_to='school_info')), 82 | ], 83 | options={ 84 | 'ordering': ['name'], 85 | }, 86 | ), 87 | migrations.CreateModel( 88 | name='Term', 89 | fields=[ 90 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 91 | ('name', models.CharField(max_length=50)), 92 | ('default_term_fee', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), 93 | ('start_date', models.DateField()), 94 | ('end_date', models.DateField()), 95 | ], 96 | ), 97 | ] 98 | -------------------------------------------------------------------------------- /administration/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('administration', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='accesslog', 20 | name='login', 21 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 22 | ), 23 | migrations.AddField( 24 | model_name='article', 25 | name='created_by', 26 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL), 27 | ), 28 | migrations.AddIndex( 29 | model_name='school', 30 | index=models.Index(fields=['name'], name='administrat_name_c974a6_idx'), 31 | ), 32 | migrations.AddIndex( 33 | model_name='school', 34 | index=models.Index(fields=['active'], name='administrat_active_1f2ae9_idx'), 35 | ), 36 | migrations.AddField( 37 | model_name='term', 38 | name='academic_year', 39 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terms', to='administration.academicyear'), 40 | ), 41 | migrations.AddIndex( 42 | model_name='accesslog', 43 | index=models.Index(fields=['login'], name='administrat_login_i_b9be62_idx'), 44 | ), 45 | migrations.AddIndex( 46 | model_name='accesslog', 47 | index=models.Index(fields=['date'], name='administrat_date_0e2943_idx'), 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /administration/migrations/0003_alter_term_default_term_fee.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-23 16:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('administration', '0002_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='term', 15 | name='default_term_fee', 16 | field=models.DecimalField(decimal_places=2, default=312500, max_digits=10), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /administration/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/administration/migrations/__init__.py -------------------------------------------------------------------------------- /administration/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | from datetime import date, datetime 4 | from user_agents import parse 5 | 6 | from .common_objs import * 7 | from users.models import CustomUser 8 | 9 | 10 | class Article(models.Model): 11 | title = models.CharField(max_length=150, blank=True, null=True) 12 | content = models.TextField(blank=True, null=True) 13 | picture = models.ImageField(upload_to="articles", blank=True, null=True) 14 | created_by = models.ForeignKey( 15 | CustomUser, on_delete=models.DO_NOTHING, blank=True, null=True 16 | ) 17 | created_at = models.DateTimeField(auto_now=True) 18 | 19 | def __str__(self): 20 | return self.title 21 | 22 | 23 | class CarouselImage(models.Model): 24 | title = models.CharField(max_length=150, blank=True, null=True) 25 | description = models.TextField(blank=True, null=True) 26 | picture = models.ImageField(upload_to="carousel") 27 | 28 | def __str__(self): 29 | return self.title 30 | 31 | 32 | class AccessLog(models.Model): 33 | login = models.ForeignKey(CustomUser, null=True, on_delete=models.SET_NULL) 34 | ua = models.CharField( 35 | max_length=2000, 36 | help_text="User agent. We can use this to determine operating system and browser in use.", 37 | ) 38 | date = models.DateTimeField( 39 | auto_now_add=True 40 | ) # Set this to add the timestamp on creation only 41 | ip = models.GenericIPAddressField() 42 | usage = models.CharField(max_length=255) 43 | 44 | def __str__(self): 45 | return f"{self.login} - {self.usage} on {self.date}" 46 | 47 | def os(self): 48 | """ 49 | Extract the operating system from the user agent string. 50 | Returns 'Unknown' if it cannot be detected. 51 | """ 52 | try: 53 | user_agent = parse(self.ua) 54 | return user_agent.os.family 55 | except Exception as e: 56 | print(f"Error extracting OS from UA: {e}") 57 | return "Unknown" 58 | 59 | def browser(self): 60 | """ 61 | Extract the browser from the user agent string. 62 | Returns 'Unknown' if it cannot be detected. 63 | """ 64 | try: 65 | user_agent = parse(self.ua) 66 | return user_agent.browser.family 67 | except Exception as e: 68 | print(f"Error extracting Browser from UA: {e}") 69 | return "Unknown" 70 | 71 | class Meta: 72 | indexes = [ 73 | models.Index(fields=["login"]), # Add index for faster querying by login 74 | models.Index(fields=["date"]), # Add index for faster querying by date 75 | ] 76 | 77 | 78 | class School(models.Model): 79 | active = models.BooleanField( 80 | default=False, 81 | help_text="DANGER..!!!! If marked, this will be the default School Information System Wide...", 82 | ) 83 | name = models.CharField(max_length=100) 84 | address = models.CharField(max_length=250) 85 | school_type = models.CharField( 86 | max_length=25, choices=SCHOOL_TYPE_CHOICE, blank=True, null=True 87 | ) 88 | students_gender = models.CharField( 89 | max_length=25, choices=SCHOOL_STUDENTS_GENDER, blank=True, null=True 90 | ) 91 | ownership = models.CharField( 92 | max_length=25, choices=SCHOOL_OWNERSHIP, blank=True, null=True 93 | ) 94 | mission = models.TextField(blank=True, null=True) 95 | vision = models.TextField(blank=True, null=True) 96 | telephone = models.CharField(max_length=20, blank=True) 97 | school_email = models.EmailField(blank=True, null=True) 98 | school_logo = models.ImageField(blank=True, null=True, upload_to="school_info") 99 | 100 | def __str__(self): 101 | return self.name 102 | 103 | class Meta: 104 | indexes = [ 105 | models.Index(fields=["name"]), # Index for quick searching by name 106 | models.Index(fields=["active"]), # Index for filtering by active status 107 | ] 108 | ordering = ["name"] # Default ordering by school name 109 | 110 | 111 | class Day(models.Model): 112 | DAY_CHOICES = ( 113 | (1, "Monday"), 114 | (2, "Tuesday"), 115 | (3, "Wednesday"), 116 | (4, "Thursday"), 117 | (5, "Friday"), 118 | (6, "Saturday"), 119 | (7, "Sunday"), 120 | ) 121 | day = models.IntegerField(choices=DAY_CHOICES, unique=True) 122 | 123 | def __str__(self): 124 | return ( 125 | self.get_day_display() 126 | ) # Using get_day_display() to retrieve the display value for the day 127 | 128 | class Meta: 129 | ordering = ("day",) 130 | 131 | 132 | class AcademicYear(models.Model): 133 | """ 134 | A database table row that maps to every academic year. 135 | """ 136 | 137 | name = models.CharField(max_length=255, unique=True) 138 | start_date = models.DateField() 139 | end_date = models.DateField(blank=True, null=True) 140 | graduation_date = models.DateField( 141 | blank=True, null=True, help_text="The date when students graduate" 142 | ) 143 | active_year = models.BooleanField( 144 | help_text="DANGER!! This is the current school year. " 145 | "There can only be one and setting this will remove it from other years. " 146 | "If you want to change the active year, click Admin, Change School Year." 147 | ) 148 | 149 | class Meta: 150 | ordering = ("-start_date",) 151 | 152 | def __str__(self): 153 | return self.name 154 | 155 | @property 156 | def status(self): 157 | now_ = date.today() 158 | if self.active_year: 159 | return "active" 160 | elif self.start_date <= now_ <= self.end_date: 161 | return "pending" 162 | elif self.start_date > now_ > self.end_date: 163 | return "ended" 164 | return "unknown" # Fallback in case status doesn't match any condition 165 | 166 | def save(self, *args, **kwargs): 167 | # Ensure only one active year at a time 168 | if self.active_year: 169 | AcademicYear.objects.exclude(pk=self.pk).update(active_year=False) 170 | super(AcademicYear, self).save(*args, **kwargs) 171 | 172 | def clean(self): 173 | """ 174 | Add custom validation to ensure the end_date is after start_date if both are provided. 175 | """ 176 | if self.end_date and self.start_date > self.end_date: 177 | raise ValidationError("End date must be after start date.") 178 | 179 | 180 | class Term(models.Model): 181 | name = models.CharField(max_length=50) # e.g., "Term 1", "Term 2" 182 | academic_year = models.ForeignKey( 183 | AcademicYear, on_delete=models.CASCADE, related_name="terms" 184 | ) 185 | default_term_fee = models.DecimalField( 186 | max_digits=10, decimal_places=2, default=312500 187 | ) 188 | start_date = models.DateField() 189 | end_date = models.DateField() 190 | 191 | def __str__(self): 192 | return f"{self.name} - {self.academic_year.name}" 193 | -------------------------------------------------------------------------------- /administration/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import AcademicYear, Term, Article, CarouselImage 3 | 4 | 5 | from users.serializers import UserSerializer 6 | 7 | 8 | class ArticleSerializer(serializers.ModelSerializer): 9 | created_by = serializers.SerializerMethodField(read_only=True) 10 | # created_at = serializers.SerializerMethodField(read_only=True) 11 | short_content = serializers.SerializerMethodField(read_only=True) 12 | 13 | class Meta: 14 | model = Article 15 | fields = [ 16 | "id", 17 | "title", 18 | "content", 19 | "short_content", 20 | "picture", 21 | "created_at", 22 | "created_by", 23 | ] 24 | 25 | def get_created_by(self, obj): 26 | user = obj.created_by 27 | serializer = UserSerializer(user, many=False) 28 | if serializer.data["first_name"]: 29 | return serializer.data["first_name"] 30 | return serializer.data["email"] 31 | 32 | def get_short_content(self, obj): 33 | content = obj.content 34 | return content[:200] 35 | 36 | 37 | class CarouselImageSerializer(serializers.ModelSerializer): 38 | class Meta: 39 | model = CarouselImage 40 | fields = ["id", "title", "description", "picture"] 41 | 42 | 43 | class AcademicYearSerializer(serializers.ModelSerializer): 44 | class Meta: 45 | model = AcademicYear 46 | fields = "__all__" 47 | 48 | 49 | class TermSerializer(serializers.ModelSerializer): 50 | academic_year = ( 51 | serializers.StringRelatedField() 52 | ) # Display AcademicYear name in term details 53 | 54 | class Meta: 55 | model = Term 56 | fields = "__all__" 57 | -------------------------------------------------------------------------------- /administration/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /administration/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from rest_framework.permissions import IsAuthenticated 3 | from .models import AcademicYear, Term, Article, CarouselImage 4 | from .serializers import ( 5 | AcademicYearSerializer, 6 | TermSerializer, 7 | ArticleSerializer, 8 | CarouselImageSerializer, 9 | ) 10 | 11 | 12 | # Article Views 13 | class ArticleListCreateView(generics.ListCreateAPIView): 14 | queryset = Article.objects.all() 15 | serializer_class = ArticleSerializer 16 | permission_classes = [IsAuthenticated] 17 | 18 | 19 | class ArticleDetailView(generics.RetrieveUpdateDestroyAPIView): 20 | queryset = Article.objects.all() 21 | serializer_class = ArticleSerializer 22 | permission_classes = [IsAuthenticated] 23 | 24 | 25 | # CarouselImage Views 26 | class CarouselImageListCreateView(generics.ListCreateAPIView): 27 | queryset = CarouselImage.objects.all() 28 | serializer_class = CarouselImageSerializer 29 | permission_classes = [IsAuthenticated] 30 | 31 | 32 | class CarouselImageDetailView(generics.RetrieveUpdateDestroyAPIView): 33 | queryset = CarouselImage.objects.all() 34 | serializer_class = CarouselImageSerializer 35 | permission_classes = [IsAuthenticated] 36 | 37 | 38 | # AcademicYear Views 39 | class AcademicYearListCreateView(generics.ListCreateAPIView): 40 | queryset = AcademicYear.objects.all() 41 | serializer_class = AcademicYearSerializer 42 | permission_classes = [IsAuthenticated] 43 | 44 | 45 | class AcademicYearDetailView(generics.RetrieveUpdateDestroyAPIView): 46 | queryset = AcademicYear.objects.all() 47 | serializer_class = AcademicYearSerializer 48 | permission_classes = [IsAuthenticated] 49 | 50 | 51 | # Term Views 52 | class TermListCreateView(generics.ListCreateAPIView): 53 | queryset = Term.objects.all() 54 | serializer_class = TermSerializer 55 | permission_classes = [IsAuthenticated] 56 | 57 | 58 | class TermDetailView(generics.RetrieveUpdateDestroyAPIView): 59 | queryset = Term.objects.all() 60 | serializer_class = TermSerializer 61 | permission_classes = [IsAuthenticated] 62 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/api/__init__.py -------------------------------------------------------------------------------- /api/academic/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from academic.views import ( 3 | SubjectListView, 4 | SubjectDetailView, 5 | BulkUploadSubjectsView, 6 | ClassRoomView, 7 | BulkUploadClassRoomsView, 8 | DepartmentListCreateView, 9 | DepartmentDetailView, 10 | ClassLevelListCreateView, 11 | ClassLevelDetailView, 12 | GradeLevelListCreateView, 13 | GradeLevelDetailView, 14 | ClassYearListCreateView, 15 | ClassYearDetailView, 16 | ReasonLeftListCreateView, 17 | ReasonLeftDetailView, 18 | StreamListCreateView, 19 | StreamDetailView, 20 | StudentClassListCreateView, 21 | StudentClassDetailView, 22 | BulkUploadStudentClassView, 23 | ) 24 | 25 | 26 | urlpatterns = [ 27 | # Department URLs 28 | path( 29 | "departments/", 30 | DepartmentListCreateView.as_view(), 31 | name="department-list-create", 32 | ), 33 | path( 34 | "departments//", 35 | DepartmentDetailView.as_view(), 36 | name="department-detail", 37 | ), 38 | # ClassLevel URLs 39 | path( 40 | "class-levels/", 41 | ClassLevelListCreateView.as_view(), 42 | name="class-level-list-create", 43 | ), 44 | path( 45 | "class-levels//", 46 | ClassLevelDetailView.as_view(), 47 | name="class-level-detail", 48 | ), 49 | # GradeLevel URLs 50 | path( 51 | "grade-levels/", 52 | GradeLevelListCreateView.as_view(), 53 | name="grade-level-list-create", 54 | ), 55 | path( 56 | "grade-levels//", 57 | GradeLevelDetailView.as_view(), 58 | name="grade-level-detail", 59 | ), 60 | # ClassYear URLs 61 | path( 62 | "class-years/", ClassYearListCreateView.as_view(), name="class-year-list-create" 63 | ), 64 | path( 65 | "class-years//", ClassYearDetailView.as_view(), name="class-year-detail" 66 | ), 67 | # ReasonLeft URLs 68 | path( 69 | "reasons-left/", 70 | ReasonLeftListCreateView.as_view(), 71 | name="reason-left-list-create", 72 | ), 73 | path( 74 | "reasons-left//", 75 | ReasonLeftDetailView.as_view(), 76 | name="reason-left-detail", 77 | ), 78 | # Stream URLs 79 | path("streams/", StreamListCreateView.as_view(), name="stream-list-create"), 80 | path("streams//", StreamDetailView.as_view(), name="stream-detail"), 81 | path("subjects/", SubjectListView.as_view(), name="subject-list"), 82 | path("subjects//", SubjectDetailView.as_view(), name="subject-detail"), 83 | path( 84 | "subjects/bulk-upload/", 85 | BulkUploadSubjectsView.as_view(), 86 | name="subject-bulk-upload", 87 | ), 88 | path("classrooms/", ClassRoomView.as_view(), name="classroom-list"), 89 | path( 90 | "classrooms/bulk-upload/", 91 | BulkUploadClassRoomsView.as_view(), 92 | name="bulk-upload-classrooms", 93 | ), 94 | # StudentClass URLs 95 | path( 96 | "student-classes/", 97 | StudentClassListCreateView.as_view(), 98 | name="student-class-list-create", 99 | ), 100 | path( 101 | "student-classes//", 102 | StudentClassDetailView.as_view(), 103 | name="student-class-detail", 104 | ), 105 | path( 106 | "student-classes/bulk-upload/", 107 | BulkUploadStudentClassView.as_view(), 108 | name="student-class-bulk-upload", 109 | ), 110 | ] 111 | -------------------------------------------------------------------------------- /api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | -------------------------------------------------------------------------------- /api/administration/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from administration.views import ( 4 | AcademicYearListCreateView, 5 | AcademicYearDetailView, 6 | TermListCreateView, 7 | TermDetailView, 8 | ) 9 | 10 | 11 | urlpatterns = [ 12 | # AcademicYear URLs 13 | path( 14 | "academic-years/", 15 | AcademicYearListCreateView.as_view(), 16 | name="academic-year-list-create", 17 | ), 18 | path( 19 | "academic-years//", 20 | AcademicYearDetailView.as_view(), 21 | name="academic-year-detail", 22 | ), 23 | # Term URLs 24 | path("terms/", TermListCreateView.as_view(), name="term-list-create"), 25 | path("terms//", TermDetailView.as_view(), name="term-detail"), 26 | ] 27 | -------------------------------------------------------------------------------- /api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = 'api' 6 | -------------------------------------------------------------------------------- /api/assignments/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import DefaultRouter 2 | from notes.views import AssignmentViewSet 3 | 4 | router = DefaultRouter() 5 | router.register(r'', AssignmentViewSet) 6 | urlpatterns = router.urls 7 | -------------------------------------------------------------------------------- /api/attendance/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from attendance.views import ( 3 | TeacherAttendanceListView, 4 | TeacherAttendanceDetailView, 5 | StudentAttendanceListView, 6 | StudentAttendanceDetailView, 7 | PeriodAttendanceListView, 8 | PeriodAttendanceDetailView, 9 | ) 10 | 11 | urlpatterns = [ 12 | path( 13 | "teacher-attendance/", 14 | TeacherAttendanceListView.as_view(), 15 | name="teacher-attendance-list", 16 | ), 17 | path( 18 | "teacher-attendance//", 19 | TeacherAttendanceDetailView.as_view(), 20 | name="teacher-attendance-detail", 21 | ), 22 | path( 23 | "student-attendance/", 24 | StudentAttendanceListView.as_view(), 25 | name="student-attendance-list", 26 | ), 27 | path( 28 | "student-attendance//", 29 | StudentAttendanceDetailView.as_view(), 30 | name="student-attendance-detail", 31 | ), 32 | path( 33 | "period-attendance/", 34 | PeriodAttendanceListView.as_view(), 35 | name="period-attendance-list", 36 | ), 37 | path( 38 | "period-attendance//", 39 | PeriodAttendanceDetailView.as_view(), 40 | name="period-attendance-detail", 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /api/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from administration.views import ( 4 | ArticleListCreateView, 5 | ArticleDetailView, 6 | CarouselImageListCreateView, 7 | CarouselImageDetailView, 8 | ) 9 | 10 | 11 | urlpatterns = [ 12 | # Article URLs 13 | path("articles/", ArticleListCreateView.as_view(), name="article-list-create"), 14 | path("articles//", ArticleDetailView.as_view(), name="article-detail"), 15 | # Carousel Image URLs 16 | path( 17 | "carousel-images/", 18 | CarouselImageListCreateView.as_view(), 19 | name="carousel-image-list-create", 20 | ), 21 | path( 22 | "carousel-images//", 23 | CarouselImageDetailView.as_view(), 24 | name="carousel-image-detail", 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /api/finance/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from finance.views import ( 3 | ReceiptsListView, 4 | ReceiptDetailView, 5 | StudentReceiptsView, 6 | PaymentListView, 7 | PaymentDetailView, 8 | UpdateStudentDebtView, 9 | PaymentAllocationListView, 10 | PaymentAllocationDetailView, 11 | ReceiptAllocationListView, 12 | ReceiptAllocationDetailView, 13 | ) 14 | 15 | 16 | urlpatterns = [ 17 | path("receipts/", ReceiptsListView.as_view(), name="receipt-list"), 18 | path("receipts//", ReceiptDetailView.as_view(), name="receipt-detail"), 19 | path( 20 | "receipts/student//", 21 | StudentReceiptsView.as_view(), 22 | name="student-receipts", 23 | ), 24 | path("payments/", PaymentListView.as_view(), name="payment-list"), 25 | path("payments//", PaymentDetailView.as_view(), name="payment-detail"), 26 | path( 27 | "payment-allocations/", 28 | PaymentAllocationListView.as_view(), 29 | name="payment-allocation-list", 30 | ), 31 | path( 32 | "payment-allocations//", 33 | PaymentAllocationDetailView.as_view(), 34 | name="payment-allocation-detail", 35 | ), 36 | path( 37 | "receipt-allocations/", 38 | ReceiptAllocationListView.as_view(), 39 | name="receipt-allocation-list", 40 | ), 41 | path( 42 | "receipt-allocations//", 43 | ReceiptAllocationDetailView.as_view(), 44 | name="receipt-allocation-detail", 45 | ), 46 | path( 47 | "update-student-debt/", 48 | UpdateStudentDebtView.as_view(), 49 | name="update-student-debt", 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /api/journals/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter -------------------------------------------------------------------------------- /api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/api/migrations/__init__.py -------------------------------------------------------------------------------- /api/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/api/models.py -------------------------------------------------------------------------------- /api/notes/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | -------------------------------------------------------------------------------- /api/schedule/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter 3 | 4 | from schedule.views import PeriodViewSet, run_generate_timetable 5 | 6 | # Initialize the router 7 | router = DefaultRouter() 8 | router.register(r"periods", PeriodViewSet, basename="periods") 9 | 10 | urlpatterns = [ 11 | # Include ViewSet routes 12 | path("", include(router.urls)), 13 | # Generate timetable endpoint 14 | path("generate-timetable/", run_generate_timetable, name="generate_timetable"), 15 | ] 16 | -------------------------------------------------------------------------------- /api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from administration.models import Article, CarouselImage 3 | 4 | from users.serializers import UserSerializer 5 | 6 | class ArticleSerializer(serializers.ModelSerializer): 7 | created_by = serializers.SerializerMethodField(read_only=True) 8 | #created_at = serializers.SerializerMethodField(read_only=True) 9 | short_content = serializers.SerializerMethodField(read_only=True) 10 | 11 | class Meta: 12 | model = Article 13 | fields = ['id', 'title', 'content', 'short_content', 'picture', 'created_at', 'created_by'] 14 | 15 | def get_created_by(self, obj): 16 | user = obj.created_by 17 | serializer = UserSerializer(user, many=False) 18 | if(serializer.data['first_name']): 19 | return serializer.data['first_name'] 20 | return serializer.data['email'] 21 | 22 | def get_short_content(self, obj): 23 | content = obj.content 24 | return content[:200] 25 | 26 | class CarouselImageSerializer(serializers.ModelSerializer): 27 | class Meta: 28 | model = CarouselImage 29 | fields = ['id', 'title', 'description', 'picture'] 30 | 31 | -------------------------------------------------------------------------------- /api/sis/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from sis.views import ( 3 | # PhoneNumberViewSet, EmergencyContactViewSet, EmergencyContactNumberViewSet, 4 | # GradeLevelViewSet, ClassYearViewSet, StudentHealthRecordViewSet, GradeScaleViewSet, 5 | # GradeScaleRuleViewSet, SchoolYearViewSet, MessageToStudentViewSet, 6 | StudentListView, 7 | StudentDetailView, 8 | BulkUploadStudentsView, 9 | ) 10 | 11 | 12 | urlpatterns = [ 13 | path("students/", StudentListView.as_view(), name="students-list"), 14 | path("students//", StudentDetailView.as_view(), name="student-detail"), 15 | path("students/bulk-upload/", BulkUploadStudentsView.as_view()), 16 | ] 17 | -------------------------------------------------------------------------------- /api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /api/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter 3 | 4 | from users.views import ( 5 | MyTokenObtainPairView, 6 | getUserProfile, 7 | UserListView, 8 | UserDetailView, 9 | ParentListView, 10 | ParentDetailView, 11 | AccountantListView, 12 | AccountantDetailView, 13 | TeacherListView, 14 | TeacherDetailView, 15 | BulkUploadTeachersView, 16 | ) 17 | 18 | 19 | urlpatterns = [ 20 | # JWT Token endpoint 21 | path("login/", MyTokenObtainPairView.as_view(), name="token_obtain_pair"), 22 | path("profile/", getUserProfile, name="users-profile"), 23 | path("users/", UserListView.as_view(), name="users-list"), 24 | path("users//", UserDetailView.as_view(), name="user-detail"), 25 | # teacher URLs 26 | path("teachers/", TeacherListView.as_view(), name="teacher-list-create"), 27 | path( 28 | "teachers/bulk-upload/", 29 | BulkUploadTeachersView.as_view(), 30 | name="teacher-bulk-upload", 31 | ), 32 | path("teachers//", TeacherDetailView.as_view(), name="accountant-detail"), 33 | # Accountant URLs 34 | path("accountants/", AccountantListView.as_view(), name="accountant-list-create"), 35 | path( 36 | "accountants//", 37 | AccountantDetailView.as_view(), 38 | name="accountant-detail", 39 | ), 40 | # Parent URLs 41 | path("parents/", ParentListView.as_view(), name="parent-list-create"), 42 | path("parents//", ParentDetailView.as_view(), name="parent-detail"), 43 | ] 44 | -------------------------------------------------------------------------------- /api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, status, views 2 | from rest_framework.decorators import api_view, permission_classes 3 | from rest_framework.permissions import IsAuthenticated, IsAdminUser 4 | from rest_framework.response import Response 5 | from django.http import Http404 6 | 7 | from administration.models import Article, CarouselImage 8 | from .serializers import ( 9 | ArticleSerializer, CarouselImageSerializer) 10 | 11 | class ArticleViewSet(viewsets.ModelViewSet): 12 | queryset = Article.objects.all() 13 | serializer_class = ArticleSerializer 14 | 15 | class CarouselImageViewSet(viewsets.ModelViewSet): 16 | queryset = CarouselImage.objects.all() 17 | serializer_class = CarouselImageSerializer 18 | 19 | 20 | class ArticleListView(views.APIView): 21 | """ 22 | List all articles, or create a new article. 23 | """ 24 | def get(self, request, format=None): 25 | articles = Article.objects.all() 26 | serializer = ArticleSerializer(articles, many=True) 27 | return Response(serializer.data) 28 | 29 | def post(self, request, format=None): 30 | serializer = ArticleSerializer(data=request.data) 31 | if serializer.is_valid(): 32 | serializer.save() 33 | return Response(serializer.data, status=status.HTTP_201_CREATED) 34 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 35 | 36 | class ArticleDetailView(views.APIView): 37 | def get_object(self, pk): 38 | try: 39 | return Article.objects.get(pk=pk) 40 | except Article.DoesNotExist: 41 | raise Http404 42 | def get(self, request, pk, format=None): 43 | article = self.get_object(pk) 44 | serializer = ArticleSerializer(article) 45 | return Response(serializer.data) 46 | 47 | def put(self, request, pk, format=None): 48 | article = self.get_object(pk) 49 | serializer = ArticleSerializer(article, data=request.data) 50 | if serializer.is_valid(): 51 | serializer.save() 52 | return Response(serializer.data) 53 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 54 | 55 | def delete(self, request, pk, format=None): 56 | article = self.get_object(pk) 57 | article.delete() 58 | return Response(status=status.HTTP_204_NO_CONTENT) 59 | 60 | -------------------------------------------------------------------------------- /attendance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/attendance/__init__.py -------------------------------------------------------------------------------- /attendance/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import * 4 | 5 | admin.site.register(AttendanceStatus) 6 | admin.site.register(TeachersAttendance) 7 | admin.site.register(StudentAttendance) 8 | admin.site.register(PeriodAttendance) 9 | -------------------------------------------------------------------------------- /attendance/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AttendanceConfig(AppConfig): 5 | name = 'attendance' 6 | -------------------------------------------------------------------------------- /attendance/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | import datetime 4 | import django.core.validators 5 | import django.db.models.deletion 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('academic', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='AttendanceStatus', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('name', models.CharField(help_text='"Present" will not be saved but may show as an option for teachers.', max_length=255, unique=True)), 23 | ('code', models.CharField(help_text="Short code used on attendance reports. Example: 'A' might be the code for 'Absent'.", max_length=10, unique=True)), 24 | ('excused', models.BooleanField(default=False)), 25 | ('absent', models.BooleanField(default=False, help_text='Used for different types of absent statuses.')), 26 | ('late', models.BooleanField(default=False, help_text='Used for tracking late statuses.')), 27 | ('half', models.BooleanField(default=False, help_text='Indicates half-day attendance. Do not check absent, otherwise it will double count.')), 28 | ], 29 | options={ 30 | 'verbose_name_plural': 'Attendance Statuses', 31 | }, 32 | ), 33 | migrations.CreateModel( 34 | name='PeriodAttendance', 35 | fields=[ 36 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('date', models.DateField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(datetime.date(1970, 1, 1))])), 38 | ('period', models.IntegerField()), 39 | ('reason_for_absence', models.CharField(blank=True, max_length=500, null=True)), 40 | ('notes', models.CharField(blank=True, max_length=500)), 41 | ('status', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='attendance.attendancestatus')), 42 | ('student', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='academic.student')), 43 | ], 44 | options={ 45 | 'ordering': ('date', 'student', 'period'), 46 | 'unique_together': {('student', 'date', 'period')}, 47 | }, 48 | ), 49 | migrations.CreateModel( 50 | name='StudentAttendance', 51 | fields=[ 52 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 53 | ('date', models.DateField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(datetime.date(1970, 1, 1))])), 54 | ('notes', models.CharField(blank=True, max_length=500)), 55 | ('ClassRoom', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.classroom')), 56 | ('status', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='attendance.attendancestatus')), 57 | ('student', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='academic.student')), 58 | ], 59 | options={ 60 | 'ordering': ('-date', 'student'), 61 | 'unique_together': {('student', 'date', 'status')}, 62 | }, 63 | ), 64 | migrations.CreateModel( 65 | name='TeachersAttendance', 66 | fields=[ 67 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 68 | ('date', models.DateField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(datetime.date(1970, 1, 1))])), 69 | ('time_in', models.TimeField(blank=True, null=True)), 70 | ('time_out', models.TimeField(blank=True, null=True)), 71 | ('notes', models.CharField(blank=True, max_length=500)), 72 | ('status', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='attendance.attendancestatus')), 73 | ('teacher', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='academic.teacher')), 74 | ], 75 | options={ 76 | 'ordering': ('-date', 'teacher'), 77 | 'unique_together': {('teacher', 'date', 'status')}, 78 | }, 79 | ), 80 | ] 81 | -------------------------------------------------------------------------------- /attendance/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/attendance/migrations/__init__.py -------------------------------------------------------------------------------- /attendance/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | from academic.models import Student 5 | from users.models import CustomUser, Accountant 6 | from academic.models import Teacher 7 | import datetime 8 | 9 | 10 | # Create your models here. 11 | class AttendanceStatus(models.Model): 12 | name = models.CharField( 13 | max_length=255, 14 | unique=True, 15 | help_text='"Present" will not be saved but may show as an option for teachers.', 16 | ) 17 | code = models.CharField( 18 | max_length=10, 19 | unique=True, 20 | help_text="Short code used on attendance reports. Example: 'A' might be the code for 'Absent'.", 21 | ) 22 | excused = models.BooleanField(default=False) 23 | absent = models.BooleanField( 24 | default=False, help_text="Used for different types of absent statuses." 25 | ) 26 | late = models.BooleanField( 27 | default=False, help_text="Used for tracking late statuses." 28 | ) 29 | half = models.BooleanField( 30 | default=False, 31 | help_text="Indicates half-day attendance. Do not check absent, otherwise it will double count.", 32 | ) 33 | 34 | class Meta: 35 | verbose_name_plural = "Attendance Statuses" 36 | 37 | def __str__(self): 38 | return self.name 39 | 40 | 41 | class TeachersAttendance(models.Model): 42 | date = models.DateField(blank=True, null=True, validators=settings.DATE_VALIDATORS) 43 | teacher = models.ForeignKey(Teacher, blank=True, on_delete=models.CASCADE) 44 | time_in = models.TimeField(blank=True, null=True) 45 | time_out = models.TimeField(blank=True, null=True) 46 | status = models.ForeignKey( 47 | AttendanceStatus, blank=True, null=True, on_delete=models.CASCADE 48 | ) 49 | notes = models.CharField(max_length=500, blank=True) 50 | 51 | class Meta: 52 | unique_together = (("teacher", "date", "status"),) 53 | ordering = ("-date", "teacher") 54 | 55 | def __str__(self): 56 | return f"{self.teacher} - {self.date} {self.status}" 57 | 58 | @property 59 | def edit(self): 60 | return f"Edit {self.teacher} - {self.date}" 61 | 62 | def save(self, *args, **kwargs): 63 | """Update for those who are late""" 64 | present, created = AttendanceStatus.objects.get_or_create(name="Present") 65 | 66 | # Check if the teacher is marked as "Present" and if they are late 67 | if ( 68 | self.status == present 69 | and self.time_in 70 | and self.time_in >= datetime.time(7, 0, 0) 71 | ): 72 | self.status.late = True # Mark status as late 73 | elif self.status != present: 74 | self.status.late = False # Reset late if not present 75 | 76 | super(TeachersAttendance, self).save(*args, **kwargs) 77 | 78 | 79 | class StudentAttendance(models.Model): 80 | student = models.ForeignKey(Student, blank=True, on_delete=models.CASCADE) 81 | date = models.DateField(blank=True, null=True, validators=settings.DATE_VALIDATORS) 82 | ClassRoom = models.ForeignKey( 83 | "academic.ClassRoom", on_delete=models.CASCADE, blank=True, null=True 84 | ) 85 | status = models.ForeignKey( 86 | AttendanceStatus, blank=True, null=True, on_delete=models.CASCADE 87 | ) 88 | notes = models.CharField(max_length=500, blank=True) 89 | 90 | class Meta: 91 | unique_together = (("student", "date", "status"),) 92 | ordering = ("-date", "student") 93 | 94 | def __str__(self): 95 | return f"{self.student.fname} - {self.date} {self.status}" 96 | 97 | @property 98 | def edit(self): 99 | return f"Edit {self.student.fname} - {self.date}" 100 | 101 | def save(self, *args, **kwargs): 102 | """Don't save if status is 'Present'""" 103 | present, created = AttendanceStatus.objects.get_or_create(name="Present") 104 | 105 | if self.status != present: 106 | super(StudentAttendance, self).save(*args, **kwargs) 107 | else: 108 | # Instead of deleting, just skip saving 109 | print( 110 | f"Attendance not saved for {self.student} on {self.date} because they are marked as 'Present'." 111 | ) 112 | 113 | 114 | class PeriodAttendance(models.Model): 115 | student = models.ForeignKey(Student, blank=True, on_delete=models.CASCADE) 116 | date = models.DateField(blank=True, null=True, validators=settings.DATE_VALIDATORS) 117 | period = ( 118 | models.IntegerField() 119 | ) # e.g., 1 for the first period, 2 for the second period, etc. 120 | status = models.ForeignKey( 121 | AttendanceStatus, blank=True, null=True, on_delete=models.CASCADE 122 | ) 123 | reason_for_absence = models.CharField(max_length=500, blank=True, null=True) 124 | notes = models.CharField(max_length=500, blank=True) 125 | 126 | class Meta: 127 | unique_together = (("student", "date", "period"),) 128 | ordering = ("date", "student", "period") 129 | 130 | def __str__(self): 131 | return f"{self.student.fname} - {self.date} Period {self.period} {self.status}" 132 | 133 | @property 134 | def edit(self): 135 | return f"Edit {self.student.fname} - {self.date} Period {self.period}" 136 | -------------------------------------------------------------------------------- /attendance/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import ( 3 | TeachersAttendance, 4 | AttendanceStatus, 5 | StudentAttendance, 6 | PeriodAttendance, 7 | ) 8 | 9 | 10 | class AttendanceStatusSerializer(serializers.ModelSerializer): 11 | 12 | class Meta: 13 | model = AttendanceStatus 14 | fields = "__all__" 15 | 16 | 17 | class TeacherAttendanceSerializer(serializers.ModelSerializer): 18 | teacher = ( 19 | serializers.StringRelatedField() 20 | ) # Display teacher's name instead of the ID 21 | status = serializers.StringRelatedField() # Display status name instead of ID 22 | date = serializers.DateField(format="%Y-%m-%d") # Date format in response 23 | 24 | class Meta: 25 | model = TeachersAttendance 26 | fields = ["id", "teacher", "date", "time_in", "time_out", "status", "notes"] 27 | 28 | 29 | class StudentAttendanceSerializer(serializers.ModelSerializer): 30 | student = serializers.StringRelatedField() # Display student name instead of ID 31 | status = serializers.StringRelatedField() # Display status name instead of ID 32 | ClassRoom = serializers.StringRelatedField() # Display classroom name instead of ID 33 | date = serializers.DateField(format="%Y-%m-%d") # Date format in response 34 | 35 | class Meta: 36 | model = StudentAttendance 37 | fields = ["id", "student", "date", "ClassRoom", "status", "notes"] 38 | 39 | 40 | class PeriodAttendanceSerializer(serializers.ModelSerializer): 41 | student = serializers.StringRelatedField() # Display student name instead of ID 42 | status = serializers.StringRelatedField() # Display status name instead of ID 43 | date = serializers.DateField(format="%Y-%m-%d") # Date format in response 44 | 45 | class Meta: 46 | model = PeriodAttendance 47 | fields = [ 48 | "id", 49 | "student", 50 | "date", 51 | "period", 52 | "status", 53 | "reason_for_absence", 54 | "notes", 55 | ] 56 | -------------------------------------------------------------------------------- /attendance/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /attendance/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework.response import Response 3 | from rest_framework import status 4 | from rest_framework.exceptions import NotFound 5 | from rest_framework.pagination import PageNumberPagination 6 | from rest_framework.filters import SearchFilter 7 | from .models import TeachersAttendance, StudentAttendance, PeriodAttendance 8 | from .serializers import ( 9 | TeacherAttendanceSerializer, 10 | StudentAttendanceSerializer, 11 | PeriodAttendanceSerializer, 12 | ) 13 | 14 | 15 | class TeacherAttendanceListView(APIView): 16 | 17 | queryset = TeachersAttendance.objects.all() 18 | serializer_class = TeacherAttendanceSerializer 19 | pagination_class = PageNumberPagination 20 | filter_backends = (SearchFilter,) 21 | search_fields = ["teacher__fname", "date"] 22 | 23 | def get(self, request): 24 | attendances = TeachersAttendance.objects.all() 25 | serializer = TeacherAttendanceSerializer(attendances, many=True) 26 | return Response(serializer.data) 27 | 28 | def post(self, request): 29 | serializer = TeacherAttendanceSerializer(data=request.data) 30 | if serializer.is_valid(): 31 | serializer.save() 32 | return Response(serializer.data, status=status.HTTP_201_CREATED) 33 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 34 | 35 | 36 | class TeacherAttendanceDetailView(APIView): 37 | def get(self, request, pk): 38 | try: 39 | attendance = TeachersAttendance.objects.get(pk=pk) 40 | except TeachersAttendance.DoesNotExist: 41 | raise NotFound(detail="Teacher Attendance record not found.") 42 | 43 | serializer = TeacherAttendanceSerializer(attendance) 44 | return Response(serializer.data) 45 | 46 | def put(self, request, pk): 47 | try: 48 | attendance = TeachersAttendance.objects.get(pk=pk) 49 | except TeachersAttendance.DoesNotExist: 50 | raise NotFound(detail="Teacher Attendance record not found.") 51 | 52 | serializer = TeacherAttendanceSerializer(attendance, data=request.data) 53 | if serializer.is_valid(): 54 | serializer.save() 55 | return Response(serializer.data) 56 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 57 | 58 | def delete(self, request, pk): 59 | try: 60 | attendance = TeachersAttendance.objects.get(pk=pk) 61 | except TeachersAttendance.DoesNotExist: 62 | raise NotFound(detail="Teacher Attendance record not found.") 63 | 64 | attendance.delete() 65 | return Response(status=status.HTTP_204_NO_CONTENT) 66 | 67 | 68 | class StudentAttendanceListView(APIView): 69 | def get(self, request): 70 | attendances = StudentAttendance.objects.all() 71 | serializer = StudentAttendanceSerializer(attendances, many=True) 72 | return Response(serializer.data) 73 | 74 | def post(self, request): 75 | serializer = StudentAttendanceSerializer(data=request.data) 76 | if serializer.is_valid(): 77 | serializer.save() 78 | return Response(serializer.data, status=status.HTTP_201_CREATED) 79 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 80 | 81 | 82 | class StudentAttendanceDetailView(APIView): 83 | def get(self, request, pk): 84 | try: 85 | attendance = StudentAttendance.objects.get(pk=pk) 86 | except StudentAttendance.DoesNotExist: 87 | raise NotFound(detail="Student Attendance record not found.") 88 | 89 | serializer = StudentAttendanceSerializer(attendance) 90 | return Response(serializer.data) 91 | 92 | def put(self, request, pk): 93 | try: 94 | attendance = StudentAttendance.objects.get(pk=pk) 95 | except StudentAttendance.DoesNotExist: 96 | raise NotFound(detail="Student Attendance record not found.") 97 | 98 | serializer = StudentAttendanceSerializer(attendance, data=request.data) 99 | if serializer.is_valid(): 100 | serializer.save() 101 | return Response(serializer.data) 102 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 103 | 104 | def delete(self, request, pk): 105 | try: 106 | attendance = StudentAttendance.objects.get(pk=pk) 107 | except StudentAttendance.DoesNotExist: 108 | raise NotFound(detail="Student Attendance record not found.") 109 | 110 | attendance.delete() 111 | return Response(status=status.HTTP_204_NO_CONTENT) 112 | 113 | 114 | class PeriodAttendanceListView(APIView): 115 | def get(self, request): 116 | attendances = PeriodAttendance.objects.all() 117 | serializer = PeriodAttendanceSerializer(attendances, many=True) 118 | return Response(serializer.data) 119 | 120 | def post(self, request): 121 | serializer = PeriodAttendanceSerializer(data=request.data) 122 | if serializer.is_valid(): 123 | serializer.save() 124 | return Response(serializer.data, status=status.HTTP_201_CREATED) 125 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 126 | 127 | 128 | class PeriodAttendanceDetailView(APIView): 129 | def get(self, request, pk): 130 | try: 131 | attendance = PeriodAttendance.objects.get(pk=pk) 132 | except PeriodAttendance.DoesNotExist: 133 | raise NotFound(detail="Period Attendance record not found.") 134 | 135 | serializer = PeriodAttendanceSerializer(attendance) 136 | return Response(serializer.data) 137 | 138 | def put(self, request, pk): 139 | try: 140 | attendance = PeriodAttendance.objects.get(pk=pk) 141 | except PeriodAttendance.DoesNotExist: 142 | raise NotFound(detail="Period Attendance record not found.") 143 | 144 | serializer = PeriodAttendanceSerializer(attendance, data=request.data) 145 | if serializer.is_valid(): 146 | serializer.save() 147 | return Response(serializer.data) 148 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 149 | 150 | def delete(self, request, pk): 151 | try: 152 | attendance = PeriodAttendance.objects.get(pk=pk) 153 | except PeriodAttendance.DoesNotExist: 154 | raise NotFound(detail="Period Attendance record not found.") 155 | 156 | attendance.delete() 157 | return Response(status=status.HTTP_204_NO_CONTENT) 158 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/db.sqlite3 -------------------------------------------------------------------------------- /examination/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/examination/__init__.py -------------------------------------------------------------------------------- /examination/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import * 3 | 4 | admin.site.register(GradeScale) 5 | admin.site.register(GradeScaleRule) 6 | admin.site.register(Result) 7 | admin.site.register(ExaminationListHandler) 8 | admin.site.register(MarksManagement) 9 | -------------------------------------------------------------------------------- /examination/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExaminationConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'examination' 7 | -------------------------------------------------------------------------------- /examination/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('academic', '0001_initial'), 13 | ('administration', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='GradeScale', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=255, unique=True)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='ExaminationListHandler', 26 | fields=[ 27 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('name', models.CharField(max_length=100)), 29 | ('start_date', models.DateField()), 30 | ('ends_date', models.DateField()), 31 | ('out_of', models.IntegerField()), 32 | ('comments', models.CharField(blank=True, help_text='Comments Regarding Exam', max_length=200, null=True)), 33 | ('created_on', models.DateTimeField(auto_now_add=True)), 34 | ('classrooms', models.ManyToManyField(related_name='class_exams', to='academic.classroom')), 35 | ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.teacher')), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='MarksManagement', 40 | fields=[ 41 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('points_scored', models.FloatField()), 43 | ('date_time', models.DateTimeField(auto_now_add=True)), 44 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='marks_entered', to='academic.teacher')), 45 | ('exam_name', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exam_marks', to='examination.examinationlisthandler')), 46 | ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_marks', to='academic.studentclass')), 47 | ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subject_marks', to='academic.subject')), 48 | ], 49 | ), 50 | migrations.CreateModel( 51 | name='Result', 52 | fields=[ 53 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 54 | ('gpa', models.FloatField(null=True)), 55 | ('cat_gpa', models.FloatField(null=True)), 56 | ('academic_year', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='administration.academicyear')), 57 | ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.student')), 58 | ('term', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='administration.term')), 59 | ], 60 | ), 61 | migrations.CreateModel( 62 | name='GradeScaleRule', 63 | fields=[ 64 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 65 | ('min_grade', models.DecimalField(decimal_places=2, max_digits=5)), 66 | ('max_grade', models.DecimalField(decimal_places=2, max_digits=5)), 67 | ('letter_grade', models.CharField(blank=True, max_length=50, null=True)), 68 | ('numeric_scale', models.DecimalField(blank=True, decimal_places=2, max_digits=5)), 69 | ('grade_scale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='examination.gradescale')), 70 | ], 71 | options={ 72 | 'indexes': [models.Index(fields=['min_grade', 'max_grade', 'grade_scale'], name='examination_min_gra_eab956_idx')], 73 | 'unique_together': {('min_grade', 'max_grade', 'grade_scale')}, 74 | }, 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /examination/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/examination/migrations/__init__.py -------------------------------------------------------------------------------- /examination/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.db import models 3 | from django.core.exceptions import ValidationError 4 | from academic.models import Student, Teacher, ClassRoom, StudentClass, Subject 5 | from administration.models import AcademicYear, Term 6 | 7 | 8 | class GradeScale(models.Model): 9 | """Translate a numeric grade to some other scale. 10 | Example: Letter grade or 4.0 scale.""" 11 | 12 | name = models.CharField(max_length=255, unique=True) 13 | 14 | def __str__(self): 15 | return self.name 16 | 17 | def get_rule(self, grade): 18 | if grade is None: 19 | return None 20 | rule = self.gradescalerule_set.filter( 21 | min_grade__lte=grade, max_grade__gte=grade 22 | ).first() 23 | if not rule: 24 | # Optionally log or raise a warning 25 | print(f"No rule found for grade: {grade}") 26 | return rule 27 | 28 | def to_letter(self, grade): 29 | rule = self.get_rule(grade) 30 | if rule: 31 | return rule.letter_grade 32 | return None # Return None if no rule found 33 | 34 | def to_numeric(self, grade): 35 | rule = self.get_rule(grade) 36 | if rule: 37 | return rule.numeric_scale 38 | return None # Return None if no rule found 39 | 40 | 41 | class GradeScaleRule(models.Model): 42 | """One rule for a grade scale.""" 43 | 44 | min_grade = models.DecimalField(max_digits=5, decimal_places=2) 45 | max_grade = models.DecimalField(max_digits=5, decimal_places=2) 46 | letter_grade = models.CharField(max_length=50, blank=True, null=True) 47 | numeric_scale = models.DecimalField(max_digits=5, decimal_places=2, blank=True) 48 | grade_scale = models.ForeignKey(GradeScale, on_delete=models.CASCADE) 49 | 50 | class Meta: 51 | unique_together = ("min_grade", "max_grade", "grade_scale") 52 | indexes = [ 53 | models.Index(fields=["min_grade", "max_grade", "grade_scale"]), 54 | ] 55 | 56 | def __str__(self): 57 | return f"{self.min_grade}-{self.max_grade} {self.letter_grade} {self.numeric_scale}" 58 | 59 | def clean(self): 60 | """Ensure consistency between letter grade and numeric scale.""" 61 | if not self.letter_grade and not self.numeric_scale: 62 | raise ValidationError( 63 | "Either a letter grade or numeric scale must be provided." 64 | ) 65 | if self.letter_grade and self.numeric_scale is None: 66 | raise ValidationError( 67 | "If a letter grade is provided, numeric scale must also be provided." 68 | ) 69 | if self.numeric_scale and self.letter_grade is None: 70 | raise ValidationError( 71 | "If a numeric scale is provided, a letter grade must also be provided." 72 | ) 73 | 74 | def save(self, *args, **kwargs): 75 | if self.min_grade >= self.max_grade: 76 | raise ValidationError("min_grade must be less than max_grade.") 77 | super().save(*args, **kwargs) 78 | 79 | 80 | class Result(models.Model): 81 | student = models.ForeignKey(Student, on_delete=models.CASCADE) 82 | gpa = models.FloatField(null=True) 83 | cat_gpa = models.FloatField(null=True) 84 | academic_year = models.ForeignKey(AcademicYear, on_delete=models.CASCADE) 85 | term = models.OneToOneField(Term, on_delete=models.SET_NULL, blank=True, null=True) 86 | 87 | def __str__(self): 88 | return str(self.student) 89 | 90 | def clean(self): 91 | """Validate that GPA is within a valid range (0.0 - 4.0).""" 92 | if self.gpa is not None and (self.gpa < 0.0 or self.gpa > 4.0): 93 | raise ValidationError("GPA must be between 0.0 and 4.0.") 94 | if self.cat_gpa is not None and (self.cat_gpa < 0.0 or self.cat_gpa > 4.0): 95 | raise ValidationError("CAT GPA must be between 0.0 and 4.0.") 96 | 97 | 98 | class ExaminationListHandler(models.Model): 99 | name = models.CharField(max_length=100) 100 | start_date = models.DateField() 101 | ends_date = models.DateField() 102 | out_of = models.IntegerField() 103 | classrooms = models.ManyToManyField(ClassRoom, related_name="class_exams") 104 | comments = models.CharField( 105 | max_length=200, blank=True, null=True, help_text="Comments Regarding Exam" 106 | ) 107 | created_by = models.ForeignKey(Teacher, on_delete=models.CASCADE, null=True) 108 | created_on = models.DateTimeField(auto_now_add=True) 109 | 110 | @property 111 | def status(self): 112 | today = datetime.now().date() 113 | if today > self.ends_date: 114 | return "Done" 115 | elif self.start_date <= today <= self.ends_date: 116 | return "Ongoing" 117 | return "Coming Up" 118 | 119 | def __str__(self): 120 | return self.name 121 | 122 | def clean(self): 123 | """Ensure the start date is not later than the end date.""" 124 | if self.start_date > self.ends_date: 125 | raise ValidationError("Start date cannot be later than end date.") 126 | super(ExaminationListHandler, self).clean() 127 | 128 | 129 | class MarksManagement(models.Model): 130 | exam_name = models.ForeignKey( 131 | ExaminationListHandler, on_delete=models.CASCADE, related_name="exam_marks" 132 | ) 133 | points_scored = models.FloatField() 134 | subject = models.ForeignKey( 135 | Subject, on_delete=models.CASCADE, related_name="subject_marks" 136 | ) 137 | student = models.ForeignKey( 138 | StudentClass, on_delete=models.CASCADE, related_name="student_marks" 139 | ) 140 | created_by = models.ForeignKey( 141 | Teacher, on_delete=models.CASCADE, related_name="marks_entered" 142 | ) 143 | date_time = models.DateTimeField(auto_now_add=True) 144 | 145 | def __str__(self): 146 | return f"{self.exam_name} - {self.student} - {self.points_scored}" 147 | 148 | def clean(self): 149 | """Validate points scored based on the exam's out_of value.""" 150 | if self.points_scored < 0 or self.points_scored > self.exam_name.out_of: 151 | raise ValidationError( 152 | f"Points scored must be between 0 and {self.exam_name.out_of}." 153 | ) 154 | super(MarksManagement, self).clean() 155 | -------------------------------------------------------------------------------- /examination/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import GradeScale, GradeScaleRule 4 | 5 | 6 | class GradeScaleSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = GradeScale 9 | fields = "__all__" 10 | 11 | 12 | class GradeScaleRuleSerializer(serializers.ModelSerializer): 13 | class Meta: 14 | model = GradeScaleRule 15 | fields = "__all__" 16 | -------------------------------------------------------------------------------- /examination/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examination/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /finance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/finance/__init__.py -------------------------------------------------------------------------------- /finance/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import ReceiptAllocation, Receipt, PaymentAllocation, Payment 3 | 4 | admin.site.register(ReceiptAllocation) 5 | admin.site.register(Receipt) 6 | admin.site.register(PaymentAllocation) 7 | admin.site.register(Payment) 8 | -------------------------------------------------------------------------------- /finance/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FinanceConfig(AppConfig): 5 | name = 'finance' 6 | -------------------------------------------------------------------------------- /finance/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Payment', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('payment_no', models.IntegerField(unique=True)), 19 | ('date', models.DateField(auto_now_add=True)), 20 | ('paid_to', models.CharField(max_length=255, null=True)), 21 | ('amount', models.DecimalField(decimal_places=2, max_digits=10)), 22 | ('status', models.CharField(choices=[('Pending', 'Pending'), ('Completed', 'Completed'), ('Cancelled', 'Cancelled')], default='Pending', max_length=20)), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='PaymentAllocation', 27 | fields=[ 28 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('name', models.CharField(max_length=255, null=True)), 30 | ('abbr', models.CharField(blank=True, max_length=50, null=True)), 31 | ], 32 | ), 33 | migrations.CreateModel( 34 | name='Receipt', 35 | fields=[ 36 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('receipt_no', models.IntegerField(unique=True)), 38 | ('date', models.DateField(auto_now_add=True)), 39 | ('payer', models.CharField(max_length=255, null=True)), 40 | ('amount', models.DecimalField(decimal_places=2, max_digits=10)), 41 | ('status', models.CharField(choices=[('Pending', 'Pending'), ('Completed', 'Completed'), ('Cancelled', 'Cancelled')], default='Pending', max_length=20)), 42 | ], 43 | ), 44 | migrations.CreateModel( 45 | name='ReceiptAllocation', 46 | fields=[ 47 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 48 | ('name', models.CharField(max_length=255, null=True)), 49 | ('abbr', models.CharField(blank=True, max_length=50, null=True)), 50 | ], 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /finance/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('academic', '0002_initial'), 14 | ('finance', '0001_initial'), 15 | ('users', '0001_initial'), 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ] 18 | 19 | operations = [ 20 | migrations.AddField( 21 | model_name='payment', 22 | name='paid_by', 23 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.accountant'), 24 | ), 25 | migrations.AddField( 26 | model_name='payment', 27 | name='user', 28 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 29 | ), 30 | migrations.AddField( 31 | model_name='payment', 32 | name='paid_for', 33 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.paymentallocation'), 34 | ), 35 | migrations.AddField( 36 | model_name='receipt', 37 | name='received_by', 38 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.accountant'), 39 | ), 40 | migrations.AddField( 41 | model_name='receipt', 42 | name='student', 43 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='academic.student'), 44 | ), 45 | migrations.AddField( 46 | model_name='receipt', 47 | name='paid_for', 48 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.receiptallocation'), 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /finance/migrations/0003_alter_payment_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-02-04 15:25 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('finance', '0002_initial'), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='payment', 18 | name='user', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /finance/migrations/0004_remove_payment_payment_no_remove_receipt_receipt_no_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-02-27 11:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('finance', '0003_alter_payment_user'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='payment', 15 | name='payment_no', 16 | ), 17 | migrations.RemoveField( 18 | model_name='receipt', 19 | name='receipt_no', 20 | ), 21 | migrations.AddField( 22 | model_name='payment', 23 | name='payment_number', 24 | field=models.IntegerField(blank=True, db_index=True, null=True, unique=True), 25 | ), 26 | migrations.AddField( 27 | model_name='receipt', 28 | name='receipt_number', 29 | field=models.IntegerField(blank=True, null=True, unique=True), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /finance/migrations/0005_receipt_paid_through_alter_receipt_payer_debtrecord.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-03-01 18:11 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('academic', '0009_alter_student_options_remove_teacher_isteacher'), 11 | ('administration', '0003_alter_term_default_term_fee'), 12 | ('finance', '0004_remove_payment_payment_no_remove_receipt_receipt_no_and_more'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='receipt', 18 | name='paid_through', 19 | field=models.CharField(choices=[('CRDB', 'CRDB'), ('NMB', 'NMB'), ('NBC', 'NBC'), ('HATI MALIPO', 'HATI MALIPO'), ('Unknown', 'Unknown')], default='Unknown', max_length=20), 20 | ), 21 | migrations.AlterField( 22 | model_name='receipt', 23 | name='payer', 24 | field=models.CharField(default='Unknown', max_length=255), 25 | ), 26 | migrations.CreateModel( 27 | name='DebtRecord', 28 | fields=[ 29 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('amount_added', models.DecimalField(decimal_places=2, max_digits=10)), 31 | ('date_updated', models.DateTimeField(auto_now_add=True)), 32 | ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='debt_records', to='academic.student')), 33 | ('term', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='administration.term')), 34 | ], 35 | options={ 36 | 'unique_together': {('student', 'term')}, 37 | }, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /finance/migrations/0006_receipt_payment_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-03-01 20:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('finance', '0005_receipt_paid_through_alter_receipt_payer_debtrecord'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='receipt', 15 | name='payment_date', 16 | field=models.DateField(default='2000-01-01'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /finance/migrations/0007_payment_paid_through_alter_receipt_paid_through.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-03-02 18:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('finance', '0006_receipt_payment_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='payment', 15 | name='paid_through', 16 | field=models.CharField(choices=[('CRDB', 'CRDB'), ('NMB', 'NMB'), ('NBC', 'NBC'), ('HATI MALIPO', 'HATI MALIPO'), ('CASH', 'CASH'), ('Unknown', 'Unknown')], default='CASH', max_length=20), 17 | ), 18 | migrations.AlterField( 19 | model_name='receipt', 20 | name='paid_through', 21 | field=models.CharField(choices=[('CRDB', 'CRDB'), ('NMB', 'NMB'), ('NBC', 'NBC'), ('HATI MALIPO', 'HATI MALIPO'), ('CASH', 'CASH'), ('Unknown', 'Unknown')], default='Unknown', max_length=20), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /finance/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/finance/migrations/__init__.py -------------------------------------------------------------------------------- /finance/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models, transaction 2 | from django.core.exceptions import ValidationError 3 | from administration.models import Term 4 | from users.models import Accountant, CustomUser as User 5 | from academic.models import Teacher, Student 6 | 7 | 8 | class PaymentStatus(models.TextChoices): 9 | PENDING = "Pending", "Pending" 10 | COMPLETED = "Completed", "Completed" 11 | CANCELLED = "Cancelled", "Cancelled" 12 | 13 | 14 | class PaymentThrough(models.TextChoices): 15 | CRDB = "CRDB", "CRDB" 16 | NMB = "NMB", "NMB" 17 | NBC = "NBC", "NBC" 18 | HATI_MALIPO = "HATI MALIPO", "HATI MALIPO" 19 | CASH = "CASH", "CASH" 20 | UNKNOWN = "Unknown", "Unknown" 21 | 22 | 23 | class DebtRecord(models.Model): 24 | student = models.ForeignKey( 25 | Student, on_delete=models.CASCADE, related_name="debt_records" 26 | ) 27 | term = models.ForeignKey(Term, on_delete=models.CASCADE) 28 | amount_added = models.DecimalField(max_digits=10, decimal_places=2) 29 | date_updated = models.DateTimeField(auto_now_add=True) 30 | 31 | class Meta: 32 | unique_together = ( 33 | "student", 34 | "term", 35 | ) # Ensures a student’s debt is only updated once per term 36 | 37 | def __str__(self): 38 | return f"{self.student.full_name} - {self.term.name} - {self.amount_added}" 39 | 40 | 41 | class ReceiptAllocation(models.Model): 42 | name = models.CharField(max_length=255, null=True) 43 | abbr = models.CharField(max_length=50, blank=True, null=True) 44 | 45 | def __str__(self): 46 | return self.name 47 | 48 | 49 | class PaymentAllocation(models.Model): 50 | name = models.CharField(max_length=255, null=True) 51 | abbr = models.CharField(max_length=50, blank=True, null=True) 52 | 53 | def __str__(self): 54 | return self.name 55 | 56 | 57 | class Receipt(models.Model): 58 | receipt_number = models.IntegerField(unique=True, blank=True, null=True) 59 | date = models.DateField(auto_now_add=True) 60 | payer = models.CharField(max_length=255, blank=False, null=False, default="Unknown") 61 | paid_for = models.ForeignKey( 62 | ReceiptAllocation, on_delete=models.SET_NULL, null=True 63 | ) 64 | student = models.ForeignKey( 65 | Student, on_delete=models.SET_NULL, blank=True, null=True 66 | ) 67 | amount = models.DecimalField(max_digits=10, decimal_places=2) 68 | paid_through = models.CharField( 69 | max_length=20, choices=PaymentThrough.choices, default=PaymentThrough.UNKNOWN 70 | ) 71 | payment_date = models.DateField(default="2000-01-01") 72 | status = models.CharField( 73 | max_length=20, choices=PaymentStatus.choices, default=PaymentStatus.PENDING 74 | ) 75 | received_by = models.ForeignKey(Accountant, on_delete=models.SET_NULL, null=True) 76 | 77 | def __str__(self): 78 | return f"Receipt {self.receipt_number} | {self.date} | {self.paid_for} | {self.payer}" 79 | 80 | def clean(self): 81 | super().clean() 82 | if self.amount <= 0: 83 | raise ValidationError("Amount must be a positive value.") 84 | 85 | def save(self, *args, **kwargs): 86 | if not self.receipt_number: 87 | with transaction.atomic(): 88 | last_receipt = ( 89 | Receipt.objects.select_for_update() 90 | .order_by("-receipt_number") 91 | .first() 92 | ) 93 | self.receipt_number = ( 94 | (last_receipt.receipt_number + 1) if last_receipt else 1 95 | ) 96 | 97 | super().save(*args, **kwargs) 98 | 99 | if ( 100 | self.student 101 | and self.paid_for 102 | and self.paid_for.name.lower() == "school fees" 103 | ): 104 | self.student.clear_debt(self.amount) 105 | 106 | 107 | class Payment(models.Model): 108 | payment_number = models.IntegerField( 109 | unique=True, blank=True, null=True, db_index=True 110 | ) 111 | date = models.DateField(auto_now_add=True) 112 | paid_to = models.CharField(max_length=255, null=True) 113 | user = models.ForeignKey( 114 | User, blank=True, null=True, on_delete=models.SET_NULL, related_name="payments" 115 | ) 116 | paid_for = models.ForeignKey( 117 | PaymentAllocation, on_delete=models.SET_NULL, null=True 118 | ) 119 | paid_through = models.CharField( 120 | max_length=20, choices=PaymentThrough.choices, default=PaymentThrough.CASH 121 | ) 122 | amount = models.DecimalField(max_digits=10, decimal_places=2) 123 | status = models.CharField( 124 | max_length=20, choices=PaymentStatus.choices, default=PaymentStatus.PENDING 125 | ) 126 | paid_by = models.ForeignKey(Accountant, on_delete=models.SET_NULL, null=True) 127 | 128 | def __str__(self): 129 | return f"Payment {self.payment_number} | {self.date} | {self.paid_for} | {self.paid_to}" 130 | 131 | def clean(self): 132 | if self.amount <= 0: 133 | raise ValidationError("Amount must be a positive value.") 134 | 135 | def save(self, *args, **kwargs): 136 | if not self.payment_number: 137 | last_payment = Payment.objects.order_by("-payment_number").first() 138 | self.payment_number = ( 139 | (last_payment.payment_number + 1) if last_payment else 1 140 | ) 141 | 142 | super().save(*args, **kwargs) 143 | 144 | def handle_salary_payment(self): 145 | if self.paid_for.name.lower() == "salary": 146 | if isinstance(self.paid_to, Teacher) or isinstance( 147 | self.paid_to, Accountant 148 | ): 149 | self.paid_to.unpaid_salary -= self.amount 150 | self.paid_to.save() 151 | self.save() 152 | -------------------------------------------------------------------------------- /finance/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Receipt, Payment, ReceiptAllocation, PaymentAllocation 3 | from academic.models import Student 4 | from users.models import CustomUser, Accountant 5 | from sis.serializers import StudentSerializer 6 | from users.serializers import AccountantSerializer, UserSerializer 7 | 8 | 9 | class ReceiptAllocationSerializer(serializers.ModelSerializer): 10 | class Meta: 11 | model = ReceiptAllocation 12 | fields = ["id", "name", "abbr"] 13 | 14 | 15 | class PaymentAllocationSerializer(serializers.ModelSerializer): 16 | class Meta: 17 | model = PaymentAllocation 18 | fields = ["id", "name", "abbr"] 19 | 20 | 21 | class ReceiptSerializer(serializers.ModelSerializer): 22 | student = serializers.PrimaryKeyRelatedField( 23 | queryset=Student.objects.all() 24 | ) # Allow passing student ID for creation 25 | paid_for = serializers.PrimaryKeyRelatedField( 26 | queryset=ReceiptAllocation.objects.all() 27 | ) # Allow passing ReceiptAllocation ID for creation 28 | received_by = serializers.PrimaryKeyRelatedField( 29 | queryset=Accountant.objects.all() 30 | ) # Allow passing Accountant ID for creation 31 | 32 | student_details = StudentSerializer(read_only=True, source="student") 33 | paid_for_details = ReceiptAllocationSerializer(read_only=True, source="paid_for") 34 | received_by_details = AccountantSerializer(read_only=True, source="received_by") 35 | 36 | class Meta: 37 | model = Receipt 38 | fields = ( 39 | "id", 40 | "receipt_number", 41 | "date", 42 | "payer", 43 | "paid_for", 44 | "paid_for_details", # Embedded ReceiptAllocation details 45 | "student", 46 | "student_details", # Embedded Student details 47 | "amount", 48 | "paid_through", 49 | "payment_date", 50 | "status", 51 | "received_by", 52 | "received_by_details", # Embedded Accountant details 53 | ) 54 | 55 | def validate_amount(self, value): 56 | if value <= 0: 57 | raise serializers.ValidationError("Amount must be a positive value.") 58 | return value 59 | 60 | def create(self, validated_data): 61 | """ 62 | Override create to handle debt reduction when a receipt is created. 63 | """ 64 | receipt = Receipt.objects.create(**validated_data) 65 | 66 | # Handle student debt reduction if paid_for is "School Fees" 67 | if ( 68 | receipt.paid_for 69 | and receipt.paid_for.name.lower() == "school fees" 70 | and receipt.student 71 | ): 72 | receipt.student.clear_debt(receipt.amount) 73 | 74 | return receipt 75 | 76 | 77 | class PaymentSerializer(serializers.ModelSerializer): 78 | paid_for = PaymentAllocationSerializer(read_only=True) 79 | paid_by = AccountantSerializer(read_only=True) 80 | user = UserSerializer(read_only=True) 81 | paid_for_id = serializers.PrimaryKeyRelatedField( 82 | queryset=PaymentAllocation.objects.all(), source="paid_for", write_only=True 83 | ) 84 | paid_by_id = serializers.PrimaryKeyRelatedField( 85 | queryset=Accountant.objects.all(), source="paid_by", write_only=True 86 | ) 87 | user_id = serializers.PrimaryKeyRelatedField( 88 | queryset=CustomUser.objects.all(), 89 | source="user", 90 | write_only=True, 91 | required=False, 92 | ) 93 | 94 | class Meta: 95 | model = Payment 96 | fields = [ 97 | "id", 98 | "payment_number", 99 | "date", 100 | "paid_to", 101 | "amount", 102 | "status", 103 | "paid_for", 104 | "paid_through", 105 | "paid_by", 106 | "user", 107 | "paid_for_id", 108 | "paid_by_id", 109 | "user_id", 110 | ] 111 | 112 | def validate_amount(self, value): 113 | if value <= 0: 114 | raise serializers.ValidationError("Amount must be a positive value.") 115 | return value 116 | -------------------------------------------------------------------------------- /finance/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "school.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /media/articles/Copy_of_HIC_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/media/articles/Copy_of_HIC_logo.jpg -------------------------------------------------------------------------------- /media/articles/Copy_of_HIC_logo_KnCOfnt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/media/articles/Copy_of_HIC_logo_KnCOfnt.jpg -------------------------------------------------------------------------------- /media/articles/Screenshot_from_2020-12-21_09-39-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/media/articles/Screenshot_from_2020-12-21_09-39-21.png -------------------------------------------------------------------------------- /notes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/notes/__init__.py -------------------------------------------------------------------------------- /notes/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import * 4 | 5 | admin.site.register(Assignment) 6 | admin.site.register(GradedAssignment) 7 | admin.site.register(Question) 8 | admin.site.register(Choice) 9 | admin.site.register(SpecificExplanations) 10 | admin.site.register(Concept) 11 | admin.site.register(Note) 12 | -------------------------------------------------------------------------------- /notes/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NotesConfig(AppConfig): 5 | name = 'notes' 6 | -------------------------------------------------------------------------------- /notes/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Assignment', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=50)), 19 | ], 20 | ), 21 | migrations.CreateModel( 22 | name='Choice', 23 | fields=[ 24 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('title', models.CharField(blank=True, max_length=50, null=True)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='Concept', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('name', models.CharField(blank=True, max_length=255, null=True)), 33 | ('explanation', models.TextField(blank=True, null=True)), 34 | ('image', models.ImageField(blank=True, null=True, upload_to='concept images')), 35 | ], 36 | ), 37 | migrations.CreateModel( 38 | name='GradedAssignment', 39 | fields=[ 40 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 41 | ('grade', models.FloatField()), 42 | ], 43 | ), 44 | migrations.CreateModel( 45 | name='Note', 46 | fields=[ 47 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 48 | ], 49 | ), 50 | migrations.CreateModel( 51 | name='Question', 52 | fields=[ 53 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 54 | ('question', models.CharField(max_length=200)), 55 | ('order', models.SmallIntegerField()), 56 | ], 57 | ), 58 | migrations.CreateModel( 59 | name='SpecificExplanations', 60 | fields=[ 61 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 62 | ('name', models.CharField(blank=True, max_length=255, null=True)), 63 | ('explanation', models.TextField(blank=True, null=True)), 64 | ], 65 | ), 66 | ] 67 | -------------------------------------------------------------------------------- /notes/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('academic', '0002_initial'), 14 | ('notes', '0001_initial'), 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.AddField( 20 | model_name='assignment', 21 | name='teacher', 22 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 23 | ), 24 | migrations.AddField( 25 | model_name='concept', 26 | name='sub_topic', 27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.subtopic'), 28 | ), 29 | migrations.AddField( 30 | model_name='gradedassignment', 31 | name='assignment', 32 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='notes.assignment'), 33 | ), 34 | migrations.AddField( 35 | model_name='gradedassignment', 36 | name='student', 37 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.student'), 38 | ), 39 | migrations.AddField( 40 | model_name='note', 41 | name='notes', 42 | field=models.ManyToManyField(blank=True, to='notes.concept'), 43 | ), 44 | migrations.AddField( 45 | model_name='note', 46 | name='sub_topic', 47 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.subtopic'), 48 | ), 49 | migrations.AddField( 50 | model_name='question', 51 | name='answer', 52 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='answer', to='notes.choice'), 53 | ), 54 | migrations.AddField( 55 | model_name='question', 56 | name='assignment', 57 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='notes.assignment'), 58 | ), 59 | migrations.AddField( 60 | model_name='question', 61 | name='choices', 62 | field=models.ManyToManyField(to='notes.choice'), 63 | ), 64 | migrations.AddField( 65 | model_name='specificexplanations', 66 | name='examples', 67 | field=models.ManyToManyField(blank=True, to='notes.question'), 68 | ), 69 | migrations.AddField( 70 | model_name='specificexplanations', 71 | name='sub_topic', 72 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='academic.subtopic'), 73 | ), 74 | migrations.AddField( 75 | model_name='concept', 76 | name='list_of_explanations', 77 | field=models.ManyToManyField(blank=True, to='notes.specificexplanations'), 78 | ), 79 | ] 80 | -------------------------------------------------------------------------------- /notes/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/notes/migrations/__init__.py -------------------------------------------------------------------------------- /notes/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from academic.models import Student, SubTopic 4 | from users.models import CustomUser as User 5 | 6 | 7 | class Assignment(models.Model): 8 | title = models.CharField(max_length=50) 9 | teacher = models.ForeignKey(User, on_delete=models.CASCADE) 10 | 11 | def __str__(self): 12 | return self.title 13 | 14 | 15 | class GradedAssignment(models.Model): 16 | student = models.ForeignKey(Student, on_delete=models.CASCADE) 17 | assignment = models.ForeignKey( 18 | Assignment, on_delete=models.SET_NULL, blank=True, null=True 19 | ) 20 | grade = models.FloatField() 21 | 22 | def __str__(self): 23 | return self.student.username 24 | 25 | 26 | class Choice(models.Model): 27 | title = models.CharField(max_length=50, blank=True, null=True) 28 | 29 | def __str__(self): 30 | return self.title 31 | 32 | 33 | class Question(models.Model): 34 | question = models.CharField(max_length=200) 35 | choices = models.ManyToManyField(Choice) 36 | answer = models.ForeignKey( 37 | Choice, on_delete=models.CASCADE, related_name="answer", blank=True, null=True 38 | ) 39 | assignment = models.ForeignKey( 40 | Assignment, 41 | on_delete=models.CASCADE, 42 | related_name="questions", 43 | blank=True, 44 | null=True, 45 | ) 46 | order = models.SmallIntegerField() 47 | 48 | def __str__(self): 49 | return self.question 50 | 51 | 52 | class SpecificExplanations(models.Model): 53 | sub_topic = models.ForeignKey( 54 | SubTopic, on_delete=models.CASCADE, blank=True, null=True 55 | ) 56 | name = models.CharField(max_length=255, blank=True, null=True) 57 | explanation = models.TextField(blank=True, null=True) 58 | examples = models.ManyToManyField(Question, blank=True) 59 | 60 | def __str__(self): 61 | return f"{self.name} {self.sub_topic}" 62 | 63 | 64 | class Concept(models.Model): 65 | sub_topic = models.ForeignKey( 66 | SubTopic, on_delete=models.CASCADE, blank=True, null=True 67 | ) 68 | name = models.CharField(max_length=255, blank=True, null=True) 69 | explanation = models.TextField(blank=True, null=True) 70 | image = models.ImageField( 71 | verbose_name=None, upload_to="concept images", blank=True, null=True 72 | ) 73 | list_of_explanations = models.ManyToManyField(SpecificExplanations, blank=True) 74 | 75 | def __str__(self): 76 | return f"{self.name} {self.sub_topic}" 77 | 78 | 79 | class Note(models.Model): 80 | sub_topic = models.ForeignKey( 81 | SubTopic, on_delete=models.CASCADE, blank=True, null=True 82 | ) 83 | notes = models.ManyToManyField(Concept, blank=True) 84 | 85 | def __str__(self): 86 | return f"{self.sub_topic}" 87 | -------------------------------------------------------------------------------- /notes/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from users.models import CustomUser as User 4 | from .models import ( 5 | Assignment, 6 | Question, 7 | Choice, 8 | GradedAssignment, 9 | ) 10 | 11 | 12 | class StringSerializer(serializers.StringRelatedField): 13 | def to_internal_value(self, value): 14 | return value 15 | 16 | 17 | class QuestionSerializer(serializers.ModelSerializer): 18 | choices = StringSerializer(many=True) 19 | 20 | class Meta: 21 | model = Question 22 | fields = ("id", "choices", "question", "order") 23 | 24 | 25 | class AssignmentSerializer(serializers.ModelSerializer): 26 | questions = serializers.SerializerMethodField() 27 | teacher = StringSerializer(many=False) 28 | 29 | class Meta: 30 | model = Assignment 31 | fields = "__all__" 32 | 33 | def get_questions(self, obj): 34 | questions = QuestionSerializer(obj.questions.all(), many=True).data 35 | return questions 36 | 37 | def create(self, request): 38 | data = request.data 39 | 40 | assignment = Assignment() 41 | teacher = User.objects.get(username=data["teacher"]) 42 | assignment.teacher = teacher 43 | assignment.title = data["title"] 44 | assignment.save() 45 | 46 | order = 1 47 | for q in data["questions"]: 48 | newQ = Question() 49 | newQ.question = q["title"] 50 | newQ.order = order 51 | newQ.save() 52 | 53 | for c in q["choices"]: 54 | newC = Choice() 55 | newC.title = c 56 | newC.save() 57 | newQ.choices.add(newC) 58 | 59 | newQ.answer = Choice.objects.get(title=q["answer"]) 60 | newQ.assignment = assignment 61 | newQ.save() 62 | order += 1 63 | return assignment 64 | 65 | 66 | class GradedAssignmentSerializer(serializers.ModelSerializer): 67 | student = StringSerializer(many=False) 68 | 69 | class Meta: 70 | model = GradedAssignment 71 | fields = "__all__" 72 | 73 | def create(self, request): 74 | data = request.data 75 | print(data) 76 | 77 | assignment = Assignment.objects.get(id=data["asntId"]) 78 | student = User.objects.get(username=data["username"]) 79 | 80 | graded_asnt = GradedAssignment() 81 | graded_asnt.assignment = assignment 82 | graded_asnt.student = student 83 | 84 | questions = [q for q in assignment.questions.all()] 85 | answers = [data["answers"][a] for a in data["answers"]] 86 | 87 | answered_correct_count = 0 88 | for i in range(len(questions)): 89 | if questions[i].answer.title == answers[i]: 90 | answered_correct_count += 1 91 | i += 1 92 | 93 | grade = answered_correct_count / len(questions) * 100 94 | graded_asnt.grade = grade 95 | graded_asnt.save() 96 | return graded_asnt 97 | -------------------------------------------------------------------------------- /notes/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /notes/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.generics import ListAPIView, CreateAPIView 3 | from rest_framework.response import Response 4 | from rest_framework.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST 5 | 6 | from .models import * 7 | from .serializers import * 8 | 9 | 10 | class AssignmentViewSet(viewsets.ModelViewSet): 11 | serializer_class = AssignmentSerializer 12 | queryset = Assignment.objects.all() 13 | 14 | def create(self, request): 15 | serializer = AssignmentSerializer(data=request.data) 16 | if serializer.is_valid(): 17 | assignment = serializer.create(request) 18 | if assignment: 19 | return Response(status=HTTP_201_CREATED) 20 | return Response(status=HTTP_400_BAD_REQUEST) 21 | 22 | 23 | class GradedAssignmentListView(ListAPIView): 24 | serializer_class = GradedAssignmentSerializer 25 | 26 | def get_queryset(self): 27 | queryset = GradedAssignment.objects.all() 28 | username = self.request.query_params.get("username", None) 29 | if username is not None: 30 | queryset = queryset.filter(student__username=username) 31 | return queryset 32 | 33 | 34 | class GradedAssignmentCreateView(CreateAPIView): 35 | serializer_class = GradedAssignmentSerializer 36 | queryset = GradedAssignment.objects.all() 37 | 38 | def post(self, request): 39 | print(request.data) 40 | serializer = GradedAssignmentSerializer(data=request.data) 41 | serializer.is_valid() 42 | graded_assignment = serializer.create(request) 43 | if graded_assignment: 44 | return Response(status=HTTP_201_CREATED) 45 | return Response(status=HTTP_400_BAD_REQUEST) 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 2 | distlib==0.3.5 3 | Django==5.1 4 | django-cors-headers==4.4.0 5 | django-debug-toolbar==4.4.6 6 | django-filter==25.1 7 | djangorestframework==3.15.2 8 | djangorestframework-simplejwt==5.3.1 9 | et-xmlfile==1.1.0 10 | filelock==3.8.0 11 | openpyxl==3.1.5 12 | pillow==10.4.0 13 | platformdirs==2.5.2 14 | psycopg2==2.9.9 15 | PyJWT==2.9.0 16 | python-decouple==3.8 17 | sqlparse==0.5.1 18 | typing_extensions==4.12.2 19 | tzdata==2024.1 20 | ua-parser==1.0.0 21 | ua-parser-builtins==0.18.0.post1 22 | user-agents==2.2.0 23 | virtualenv==20.16.3 24 | -------------------------------------------------------------------------------- /schedule/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/schedule/__init__.py -------------------------------------------------------------------------------- /schedule/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | -------------------------------------------------------------------------------- /schedule/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ScheduleConfig(AppConfig): 5 | name = 'schedule' 6 | -------------------------------------------------------------------------------- /schedule/management/commands/generate_timetable.py: -------------------------------------------------------------------------------- 1 | # timetable/management/commands/generate_timetable.py 2 | from django.core.management.base import BaseCommand 3 | from datetime import time, timedelta 4 | from academic.models import AllocatedSubject 5 | from schedule.models import Period 6 | from administration.models import Term 7 | 8 | 9 | class Command(BaseCommand): 10 | help = "Generate a timetable for the school learning days with a break after the fourth period." 11 | 12 | def handle(self, *args, **kwargs): 13 | days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] 14 | period_duration = 40 15 | break_duration = 20 16 | periods_per_day = 8 17 | 18 | current_term = Term.objects.filter(is_current=True).first() 19 | if not current_term: 20 | self.stdout.write(self.style.ERROR("No current term set.")) 21 | return 22 | 23 | allocated_subjects = AllocatedSubject.objects.filter(term=current_term) 24 | if not allocated_subjects.exists(): 25 | self.stdout.write( 26 | self.style.WARNING("No AllocatedSubjects found for the current term.") 27 | ) 28 | return 29 | 30 | for allocated_subject in allocated_subjects: 31 | weekly_limit = allocated_subject.weekly_periods 32 | max_daily_periods = allocated_subject.max_daily_periods 33 | classroom = allocated_subject.class_room 34 | subject = allocated_subject.subject 35 | teacher = allocated_subject.teacher 36 | 37 | day_periods = {day: 0 for day in days} 38 | 39 | for day in days: 40 | time_pointer = time(8, 0) 41 | consecutive_periods = 0 42 | 43 | for period_index in range(periods_per_day): 44 | # Handle the break 45 | if period_index == 4: 46 | time_pointer = ( 47 | time_pointer.hour * 60 48 | + time_pointer.minute 49 | + break_duration 50 | ) // 60, (time_pointer.minute + break_duration) % 60 51 | time_pointer = time(*time_pointer) 52 | continue 53 | 54 | # Check weekly and daily limits 55 | if ( 56 | day_periods[day] >= weekly_limit 57 | or consecutive_periods >= max_daily_periods 58 | ): 59 | break 60 | 61 | # Calculate end time 62 | end_time_minutes = ( 63 | time_pointer.hour * 60 + time_pointer.minute + period_duration 64 | ) 65 | end_time = time(end_time_minutes // 60, end_time_minutes % 60) 66 | 67 | # Create the period 68 | Period.objects.create( 69 | day_of_week=day, 70 | start_time=time_pointer, 71 | end_time=end_time, 72 | classroom=classroom, 73 | subject=subject, 74 | teacher=teacher, 75 | ) 76 | 77 | day_periods[day] += 1 78 | consecutive_periods += 1 79 | time_pointer = end_time 80 | 81 | self.stdout.write(self.style.SUCCESS("Timetable generated successfully!")) 82 | -------------------------------------------------------------------------------- /schedule/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 15:16 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('academic', '0003_allocatedsubject_delete_subjectallocation'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Period', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('day_of_week', models.CharField(choices=[('Monday', 'Monday'), ('Tuesday', 'Tuesday'), ('Wednesday', 'Wednesday'), ('Thursday', 'Thursday'), ('Friday', 'Friday')], max_length=10)), 21 | ('start_time', models.TimeField()), 22 | ('end_time', models.TimeField()), 23 | ('classroom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.classroom')), 24 | ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.allocatedsubject')), 25 | ('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic.teacher')), 26 | ], 27 | options={ 28 | 'unique_together': {('day_of_week', 'start_time', 'classroom')}, 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /schedule/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/schedule/migrations/__init__.py -------------------------------------------------------------------------------- /schedule/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from academic.models import ClassRoom, Teacher, AllocatedSubject 4 | 5 | 6 | class Period(models.Model): 7 | day_of_week = models.CharField( 8 | max_length=10, 9 | choices=[ 10 | ("Monday", "Monday"), 11 | ("Tuesday", "Tuesday"), 12 | ("Wednesday", "Wednesday"), 13 | ("Thursday", "Thursday"), 14 | ("Friday", "Friday"), 15 | ], 16 | ) 17 | start_time = models.TimeField() 18 | end_time = models.TimeField() 19 | classroom = models.ForeignKey(ClassRoom, on_delete=models.CASCADE) 20 | subject = models.ForeignKey(AllocatedSubject, on_delete=models.CASCADE) 21 | teacher = models.ForeignKey(Teacher, on_delete=models.CASCADE) 22 | 23 | class Meta: 24 | unique_together = ("day_of_week", "start_time", "classroom") 25 | 26 | def __str__(self): 27 | return f"{self.classroom} - {self.subject} ({self.day_of_week} {self.start_time}-{self.end_time})" 28 | -------------------------------------------------------------------------------- /schedule/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Period 3 | from academic.models import AllocatedSubject 4 | from academic.serializers import ( 5 | ClassRoomSerializer, 6 | SubjectSerializer, 7 | ) 8 | from users.serializers import TeacherSerializer 9 | from administration.serializers import TermSerializer 10 | 11 | 12 | class PeriodSerializer(serializers.ModelSerializer): 13 | teacher = TeacherSerializer(read_only=True) # Include teacher details 14 | subject = SubjectSerializer(read_only=True) # Include subject details 15 | classroom = ClassRoomSerializer(read_only=True) # Include classroom details 16 | term = TermSerializer(read_only=True) # Include term details 17 | 18 | class Meta: 19 | model = Period 20 | fields = [ 21 | "id", 22 | "day_of_week", 23 | "start_time", 24 | "end_time", 25 | "teacher", 26 | "subject", 27 | "classroom", 28 | "term", 29 | ] 30 | read_only_fields = ["id"] 31 | 32 | def create(self, validated_data): 33 | """ 34 | Override create to handle dynamic assignment. 35 | """ 36 | allocated_subject = validated_data.pop("allocated_subject", None) 37 | if not allocated_subject: 38 | raise serializers.ValidationError( 39 | {"error": "AllocatedSubject is required to create a period."} 40 | ) 41 | 42 | teacher = allocated_subject.teacher 43 | subject = allocated_subject.subject 44 | classroom = allocated_subject.class_room 45 | term = allocated_subject.term 46 | 47 | return Period.objects.create( 48 | **validated_data, 49 | teacher=teacher, 50 | subject=subject, 51 | classroom=classroom, 52 | term=term, 53 | ) 54 | -------------------------------------------------------------------------------- /schedule/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /schedule/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.core.management import call_command 3 | from io import StringIO 4 | from rest_framework import viewsets 5 | from rest_framework.views import APIView 6 | from rest_framework.response import Response 7 | from rest_framework import status 8 | from .models import Period 9 | from .serializers import PeriodSerializer 10 | from academic.models import AllocatedSubject 11 | 12 | 13 | class PeriodCreateView(APIView): 14 | def post(self, request, *args, **kwargs): 15 | allocated_subject_id = request.data.get("allocated_subject") 16 | try: 17 | allocated_subject = AllocatedSubject.objects.get(id=allocated_subject_id) 18 | except AllocatedSubject.DoesNotExist: 19 | return Response( 20 | {"error": "AllocatedSubject not found."}, 21 | status=status.HTTP_404_NOT_FOUND, 22 | ) 23 | 24 | serializer = PeriodSerializer(data=request.data) 25 | if serializer.is_valid(): 26 | serializer.save(allocated_subject=allocated_subject) 27 | return Response(serializer.data, status=status.HTTP_201_CREATED) 28 | 29 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 30 | 31 | 32 | class PeriodViewSet(viewsets.ModelViewSet): 33 | queryset = Period.objects.all() 34 | serializer_class = PeriodSerializer 35 | 36 | 37 | def run_generate_timetable(request): 38 | """ 39 | View to trigger the timetable generation management command. 40 | """ 41 | output = StringIO() 42 | try: 43 | call_command("generate_timetable", stdout=output) 44 | return JsonResponse({"status": "success", "message": output.getvalue()}) 45 | except Exception as e: 46 | return JsonResponse({"status": "error", "message": str(e)}) 47 | -------------------------------------------------------------------------------- /school/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/school/__init__.py -------------------------------------------------------------------------------- /school/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for school project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'school.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /school/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from datetime import timedelta, date 4 | from django.core.validators import MinValueValidator # Could use MaxValueValidator too 5 | 6 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 7 | BASE_DIR = Path(__file__).resolve().parent.parent 8 | 9 | 10 | # Quick-start development settings - unsuitable for production 11 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 12 | 13 | # SECURITY WARNING: keep the secret key used in production secret! 14 | SECRET_KEY = "JVHhdwUt8899h ghhiodfidwbfew" 15 | 16 | # SECURITY WARNING: don't run with debug turned on in production! 17 | DEBUG = True 18 | 19 | ALLOWED_HOSTS = ["127.0.0.1", "localhost", "192.168.28.14"] 20 | 21 | 22 | DATE_VALIDATORS = [MinValueValidator(date(1970, 1, 1))] # Unix epoch! 23 | 24 | 25 | # Application definition 26 | 27 | INSTALLED_APPS = [ 28 | "django.contrib.admin", 29 | "django.contrib.auth", 30 | "django.contrib.contenttypes", 31 | "django.contrib.sessions", 32 | "django.contrib.messages", 33 | "django.contrib.staticfiles", 34 | "corsheaders", 35 | "rest_framework", 36 | "rest_framework_simplejwt", 37 | "debug_toolbar", 38 | "academic.apps.AcademicConfig", 39 | "administration.apps.AdministrationConfig", 40 | "attendance.apps.AttendanceConfig", 41 | "examination.apps.ExaminationConfig", 42 | "finance.apps.FinanceConfig", 43 | "notes.apps.NotesConfig", 44 | "schedule.apps.ScheduleConfig", 45 | "sis.apps.SisConfig", 46 | "users.apps.UsersConfig", 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | "corsheaders.middleware.CorsMiddleware", 51 | "debug_toolbar.middleware.DebugToolbarMiddleware", 52 | "django.middleware.security.SecurityMiddleware", 53 | "django.contrib.sessions.middleware.SessionMiddleware", 54 | "django.middleware.common.CommonMiddleware", 55 | "django.middleware.csrf.CsrfViewMiddleware", 56 | "django.contrib.auth.middleware.AuthenticationMiddleware", 57 | "django.contrib.messages.middleware.MessageMiddleware", 58 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 59 | ] 60 | 61 | ROOT_URLCONF = "school.urls" 62 | 63 | TEMPLATES = [ 64 | { 65 | "BACKEND": "django.template.backends.django.DjangoTemplates", 66 | "DIRS": [ 67 | os.path.join(BASE_DIR, "templates/build"), 68 | ], 69 | "APP_DIRS": True, 70 | "OPTIONS": { 71 | "context_processors": [ 72 | "django.template.context_processors.debug", 73 | "django.template.context_processors.request", 74 | "django.contrib.auth.context_processors.auth", 75 | "django.contrib.messages.context_processors.messages", 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | WSGI_APPLICATION = "school.wsgi.application" 82 | 83 | 84 | # Database 85 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 86 | 87 | DATABASES = { 88 | "default": { 89 | "ENGINE": "django.db.backends.postgresql", 90 | "NAME": "scms", 91 | "USER": "postgres", 92 | "PASSWORD": "Siah.1921#", 93 | "HOST": "127.0.0.1", 94 | "PORT": "5432", 95 | } 96 | } 97 | 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 105 | }, 106 | { 107 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 108 | }, 109 | { 110 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 111 | }, 112 | { 113 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 114 | }, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 120 | 121 | LANGUAGE_CODE = "en-us" 122 | 123 | TIME_ZONE = "UTC" 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = True 130 | 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 134 | 135 | 136 | STATIC_URL = "/static/" 137 | MEDIA_URL = "/images/" 138 | # STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static_in_env')] 139 | MEDIA_ROOT = BASE_DIR / "static/images" 140 | STATIC_ROOT = BASE_DIR / "staticfiles" 141 | 142 | STATICFILES_DIRS = [BASE_DIR / "static", BASE_DIR / "templates/build/static"] 143 | 144 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 145 | 146 | REST_FRAMEWORK = { 147 | # Use Django's standard `django.contrib.auth` permissions, 148 | # or allow read-only access for unauthenticated users. 149 | "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"], 150 | "DEFAULT_AUTHENTICATION_CLASSES": ( 151 | "rest_framework_simplejwt.authentication.JWTAuthentication", 152 | ), 153 | } 154 | 155 | SIMPLE_JWT = { 156 | "ACCESS_TOKEN_LIFETIME": timedelta(days=30), 157 | "REFRESH_TOKEN_LIFETIME": timedelta(days=1), 158 | "ROTATE_REFRESH_TOKENS": False, 159 | "BLACKLIST_AFTER_ROTATION": True, 160 | "UPDATE_LAST_LOGIN": False, 161 | "ALGORITHM": "HS256", 162 | "VERIFYING_KEY": None, 163 | "AUDIENCE": None, 164 | "ISSUER": None, 165 | "AUTH_HEADER_TYPES": ("Bearer",), 166 | "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", 167 | "USER_ID_FIELD": "id", 168 | "USER_ID_CLAIM": "user_id", 169 | "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), 170 | "TOKEN_TYPE_CLAIM": "token_type", 171 | "JTI_CLAIM": "jti", 172 | "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", 173 | "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), 174 | "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), 175 | } 176 | 177 | 178 | CORS_ALLOW_ALL_ORIGINS = True 179 | 180 | AUTH_USER_MODEL = "users.CustomUser" 181 | 182 | INTERNAL_IPS = [ 183 | "127.0.0.1", 184 | ] 185 | -------------------------------------------------------------------------------- /school/urls.py: -------------------------------------------------------------------------------- 1 | import debug_toolbar 2 | from django.contrib import admin 3 | from django.urls import path, include 4 | from django.conf import settings 5 | from django.conf.urls.static import static 6 | from django.views.generic import TemplateView 7 | from django.http import JsonResponse 8 | 9 | 10 | def custom_404_handler(request, exception): 11 | return JsonResponse( 12 | { 13 | "error": "Page Not Found", 14 | "detail": f"The requested URL {request.path} was not found on this server.", 15 | }, 16 | status=404, 17 | ) 18 | 19 | 20 | urlpatterns = [ 21 | path("admin/", admin.site.urls), 22 | path("", TemplateView.as_view(template_name="index.html")), 23 | path("api/academic/", include("api.academic.urls")), 24 | path("api/administration/", include("api.administration.urls")), 25 | path("api/attendance/", include("api.attendance.urls")), 26 | path("api/assignments/", include("api.assignments.urls")), 27 | path("api/blog/", include("api.blog.urls")), 28 | path("api/finance/", include("api.finance.urls")), 29 | # path('api/journals/', include('api.journals.urls')), 30 | # path("api/notes/", include("api.notes.urls")), 31 | path("api/users/", include("api.users.urls")), 32 | path("api/timetable/", include("api.schedule.urls")), 33 | path("api/sis/", include("api.sis.urls")), 34 | path("__debug__/", include(debug_toolbar.urls)), 35 | ] 36 | 37 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 38 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 39 | 40 | handler404 = custom_404_handler 41 | -------------------------------------------------------------------------------- /school/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for school project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'school.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /sis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/sis/__init__.py -------------------------------------------------------------------------------- /sis/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from academic.models import * 4 | 5 | admin.site.register(ReasonLeft) 6 | admin.site.register(StudentsPreviousAcademicHistory) 7 | admin.site.register(StudentFile) 8 | admin.site.register(StudentHealthRecord) 9 | admin.site.register(FamilyAccessUser) 10 | admin.site.register(Parent) 11 | admin.site.register(Student) 12 | -------------------------------------------------------------------------------- /sis/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SisConfig(AppConfig): 5 | name = 'sis' 6 | -------------------------------------------------------------------------------- /sis/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='StudentBulkUpload', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('date_uploaded', models.DateTimeField(auto_now=True)), 19 | ('csv_file', models.FileField(upload_to='api/sis/students/bulkupload')), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /sis/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/sis/migrations/__init__.py -------------------------------------------------------------------------------- /sis/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class StudentBulkUpload(models.Model): 5 | date_uploaded = models.DateTimeField(auto_now=True) 6 | csv_file = models.FileField(upload_to="api/sis/students/bulkupload") 7 | -------------------------------------------------------------------------------- /sis/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from academic.models import ( 4 | StudentsMedicalHistory, 5 | Student, 6 | Parent, 7 | ReasonLeft, 8 | ClassLevel, 9 | ClassYear, 10 | ) 11 | from academic.serializers import ClassLevelSerializer, ClassYearSerializer 12 | 13 | 14 | class ReasonLeftSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = ReasonLeft 17 | fields = "__all__" 18 | 19 | 20 | class StudentHealthRecordSerializer(serializers.ModelSerializer): 21 | class Meta: 22 | model = StudentsMedicalHistory 23 | fields = "__all__" 24 | 25 | 26 | class ParentSerializer(serializers.ModelSerializer): 27 | class Meta: 28 | model = Parent 29 | fields = "__all__" 30 | 31 | 32 | class SiblingSerializer(serializers.ModelSerializer): 33 | full_name = serializers.SerializerMethodField() 34 | class_level = serializers.SerializerMethodField() 35 | 36 | class Meta: 37 | model = Student 38 | fields = [ 39 | "id", 40 | "first_name", 41 | "middle_name", 42 | "last_name", 43 | "full_name", 44 | "admission_number", 45 | "gender", 46 | "class_level", 47 | "class_of_year", 48 | ] 49 | 50 | def get_full_name(self, obj): 51 | return obj.full_name 52 | 53 | def get_class_level(self, obj): 54 | return obj.class_level.name if obj.class_level else None 55 | 56 | 57 | class StudentSerializer(serializers.ModelSerializer): 58 | full_name = serializers.SerializerMethodField() 59 | class_level_display = serializers.SerializerMethodField() 60 | class_of_year_display = serializers.SerializerMethodField() 61 | parent_guardian_display = serializers.SerializerMethodField() 62 | siblings = SiblingSerializer(many=True, read_only=True) 63 | 64 | class_level = serializers.CharField(write_only=True, required=True) 65 | class_of_year = serializers.CharField( 66 | write_only=False, required=False, allow_null=True 67 | ) 68 | 69 | class Meta: 70 | model = Student 71 | fields = [ 72 | "id", 73 | "first_name", 74 | "middle_name", 75 | "last_name", 76 | "admission_number", 77 | "parent_contact", 78 | "region", 79 | "city", 80 | "street", 81 | "gender", 82 | "religion", 83 | "date_of_birth", 84 | "std_vii_number", 85 | "prems_number", 86 | "full_name", 87 | "class_level_display", 88 | "class_of_year_display", 89 | "parent_guardian_display", 90 | "class_level", # write-only 91 | "class_of_year", # write-only 92 | "siblings", 93 | ] 94 | 95 | def get_full_name(self, obj): 96 | return obj.full_name 97 | 98 | def get_class_level_display(self, obj): 99 | return obj.class_level.name if obj.class_level else None 100 | 101 | def get_class_of_year_display(self, obj): 102 | return obj.class_of_year.full_name if obj.class_of_year else None 103 | 104 | def get_parent_guardian_display(self, obj): 105 | return obj.parent_guardian.email if obj.parent_guardian else None 106 | 107 | def validate_and_create_student(self, data): 108 | print("validated_data:", data) 109 | class_level_name = data.pop("class_level", None) 110 | if not class_level_name: 111 | raise serializers.ValidationError("Missing 'class_level' field.") 112 | 113 | try: 114 | class_level = ClassLevel.objects.get(name__iexact=class_level_name) 115 | except ClassLevel.DoesNotExist: 116 | raise serializers.ValidationError( 117 | f"Class level '{class_level_name}' does not exist." 118 | ) 119 | data["class_level"] = class_level 120 | date_of_birth = (data.get("date_of_birth", "2000-01-01"),) 121 | religion = data.get("religion", None) 122 | 123 | class_of_year_name = data.pop("class_of_year", None) 124 | if class_of_year_name: 125 | try: 126 | class_year = ClassYear.objects.get(year=class_of_year_name) 127 | data["class_of_year"] = class_year 128 | except ClassYear.DoesNotExist: 129 | raise serializers.ValidationError( 130 | f"Class year '{class_of_year_name}' does not exist." 131 | ) 132 | 133 | # Normalize names 134 | data["first_name"] = data["first_name"].title() 135 | data["middle_name"] = data.get("middle_name", "").title() 136 | data["last_name"] = data["last_name"].title() 137 | 138 | parent = None 139 | contact = data.get("parent_contact") 140 | if contact: 141 | parent, _ = Parent.objects.get_or_create( 142 | phone_number=contact, 143 | defaults={ 144 | "first_name": data["middle_name"] or "Unknown", 145 | "last_name": data["last_name"], 146 | "email": f"parent_of_{data['first_name']}_{data['last_name']}@hayatul.com", 147 | }, 148 | ) 149 | data["parent_guardian"] = parent 150 | 151 | return Student.objects.create(**data) 152 | 153 | def create(self, validated_data): 154 | return self.validate_and_create_student(validated_data) 155 | 156 | def update(self, instance, validated_data): 157 | class_level_name = validated_data.pop("class_level", None) 158 | if class_level_name: 159 | try: 160 | class_level = ClassLevel.objects.get(name__iexact=class_level_name) 161 | instance.class_level = class_level 162 | except ClassLevel.DoesNotExist: 163 | raise serializers.ValidationError( 164 | f"Class level '{class_level_name}' does not exist." 165 | ) 166 | 167 | class_year_name = validated_data.pop("class_of_year", None) 168 | if class_year_name: 169 | try: 170 | class_year = ClassYear.objects.get(year=class_year_name) 171 | instance.class_of_year = class_year 172 | except ClassYear.DoesNotExist: 173 | raise serializers.ValidationError( 174 | f"Class year '{class_year_name}' does not exist." 175 | ) 176 | 177 | instance.first_name = validated_data.get( 178 | "first_name", instance.first_name 179 | ).title() 180 | instance.middle_name = validated_data.get( 181 | "middle_name", instance.middle_name 182 | ).title() 183 | instance.last_name = validated_data.get("last_name", instance.last_name).title() 184 | 185 | for field in [ 186 | "admission_number", 187 | "parent_contact", 188 | "region", 189 | "city", 190 | "street", 191 | "gender", 192 | "religion", 193 | "date_of_birth", 194 | "std_vii_number", 195 | "prems_number", 196 | ]: 197 | if field in validated_data: 198 | setattr(instance, field, validated_data[field]) 199 | 200 | # Update parent if needed 201 | contact = validated_data.get("parent_contact", instance.parent_contact) 202 | if contact: 203 | parent, _ = Parent.objects.get_or_create( 204 | phone_number=contact, 205 | defaults={ 206 | "first_name": instance.middle_name or "Unknown", 207 | "last_name": instance.last_name, 208 | "email": f"parent_of_{instance.first_name}_{instance.last_name}@hayatul.com", 209 | }, 210 | ) 211 | instance.parent_guardian = parent 212 | 213 | instance.save() 214 | return instance 215 | 216 | def bulk_create(self, student_data_list): 217 | created_students = [] 218 | errors = [] 219 | 220 | for data in student_data_list: 221 | try: 222 | student = self.validate_and_create_student(data) 223 | created_students.append(student) 224 | except serializers.ValidationError as e: 225 | data["error"] = str(e) 226 | errors.append(data) 227 | 228 | return created_students, errors 229 | -------------------------------------------------------------------------------- /sis/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /sis/views.py: -------------------------------------------------------------------------------- 1 | import openpyxl 2 | from django.db import transaction 3 | from django_filters.rest_framework import FilterSet, CharFilter, DjangoFilterBackend 4 | from rest_framework import views 5 | from rest_framework.views import APIView 6 | from rest_framework.permissions import IsAuthenticated, IsAdminUser 7 | from rest_framework.response import Response 8 | from rest_framework.pagination import PageNumberPagination 9 | from rest_framework import status, generics 10 | from django.http import Http404 11 | 12 | 13 | from academic.models import Student, ClassLevel, Parent 14 | from .serializers import StudentSerializer 15 | 16 | # Students filter 17 | 18 | 19 | class StudentFilter(FilterSet): 20 | first_name = CharFilter(field_name="first_name", lookup_expr="icontains") 21 | middle_name = CharFilter(field_name="middle_name", lookup_expr="icontains") 22 | last_name = CharFilter(field_name="last_name", lookup_expr="icontains") 23 | class_level = CharFilter(method="filter_by_class_level") 24 | 25 | class Meta: 26 | model = Student 27 | fields = ["first_name", "middle_name", "last_name", "class_level"] 28 | 29 | def filter_by_class_level(self, queryset, name, value): 30 | return queryset.filter(class_level__name__icontains=value) 31 | 32 | 33 | class StudentListView(generics.ListCreateAPIView): 34 | queryset = Student.objects.all() 35 | serializer_class = StudentSerializer 36 | filter_backends = [DjangoFilterBackend] 37 | filterset_class = StudentFilter 38 | 39 | def create(self, request, *args, **kwargs): 40 | serializer = self.get_serializer(data=request.data) 41 | serializer.is_valid(raise_exception=True) 42 | student = serializer.save() 43 | return Response( 44 | self.get_serializer(student).data, status=status.HTTP_201_CREATED 45 | ) 46 | 47 | 48 | class StudentDetailView(views.APIView): 49 | permission_classes = [IsAuthenticated] 50 | 51 | def get_object(self, pk): 52 | try: 53 | return Student.objects.get(pk=pk) 54 | except Student.DoesNotExist: 55 | raise Http404 56 | 57 | def get(self, request, pk, format=None): 58 | student = self.get_object(pk) 59 | serializer = StudentSerializer(student) 60 | return Response(serializer.data) 61 | 62 | def put(self, request, pk, format=None): 63 | student = self.get_object(pk) 64 | serializer = StudentSerializer(student, data=request.data) 65 | if serializer.is_valid(): 66 | serializer.save() 67 | return Response(serializer.data) 68 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 69 | 70 | def delete(self, request, pk, format=None): 71 | student = self.get_object(pk) 72 | student.delete() 73 | return Response(status=status.HTTP_204_NO_CONTENT) 74 | 75 | 76 | class BulkUploadStudentsView(APIView): 77 | """ 78 | API View to handle bulk uploading of students from an Excel file. 79 | """ 80 | 81 | def post(self, request, *args, **kwargs): 82 | file = request.FILES.get("file") 83 | if not file: 84 | return Response( 85 | {"error": "No file provided."}, status=status.HTTP_400_BAD_REQUEST 86 | ) 87 | 88 | try: 89 | workbook = openpyxl.load_workbook(file) 90 | sheet = workbook.active 91 | 92 | columns = [ 93 | "first_name", 94 | "middle_name", 95 | "last_name", 96 | "admission_number", 97 | "parent_contact", 98 | "region", 99 | "city", 100 | "class_level", 101 | "gender", 102 | "date_of_birth", 103 | ] 104 | 105 | students_to_create = [] 106 | not_created = [] 107 | created_students = [] 108 | new_parents_created = 0 109 | updated_students_info = [] 110 | skipped_students = [] 111 | 112 | for i, row in enumerate( 113 | sheet.iter_rows(min_row=2, values_only=True), start=2 114 | ): 115 | student_data = dict(zip(columns, row)) 116 | 117 | # Normalize names 118 | first_name = (student_data["first_name"] or "").lower() 119 | middle_name = (student_data["middle_name"] or "").lower() 120 | last_name = (student_data["last_name"] or "").lower() 121 | 122 | try: 123 | class_level = ClassLevel.objects.get( 124 | name=student_data["class_level"] 125 | ) 126 | parent_contact = student_data["parent_contact"] 127 | parent = None 128 | update_reasons = [] 129 | 130 | if parent_contact: 131 | parent, parent_created = Parent.objects.get_or_create( 132 | phone_number=parent_contact, 133 | defaults={ 134 | "first_name": middle_name, 135 | "last_name": last_name, 136 | "email": f"parent_of_{first_name}_{last_name}@hayatul.com", 137 | }, 138 | ) 139 | if parent_created: 140 | new_parents_created += 1 141 | 142 | admission_number = student_data["admission_number"] 143 | student_exists = Student.objects.filter( 144 | admission_number=admission_number 145 | ).first() 146 | 147 | if student_exists: 148 | # Track updates to existing students 149 | updated = False 150 | 151 | if not student_exists.parent_guardian and parent: 152 | student_exists.parent_guardian = parent 153 | update_reasons.append("parent added") 154 | updated = True 155 | 156 | existing_sibling = ( 157 | Student.objects.filter(parent_contact=parent_contact) 158 | .exclude(id=student_exists.id) 159 | .first() 160 | ) 161 | if ( 162 | existing_sibling 163 | and not student_exists.siblings.filter( 164 | id=existing_sibling.id 165 | ).exists() 166 | ): 167 | student_exists.siblings.add(existing_sibling) 168 | existing_sibling.siblings.add(student_exists) 169 | update_reasons.append("sibling added") 170 | updated = True 171 | 172 | if updated: 173 | student_exists.save() 174 | updated_students_info.append( 175 | { 176 | "admission_number": admission_number, 177 | "full_name": f"{student_exists.first_name} {student_exists.last_name}", 178 | "reasons": update_reasons, 179 | } 180 | ) 181 | else: 182 | skipped_students.append( 183 | { 184 | "admission_number": admission_number, 185 | "full_name": f"{student_exists.first_name} {student_exists.last_name}", 186 | "reason": "Student already exists and no updates were needed.", 187 | } 188 | ) 189 | continue # Skip creating duplicate 190 | 191 | # Prepare new student 192 | student = Student( 193 | first_name=first_name, 194 | middle_name=middle_name, 195 | last_name=last_name, 196 | admission_number=admission_number, 197 | parent_contact=parent_contact, 198 | region=student_data["region"], 199 | city=student_data["city"], 200 | class_level=class_level, 201 | gender=student_data["gender"], 202 | date_of_birth=student_data["date_of_birth"], 203 | parent_guardian=parent, 204 | ) 205 | 206 | existing_sibling = Student.objects.filter( 207 | parent_contact=parent_contact 208 | ).first() 209 | students_to_create.append((student, existing_sibling)) 210 | 211 | except Exception as e: 212 | student_data["error"] = str(e) 213 | not_created.append(student_data) 214 | 215 | # Save new students 216 | with transaction.atomic(): 217 | for student, existing_sibling in students_to_create: 218 | student.save() 219 | created_students.append(student) 220 | 221 | if ( 222 | existing_sibling 223 | and not student.siblings.filter(id=existing_sibling.id).exists() 224 | ): 225 | student.siblings.add(existing_sibling) 226 | existing_sibling.siblings.add(student) 227 | updated_students_info.append( 228 | { 229 | "admission_number": student.admission_number, 230 | "full_name": f"{student.first_name} {student.last_name}", 231 | "reasons": ["sibling added"], 232 | } 233 | ) 234 | 235 | return Response( 236 | { 237 | "message": f"{len(created_students)} students successfully uploaded. {new_parents_created} parents created successfully. ", 238 | "updated_students": updated_students_info, 239 | "skipped_students": skipped_students, 240 | "not_created": not_created, 241 | }, 242 | status=status.HTTP_201_CREATED, 243 | ) 244 | 245 | except Exception as e: 246 | return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) 247 | 248 | 249 | """ 250 | class StudentHealthRecordViewSet(viewsets.ModelViewSet): 251 | queryset = StudentHealthRecord.objects.all() 252 | serializer_class = StudentHealthRecordSerializer 253 | 254 | class GradeScaleViewSet(viewsets.ModelViewSet): 255 | queryset = GradeScale.objects.all() 256 | serializer_class = GradeScaleSerializer 257 | 258 | class GradeScaleRuleViewSet(viewsets.ModelViewSet): 259 | queryset = GradeScaleRule.objects.all() 260 | serializer_class = GradeScaleRuleSerializer 261 | 262 | class SchoolYearViewSet(viewsets.ModelViewSet): 263 | queryset = SchoolYear.objects.all() 264 | serializer_class = SchoolYearSerializer 265 | 266 | class MessageToStudentViewSet(viewsets.ModelViewSet): 267 | queryset = MessageToStudent.objects.all() 268 | serializer_class = MessageToStudentSerializer 269 | """ 270 | -------------------------------------------------------------------------------- /static/images/articles/Copy_of_HIC_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/static/images/articles/Copy_of_HIC_logo.jpg -------------------------------------------------------------------------------- /static/images/carousel/20200918_100730.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/static/images/carousel/20200918_100730.jpg -------------------------------------------------------------------------------- /static/images/carousel/20200921_093334.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/static/images/carousel/20200921_093334.jpg -------------------------------------------------------------------------------- /static/images/carousel/20200921_093729.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/static/images/carousel/20200921_093729.jpg -------------------------------------------------------------------------------- /static/images/nursery/20201128_091608.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/static/images/nursery/20201128_091608.jpg -------------------------------------------------------------------------------- /static/images/nursery/20201128_091634.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/static/images/nursery/20201128_091634.jpg -------------------------------------------------------------------------------- /static/images/primary/20201128_092423.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/static/images/primary/20201128_092423.jpg -------------------------------------------------------------------------------- /static/images/primary/20201128_092541.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/static/images/primary/20201128_092541.jpg -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HayatulAPI 6 | 8 | 9 | 10 | 11 |
12 |

Hayatul Islamiya Complex API System

13 | 14 |

Important urls

15 |
16 |
17 |
Links
18 | 26 |
API Links
27 |
28 | 77 |
78 |
79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .forms import CustomUserCreationForm, CustomUserChangeForm 5 | from .models import CustomUser, Accountant 6 | 7 | 8 | class CustomUserAdmin(UserAdmin): 9 | add_form = CustomUserCreationForm 10 | form = CustomUserChangeForm 11 | model = CustomUser 12 | list_display = ('email', 'is_staff', 'is_active', 'is_accountant', 'is_teacher',) 13 | list_filter = ('email', 'is_staff', 'is_active', 'is_accountant', 'is_teacher',) 14 | fieldsets = ( 15 | (None, {'fields': ('first_name', 'middle_name', 'last_name','email', 'password')}), 16 | ('Permissions', {'fields': ('is_staff', 'is_active', 'is_accountant', 'is_teacher',)}), 17 | ) 18 | add_fieldsets = ( 19 | (None, { 20 | 'classes': ('wide',), 21 | 'fields': ('first_name', 'middle_name', 'last_name', 'email', 'password1', 'password2', 'is_staff', 'is_active', 'is_accountant', 'is_teacher',)} 22 | ), 23 | ) 24 | search_fields = ('email',) 25 | ordering = ('email',) 26 | 27 | 28 | admin.site.register(CustomUser, CustomUserAdmin) 29 | admin.site.register(Accountant) -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.forms import UserCreationForm, UserChangeForm 2 | 3 | from .models import CustomUser 4 | 5 | 6 | class CustomUserCreationForm(UserCreationForm): 7 | 8 | class Meta(UserCreationForm): 9 | model = CustomUser 10 | fields = ('email',) 11 | 12 | 13 | class CustomUserChangeForm(UserChangeForm): 14 | 15 | class Meta: 16 | model = CustomUser 17 | fields = ('email',) -------------------------------------------------------------------------------- /users/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.base_user import BaseUserManager 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class CustomUserManager(BaseUserManager): 6 | """ 7 | Custom user model manager where email is the unique identifiers 8 | for authentication instead of usernames. 9 | """ 10 | def create_user(self, email, password, **extra_fields): 11 | """ 12 | Create and save a User with the given email and password. 13 | """ 14 | if not email: 15 | raise ValueError(_('The Email must be set')) 16 | email = self.normalize_email(email) 17 | user = self.model(email=email, **extra_fields) 18 | user.set_password(password) 19 | user.save() 20 | return user 21 | 22 | def create_superuser(self, email, password, **extra_fields): 23 | """ 24 | Create and save a SuperUser with the given email and password. 25 | """ 26 | extra_fields.setdefault('is_staff', True) 27 | extra_fields.setdefault('is_superuser', True) 28 | extra_fields.setdefault('is_active', True) 29 | 30 | if extra_fields.get('is_staff') is not True: 31 | raise ValueError(_('Superuser must have is_staff=True.')) 32 | if extra_fields.get('is_superuser') is not True: 33 | raise ValueError(_('Superuser must have is_superuser=True.')) 34 | return self.create_user(email, password, **extra_fields) -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-16 14:22 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('auth', '0012_alter_user_first_name_max_length'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Accountant', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('username', models.CharField(blank=True, max_length=250, unique=True)), 21 | ('first_name', models.CharField(blank=True, max_length=300)), 22 | ('middle_name', models.CharField(blank=True, max_length=100)), 23 | ('last_name', models.CharField(blank=True, max_length=300)), 24 | ('gender', models.CharField(blank=True, choices=[('Male', 'Male'), ('Female', 'Female'), ('Other', 'Other')], max_length=10)), 25 | ('email', models.EmailField(blank=True, max_length=254, null=True)), 26 | ('empId', models.CharField(blank=True, max_length=8, null=True, unique=True)), 27 | ('tin_number', models.CharField(blank=True, max_length=9, null=True)), 28 | ('nssf_number', models.CharField(blank=True, max_length=9, null=True)), 29 | ('salary', models.IntegerField(blank=True, null=True)), 30 | ('unpaid_salary', models.DecimalField(decimal_places=2, default=0, max_digits=10)), 31 | ('national_id', models.CharField(blank=True, max_length=100, null=True)), 32 | ('address', models.CharField(blank=True, max_length=255)), 33 | ('phone_number', models.CharField(blank=True, max_length=150)), 34 | ('alt_email', models.EmailField(blank=True, help_text='Personal Email apart from the one given by the school', max_length=254, null=True)), 35 | ('date_of_birth', models.DateField(blank=True, null=True)), 36 | ('image', models.ImageField(blank=True, null=True, upload_to='Employee_images')), 37 | ('isAccountant', models.BooleanField(default=True)), 38 | ('inactive', models.BooleanField(default=False)), 39 | ], 40 | options={ 41 | 'ordering': ('first_name', 'last_name'), 42 | }, 43 | ), 44 | migrations.CreateModel( 45 | name='CustomUser', 46 | fields=[ 47 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 48 | ('password', models.CharField(max_length=128, verbose_name='password')), 49 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 50 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 51 | ('first_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='first name')), 52 | ('middle_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='middle name')), 53 | ('last_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='last name')), 54 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 55 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now)), 56 | ('is_staff', models.BooleanField(default=False)), 57 | ('is_active', models.BooleanField(default=True)), 58 | ('is_accountant', models.BooleanField(default=False)), 59 | ('is_teacher', models.BooleanField(default=False)), 60 | ('is_parent', models.BooleanField(default=False)), 61 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), 62 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), 63 | ], 64 | options={ 65 | 'abstract': False, 66 | }, 67 | ), 68 | ] 69 | -------------------------------------------------------------------------------- /users/migrations/0002_alter_customuser_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-01-29 20:38 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='customuser', 17 | options={'ordering': ['email']}, 18 | ), 19 | migrations.RemoveField( 20 | model_name='accountant', 21 | name='isAccountant', 22 | ), 23 | migrations.AddField( 24 | model_name='accountant', 25 | name='user', 26 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accountant', to=settings.AUTH_USER_MODEL), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /users/migrations/0003_customuser_phone_number.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1 on 2025-05-04 11:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0002_alter_customuser_options_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='customuser', 15 | name='phone_number', 16 | field=models.CharField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mwinamijr/django-scms/82c3717d4d21fd2f42ad2385136771c95db194ce/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractBaseUser 3 | from django.contrib.auth.models import PermissionsMixin 4 | from django.contrib.auth.models import Group 5 | from django.utils.crypto import get_random_string 6 | from django.utils.translation import gettext_lazy as _ 7 | from django.utils import timezone 8 | 9 | from administration.common_objs import * 10 | from .managers import CustomUserManager 11 | 12 | 13 | class CustomUser(AbstractBaseUser, PermissionsMixin): 14 | first_name = models.CharField( 15 | max_length=100, blank=True, null=True, verbose_name="first name" 16 | ) 17 | middle_name = models.CharField( 18 | max_length=100, blank=True, null=True, verbose_name="middle name" 19 | ) 20 | last_name = models.CharField( 21 | max_length=100, blank=True, null=True, verbose_name="last name" 22 | ) 23 | phone_number = models.CharField(blank=True, null=True) 24 | email = models.EmailField(_("email address"), unique=True) 25 | date_joined = models.DateTimeField(default=timezone.now) 26 | is_staff = models.BooleanField(default=False) 27 | is_active = models.BooleanField(default=True) 28 | is_accountant = models.BooleanField(default=False) 29 | is_teacher = models.BooleanField(default=False) 30 | is_parent = models.BooleanField(default=False) 31 | 32 | USERNAME_FIELD = "email" 33 | REQUIRED_FIELDS = [] 34 | 35 | objects = CustomUserManager() 36 | 37 | class Meta: 38 | ordering = ["email"] 39 | 40 | def __str__(self): 41 | return self.email 42 | 43 | 44 | class Accountant(models.Model): 45 | user = models.OneToOneField( 46 | CustomUser, 47 | on_delete=models.CASCADE, 48 | related_name="accountant", 49 | null=True, 50 | blank=True, 51 | ) 52 | username = models.CharField(unique=True, max_length=250, blank=True) 53 | first_name = models.CharField(max_length=300, blank=True) 54 | middle_name = models.CharField(max_length=100, blank=True) 55 | last_name = models.CharField(max_length=300, blank=True) 56 | gender = models.CharField(max_length=10, choices=GENDER_CHOICE, blank=True) 57 | email = models.EmailField(blank=True, null=True) 58 | empId = models.CharField(max_length=8, null=True, blank=True, unique=True) 59 | tin_number = models.CharField(max_length=9, null=True, blank=True) 60 | nssf_number = models.CharField(max_length=9, null=True, blank=True) 61 | salary = models.IntegerField(blank=True, null=True) 62 | unpaid_salary = models.DecimalField(max_digits=10, decimal_places=2, default=0) 63 | national_id = models.CharField(max_length=100, blank=True, null=True) 64 | address = models.CharField(max_length=255, blank=True) 65 | phone_number = models.CharField(max_length=150, blank=True) 66 | alt_email = models.EmailField( 67 | blank=True, 68 | null=True, 69 | help_text="Personal Email apart from the one given by the school", 70 | ) 71 | date_of_birth = models.DateField(blank=True, null=True) 72 | image = models.ImageField(upload_to="Employee_images", blank=True, null=True) 73 | inactive = models.BooleanField(default=False) 74 | 75 | class Meta: 76 | ordering = ("first_name", "last_name") 77 | 78 | def __str__(self): 79 | return f"{self.first_name} {self.last_name}" 80 | 81 | @property 82 | def deleted(self): 83 | return self.inactive 84 | 85 | def save(self, *args, **kwargs): 86 | """ 87 | When an accountant is created, generate a CustomUser instance for login. 88 | """ 89 | # Generate unique username 90 | if not self.username: 91 | self.username = f"{self.first_name.lower()}{self.last_name.lower()}{get_random_string(4)}" 92 | 93 | if not self.user: 94 | # Create the user if it doesn't exist 95 | user = CustomUser.objects.create( 96 | first_name=self.first_name, 97 | last_name=self.last_name, 98 | email=self.email, 99 | is_accountant=True, 100 | ) 101 | 102 | # Set a default password using empId (if available) or fallback 103 | default_password = f"Complex.{self.empId[-4:] if self.empId and len(self.empId) >= 4 else '0000'}" 104 | user.set_password(default_password) 105 | user.save() 106 | 107 | # Attach the created user to the accountant 108 | self.user = user 109 | 110 | # Add user to "accountant" group 111 | group, _ = Group.objects.get_or_create(name="accountant") 112 | user.groups.add(group) 113 | 114 | super().save(*args, **kwargs) 115 | 116 | def update_unpaid_salary(self): 117 | # Update unpaid salary at the start of each month 118 | current_month = timezone.now().month 119 | if self.unpaid_salary > 0: 120 | self.unpaid_salary += self.salary # Add salary amount to unpaid salary 121 | else: 122 | self.unpaid_salary = ( 123 | self.salary 124 | ) # If unpaid salary is 0, set the first month's salary 125 | self.save() 126 | -------------------------------------------------------------------------------- /users/serializers.py: -------------------------------------------------------------------------------- 1 | from django.db import transaction 2 | from django.contrib.auth.models import Group 3 | from rest_framework import serializers 4 | from rest_framework_simplejwt.tokens import RefreshToken 5 | from academic.models import Teacher, Subject, Parent 6 | from .models import CustomUser, Accountant 7 | 8 | 9 | class UserSerializer(serializers.ModelSerializer): 10 | username = serializers.SerializerMethodField(read_only=True) 11 | isAdmin = serializers.SerializerMethodField(read_only=True) 12 | isAccountant = serializers.SerializerMethodField(read_only=True) 13 | isTeacher = serializers.SerializerMethodField(read_only=True) 14 | isParent = serializers.SerializerMethodField(read_only=True) 15 | accountant_details = serializers.SerializerMethodField(read_only=True) 16 | teacher_details = serializers.SerializerMethodField(read_only=True) 17 | parent_details = serializers.SerializerMethodField(read_only=True) 18 | 19 | class Meta: 20 | model = CustomUser 21 | fields = [ 22 | "id", 23 | "email", 24 | "username", 25 | "first_name", 26 | "middle_name", 27 | "last_name", 28 | "isAdmin", 29 | "isAccountant", 30 | "isTeacher", 31 | "isParent", 32 | "accountant_details", 33 | "teacher_details", 34 | "parent_details", 35 | ] 36 | 37 | def get_isAdmin(self, obj): 38 | return obj.is_staff 39 | 40 | def get_isAccountant(self, obj): 41 | return obj.is_accountant 42 | 43 | def get_isTeacher(self, obj): 44 | return obj.is_teacher 45 | 46 | def get_isParent(self, obj): 47 | return obj.is_parent 48 | 49 | def get_username(self, obj): 50 | if obj.first_name and obj.last_name: 51 | return f"{obj.first_name} {obj.last_name}" 52 | return obj.email or "Unknown User" 53 | 54 | def get_accountant_details(self, obj): 55 | """Return accountant details if the user is an accountant.""" 56 | if obj.is_accountant and hasattr(obj, "accountant"): 57 | return AccountantSerializer(obj.accountant).data 58 | return None 59 | 60 | def get_teacher_details(self, obj): 61 | """Return teacher details if the user is a teacher.""" 62 | if obj.is_teacher and hasattr(obj, "teacher"): 63 | return TeacherSerializer(obj.teacher).data 64 | return None 65 | 66 | def get_parent_details(self, obj): 67 | """Return parent details if the user is a parent.""" 68 | if obj.is_parent and hasattr(obj, "parent"): 69 | return ParentSerializer(obj.parent).data 70 | return None 71 | 72 | 73 | class UserSerializerWithToken(UserSerializer): 74 | token = serializers.SerializerMethodField(read_only=True) 75 | 76 | class Meta: 77 | model = CustomUser 78 | fields = UserSerializer.Meta.fields + ["token"] 79 | 80 | def get_token(self, obj): 81 | try: 82 | token = RefreshToken.for_user(obj) 83 | return str(token.access_token) 84 | except Exception: 85 | return None 86 | 87 | 88 | class AccountantSerializer(serializers.ModelSerializer): 89 | payments = serializers.SerializerMethodField() 90 | 91 | class Meta: 92 | model = Accountant 93 | fields = [ 94 | "id", 95 | "username", 96 | "first_name", 97 | "middle_name", 98 | "last_name", 99 | "email", 100 | "phone_number", 101 | "empId", 102 | "address", 103 | "gender", 104 | "national_id", 105 | "nssf_number", 106 | "tin_number", 107 | "date_of_birth", 108 | "salary", 109 | "unpaid_salary", 110 | "payments", 111 | ] 112 | 113 | def get_payments(self, obj): 114 | """Lazy import to avoid circular import issue""" 115 | from finance.serializers import PaymentSerializer # Import inside method 116 | 117 | if obj.user: 118 | payments = obj.user.payments.all() 119 | return PaymentSerializer(payments, many=True).data 120 | return [] 121 | 122 | def validate_email(self, value): 123 | request = self.context.get("request", None) 124 | accountant_id = ( 125 | self.instance.id if self.instance else None 126 | ) # Get accountant ID if updating 127 | 128 | if Accountant.objects.filter(email=value).exclude(id=accountant_id).exists(): 129 | raise serializers.ValidationError( 130 | "A accountant with this email already exists." 131 | ) 132 | 133 | return value 134 | 135 | def validate_phone_number(self, value): 136 | accountant_id = ( 137 | self.instance.id if self.instance else None 138 | ) # Get accountant ID if updating 139 | 140 | if ( 141 | Accountant.objects.filter(phone_number=value) 142 | .exclude(id=accountant_id) 143 | .exists() 144 | ): 145 | raise serializers.ValidationError( 146 | "A accountant with this phone number already exists." 147 | ) 148 | 149 | return value 150 | 151 | 152 | class TeacherSerializer(serializers.ModelSerializer): 153 | subject_specialization = serializers.ListField( 154 | child=serializers.CharField(), write_only=True, required=True 155 | ) 156 | subject_specialization_display = serializers.StringRelatedField( 157 | many=True, source="subject_specialization", read_only=True 158 | ) 159 | payments = serializers.SerializerMethodField() 160 | 161 | class Meta: 162 | model = Teacher 163 | fields = [ 164 | "id", 165 | "username", 166 | "first_name", 167 | "middle_name", 168 | "last_name", 169 | "email", 170 | "phone_number", 171 | "empId", 172 | "short_name", 173 | "subject_specialization", 174 | "subject_specialization_display", 175 | "address", 176 | "gender", 177 | "national_id", 178 | "nssf_number", 179 | "tin_number", 180 | "date_of_birth", 181 | "salary", 182 | "unpaid_salary", 183 | "payments", 184 | ] 185 | 186 | def get_payments(self, obj): 187 | """Lazy import to avoid circular import issue""" 188 | from finance.serializers import PaymentSerializer # Import inside method 189 | 190 | if obj.user: 191 | payments = obj.user.payments.all() 192 | return PaymentSerializer(payments, many=True).data 193 | return [] 194 | 195 | def validate_email(self, value): 196 | request = self.context.get("request", None) 197 | teacher_id = ( 198 | self.instance.id if self.instance else None 199 | ) # Get teacher ID if updating 200 | 201 | if Teacher.objects.filter(email=value).exclude(id=teacher_id).exists(): 202 | raise serializers.ValidationError( 203 | "A teacher with this email already exists." 204 | ) 205 | 206 | return value 207 | 208 | def validate_phone_number(self, value): 209 | teacher_id = ( 210 | self.instance.id if self.instance else None 211 | ) # Get teacher ID if updating 212 | 213 | if Teacher.objects.filter(phone_number=value).exclude(id=teacher_id).exists(): 214 | raise serializers.ValidationError( 215 | "A teacher with this phone number already exists." 216 | ) 217 | 218 | return value 219 | 220 | def validate_subject_specialization(self, value): 221 | """ 222 | Validate that all subject names in the input exist in the database. 223 | Ensure it works for both create and update operations. 224 | """ 225 | if not isinstance(value, list): 226 | raise serializers.ValidationError( 227 | "Subject specialization should be a list of subject names." 228 | ) 229 | 230 | # Get existing subjects matching the provided names 231 | existing_subjects = Subject.objects.filter(name__in=value).distinct() 232 | 233 | # Extract names of found subjects 234 | existing_subject_names = set(existing_subjects.values_list("name", flat=True)) 235 | 236 | # Identify missing subjects 237 | missing_subjects = set(value) - existing_subject_names 238 | 239 | if missing_subjects: 240 | raise serializers.ValidationError( 241 | f"The following subjects do not exist: {', '.join(missing_subjects)}" 242 | ) 243 | 244 | return existing_subjects # Return the queryset instead of a list of names 245 | 246 | def create(self, validated_data): 247 | subject_specialization_data = validated_data.pop("subject_specialization") 248 | teacher = Teacher.objects.create(**validated_data) 249 | subjects = Subject.objects.filter(name__in=subject_specialization_data) 250 | teacher.subject_specialization.set(subjects) 251 | return teacher 252 | 253 | 254 | class ParentSerializer(serializers.ModelSerializer): 255 | children_details = serializers.SerializerMethodField() 256 | 257 | class Meta: 258 | model = Parent 259 | fields = [ 260 | "id", 261 | "first_name", 262 | "middle_name", 263 | "last_name", 264 | "email", 265 | "phone_number", 266 | "address", 267 | "gender", 268 | "date_of_birth", 269 | "children_details", 270 | ] 271 | 272 | def get_children_details(self, obj): 273 | """Returns a list of children associated with the parent.""" 274 | return [ 275 | { 276 | "id": child.id, 277 | "first_name": child.first_name, 278 | "last_name": child.last_name, 279 | } 280 | for child in obj.children.all() 281 | ] 282 | 283 | def validate_email(self, value): 284 | """Ensure email uniqueness among parents.""" 285 | if Parent.objects.filter(email=value).exists(): 286 | raise serializers.ValidationError( 287 | "A parent with this email already exists." 288 | ) 289 | return value 290 | 291 | def validate_phone_number(self, value): 292 | """Ensure phone number uniqueness among parents.""" 293 | if Parent.objects.filter(phone_number=value).exists(): 294 | raise serializers.ValidationError( 295 | "A parent with this phone number already exists." 296 | ) 297 | return value 298 | 299 | @transaction.atomic 300 | def create(self, validated_data): 301 | """Creates a Parent and lets the model handle user creation.""" 302 | parent = Parent(**validated_data) 303 | parent.save() # This triggers the model's save() where user is created 304 | return parent 305 | 306 | @transaction.atomic 307 | def update(self, instance, validated_data): 308 | """Updates a Parent and syncs changes to the associated CustomUser.""" 309 | email = validated_data.get("email", instance.email) 310 | first_name = validated_data.get("first_name", instance.first_name) 311 | last_name = validated_data.get("last_name", instance.last_name) 312 | 313 | # Update Parent 314 | parent = super().update(instance, validated_data) 315 | 316 | # If the Parent has an associated CustomUser, update it as well 317 | if hasattr(parent, "user") and parent.user: 318 | user = parent.user 319 | user.email = email 320 | user.first_name = first_name 321 | user.last_name = last_name 322 | user.save() 323 | 324 | return parent 325 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | --------------------------------------------------------------------------------