feat: add school management auth and student CRUD UI

This commit is contained in:
Sina
2026-09-24 23:34:21 +03:30
parent 12e9efcb5c
commit 8a196f8c4f
20 changed files with 326 additions and 9 deletions

View File

@@ -1,3 +1,10 @@
from django.contrib import admin
# Register your models here.
from .models import Student
@admin.register(Student)
class StudentAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'age', 'grade', 'created_at')
search_fields = ('first_name', 'last_name')
list_filter = ('grade',)

20
students/forms.py Normal file
View File

@@ -0,0 +1,20 @@
from django import forms
from .models import Student
class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = ('first_name', 'last_name', 'age', 'grade')
widgets = {
'first_name': forms.TextInput(attrs={'placeholder': 'مثلاً: سارا'}),
'last_name': forms.TextInput(attrs={'placeholder': 'مثلاً: احمدی'}),
'age': forms.NumberInput(attrs={'min': 5, 'max': 25, 'placeholder': 'سن'}),
}
def clean_age(self):
age = self.cleaned_data['age']
if not 5 <= age <= 25:
raise forms.ValidationError('سن باید بین ۵ تا ۲۵ سال باشد.')
return age

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.17 on 2026-09-24 20:02
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Student',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=80, verbose_name='نام')),
('last_name', models.CharField(max_length=120, verbose_name='نام خانوادگی')),
('age', models.PositiveSmallIntegerField(verbose_name='سن')),
('grade', models.CharField(choices=[('اول', 'پایه اول'), ('دوم', 'پایه دوم'), ('سوم', 'پایه سوم'), ('چهارم', 'پایه چهارم'), ('پنجم', 'پایه پنجم'), ('ششم', 'پایه ششم'), ('هفتم', 'پایه هفتم'), ('هشتم', 'پایه هشتم'), ('نهم', 'پایه نهم'), ('دهم', 'پایه دهم'), ('یازدهم', 'پایه یازدهم'), ('دوازدهم', 'پایه دوازدهم')], max_length=20, verbose_name='پایه تحصیلی')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='تاریخ ثبت')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='آخرین ویرایش')),
],
options={
'verbose_name': 'دانش\u200cآموز',
'verbose_name_plural': 'دانش\u200cآموزان',
'ordering': ['-created_at'],
},
),
]

View File

@@ -1,3 +1,33 @@
from django.db import models
# Create your models here.
class Student(models.Model):
GRADE_CHOICES = [
('اول', 'پایه اول'),
('دوم', 'پایه دوم'),
('سوم', 'پایه سوم'),
('چهارم', 'پایه چهارم'),
('پنجم', 'پایه پنجم'),
('ششم', 'پایه ششم'),
('هفتم', 'پایه هفتم'),
('هشتم', 'پایه هشتم'),
('نهم', 'پایه نهم'),
('دهم', 'پایه دهم'),
('یازدهم', 'پایه یازدهم'),
('دوازدهم', 'پایه دوازدهم'),
]
first_name = models.CharField('نام', max_length=80)
last_name = models.CharField('نام خانوادگی', max_length=120)
age = models.PositiveSmallIntegerField('سن')
grade = models.CharField('پایه تحصیلی', max_length=20, choices=GRADE_CHOICES)
created_at = models.DateTimeField('تاریخ ثبت', auto_now_add=True)
updated_at = models.DateTimeField('آخرین ویرایش', auto_now=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'دانش‌آموز'
verbose_name_plural = 'دانش‌آموزان'
def __str__(self):
return f'{self.first_name} {self.last_name}'

View File

@@ -1,3 +1,42 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
# Create your tests here.
from .models import Student
class StudentManagementTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(username='manager', password='Strong-pass-123')
self.student = Student.objects.create(first_name='سارا', last_name='احمدی', age=10, grade='چهارم')
def test_dashboard_requires_login(self):
response = self.client.get(reverse('dashboard'))
self.assertRedirects(response, f'{reverse("login")}?next={reverse("dashboard")}')
def test_manager_can_create_update_and_delete_student(self):
self.client.force_login(self.user)
create_response = self.client.post(reverse('student_add'), {
'first_name': 'علی', 'last_name': 'رضایی', 'age': 11, 'grade': 'پنجم',
})
self.assertRedirects(create_response, reverse('dashboard'))
new_student = Student.objects.get(first_name='علی')
update_response = self.client.post(reverse('student_edit', args=[new_student.pk]), {
'first_name': 'علی', 'last_name': 'کریمی', 'age': 12, 'grade': 'ششم',
})
self.assertRedirects(update_response, reverse('dashboard'))
new_student.refresh_from_db()
self.assertEqual(new_student.last_name, 'کریمی')
delete_response = self.client.post(reverse('student_delete', args=[new_student.pk]))
self.assertRedirects(delete_response, reverse('dashboard'))
self.assertFalse(Student.objects.filter(pk=new_student.pk).exists())
def test_invalid_age_is_rejected(self):
self.client.force_login(self.user)
response = self.client.post(reverse('student_add'), {
'first_name': 'نوید', 'last_name': 'محمدی', 'age': 3, 'grade': 'اول',
})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'سن باید بین ۵ تا ۲۵ سال باشد')

21
students/urls.py Normal file
View File

@@ -0,0 +1,21 @@
from django.urls import path
from .views import (
DashboardView,
ManagerLoginView,
ManagerLogoutView,
ManagerSignupView,
StudentCreateView,
StudentDeleteView,
StudentUpdateView,
)
urlpatterns = [
path('', DashboardView.as_view(), name='dashboard'),
path('login/', ManagerLoginView.as_view(), name='login'),
path('signup/', ManagerSignupView.as_view(), name='signup'),
path('logout/', ManagerLogoutView.as_view(), name='logout'),
path('students/add/', StudentCreateView.as_view(), name='student_add'),
path('students/<int:pk>/edit/', StudentUpdateView.as_view(), name='student_edit'),
path('students/<int:pk>/delete/', StudentDeleteView.as_view(), name='student_delete'),
]

View File

@@ -1,3 +1,65 @@
from django.shortcuts import render
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, ListView, TemplateView, UpdateView
# Create your views here.
from .forms import StudentForm
from .models import Student
class DashboardView(LoginRequiredMixin, ListView):
model = Student
template_name = 'students/dashboard.html'
context_object_name = 'students'
def get_queryset(self):
return Student.objects.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['total_students'] = Student.objects.count()
context['average_age'] = round(sum(student.age for student in context['students']) / context['total_students'], 1) if context['total_students'] else 0
context['grade_count'] = Student.objects.values('grade').distinct().count()
return context
class ManagerLoginView(LoginView):
template_name = 'registration/login.html'
redirect_authenticated_user = True
class ManagerSignupView(CreateView):
form_class = UserCreationForm
template_name = 'registration/signup.html'
success_url = reverse_lazy('dashboard')
def form_valid(self, form):
response = super().form_valid(form)
login(self.request, self.object)
return response
class ManagerLogoutView(LogoutView):
next_page = reverse_lazy('login')
class StudentCreateView(LoginRequiredMixin, CreateView):
model = Student
form_class = StudentForm
template_name = 'students/student_form.html'
success_url = reverse_lazy('dashboard')
class StudentUpdateView(LoginRequiredMixin, UpdateView):
model = Student
form_class = StudentForm
template_name = 'students/student_form.html'
success_url = reverse_lazy('dashboard')
class StudentDeleteView(LoginRequiredMixin, DeleteView):
model = Student
template_name = 'students/student_confirm_delete.html'
success_url = reverse_lazy('dashboard')