This commit is contained in:
Dmitriy
2025-06-23 01:24:34 +03:00
commit 60b4e0e839
303 changed files with 35737 additions and 0 deletions

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
# Используем официальный образ Python
FROM python:3.11-slim
# Устанавливаем переменные окружения
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Устанавливаем рабочую директорию
WORKDIR /app
# Устанавливаем зависимости
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Копируем исходный код проекта
COPY . /app/
# Собираем статические файлы
RUN python manage.py collectstatic --noinput
# Открываем порт
EXPOSE 8000
# Запускаем Gunicorn
CMD ["gunicorn", "driving_school.wsgi:application", "--bind", "0.0.0.0:8000"]

50
README.md Normal file
View File

@ -0,0 +1,50 @@
# Автошкола - Система управления
Система управления автошколой с функциями для инструкторов, диспетчеров и студентов.
## Функциональность
- Управление расписанием занятий
- Система записи на занятия
- Личный кабинет инструктора
- Личный кабинет студента
- Аналитика для администраторов
- PWA поддержка
## Установка
1. Создайте виртуальное окружение:
```bash
python -m venv venv
source venv/bin/activate # для Linux/Mac
venv\Scripts\activate # для Windows
```
2. Установите зависимости:
```bash
pip install -r requirements.txt
```
3. Примените миграции:
```bash
python manage.py migrate
```
4. Создайте суперпользователя:
```bash
python manage.py createsuperuser
```
5. Запустите сервер:
```bash
python manage.py runserver
```
## Структура проекта
- `driving_school/` - основной проект
- `accounts/` - приложение для управления пользователями
- `schedule/` - приложение для управления расписанием
- `instructor/` - приложение для инструкторов
- `student/` - приложение для студентов
- `analytics/` - приложение для аналитики

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

53
accounts/forms.py Normal file
View File

@ -0,0 +1,53 @@
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from .models import Profile
from instructor.models import Instructor
class UserRegistrationForm(UserCreationForm):
email = forms.EmailField(required=True)
first_name = forms.CharField(required=True)
last_name = forms.CharField(required=True)
phone = forms.CharField(max_length=15, required=True)
address = forms.CharField(widget=forms.Textarea, required=True)
birth_date = forms.DateField(required=True, widget=forms.DateInput(attrs={'type': 'date'}))
specialization = forms.CharField(max_length=100, required=True)
experience_years = forms.IntegerField(min_value=0, required=True)
class Meta:
model = User
fields = ('username', 'email', 'first_name', 'last_name', 'password1', 'password2')
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
if commit:
user.save()
# Создаем профиль
profile = user.profile
profile.user_type = 'instructor'
profile.phone = self.cleaned_data['phone']
profile.address = self.cleaned_data['address']
profile.birth_date = self.cleaned_data['birth_date']
profile.save()
# Создаем инструктора
instructor = Instructor.objects.create(
profile=profile,
specialization=self.cleaned_data['specialization'],
experience_years=self.cleaned_data['experience_years']
)
return user
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['phone', 'address', 'birth_date']
widgets = {
'birth_date': forms.DateInput(attrs={'type': 'date'}),
}

View File

@ -0,0 +1,30 @@
# Generated by Django 5.0.2 on 2025-06-10 19:01
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_type', models.CharField(choices=[('student', 'Студент'), ('instructor', 'Инструктор'), ('dispatcher', 'Диспетчер'), ('admin', 'Администратор')], max_length=20)),
('phone', models.CharField(blank=True, max_length=15)),
('address', models.TextField(blank=True)),
('birth_date', models.DateField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

32
accounts/models.py Normal file
View File

@ -0,0 +1,32 @@
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
class Profile(models.Model):
USER_TYPE_CHOICES = (
('student', 'Студент'),
('instructor', 'Инструктор'),
('dispatcher', 'Диспетчер'),
('admin', 'Администратор'),
)
user = models.OneToOneField(User, on_delete=models.CASCADE)
user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES)
phone = models.CharField(max_length=15, blank=True)
address = models.TextField(blank=True)
birth_date = models.DateField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.username} - {self.get_user_type_display()}"
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()

