feat: add school management auth and student CRUD UI
This commit is contained in:
24
README.md
Normal file
24
README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# مدرسهیار
|
||||||
|
|
||||||
|
سامانهی مدیریت مدرسه و پروندهی دانشآموزان با Django و Class-Based Views.
|
||||||
|
|
||||||
|
## اجرا
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -m venv .venv
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
سپس به `http://127.0.0.1:8000/` بروید و از گزینهی ثبتنام، حساب مدیر را بسازید.
|
||||||
|
|
||||||
|
## قابلیتها
|
||||||
|
|
||||||
|
- ثبتنام، ورود و خروج مدیر مدرسه با سیستم احراز هویت Django
|
||||||
|
- داشبورد آماری و فهرست دانشآموزان
|
||||||
|
- ثبت، ویرایش و حذف دانشآموز با اعتبارسنجی سن
|
||||||
|
- ذخیرهی دائمی اطلاعات در SQLite
|
||||||
|
- رابط کاربری راستبهچپ، واکنشگرا و فارسی
|
||||||
|
- تمام viewهای اصلی با CBV
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Django>=5.2,<6
|
||||||
@@ -37,6 +37,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'students',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -54,7 +55,7 @@ ROOT_URLCONF = 'school_manager.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [BASE_DIR / 'templates'],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@@ -102,9 +103,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'fa-ir'
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'Asia/Tehran'
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
@@ -115,6 +116,11 @@ USE_TZ = True
|
|||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
|
|
||||||
|
LOGIN_URL = 'login'
|
||||||
|
LOGIN_REDIRECT_URL = 'dashboard'
|
||||||
|
LOGOUT_REDIRECT_URL = 'login'
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ Including another URLconf
|
|||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('', include('students.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
1
static/css/style.css
Normal file
1
static/css/style.css
Normal file
File diff suppressed because one or more lines are too long
16
static/js/app.js
Normal file
16
static/js/app.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('.delete-action').forEach((link) => {
|
||||||
|
link.addEventListener('click', (event) => {
|
||||||
|
if (!window.confirm('آیا از حذف این پرونده مطمئن هستید؟')) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.message').forEach((message) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
message.style.transition = 'opacity .3s ease';
|
||||||
|
message.style.opacity = '0';
|
||||||
|
}, 3500);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
from django.contrib import admin
|
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
|
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.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')
|
||||||
|
|||||||
38
templates/base.html
Normal file
38
templates/base.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}مدرسهیار{% endblock %}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-shell">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<aside class="sidebar">
|
||||||
|
<a class="brand" href="{% url 'dashboard' %}">
|
||||||
|
<span class="brand-mark">م</span>
|
||||||
|
<span><strong>مدرسهیار</strong><small>مدیریت هوشمند مدرسه</small></span>
|
||||||
|
</a>
|
||||||
|
<nav class="main-nav">
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" href="{% url 'dashboard' %}"><span>▦</span> داشبورد</a>
|
||||||
|
<a class="nav-link {% if request.resolver_match.url_name == 'student_add' %}active{% endif %}" href="{% url 'student_add' %}"><span>+</span> ثبت دانشآموز</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="profile-mini"><span class="avatar">{{ user.username|first|upper }}</span><span><b>{{ user.username }}</b><small>مدیر مدرسه</small></span></div>
|
||||||
|
<form action="{% url 'logout' %}" method="post">{% csrf_token %}<button class="logout-link" type="submit">خروج از حساب <span>↪</span></button></form>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
|
<main class="main-content {% if not user.is_authenticated %}full-content{% endif %}">
|
||||||
|
{% if messages %}<div class="messages">{% for message in messages %}<div class="message {{ message.tags }}">{{ message }}</div>{% endfor %}</div>{% endif %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script src="{% static 'js/app.js' %}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
templates/registration/form_fields.html
Normal file
1
templates/registration/form_fields.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{% for field in form %}<div class="field-group">{{ field.label_tag }}{{ field }}{% if field.help_text %}<small class="help-text">{{ field.help_text|safe }}</small>{% endif %}{% for error in field.errors %}<small class="error-text">{{ error }}</small>{% endfor %}</div>{% endfor %}{% if form.non_field_errors %}<div class="error-text">{{ form.non_field_errors }}</div>{% endif %}
|
||||||
5
templates/registration/login.html
Normal file
5
templates/registration/login.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}ورود مدیر | مدرسهیار{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="auth-layout"><div class="auth-intro"><span class="eyebrow">پنل مدیریت مدرسه</span><h1>مدرسه را با آرامش مدیریت کنید.</h1><p>اطلاعات دانشآموزان را یکجا، دقیق و همیشه در دسترس داشته باشید.</p><div class="intro-note"><span>✦</span><span>فضایی امن برای مدیریت روزانه مدرسه</span></div></div><div class="auth-card"><div class="auth-icon">◈</div><h2>خوش آمدید</h2><p class="muted">برای ورود به پنل مدیریت اطلاعات خود را وارد کنید.</p><form method="post" class="form-stack">{% csrf_token %}{% include 'registration/form_fields.html' with form=form %}<button class="button button-primary button-wide" type="submit">ورود به داشبورد <span>←</span></button></form><p class="auth-footer">حساب مدیر ندارید؟ <a href="{% url 'signup' %}">ثبتنام کنید</a></p></div></section>
|
||||||
|
{% endblock %}
|
||||||
5
templates/registration/signup.html
Normal file
5
templates/registration/signup.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}ثبتنام مدیر | مدرسهیار{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="auth-layout"><div class="auth-intro"><span class="eyebrow">شروعی ساده و سریع</span><h1>پنل اختصاصی مدرسهتان را بسازید.</h1><p>با چند قدم کوتاه، مدیریت اطلاعات دانشآموزان را منظمتر کنید.</p></div><div class="auth-card"><div class="auth-icon">✦</div><h2>ساخت حساب مدیر</h2><p class="muted">اطلاعات ورود خود را برای استفاده از پنل بسازید.</p><form method="post" class="form-stack">{% csrf_token %}{% include 'registration/form_fields.html' with form=form %}<button class="button button-primary button-wide" type="submit">ساخت حساب و ورود <span>←</span></button></form><p class="auth-footer">قبلاً ثبتنام کردهاید؟ <a href="{% url 'login' %}">وارد شوید</a></p></div></section>
|
||||||
|
{% endblock %}
|
||||||
3
templates/students/dashboard.html
Normal file
3
templates/students/dashboard.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}داشبورد | مدرسهیار{% endblock %}
|
||||||
|
{% block content %}<header class="topbar"><div><span class="eyebrow">مرکز کنترل</span><h1>سلام، {{ user.username }} <span class="wave">✦</span></h1><p>امروز چه چیزی را مدیریت میکنیم؟</p></div><a class="button button-primary" href="{% url 'student_add' %}"><span>+</span> ثبت دانشآموز</a></header><section class="stats-grid"><article class="stat-card stat-main"><div class="stat-icon">♧</div><div><span>کل دانشآموزان</span><strong>{{ total_students }}</strong><small>پرونده ثبتشده در مدرسه</small></div></article><article class="stat-card"><div class="stat-icon soft-blue">⌁</div><div><span>میانگین سنی</span><strong>{{ average_age }}</strong><small>سال</small></div></article><article class="stat-card"><div class="stat-icon soft-gold">◌</div><div><span>پایههای فعال</span><strong>{{ grade_count }}</strong><small>پایه تحصیلی</small></div></article></section><section class="list-section"><div class="section-heading"><div><h2>فهرست دانشآموزان</h2><p>آخرین اطلاعات ثبتشده در سیستم</p></div><span class="count-badge">{{ total_students }} نفر</span></div>{% if students %}<div class="table-wrap"><table><thead><tr><th>دانشآموز</th><th>سن</th><th>پایه تحصیلی</th><th>تاریخ ثبت</th><th><span class="sr-only">عملیات</span></th></tr></thead><tbody>{% for student in students %}<tr><td><div class="student-cell"><span class="student-avatar avatar-{{ forloop.counter|divisibleby:3|yesno:'pink,blue' }}">{{ student.first_name|first }}</span><div><strong>{{ student.first_name }} {{ student.last_name }}</strong><small>شناسه #{{ student.pk|stringformat:'04d' }}</small></div></div></td><td>{{ student.age }} سال</td><td><span class="grade-pill">{{ student.get_grade_display }}</span></td><td>{{ student.created_at|date:'Y/m/d' }}</td><td><div class="row-actions"><a href="{% url 'student_edit' student.pk %}" title="ویرایش">✎</a><a class="delete-action" href="{% url 'student_delete' student.pk %}" title="حذف">⌫</a></div></td></tr>{% endfor %}</tbody></table></div>{% else %}<div class="empty-state"><div class="empty-icon">♧</div><h3>هنوز دانشآموزی ثبت نشده است</h3><p>اولین پرونده را ثبت کنید تا فهرست مدرسه شکل بگیرد.</p><a class="button button-primary" href="{% url 'student_add' %}">ثبت اولین دانشآموز</a></div>{% endif %}</section>{% endblock %}
|
||||||
3
templates/students/student_confirm_delete.html
Normal file
3
templates/students/student_confirm_delete.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}حذف دانشآموز | مدرسهیار{% endblock %}
|
||||||
|
{% block content %}<section class="confirm-panel"><div class="danger-icon">!</div><span class="eyebrow">حذف پرونده</span><h1>حذف {{ object }}؟</h1><p>این اطلاعات برای همیشه حذف میشود و امکان بازگردانی آن وجود ندارد.</p><form method="post" class="form-actions">{% csrf_token %}<a class="button button-ghost" href="{% url 'dashboard' %}">انصراف</a><button class="button button-danger" type="submit">بله، حذف شود</button></form></section>{% endblock %}
|
||||||
3
templates/students/student_form.html
Normal file
3
templates/students/student_form.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{% if object %}ویرایش دانشآموز{% else %}ثبت دانشآموز{% endif %} | مدرسهیار{% endblock %}
|
||||||
|
{% block content %}<div class="page-header"><div><span class="eyebrow">اطلاعات دانشآموز</span><h1>{% if object %}ویرایش اطلاعات{% else %}ثبت دانشآموز جدید{% endif %}</h1><p>اطلاعات را با دقت وارد کنید تا پرونده دانشآموز کامل بماند.</p></div><a class="button button-ghost" href="{% url 'dashboard' %}">بازگشت به فهرست</a></div><section class="form-panel"><form method="post" class="student-form">{% csrf_token %}<div class="form-grid">{% for field in form %}<div class="field-group">{{ field.label_tag }}{{ field }}{% for error in field.errors %}<small class="error-text">{{ error }}</small>{% endfor %}</div>{% endfor %}</div><div class="form-actions"><a class="button button-ghost" href="{% url 'dashboard' %}">انصراف</a><button class="button button-primary" type="submit">{% if object %}ذخیره تغییرات{% else %}ثبت دانشآموز{% endif %} <span>←</span></button></div></form></section>{% endblock %}
|
||||||
Reference in New Issue
Block a user