feat: add school management auth and student CRUD UI
This commit is contained in:
@@ -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
20
students/forms.py
Normal 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
|
||||
31
students/migrations/0001_initial.py
Normal file
31
students/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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}'
|
||||
|
||||
@@ -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
21
students/urls.py
Normal 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'),
|
||||
]
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user