15
accounts/urls.py Normal file
View File

@ -0,0 +1,15 @@
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
app_name = 'accounts'
urlpatterns = [
path('', views.home, name='home'),
path('register/', views.register, name='register'),
path('register/instructor/', views.register_instructor, name='register_instructor'),
path('profile/', views.profile, name='profile'),
path('profile/update/', views.profile_update, name='profile_update'),
path('login/', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(template_name='accounts/logout.html'), name='logout'),
]

46
accounts/views.py Normal file
View File

@ -0,0 +1,46 @@
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from .models import Profile
from .forms import UserRegistrationForm, ProfileUpdateForm
def home(request):
return render(request, 'accounts/home.html')
def register(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
user = form.save()
messages.success(request, 'Аккаунт успешно создан!')
return redirect('login')
else:
form = UserRegistrationForm()
return render(request, 'accounts/register.html', {'form': form})
def register_instructor(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
user = form.save()
messages.success(request, 'Инструктор успешно зарегистрирован!')
return redirect('login')
else:
form = UserRegistrationForm()
return render(request, 'accounts/register_instructor.html', {'form': form})
@login_required
def profile(request):
return render(request, 'accounts/profile.html')
@login_required
def profile_update(request):
if request.method == 'POST':
form = ProfileUpdateForm(request.POST, instance=request.user.profile)
if form.is_valid():
form.save()
messages.success(request, 'Профиль успешно обновлен!')
return redirect('profile')
else:
form = ProfileUpdateForm(instance=request.user.profile)
return render(request, 'accounts/profile_update.html', {'form': form})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

20
analytics/admin.py Normal file
View File

@ -0,0 +1,20 @@
from django.contrib import admin
from .models import CourseAnalytics, InstructorAnalytics, StudentAnalytics
@admin.register(CourseAnalytics)
class CourseAnalyticsAdmin(admin.ModelAdmin):
list_display = ('course', 'total_students', 'average_progress', 'completion_rate', 'revenue', 'created_at', 'updated_at')
list_filter = ('created_at', 'updated_at')
search_fields = ('course__title',)
@admin.register(InstructorAnalytics)
class InstructorAnalyticsAdmin(admin.ModelAdmin):
list_display = ('instructor', 'total_lessons', 'total_students', 'average_rating', 'completion_rate', 'created_at', 'updated_at')
list_filter = ('created_at', 'updated_at')
search_fields = ('instructor__profile__user__username', 'instructor__profile__user__email')
@admin.register(StudentAnalytics)
class StudentAnalyticsAdmin(admin.ModelAdmin):
list_display = ('student', 'total_lessons', 'attendance_rate', 'average_grade', 'created_at', 'updated_at')
list_filter = ('created_at', 'updated_at')
search_fields = ('student__profile__user__username', 'student__profile__user__email')

View File

@ -0,0 +1,56 @@
# Generated by Django 5.0.2 on 2025-06-10 20:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('instructor', '0001_initial'),
('schedule', '0003_alter_lesson_options_remove_lesson_lesson_type_and_more'),
('student', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CourseAnalytics',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('total_students', models.IntegerField(default=0)),
('average_progress', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
('completion_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
('revenue', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='schedule.course')),
],
),
migrations.CreateModel(
name='InstructorAnalytics',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('total_lessons', models.IntegerField(default=0)),
('total_students', models.IntegerField(default=0)),
('average_rating', models.DecimalField(decimal_places=2, default=5.0, max_digits=3)),
('completion_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('instructor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='instructor.instructor')),
],
),
migrations.CreateModel(
name='StudentAnalytics',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('total_lessons', models.IntegerField(default=0)),
('attendance_rate', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
('average_grade', models.DecimalField(decimal_places=2, default=0, max_digits=3)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='student.student')),
],
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.2 on 2025-06-10 20:24
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('analytics', '0001_initial'),
('course', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='courseanalytics',
name='course',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.course'),
),
]

View File

41
analytics/models.py Normal file
View File

@ -0,0 +1,41 @@
from django.db import models
from django.contrib.auth.models import User
from course.models import Course
from schedule.models import Lesson
from instructor.models import Instructor
from student.models import Student
class CourseAnalytics(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE)
total_students = models.IntegerField(default=0)
average_progress = models.DecimalField(max_digits=5, decimal_places=2, default=0)
completion_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0)
revenue = models.DecimalField(max_digits=10, decimal_places=2, default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Аналитика курса {self.course.title}"
class InstructorAnalytics(models.Model):
instructor = models.ForeignKey(Instructor, on_delete=models.CASCADE)
total_lessons = models.IntegerField(default=0)
total_students = models.IntegerField(default=0)
average_rating = models.DecimalField(max_digits=3, decimal_places=2, default=5.00)
completion_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Аналитика инструктора {self.instructor}"
class StudentAnalytics(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
total_lessons = models.IntegerField(default=0)
attendance_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0)
average_grade = models.DecimalField(max_digits=3, decimal_places=2, default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Аналитика студента {self.student}"

13
analytics/urls.py Normal file
View File

@ -0,0 +1,13 @@
from django.urls import path
from . import views
app_name = 'analytics'
urlpatterns = [
path('', views.analytics_dashboard, name='analytics_dashboard'),
path('courses/', views.course_analytics, name='course_analytics'),
path('instructors/', views.instructor_analytics, name='instructor_analytics'),
path('students/', views.student_analytics, name='student_analytics'),
path('reports/', views.generate_reports, name='generate_reports'),
path('export/', views.export_data, name='export_data'),
]

78
analytics/views.py Normal file
View File

@ -0,0 +1,78 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required, user_passes_test
from .models import CourseAnalytics, InstructorAnalytics, StudentAnalytics
from schedule.models import Course, Lesson, LessonEnrollment
from instructor.models import Instructor
from student.models import Student
import json
from django.http import HttpResponse
def is_admin(user):
return user.profile.user_type == 'admin'
@login_required
@user_passes_test(is_admin)
def analytics_dashboard(request):
course_analytics = CourseAnalytics.objects.all()
instructor_analytics = InstructorAnalytics.objects.all()
student_analytics = StudentAnalytics.objects.all()
return render(request, 'analytics/dashboard.html', {
'course_analytics': course_analytics,
'instructor_analytics': instructor_analytics,
'student_analytics': student_analytics
})
@login_required
@user_passes_test(is_admin)
def course_analytics(request):
courses = Course.objects.all()
analytics = CourseAnalytics.objects.filter(course__in=courses)
return render(request, 'analytics/course_analytics.html', {
'courses': courses,
'analytics': analytics
})
@login_required
@user_passes_test(is_admin)
def instructor_analytics(request):
instructors = Instructor.objects.all()
analytics = InstructorAnalytics.objects.filter(instructor__in=instructors)
return render(request, 'analytics/instructor_analytics.html', {
'instructors': instructors,
'analytics': analytics
})
@login_required
@user_passes_test(is_admin)
def student_analytics(request):
students = Student.objects.all()
analytics = StudentAnalytics.objects.filter(student__in=students)
return render(request, 'analytics/student_analytics.html', {
'students': students,
'analytics': analytics
})
@login_required
@user_passes_test(is_admin)
def generate_reports(request):
# Здесь будет логика генерации отчетов
return render(request, 'analytics/reports.html')
@login_required
@user_passes_test(is_admin)
def export_data(request):
data = {
'courses': list(Course.objects.values()),
'instructors': list(Instructor.objects.values()),
'students': list(Student.objects.values()),
'lessons': list(Lesson.objects.values()),
'enrollments': list(LessonEnrollment.objects.values())
}
response = HttpResponse(
json.dumps(data, ensure_ascii=False),
content_type='application/json'
)
response['Content-Disposition'] = 'attachment; filename="analytics_data.json"'
return response

1
course/__init__.py Normal file
View File

@ -0,0 +1 @@

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

9
course/admin.py Normal file
View File

@ -0,0 +1,9 @@
from django.contrib import admin
from .models import Course
@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
list_display = ('title', 'description', 'created_at', 'updated_at')
list_filter = ('created_at', 'updated_at')
search_fields = ('title', 'description')
date_hierarchy = 'created_at'

5
course/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class CourseConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'course'

11
course/forms.py Normal file
View File

@ -0,0 +1,11 @@
from django import forms
from .models import Course
class CourseForm(forms.ModelForm):
class Meta:
model = Course
fields = ['title', 'description']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
}

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.2 on 2025-06-10 19:27
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Course',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
]

View File

@ -0,0 +1 @@

Binary file not shown.

10
course/models.py Normal file
View File

@ -0,0 +1,10 @@
from django.db import models
class Course(models.Model):
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title

10
course/tests.py Normal file
View File

@ -0,0 +1,10 @@
from django.test import TestCase
from django.utils import timezone
from .models import Course
class CourseModelTest(TestCase):
def setUp(self):
self.course = Course.objects.create(
title="Тестовый курс",
description="Описание тестового курса"
)

12
course/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.urls import path
from . import views
app_name = 'course'
urlpatterns = [
path('', views.course_list, name='list'),
path('create/', views.create_course, name='create'),
path('<int:pk>/', views.course_detail, name='detail'),
path('<int:pk>/update/', views.update_course, name='update'),
path('<int:pk>/delete/', views.delete_course, name='delete'),
]

65
course/views.py Normal file
View File

@ -0,0 +1,65 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Course
from .forms import CourseForm
def course_list(request):
courses = Course.objects.all().order_by('title')
return render(request, 'course/course_list.html', {'courses': courses})
@login_required
def course_detail(request, pk):
course = get_object_or_404(Course, pk=pk)
return render(request, 'course/course_detail.html', {'course': course})
@login_required
def create_course(request):
if not hasattr(request.user, 'instructor'):
messages.error(request, 'У вас нет прав для создания курсов.')
return redirect('course:list')
if request.method == 'POST':
form = CourseForm(request.POST)
if form.is_valid():
course = form.save()
messages.success(request, 'Курс успешно создан.')
return redirect('course:list')
else:
form = CourseForm()
return render(request, 'course/course_form.html', {'form': form, 'action': 'create'})
@login_required
def update_course(request, pk):
course = get_object_or_404(Course, pk=pk)
if not hasattr(request.user, 'instructor'):
messages.error(request, 'У вас нет прав для редактирования курсов.')
return redirect('course:list')
if request.method == 'POST':
form = CourseForm(request.POST, instance=course)
if form.is_valid():
form.save()
messages.success(request, 'Курс успешно обновлен.')
return redirect('course:list')
else:
form = CourseForm(instance=course)
return render(request, 'course/course_form.html', {'form': form, 'action': 'update'})
@login_required
def delete_course(request, pk):
course = get_object_or_404(Course, pk=pk)
if not hasattr(request.user, 'instructor'):
messages.error(request, 'У вас нет прав для удаления курсов.')
return redirect('course:list')
if request.method == 'POST':
course.delete()
messages.success(request, 'Курс успешно удален.')
return redirect('course:list')
return render(request, 'course/course_confirm_delete.html', {'course': course})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

13
courses/forms.py Normal file
View File

@ -0,0 +1,13 @@
from django import forms
from .models import Course
class CourseForm(forms.ModelForm):
class Meta:
model = Course
fields = ['title', 'description', 'duration', 'price']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'duration': forms.NumberInput(attrs={'class': 'form-control'}),
'price': forms.NumberInput(attrs={'class': 'form-control'}),
}

12
courses/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.urls import path
from . import views
app_name = 'courses'
urlpatterns = [
path('', views.course_list, name='list'),
path('<int:pk>/', views.course_detail, name='detail'),
path('create/', views.create_course, name='create'),
path('<int:pk>/update/', views.update_course, name='update'),
path('<int:pk>/delete/', views.delete_course, name='delete'),
]

66
courses/views.py Normal file
View File

@ -0,0 +1,66 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Course
from .forms import CourseForm
def course_list(request):
courses = Course.objects.all()
return render(request, 'courses/course_list.html', {'courses': courses})
def course_detail(request, pk):
course = get_object_or_404(Course, pk=pk)
return render(request, 'courses/course_detail.html', {'course': course})
@login_required
def create_course(request):
if not hasattr(request.user, 'instructor'):
messages.error(request, 'У вас нет прав для создания курсов.')
return redirect('courses:list')
if request.method == 'POST':
form = CourseForm(request.POST)
if form.is_valid():
course = form.save(commit=False)
course.instructor = request.user
course.save()
messages.success(request, 'Курс успешно создан.')
return redirect('courses:list')
else:
form = CourseForm()
return render(request, 'courses/course_form.html', {'form': form, 'action': 'create'})
@login_required
def update_course(request, pk):
course = get_object_or_404(Course, pk=pk)
if not hasattr(request.user, 'instructor') or course.instructor != request.user:
messages.error(request, 'У вас нет прав для редактирования этого курса.')
return redirect('courses:list')
if request.method == 'POST':
form = CourseForm(request.POST, instance=course)
if form.is_valid():
form.save()
messages.success(request, 'Курс успешно обновлен.')
return redirect('courses:list')
else:
form = CourseForm(instance=course)
return render(request, 'courses/course_form.html', {'form': form, 'action': 'update'})
@login_required
def delete_course(request, pk):
course = get_object_or_404(Course, pk=pk)
if not hasattr(request.user, 'instructor') or course.instructor != request.user:
messages.error(request, 'У вас нет прав для удаления этого курса.')
return redirect('courses:list')
if request.method == 'POST':
course.delete()
messages.success(request, 'Курс успешно удален.')
return redirect('courses:list')
return render(request, 'courses/course_confirm_delete.html', {'course': course})

BIN
db.sqlite3 Normal file

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
driving_school/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for driving_school project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'driving_school.settings')
application = get_asgi_application()

207
driving_school/settings.py Normal file
View File

@ -0,0 +1,207 @@
"""
Django settings for driving_school project.
Generated by 'django-admin startproject' using Django 5.0.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-your-secret-key-here'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
# Third party apps
'allauth',
'allauth.account',
'crispy_forms',
'crispy_bootstrap5',
'widget_tweaks',
'pwa',
# Local apps
'course',
'accounts',
'schedule',
'instructor',
'student',
'analytics',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware',
]
ROOT_URLCONF = 'driving_school.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'driving_school.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'ru-ru'
TIME_ZONE = 'Europe/Moscow'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
MEDIA_URL = 'media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Authentication settings
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
SITE_ID = 1
LOGIN_REDIRECT_URL = 'accounts:home'
LOGOUT_REDIRECT_URL = 'accounts:home'
# Crispy Forms
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
# PWA settings
PWA_APP_NAME = 'Автошкола'
PWA_APP_DESCRIPTION = "Система управления автошколой"
PWA_APP_THEME_COLOR = '#000000'
PWA_APP_BACKGROUND_COLOR = '#ffffff'
PWA_APP_DISPLAY = 'standalone'
PWA_APP_SCOPE = '/'
PWA_APP_ORIENTATION = 'any'
PWA_APP_START_URL = '/'
PWA_APP_STATUS_BAR_COLOR = 'default'
PWA_APP_ICONS = [
{
'src': '/static/images/icons/icon-72x72.png',
'sizes': '72x72'
},
{
'src': '/static/images/icons/icon-96x96.png',
'sizes': '96x96'
},
{
'src': '/static/images/icons/icon-128x128.png',
'sizes': '128x128'
},
{
'src': '/static/images/icons/icon-144x144.png',
'sizes': '144x144'
},
{
'src': '/static/images/icons/icon-152x152.png',
'sizes': '152x152'
},
{
'src': '/static/images/icons/icon-192x192.png',
'sizes': '192x192'
},
{
'src': '/static/images/icons/icon-384x384.png',
'sizes': '384x384'
},
{
'src': '/static/images/icons/icon-512x512.png',
'sizes': '512x512'
}
]

28
driving_school/urls.py Normal file
View File

@ -0,0 +1,28 @@
"""
URL configuration for driving_school project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('accounts.urls', namespace='accounts')),
path('courses/', include('course.urls', namespace='course')),
path('schedule/', include('schedule.urls', namespace='schedule')),
path('instructor/', include('instructor.urls', namespace='instructor')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
driving_school/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for driving_school project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'driving_school.settings')
application = get_wsgi_application()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
instructor/admin.py Normal file
View File

@ -0,0 +1,15 @@
from django.contrib import admin
from .models import Instructor, InstructorSchedule
@admin.register(Instructor)
class InstructorAdmin(admin.ModelAdmin):
list_display = ('profile', 'experience_years', 'specialization', 'rating', 'is_available', 'created_at', 'updated_at')
list_filter = ('is_available', 'created_at', 'updated_at')
search_fields = ('profile__user__username', 'profile__user__email', 'specialization')
date_hierarchy = 'created_at'
@admin.register(InstructorSchedule)
class InstructorScheduleAdmin(admin.ModelAdmin):
list_display = ('instructor', 'day_of_week', 'start_time', 'end_time', 'is_available')
list_filter = ('day_of_week', 'is_available')
search_fields = ('instructor__profile__user__username', 'instructor__profile__user__email')

26
instructor/forms.py Normal file
View File

@ -0,0 +1,26 @@
from django import forms
from .models import Instructor, InstructorSchedule
class InstructorScheduleForm(forms.ModelForm):
class Meta:
model = InstructorSchedule
fields = ['day_of_week', 'start_time', 'end_time', 'is_available']
widgets = {
'start_time': forms.TimeInput(attrs={'type': 'time'}),
'end_time': forms.TimeInput(attrs={'type': 'time'}),
}
class InstructorProfileForm(forms.ModelForm):
class Meta:
model = Instructor
fields = ['experience_years', 'specialization', 'is_available']
class InstructorForm(forms.ModelForm):
class Meta:
model = Instructor
fields = ['experience_years', 'specialization', 'is_available']
widgets = {
'experience_years': forms.NumberInput(attrs={'class': 'form-control'}),
'specialization': forms.TextInput(attrs={'class': 'form-control'}),
'is_available': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}

View File

@ -0,0 +1,43 @@
# Generated by Django 5.0.2 on 2025-06-10 19:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Instructor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('experience_years', models.IntegerField(default=0)),
('specialization', models.CharField(max_length=100)),
('rating', models.DecimalField(decimal_places=2, default=5.0, max_digits=3)),
('is_available', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('profile', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='accounts.profile')),
],
),
migrations.CreateModel(
name='InstructorSchedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('day_of_week', models.IntegerField(choices=[(0, 'Понедельник'), (1, 'Вторник'), (2, 'Среда'), (3, 'Четверг'), (4, 'Пятница'), (5, 'Суббота'), (6, 'Воскресенье')])),
('start_time', models.TimeField()),
('end_time', models.TimeField()),
('is_available', models.BooleanField(default=True)),
('instructor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='instructor.instructor')),
],
options={
'unique_together': {('instructor', 'day_of_week')},
},
),
]

View File

36
instructor/models.py Normal file
View File

@ -0,0 +1,36 @@
from django.db import models
from django.contrib.auth.models import User
from accounts.models import Profile
class Instructor(models.Model):
profile = models.OneToOneField(Profile, on_delete=models.CASCADE)
experience_years = models.IntegerField(default=0)
specialization = models.CharField(max_length=100)
rating = models.DecimalField(max_digits=3, decimal_places=2, default=5.00)
is_available = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.profile.user.get_full_name()} - {self.specialization}"
class InstructorSchedule(models.Model):
instructor = models.ForeignKey(Instructor, on_delete=models.CASCADE)
day_of_week = models.IntegerField(choices=[
(0, 'Понедельник'),
(1, 'Вторник'),
(2, 'Среда'),
(3, 'Четверг'),
(4, 'Пятница'),
(5, 'Суббота'),
(6, 'Воскресенье'),
])
start_time = models.TimeField()
end_time = models.TimeField()
is_available = models.BooleanField(default=True)
class Meta:
unique_together = ('instructor', 'day_of_week')
def __str__(self):
return f"{self.instructor} - {self.get_day_of_week_display()} ({self.start_time}-{self.end_time})"

12
instructor/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.urls import path
from . import views
app_name = 'instructor'
urlpatterns = [
path('', views.instructor_list, name='list'),
path('<int:pk>/', views.instructor_detail, name='detail'),
path('<int:pk>/schedule/', views.instructor_schedule, name='schedule'),
path('profile/', views.instructor_profile, name='profile'),
]

87
instructor/views.py Normal file
View File

@ -0,0 +1,87 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Instructor, InstructorSchedule
from schedule.models import Lesson, LessonEnrollment
from .forms import InstructorScheduleForm, InstructorProfileForm, InstructorForm
from student.models import Student
@login_required
def instructor_dashboard(request):
instructor = get_object_or_404(Instructor, profile__user=request.user)
lessons = Lesson.objects.filter(instructor=instructor).order_by('start_time')
return render(request, 'instructor/dashboard.html', {
'instructor': instructor,
'lessons': lessons
})
@login_required
def instructor_schedule(request, pk):
instructor = get_object_or_404(Instructor, pk=pk)
lessons = instructor.lessons.all().order_by('date', 'start_time')
return render(request, 'instructor/instructor_schedule.html', {'instructor': instructor, 'lessons': lessons})
@login_required
def edit_schedule(request):
instructor = get_object_or_404(Instructor, profile__user=request.user)
if request.method == 'POST':
form = InstructorScheduleForm(request.POST)
if form.is_valid():
schedule = form.save(commit=False)
schedule.instructor = instructor
schedule.save()
messages.success(request, 'Расписание успешно обновлено!')
return redirect('instructor_schedule')
else:
form = InstructorScheduleForm()
return render(request, 'instructor/edit_schedule.html', {'form': form})
@login_required
def student_list(request):
instructor = get_object_or_404(Instructor, profile__user=request.user)
enrollments = LessonEnrollment.objects.filter(lesson__instructor=instructor).select_related('student')
students = {enrollment.student for enrollment in enrollments}
return render(request, 'instructor/student_list.html', {
'instructor': instructor,
'students': students
})
@login_required
def student_detail(request, pk):
instructor = get_object_or_404(Instructor, profile__user=request.user)
student = get_object_or_404(Student, pk=pk)
enrollments = LessonEnrollment.objects.filter(
lesson__instructor=instructor,
student=student
).select_related('lesson')
return render(request, 'instructor/student_detail.html', {
'instructor': instructor,
'student': student,
'enrollments': enrollments
})
@login_required
def instructor_profile(request):
if not hasattr(request.user, 'instructor'):
messages.error(request, 'У вас нет прав для просмотра профиля инструктора.')
return redirect('home')
if request.method == 'POST':
form = InstructorForm(request.POST, instance=request.user.instructor)
if form.is_valid():
form.save()
messages.success(request, 'Профиль успешно обновлен.')
return redirect('instructor:profile')
else:
form = InstructorForm(instance=request.user.instructor)
return render(request, 'instructor/instructor_profile.html', {'form': form})
def instructor_list(request):
instructors = Instructor.objects.all()
return render(request, 'instructor/instructor_list.html', {'instructors': instructors})
@login_required
def instructor_detail(request, pk):
instructor = get_object_or_404(Instructor, pk=pk)
return render(request, 'instructor/instructor_detail.html', {'instructor': instructor})

22
manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'driving_school.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
Django>=5.0,<6.0
django-allauth
django-crispy-forms
crispy-bootstrap5
django-widget-tweaks
django-pwa
gunicorn

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
schedule/admin.py Normal file
View File

@ -0,0 +1,15 @@
from django.contrib import admin
from .models import Lesson, LessonEnrollment
@admin.register(Lesson)
class LessonAdmin(admin.ModelAdmin):
list_display = ('course', 'instructor', 'date', 'start_time', 'end_time', 'location', 'max_students', 'created_at', 'updated_at')
list_filter = ('date', 'created_at', 'updated_at')
search_fields = ('course__title', 'instructor__profile__user__username', 'location')
date_hierarchy = 'date'
@admin.register(LessonEnrollment)
class LessonEnrollmentAdmin(admin.ModelAdmin):
list_display = ('lesson', 'student', 'created_at')
list_filter = ('created_at',)
search_fields = ('lesson__course__title', 'student__profile__user__username')

29
schedule/forms.py Normal file
View File

@ -0,0 +1,29 @@
from django import forms
from .models import Lesson, LessonEnrollment
class LessonForm(forms.ModelForm):
class Meta:
model = Lesson
fields = ['course', 'date', 'start_time', 'end_time', 'location', 'max_students']
widgets = {
'date': forms.DateInput(attrs={'type': 'date'}),
'start_time': forms.TimeInput(attrs={'type': 'time'}),
'end_time': forms.TimeInput(attrs={'type': 'time'}),
}
def clean(self):
cleaned_data = super().clean()
start_time = cleaned_data.get('start_time')
end_time = cleaned_data.get('end_time')
if start_time and end_time:
# Проверяем, что время окончания больше времени начала
if start_time.time() >= end_time:
raise forms.ValidationError('Время окончания должно быть позже времени начала')
return cleaned_data
class EnrollmentForm(forms.ModelForm):
class Meta:
model = LessonEnrollment
fields = ['lesson', 'student']

View File

@ -0,0 +1,60 @@
# Generated by Django 5.0.2 on 2025-06-10 19:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('course', '0001_initial'),
('instructor', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Course',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField()),
('duration', models.IntegerField(help_text='Длительность в часах')),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Lesson',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('lesson_type', models.CharField(choices=[('theory', 'Теоретическое'), ('practice', 'Практическое'), ('exam', 'Экзамен')], max_length=20)),
('start_time', models.DateTimeField()),
('end_time', models.TimeField()),
('max_students', models.PositiveIntegerField(default=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='course.course')),
('instructor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='instructor.instructor')),
],
options={
'verbose_name': 'Занятие',
'verbose_name_plural': 'Занятия',
'ordering': ['start_time'],
},
),
migrations.CreateModel(
name='LessonEnrollment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='schedule.lesson')),
],
options={
'verbose_name': 'Запись на занятие',
'verbose_name_plural': 'Записи на занятия',
},
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.0.2 on 2025-06-10 19:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('schedule', '0001_initial'),
('student', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='lessonenrollment',
name='student',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='student.student'),
),
migrations.AlterUniqueTogether(
name='lessonenrollment',
unique_together={('lesson', 'student')},
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 5.0.2 on 2025-06-10 20:05
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('schedule', '0002_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='lesson',
options={'ordering': ['date', 'start_time'], 'verbose_name': 'Занятие', 'verbose_name_plural': 'Занятия'},
),
migrations.RemoveField(
model_name='lesson',
name='lesson_type',
),
migrations.AddField(
model_name='lesson',
name='date',
field=models.DateField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name='lesson',
name='enrolled_students',
field=models.ManyToManyField(blank=True, related_name='enrolled_lessons', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='lesson',
name='location',
field=models.CharField(default='Учебный класс', max_length=200),
),
migrations.AlterField(
model_name='lesson',
name='course',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='schedule.course'),
),
migrations.AlterField(
model_name='lesson',
name='instructor',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='lesson',
name='start_time',
field=models.TimeField(),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.2 on 2025-06-10 20:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('instructor', '0001_initial'),
('schedule', '0003_alter_lesson_options_remove_lesson_lesson_type_and_more'),
]
operations = [
migrations.RemoveField(
model_name='lesson',
name='enrolled_students',
),
migrations.AlterField(
model_name='lesson',
name='instructor',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='instructor.instructor'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.2 on 2025-06-10 20:24
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('analytics', '0002_alter_courseanalytics_course'),
('course', '0001_initial'),
('schedule', '0004_remove_lesson_enrolled_students_and_more'),
]
operations = [
migrations.AlterField(
model_name='lesson',
name='course',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='course.course'),
),
migrations.DeleteModel(
name='Course',
),
]

View File

Some files were not shown because too many files have changed in this diff Show More