Compare commits

...

10 Commits

Author SHA1 Message Date
Mikan
ead0ee40ee pre add submodule 2025-05-19 18:07:12 +03:00
Mikan
d8545bb7cf auth 2025-05-19 17:22:00 +03:00
Mikan
a0a337045b common api 2025-05-19 17:10:30 +03:00
Mikan
59df2c55d9 employees api 2025-05-19 17:00:02 +03:00
Mikan
5623d01134 begin api 2025-05-19 12:54:50 +03:00
Mikan
ba4df6674b full admin 2025-05-18 23:05:48 +03:00
Mikan
f1dd7fa22a outreach 2025-05-18 22:48:39 +03:00
Mikan
c0865fdf51 education + employees 2025-05-18 22:16:31 +03:00
Mikan
30ef912c0b users 2025-05-18 21:46:02 +03:00
Mikan
d70c5f73f5 models 2025-05-18 21:29:04 +03:00
84 changed files with 3058 additions and 57 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/db.sqlite3

0
.gitmodules vendored Normal file
View File

17
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="db" uuid="018e0f6c-5916-4f25-8264-dc6301fd54fd">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:C:\Users\Mikan\PycharmProjects\tisbiProFi\db.sqlite3</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

1
.idea/vcs.xml generated
View File

@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/frontend/react-source" vcs="Git" />
</component>
</project>

View File

@@ -1,15 +0,0 @@
from django.db import models
class ContactMixin(models.Model):
first_name = models.CharField(max_length=100, verbose_name="Имя")
last_name = models.CharField(max_length=100, verbose_name="Фамилия")
middle_name = models.CharField(max_length=100, blank=True, null=True, verbose_name="Отчество")
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name="Телефон")
email = models.EmailField(blank=True, null=True, verbose_name="Почта")
def __str__(self):
return f"{self.last_name} {self.first_name}"
class Meta:
abstract = True

View File

@@ -0,0 +1,15 @@
from django.db import models
class ContactMixin(models.Model):
first_name = models.CharField(max_length=100, verbose_name="Имя")
last_name = models.CharField(max_length=100, verbose_name="Фамилия", blank=True, null=True)
middle_name = models.CharField(max_length=100, blank=True, null=True, verbose_name="Отчество")
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name="Телефон")
email = models.EmailField(blank=True, null=True, verbose_name="Почта")
def __str__(self):
return " ".join(filter(bool, [self.last_name, self.first_name]))
class Meta:
abstract = True

0
education/__init__.py Normal file
View File

58
education/admin.py Normal file
View File

@@ -0,0 +1,58 @@
from django.contrib import admin
from .models import AcademicYear, Department, DepartmentHead, Specialty, Group, Student
@admin.register(AcademicYear)
class AcademicYearAdmin(admin.ModelAdmin):
list_display = ('year',)
search_fields = ('year',)
@admin.register(Department)
class DepartmentAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
@admin.register(DepartmentHead)
class DepartmentHeadAdmin(admin.ModelAdmin):
list_display = ('department', 'head')
list_filter = ('department',)
autocomplete_fields = ('department', 'head')
search_fields = ('department__name', 'head__account__first_name', 'head__account__last_name')
@admin.register(Specialty)
class SpecialtyAdmin(admin.ModelAdmin):
list_display = ('code', 'name', 'department')
list_filter = ('department',)
search_fields = ('code', 'name', 'department__name')
@admin.register(Group)
class GroupAdmin(admin.ModelAdmin):
list_display = ('name', 'specialty', 'curator')
list_filter = ('specialty__department', 'specialty')
search_fields = ('name', 'specialty__name', 'curator__account__first_name', 'curator__account__last_name')
autocomplete_fields = ('specialty', 'curator')
@admin.register(Student)
class StudentAdmin(admin.ModelAdmin):
list_display = ('full_name', 'group', 'account')
list_filter = ('group__specialty__department', 'group__specialty', 'group')
search_fields = (
'account__first_name', 'account__last_name', 'account__middle_name',
'group__name', 'account__login'
)
autocomplete_fields = ('group', 'account')
@admin.display(description='ФИО')
def full_name(self, obj):
return str(obj)
fieldsets = (
('Основная информация', {
'fields': ('account', 'group')
}),
)

7
education/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class EducationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'education'
verbose_name = "Учебный процесс"

57
education/filters.py Normal file
View File

@@ -0,0 +1,57 @@
import django_filters
from .models import AcademicYear, Department, DepartmentHead, Specialty, Group, Student
class AcademicYearFilter(django_filters.FilterSet):
year = django_filters.CharFilter(lookup_expr="icontains")
class Meta:
model = AcademicYear
fields = ["year"]
class DepartmentFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr="icontains")
class Meta:
model = Department
fields = ["name"]
class DepartmentHeadFilter(django_filters.FilterSet):
department_name = django_filters.CharFilter(field_name="department__name", lookup_expr="icontains")
head_name = django_filters.CharFilter(field_name="head__first_name", lookup_expr="icontains")
class Meta:
model = DepartmentHead
fields = ["department_name", "head_name"]
class SpecialtyFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr="icontains")
code = django_filters.CharFilter(lookup_expr="icontains")
department_name = django_filters.CharFilter(field_name="department__name", lookup_expr="icontains")
class Meta:
model = Specialty
fields = ["name", "code", "department_name"]
class GroupFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr="icontains")
curator_name = django_filters.CharFilter(field_name="curator__first_name", lookup_expr="icontains")
specialty_name = django_filters.CharFilter(field_name="specialty__name", lookup_expr="icontains")
class Meta:
model = Group
fields = ["name", "curator_name", "specialty_name"]
class StudentFilter(django_filters.FilterSet):
first_name = django_filters.CharFilter(field_name="account__first_name", lookup_expr="icontains")
last_name = django_filters.CharFilter(field_name="account__last_name", lookup_expr="icontains")
group_name = django_filters.CharFilter(field_name="group__name", lookup_expr="icontains")
specialty_name = django_filters.CharFilter(field_name="group__specialty__name", lookup_expr="icontains")
class Meta:
model = Student
fields = ["first_name", "last_name", "group_name", "specialty_name"]

View File

@@ -0,0 +1,84 @@
# Generated by Django 4.2.21 on 2025-05-18 18:14
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AcademicYear',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('year', models.CharField(max_length=9, verbose_name='Год')),
],
options={
'verbose_name': 'Учебный год',
'verbose_name_plural': 'Учебные годы',
},
),
migrations.CreateModel(
name='Department',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название отделения')),
],
options={
'verbose_name': 'Учебное отделение',
'verbose_name_plural': 'Учебные отделения',
},
),
migrations.CreateModel(
name='DepartmentHead',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'verbose_name': 'Руководитель отделения',
'verbose_name_plural': 'Руководители отделений',
},
),
migrations.CreateModel(
name='Group',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, verbose_name='Название группы')),
],
options={
'verbose_name': 'Группа',
'verbose_name_plural': 'Группы',
},
),
migrations.CreateModel(
name='Specialty',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название специальности')),
('code', models.CharField(max_length=20, verbose_name='Код специальности')),
],
options={
'verbose_name': 'Специальность',
'verbose_name_plural': 'Специальности',
},
),
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=100, verbose_name='Имя')),
('last_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Фамилия')),
('middle_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Отчество')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Телефон')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Почта')),
],
options={
'verbose_name': 'Студент',
'verbose_name_plural': 'Студенты',
},
),
]

View File

@@ -0,0 +1,58 @@
# Generated by Django 4.2.21 on 2025-05-18 18:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('employees', '0001_initial'),
('education', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='student',
name='account',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Аккаунт'),
),
migrations.AddField(
model_name='student',
name='group',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.group', verbose_name='Группа'),
),
migrations.AddField(
model_name='specialty',
name='department',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.department', verbose_name='Учебное отделение'),
),
migrations.AddField(
model_name='group',
name='curator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='employees.employee', verbose_name='Куратор'),
),
migrations.AddField(
model_name='group',
name='specialty',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.specialty', verbose_name='Специальность'),
),
migrations.AddField(
model_name='departmenthead',
name='department',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.department', verbose_name='Учебное отделение'),
),
migrations.AddField(
model_name='departmenthead',
name='head',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='employees.employee', verbose_name='Руководитель'),
),
migrations.AlterUniqueTogether(
name='departmenthead',
unique_together={('department', 'head')},
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 4.2.21 on 2025-05-18 18:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('education', '0002_initial'),
]
operations = [
migrations.RemoveField(
model_name='student',
name='email',
),
migrations.RemoveField(
model_name='student',
name='first_name',
),
migrations.RemoveField(
model_name='student',
name='last_name',
),
migrations.RemoveField(
model_name='student',
name='middle_name',
),
migrations.RemoveField(
model_name='student',
name='phone',
),
]

View File

135
education/models.py Normal file
View File

@@ -0,0 +1,135 @@
from django.db import models
from core.models import ContactMixin # Предполагается, что ContactMixin находится в приложении core
from employees.models import Employee # Предполагается, что модель Employee находится в приложении employees
from users.models import Account # Предполагается, что модель Account находится в приложении users
## Учебный процесс (название приложения: education)
class AcademicYear(models.Model):
year = models.CharField(max_length=9, verbose_name="Год") # Например, "2023-2024"
def __str__(self):
return self.year
class Meta:
verbose_name = "Учебный год"
verbose_name_plural = "Учебные годы"
ordering = ['-year']
class Department(models.Model):
name = models.CharField(max_length=100, verbose_name="Название отделения")
def __str__(self):
return self.name
class Meta:
verbose_name = "Учебное отделение"
verbose_name_plural = "Учебные отделения"
ordering = ["name"]
class DepartmentHead(models.Model):
department = models.ForeignKey(
Department, on_delete=models.CASCADE, verbose_name="Учебное отделение"
)
head = models.OneToOneField(
Employee, on_delete=models.CASCADE, verbose_name="Руководитель"
)
def __str__(self):
return f"{self.head} - {self.department}"
class Meta:
verbose_name = "Руководитель отделения"
verbose_name_plural = "Руководители отделений"
unique_together = ("department", "head")
ordering = ["department", "head"]
class Specialty(models.Model):
name = models.CharField(max_length=200, verbose_name="Название специальности")
code = models.CharField(max_length=20, verbose_name="Код специальности")
department = models.ForeignKey(
Department, on_delete=models.CASCADE, verbose_name="Учебное отделение"
)
def __str__(self):
return f"{self.code} - {self.name}"
class Meta:
verbose_name = "Специальность"
verbose_name_plural = "Специальности"
ordering = ["code"]
class Group(models.Model):
name = models.CharField(max_length=20, verbose_name="Название группы")
curator = models.ForeignKey(
Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Куратор"
)
specialty = models.ForeignKey(
Specialty, on_delete=models.CASCADE, verbose_name="Специальность"
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Студенческая группа"
verbose_name_plural = "Студенческие группы"
ordering = ["name", "specialty"]
class Student(models.Model):
group = models.ForeignKey(
Group, on_delete=models.CASCADE, verbose_name="Группа"
)
account = models.OneToOneField(
Account, on_delete=models.CASCADE, unique=True, verbose_name="Аккаунт"
)
@property
def first_name(self):
return self.account.first_name
@first_name.setter
def first_name(self, value):
self.account.first_name = value
@property
def last_name(self):
return self.account.last_name
@last_name.setter
def last_name(self, value):
self.account.last_name = value
@property
def middle_name(self):
return self.account.middle_name
@middle_name.setter
def middle_name(self, value):
self.account.middle_name = value
@property
def phone(self):
return self.account.phone
@phone.setter
def phone(self, value):
self.account.phone = value
@property
def email(self):
return self.account.email
@email.setter
def email(self, value):
self.account.email = value
def __str__(self):
return f"{self.last_name} {self.first_name} - {self.group}"
class Meta:
verbose_name = "Студент"
verbose_name_plural = "Студенты"
ordering = ["group", "account"]

75
education/serializers.py Normal file
View File

@@ -0,0 +1,75 @@
from rest_framework import serializers
from .models import AcademicYear, Department, DepartmentHead, Specialty, Group, Student
class AcademicYearSerializer(serializers.ModelSerializer):
class Meta:
model = AcademicYear
fields = "__all__"
class StudyDepartmentSerializer(serializers.ModelSerializer):
class Meta:
model = Department
fields = "__all__"
class StudyDepartmentHeadSerializer(serializers.ModelSerializer):
head_name = serializers.SerializerMethodField()
class Meta:
model = DepartmentHead
fields = ["id", "department", "head", "head_name"]
def get_head_name(self, obj):
return f"{obj.head.first_name} {obj.head.last_name}"
class SpecialtySerializer(serializers.ModelSerializer):
class Meta:
model = Specialty
fields = "__all__"
class GroupSerializer(serializers.ModelSerializer):
curator_name = serializers.SerializerMethodField()
class Meta:
model = Group
fields = ["id", "name", "curator", "specialty", "curator_name"]
def get_curator_name(self, obj):
if obj.curator:
return f"{obj.curator.first_name} {obj.curator.last_name}"
return None
class StudentSerializer(serializers.ModelSerializer):
first_name = serializers.CharField(source="account.first_name")
last_name = serializers.CharField(source="account.last_name")
middle_name = serializers.CharField(source="account.middle_name")
phone = serializers.CharField(source="account.phone")
email = serializers.CharField(source="account.email")
class Meta:
model = Student
fields = [
"id",
"group",
"first_name",
"last_name",
"middle_name",
"phone",
"email",
]
def update(self, instance, validated_data):
account_data = validated_data.pop("account", {})
instance.account.first_name = account_data.get("first_name", instance.account.first_name)
instance.account.last_name = account_data.get("last_name", instance.account.last_name)
instance.account.middle_name = account_data.get("middle_name", instance.account.middle_name)
instance.account.phone = account_data.get("phone", instance.account.phone)
instance.account.email = account_data.get("email", instance.account.email)
instance.account.save()
return super().update(instance, validated_data)

3
education/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

22
education/urls.py Normal file
View File

@@ -0,0 +1,22 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
AcademicYearViewSet,
DepartmentViewSet,
DepartmentHeadViewSet,
SpecialtyViewSet,
GroupViewSet,
StudentViewSet,
)
router = DefaultRouter()
router.register(r"academic-years", AcademicYearViewSet, basename="academic-year")
router.register(r"departments", DepartmentViewSet, basename="department")
router.register(r"department-heads", DepartmentHeadViewSet, basename="department-head")
router.register(r"specialties", SpecialtyViewSet, basename="specialty")
router.register(r"groups", GroupViewSet, basename="group")
router.register(r"students", StudentViewSet, basename="student")
urlpatterns = [
path("", include(router.urls)),
]

73
education/views.py Normal file
View File

@@ -0,0 +1,73 @@
from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from .models import AcademicYear, Department, DepartmentHead, Specialty, Group, Student
from .serializers import (
AcademicYearSerializer,
StudyDepartmentSerializer,
StudyDepartmentHeadSerializer,
SpecialtySerializer,
GroupSerializer,
StudentSerializer,
)
from .filters import (
AcademicYearFilter,
DepartmentFilter,
DepartmentHeadFilter,
SpecialtyFilter,
GroupFilter,
StudentFilter,
)
class AcademicYearViewSet(viewsets.ModelViewSet):
queryset = AcademicYear.objects.all()
serializer_class = AcademicYearSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = AcademicYearFilter
search_fields = ["year"]
ordering_fields = ["year"]
class DepartmentViewSet(viewsets.ModelViewSet):
queryset = Department.objects.all()
serializer_class = StudyDepartmentSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = DepartmentFilter
search_fields = ["name"]
ordering_fields = ["name"]
class DepartmentHeadViewSet(viewsets.ModelViewSet):
queryset = DepartmentHead.objects.all()
serializer_class = StudyDepartmentHeadSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = DepartmentHeadFilter
search_fields = ["department__name", "head__first_name"]
ordering_fields = ["department", "head"]
class SpecialtyViewSet(viewsets.ModelViewSet):
queryset = Specialty.objects.all()
serializer_class = SpecialtySerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = SpecialtyFilter
search_fields = ["name", "code", "department__name"]
ordering_fields = ["name", "code"]
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = GroupFilter
search_fields = ["name", "curator__first_name", "specialty__name"]
ordering_fields = ["name", "specialty"]
class StudentViewSet(viewsets.ModelViewSet):
queryset = Student.objects.all()
serializer_class = StudentSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = StudentFilter
search_fields = ["account__first_name", "account__last_name", "group__name"]
ordering_fields = ["group", "account__last_name"]

0
employees/__init__.py Normal file
View File

35
employees/admin.py Normal file
View File

@@ -0,0 +1,35 @@
from django.contrib import admin
from .models import Position, Department, Employee
@admin.register(Position)
class PositionAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
@admin.register(Department)
class DepartmentAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
@admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin):
list_display = ('full_name', 'position', 'department', 'account')
list_filter = ('department', 'position')
search_fields = (
'account__first_name', 'account__last_name', 'account__middle_name',
'account__phone', 'account__email', 'position__name', 'department__name'
)
autocomplete_fields = ('department', 'position', 'account')
fieldsets = (
('Основная информация', {
'fields': ('account', 'department', 'position')
}),
)
@admin.display(description='ФИО')
def full_name(self, obj):
return str(obj)

8
employees/apps.py Normal file
View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class EmployeesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'employees'
verbose_name = "Структура колледжа"

12
employees/filters.py Normal file
View File

@@ -0,0 +1,12 @@
import django_filters
from .models import Employee
class EmployeeFilter(django_filters.FilterSet):
first_name = django_filters.CharFilter(field_name="account__first_name", lookup_expr="icontains")
last_name = django_filters.CharFilter(field_name="account__last_name", lookup_expr="icontains")
department_name = django_filters.CharFilter(field_name="department__name", lookup_expr="icontains")
position_name = django_filters.CharFilter(field_name="position__name", lookup_expr="icontains")
class Meta:
model = Employee
fields = ["first_name", "last_name", "department_name", "position_name"]

View File

@@ -0,0 +1,51 @@
# Generated by Django 4.2.21 on 2025-05-18 18:14
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Department',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название подразделения')),
],
options={
'verbose_name': 'Подразделение',
'verbose_name_plural': 'Подразделения',
},
),
migrations.CreateModel(
name='Employee',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100, verbose_name='Имя')),
('last_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Фамилия')),
('middle_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Отчество')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Телефон')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Почта')),
],
options={
'verbose_name': 'Сотрудник',
'verbose_name_plural': 'Сотрудники',
},
),
migrations.CreateModel(
name='Position',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название должности')),
],
options={
'verbose_name': 'Должность',
'verbose_name_plural': 'Должности',
},
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 4.2.21 on 2025-05-18 18:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('employees', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='employee',
name='account',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Аккаунт'),
),
migrations.AddField(
model_name='employee',
name='department',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='employees.department', verbose_name='Подразделение'),
),
migrations.AddField(
model_name='employee',
name='position',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='employees.position', verbose_name='Должность'),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 4.2.21 on 2025-05-18 18:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('employees', '0002_initial'),
]
operations = [
migrations.RemoveField(
model_name='employee',
name='email',
),
migrations.RemoveField(
model_name='employee',
name='first_name',
),
migrations.RemoveField(
model_name='employee',
name='last_name',
),
migrations.RemoveField(
model_name='employee',
name='middle_name',
),
migrations.RemoveField(
model_name='employee',
name='phone',
),
]

View File

89
employees/models.py Normal file
View File

@@ -0,0 +1,89 @@
from django.db import models
from core.models import ContactMixin # Предполагается, что ContactMixin находится в приложении core
## Учет сотрудников (название приложения: employees)
class Position(models.Model):
name = models.CharField(max_length=100, verbose_name="Название должности")
def __str__(self):
return self.name
class Meta:
verbose_name = "Должность"
verbose_name_plural = "Должности"
ordering = ["name"]
class Department(models.Model):
name = models.CharField(max_length=100, verbose_name="Название подразделения")
def __str__(self):
return self.name
class Meta:
verbose_name = "Подразделение"
verbose_name_plural = "Подразделения"
ordering = ["name"]
class Employee(models.Model):
department = models.ForeignKey(
Department, on_delete=models.CASCADE, verbose_name="Подразделение"
)
position = models.ForeignKey(
Position, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Должность"
)
account = models.OneToOneField(
"users.Account", on_delete=models.CASCADE, unique=True, verbose_name="Аккаунт"
)
@property
def first_name(self):
return self.account.first_name
@first_name.setter
def first_name(self, value):
self.account.first_name = value
@property
def last_name(self):
return self.account.last_name
@last_name.setter
def last_name(self, value):
self.account.last_name = value
@property
def middle_name(self):
return self.account.middle_name
@middle_name.setter
def middle_name(self, value):
self.account.middle_name = value
@property
def phone(self):
return self.account.phone
@phone.setter
def phone(self, value):
self.account.phone = value
@property
def email(self):
return self.account.email
@email.setter
def email(self, value):
self.account.email = value
def __str__(self):
full_name = " ".join(filter(bool, [self.last_name, self.first_name]))
return f"{full_name} - {self.position}"
class Meta:
verbose_name = "Сотрудник"
verbose_name_plural = "Сотрудники"
ordering = ["department", "position", "account"]

44
employees/serializers.py Normal file
View File

@@ -0,0 +1,44 @@
from rest_framework import serializers
from .models import Position, Department, Employee
class PositionSerializer(serializers.ModelSerializer):
class Meta:
model = Position
fields = "__all__"
class DepartmentSerializer(serializers.ModelSerializer):
class Meta:
model = Department
fields = "__all__"
class EmployeeSerializer(serializers.ModelSerializer):
first_name = serializers.CharField(source="account.first_name")
last_name = serializers.CharField(source="account.last_name")
middle_name = serializers.CharField(source="account.middle_name", allow_null=True)
phone = serializers.CharField(source="account.phone", allow_null=True)
email = serializers.CharField(source="account.email")
class Meta:
model = Employee
fields = [
"id",
"department",
"position",
"first_name",
"last_name",
"middle_name",
"phone",
"email",
]
def update(self, instance, validated_data):
account_data = validated_data.pop("account", {})
instance.account.first_name = account_data.get("first_name", instance.account.first_name)
instance.account.last_name = account_data.get("last_name", instance.account.last_name)
instance.account.middle_name = account_data.get("middle_name", instance.account.middle_name)
instance.account.phone = account_data.get("phone", instance.account.phone)
instance.account.email = account_data.get("email", instance.account.email)
instance.account.save()
return super().update(instance, validated_data)

3
employees/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
employees/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import PositionViewSet, DepartmentViewSet, EmployeeViewSet
router = DefaultRouter()
router.register(r"positions", PositionViewSet, basename="position")
router.register(r"departments", DepartmentViewSet, basename="department")
router.register(r"employees", EmployeeViewSet, basename="employee")
urlpatterns = [
path("", include(router.urls)),
]

30
employees/views.py Normal file
View File

@@ -0,0 +1,30 @@
from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from .models import Position, Department, Employee
from .serializers import PositionSerializer, DepartmentSerializer, EmployeeSerializer
from .filters import EmployeeFilter # Фильтры будут добавлены позже
class PositionViewSet(viewsets.ModelViewSet):
queryset = Position.objects.all()
serializer_class = PositionSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name"]
ordering_fields = ["name"]
class DepartmentViewSet(viewsets.ModelViewSet):
queryset = Department.objects.all()
serializer_class = DepartmentSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name"]
ordering_fields = ["name"]
class EmployeeViewSet(viewsets.ModelViewSet):
queryset = Employee.objects.all()
serializer_class = EmployeeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = EmployeeFilter
search_fields = ["account__first_name", "account__last_name", "position__name", "department__name"]
ordering_fields = ["department", "position", "account__last_name"]

0
events/__init__.py Normal file
View File

117
events/admin.py Normal file
View File

@@ -0,0 +1,117 @@
from django.contrib import admin
from .models import (
EventStatus, ActivityType, Event, EventOrganizer,
EventDetail, EventParticipant, MethodicalFile, RelevantMethodicalFile
)
@admin.register(EventStatus)
class EventStatusAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
@admin.register(ActivityType)
class ActivityTypeAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
class EventOrganizerInline(admin.TabularInline):
model = EventOrganizer
extra = 1
autocomplete_fields = ('employee',)
class EventDetailInline(admin.TabularInline):
model = EventDetail
extra = 1
autocomplete_fields = ('activity_type',)
class EventParticipantInline(admin.TabularInline):
model = EventParticipant
extra = 1
autocomplete_fields = ('student',)
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
list_display = ('name', 'date', 'location', 'status', 'academic_year')
list_filter = ('status', 'academic_year', 'date')
search_fields = ('name', 'location')
inlines = [EventOrganizerInline, EventDetailInline, EventParticipantInline]
autocomplete_fields = ('academic_year', 'status')
fieldsets = (
('Основная информация', {
'fields': ('name', 'academic_year', 'status', 'date', 'location')
}),
('Дополнительно', {
'fields': ('description',)
}),
)
@admin.register(EventOrganizer)
class EventOrganizerAdmin(admin.ModelAdmin):
list_display = ('event', 'employee')
list_filter = ('event__academic_year',)
search_fields = ('employee__account__first_name', 'employee__account__last_name', 'event__name')
autocomplete_fields = ('employee', 'event')
@admin.register(EventDetail)
class EventDetailAdmin(admin.ModelAdmin):
list_display = ('event', 'activity_type')
list_filter = ('activity_type',)
search_fields = ('event__name', 'activity_type__name')
autocomplete_fields = ('event', 'activity_type')
@admin.register(EventParticipant)
class EventParticipantAdmin(admin.ModelAdmin):
list_display = ('student_full_name', 'event', 'points')
list_filter = ('event__academic_year', 'event')
search_fields = (
'student__account__first_name', 'student__account__last_name',
'event__name'
)
autocomplete_fields = ('student', 'event')
@admin.display(description='ФИО студента')
def student_full_name(self, obj):
return str(obj.student)
class RelevantMethodicalFileInline(admin.TabularInline):
model = RelevantMethodicalFile
extra = 1
autocomplete_fields = ('activity_type',)
@admin.register(MethodicalFile)
class MethodicalFileAdmin(admin.ModelAdmin):
list_display = ('name', 'specialty', 'upload_date', 'employee')
list_filter = ('specialty', 'upload_date')
search_fields = ('name', 'specialty__name', 'employee__account__first_name', 'employee__account__last_name')
autocomplete_fields = ('specialty', 'employee')
fieldsets = (
('Основная информация', {
'fields': ('name', 'specialty', 'file_path')
}),
('Дополнительно', {
'fields': ('employee',)
}),
)
inlines = [RelevantMethodicalFileInline]
@admin.register(RelevantMethodicalFile)
class RelevantMethodicalFileAdmin(admin.ModelAdmin):
list_display = ('methodical_file', 'activity_type')
list_filter = ('activity_type',)
search_fields = ('methodical_file__name', 'activity_type__name')
autocomplete_fields = ('methodical_file', 'activity_type')

7
events/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class EventsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'events'
verbose_name = 'Мероприятия'

34
events/filters.py Normal file
View File

@@ -0,0 +1,34 @@
import django_filters
from .models import Event, EventParticipant, MethodicalFile
class EventFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr="icontains")
location = django_filters.CharFilter(lookup_expr="icontains")
date_after = django_filters.DateFilter(field_name="date", lookup_expr="gte")
date_before = django_filters.DateFilter(field_name="date", lookup_expr="lte")
class Meta:
model = Event
fields = ["name", "location", "date_after", "date_before"]
class EventParticipantFilter(django_filters.FilterSet):
student_name = django_filters.CharFilter(
field_name="student__account__first_name", lookup_expr="icontains"
)
event_name = django_filters.CharFilter(field_name="event__name", lookup_expr="icontains")
class Meta:
model = EventParticipant
fields = ["student_name", "event_name", "points"]
class MethodicalFileFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr="icontains")
specialty_name = django_filters.CharFilter(
field_name="specialty__name", lookup_expr="icontains"
)
class Meta:
model = MethodicalFile
fields = ["name", "specialty_name", "upload_date"]

View File

@@ -0,0 +1,125 @@
# Generated by Django 4.2.21 on 2025-05-18 18:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('employees', '0001_initial'),
('education', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ActivityType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название вида деятельности')),
],
options={
'verbose_name': 'Деятельность мероприятия',
'verbose_name_plural': 'Деятельности мероприятий',
},
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Место проведения')),
('date', models.DateField(verbose_name='Дата проведения')),
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
('academic_year', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.academicyear', verbose_name='Учебный год')),
],
options={
'verbose_name': 'Мероприятие',
'verbose_name_plural': 'Мероприятия',
},
),
migrations.CreateModel(
name='EventStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название статуса')),
],
options={
'verbose_name': 'Статус проведения',
'verbose_name_plural': 'Статусы проведения',
},
),
migrations.CreateModel(
name='MethodicalFile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('file_path', models.FileField(max_length=500, upload_to='', verbose_name='Файл')),
('upload_date', models.DateField(auto_now_add=True, verbose_name='Дата добавления')),
('employee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='employees.employee', verbose_name='Сотрудник')),
('specialty', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.specialty', verbose_name='Специальность')),
],
options={
'verbose_name': 'Методический файл',
'verbose_name_plural': 'Методические файлы',
},
),
migrations.CreateModel(
name='RelevantMethodicalFile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('activity_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.activitytype', verbose_name='Вид деятельности')),
('methodical_file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.methodicalfile', verbose_name='Методический файл')),
],
options={
'verbose_name': 'Релевантный методический файл',
'verbose_name_plural': 'Релевантные методические файлы',
},
),
migrations.CreateModel(
name='EventParticipant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('points', models.PositiveIntegerField(blank=True, default=0, verbose_name='Баллы за участие')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event', verbose_name='Мероприятие')),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.student', verbose_name='Студент')),
],
options={
'verbose_name': 'Участник мероприятия',
'verbose_name_plural': 'Участники мероприятий',
},
),
migrations.AddField(
model_name='event',
name='status',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='events.eventstatus', verbose_name='Статус мероприятия'),
),
migrations.CreateModel(
name='EventOrganizer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='employees.employee', verbose_name='Сотрудник-организатор')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event', verbose_name='Мероприятие')),
],
options={
'verbose_name': 'Организатор мероприятия',
'verbose_name_plural': 'Организаторы мероприятий',
'unique_together': {('employee', 'event')},
},
),
migrations.CreateModel(
name='EventDetail',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('activity_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.activitytype', verbose_name='Вид деятельности')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.event', verbose_name='Мероприятие')),
],
options={
'verbose_name': 'Деталь мероприятия',
'verbose_name_plural': 'Детали мероприятий',
'unique_together': {('activity_type', 'event')},
},
),
]

View File

135
events/models.py Normal file
View File

@@ -0,0 +1,135 @@
from django.db import models
from education.models import AcademicYear, Specialty, Student # Предполагается, что модели находятся в приложении education
from employees.models import Employee # Предполагается, что модель Employee находится в приложении employees
## Учет мероприятий (название приложения: events)
class EventStatus(models.Model):
name = models.CharField(max_length=100, verbose_name="Название статуса")
def __str__(self):
return self.name
class Meta:
verbose_name = "Статус проведения"
verbose_name_plural = "Статусы проведения"
ordering = ["name"]
class ActivityType(models.Model):
name = models.CharField(max_length=100, verbose_name="Название вида деятельности")
def __str__(self):
return self.name
class Meta:
verbose_name = "Деятельность мероприятия"
verbose_name_plural = "Деятельности мероприятий"
ordering = ["name"]
class Event(models.Model):
academic_year = models.ForeignKey(
AcademicYear, on_delete=models.CASCADE, verbose_name="Учебный год"
)
status = models.ForeignKey(
EventStatus, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Статус мероприятия"
)
name = models.CharField(max_length=200, verbose_name="Название")
location = models.CharField(max_length=255, blank=True, null=True, verbose_name="Место проведения")
date = models.DateField(verbose_name="Дата проведения")
description = models.TextField(blank=True, null=True, verbose_name="Описание")
def __str__(self):
return f"{self.name} ({self.date})"
class Meta:
verbose_name = "Мероприятие"
verbose_name_plural = "Мероприятия"
ordering = ["date", "name"]
class EventOrganizer(models.Model):
employee = models.ForeignKey(
Employee, on_delete=models.CASCADE, verbose_name="Сотрудник-организатор"
)
event = models.ForeignKey(
Event, on_delete=models.CASCADE, verbose_name="Мероприятие"
)
def __str__(self):
return f"{self.employee} - {self.event}"
class Meta:
verbose_name = "Организатор мероприятия"
verbose_name_plural = "Организаторы мероприятий"
unique_together = ("employee", "event")
ordering = ["employee", "event"]
class EventDetail(models.Model):
activity_type = models.ForeignKey(
ActivityType, on_delete=models.CASCADE, verbose_name="Вид деятельности"
)
event = models.ForeignKey(
Event, on_delete=models.CASCADE, verbose_name="Мероприятие"
)
def __str__(self):
return f"{self.activity_type} - {self.event}"
class Meta:
verbose_name = "Деталь мероприятия"
verbose_name_plural = "Детали мероприятий"
unique_together = ("activity_type", "event")
ordering = ["activity_type", "event"]
class EventParticipant(models.Model):
student = models.ForeignKey(
Student, on_delete=models.CASCADE, verbose_name="Студент"
)
event = models.ForeignKey(
Event, on_delete=models.CASCADE, verbose_name="Мероприятие"
)
points = models.PositiveIntegerField(blank=True, default=0, verbose_name="Баллы за участие")
def __str__(self):
return f"{self.student} - {self.event}"
class Meta:
verbose_name = "Участник мероприятия"
verbose_name_plural = "Участники мероприятий"
ordering = ["event", "student"]
class MethodicalFile(models.Model):
specialty = models.ForeignKey(
Specialty, on_delete=models.CASCADE, verbose_name="Специальность"
)
name = models.CharField(max_length=200, verbose_name="Название")
file_path = models.FileField(max_length=500, verbose_name="Файл")
upload_date = models.DateField(auto_now_add=True, verbose_name="Дата добавления")
employee = models.ForeignKey(
Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Сотрудник"
)
def __str__(self):
return f"{self.name} ({self.specialty})"
class Meta:
verbose_name = "Методический файл"
verbose_name_plural = "Методические файлы"
ordering = ["specialty", "name", "upload_date", "employee"]
class RelevantMethodicalFile(models.Model):
methodical_file = models.ForeignKey(
MethodicalFile, on_delete=models.CASCADE, verbose_name="Методический файл"
)
activity_type = models.ForeignKey(
ActivityType, on_delete=models.CASCADE, verbose_name="Вид деятельности"
)
def __str__(self):
return f"{self.methodical_file} - {self.activity_type}"
class Meta:
verbose_name = "Релевантный методический файл"
verbose_name_plural = "Релевантные методические файлы"
ordering = ["methodical_file", "activity_type"]

68
events/serializers.py Normal file
View File

@@ -0,0 +1,68 @@
from rest_framework import serializers
from .models import (
EventStatus,
ActivityType,
Event,
EventOrganizer,
EventDetail,
EventParticipant,
MethodicalFile,
RelevantMethodicalFile,
)
class EventStatusSerializer(serializers.ModelSerializer):
class Meta:
model = EventStatus
fields = "__all__"
class ActivityTypeSerializer(serializers.ModelSerializer):
class Meta:
model = ActivityType
fields = "__all__"
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = "__all__"
class EventOrganizerSerializer(serializers.ModelSerializer):
employee_name = serializers.SerializerMethodField()
class Meta:
model = EventOrganizer
fields = ["id", "employee", "event", "employee_name"]
def get_employee_name(self, obj):
return f"{obj.employee.account.first_name} {obj.employee.account.last_name}"
class EventDetailSerializer(serializers.ModelSerializer):
class Meta:
model = EventDetail
fields = "__all__"
class EventParticipantSerializer(serializers.ModelSerializer):
student_name = serializers.SerializerMethodField()
class Meta:
model = EventParticipant
fields = ["id", "student", "event", "points", "student_name"]
def get_student_name(self, obj):
return f"{obj.student.account.last_name} {obj.student.account.first_name}"
class MethodicalFileSerializer(serializers.ModelSerializer):
class Meta:
model = MethodicalFile
fields = "__all__"
class RelevantMethodicalFileSerializer(serializers.ModelSerializer):
class Meta:
model = RelevantMethodicalFile
fields = "__all__"

3
events/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

26
events/urls.py Normal file
View File

@@ -0,0 +1,26 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
EventStatusViewSet,
ActivityTypeViewSet,
EventViewSet,
EventOrganizerViewSet,
EventDetailViewSet,
EventParticipantViewSet,
MethodicalFileViewSet,
RelevantMethodicalFileViewSet,
)
router = DefaultRouter()
router.register(r"event-statuses", EventStatusViewSet, basename="event-status")
router.register(r"activity-types", ActivityTypeViewSet, basename="activity-type")
router.register(r"events", EventViewSet, basename="event")
router.register(r"event-organizers", EventOrganizerViewSet, basename="event-organizer")
router.register(r"event-details", EventDetailViewSet, basename="event-detail")
router.register(r"event-participants", EventParticipantViewSet, basename="event-participant")
router.register(r"methodical-files", MethodicalFileViewSet, basename="methodical-file")
router.register(r"relevant-methodical-files", RelevantMethodicalFileViewSet, basename="relevant-methodical-file")
urlpatterns = [
path("", include(router.urls)),
]

97
events/views.py Normal file
View File

@@ -0,0 +1,97 @@
from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from .models import (
EventStatus,
ActivityType,
Event,
EventOrganizer,
EventDetail,
EventParticipant,
MethodicalFile,
RelevantMethodicalFile,
)
from .serializers import (
EventStatusSerializer,
ActivityTypeSerializer,
EventSerializer,
EventOrganizerSerializer,
EventDetailSerializer,
EventParticipantSerializer,
MethodicalFileSerializer,
RelevantMethodicalFileSerializer,
)
from .filters import (
EventFilter,
EventParticipantFilter,
MethodicalFileFilter,
) # Фильтры будут добавлены позже
class EventStatusViewSet(viewsets.ModelViewSet):
queryset = EventStatus.objects.all()
serializer_class = EventStatusSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name"]
ordering_fields = ["name"]
class ActivityTypeViewSet(viewsets.ModelViewSet):
queryset = ActivityType.objects.all()
serializer_class = ActivityTypeSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name"]
ordering_fields = ["name"]
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = EventFilter
search_fields = ["name", "location", "description"]
ordering_fields = ["date", "name"]
class EventOrganizerViewSet(viewsets.ModelViewSet):
queryset = EventOrganizer.objects.all()
serializer_class = EventOrganizerSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["employee", "event"]
search_fields = ["employee__account__first_name", "employee__account__last_name"]
ordering_fields = ["employee", "event"]
class EventDetailViewSet(viewsets.ModelViewSet):
queryset = EventDetail.objects.all()
serializer_class = EventDetailSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["activity_type", "event"]
search_fields = ["activity_type__name"]
ordering_fields = ["activity_type", "event"]
class EventParticipantViewSet(viewsets.ModelViewSet):
queryset = EventParticipant.objects.all()
serializer_class = EventParticipantSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = EventParticipantFilter
search_fields = ["student__account__first_name", "student__account__last_name"]
ordering_fields = ["event", "student", "points"]
class MethodicalFileViewSet(viewsets.ModelViewSet):
queryset = MethodicalFile.objects.all()
serializer_class = MethodicalFileSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = MethodicalFileFilter
search_fields = ["name", "specialty__name"]
ordering_fields = ["specialty", "name", "upload_date"]
class RelevantMethodicalFileViewSet(viewsets.ModelViewSet):
queryset = RelevantMethodicalFile.objects.all()
serializer_class = RelevantMethodicalFileSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["methodical_file", "activity_type"]
search_fields = ["methodical_file__name", "activity_type__name"]
ordering_fields = ["methodical_file", "activity_type"]

0
frontend/__init__.py Normal file
View File

3
frontend/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
frontend/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class FrontendConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'frontend'

View File

3
frontend/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
frontend/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
frontend/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
outreach/__init__.py Normal file
View File

90
outreach/admin.py Normal file
View File

@@ -0,0 +1,90 @@
from django.contrib import admin
from .models import (
InstitutionType, Representative, School, Partner,
PartnerRepresentative, SchoolResponsibility
)
@admin.register(InstitutionType)
class InstitutionTypeAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
@admin.register(Representative)
class RepresentativeAdmin(admin.ModelAdmin):
list_display = ('full_name', 'position')
search_fields = ('first_name', 'last_name', 'middle_name', 'phone', 'email', 'position')
@admin.display(description='ФИО')
def full_name(self, obj):
return str(obj)
fieldsets = (
('Основная информация', {
'fields': ('first_name', 'last_name', 'middle_name', 'position')
}),
('Контактные данные', {
'fields': ('phone', 'email')
}),
)
class PartnerRepresentativeInline(admin.TabularInline):
model = PartnerRepresentative
extra = 1
autocomplete_fields = ('representative',)
@admin.register(Partner)
class PartnerAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
inlines = [PartnerRepresentativeInline]
fieldsets = (
('Основная информация', {
'fields': ('name', 'description')
}),
)
@admin.register(PartnerRepresentative)
class PartnerRepresentativeAdmin(admin.ModelAdmin):
list_display = ('representative', 'partner')
search_fields = ('representative__first_name', 'representative__last_name', 'partner__name')
autocomplete_fields = ('representative', 'partner')
@admin.register(School)
class SchoolAdmin(admin.ModelAdmin):
list_display = ('name', 'institution_type', 'director')
list_filter = ('institution_type',)
search_fields = ('name', 'address', 'director__first_name', 'director__last_name')
autocomplete_fields = ('director', 'institution_type')
fieldsets = (
('Основная информация', {
'fields': ('name', 'institution_type', 'address')
}),
('Директор', {
'fields': ('director',)
}),
)
@admin.register(SchoolResponsibility)
class SchoolResponsibilityAdmin(admin.ModelAdmin):
list_display = ('academic_year', 'school', 'responsible')
list_filter = ('academic_year', 'school__institution_type')
search_fields = (
'school__name', 'responsible__first_name',
'responsible__last_name', 'academic_year__year'
)
autocomplete_fields = ('school', 'responsible', 'academic_year')
fieldsets = (
('Ответственность', {
'fields': ('academic_year', 'school', 'responsible')
}),
)

7
outreach/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class OutreachConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'outreach'
verbose_name = 'Профориентационная работа'

25
outreach/filters.py Normal file
View File

@@ -0,0 +1,25 @@
import django_filters
from .models import School, SchoolResponsibility
class SchoolFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr="icontains")
address = django_filters.CharFilter(lookup_expr="icontains")
institution_type_name = django_filters.CharFilter(
field_name="institution_type__name", lookup_expr="icontains"
)
class Meta:
model = School
fields = ["name", "address", "institution_type_name"]
class SchoolResponsibilityFilter(django_filters.FilterSet):
academic_year = django_filters.CharFilter(field_name="academic_year__year", lookup_expr="exact")
school_name = django_filters.CharFilter(field_name="school__name", lookup_expr="icontains")
responsible_name = django_filters.CharFilter(
field_name="responsible__account__first_name", lookup_expr="icontains"
)
class Meta:
model = SchoolResponsibility
fields = ["academic_year", "school_name", "responsible_name"]

View File

@@ -0,0 +1,96 @@
# Generated by Django 4.2.21 on 2025-05-18 18:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('employees', '0001_initial'),
('education', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='InstitutionType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name='Название вида учебного заведения')),
],
options={
'verbose_name': 'Вид учебного заведения',
'verbose_name_plural': 'Виды учебных заведений',
},
),
migrations.CreateModel(
name='Partner',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
],
options={
'verbose_name': 'Партнер',
'verbose_name_plural': 'Партнеры',
},
),
migrations.CreateModel(
name='Representative',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=100, verbose_name='Имя')),
('last_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Фамилия')),
('middle_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Отчество')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Телефон')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Почта')),
('position', models.CharField(blank=True, max_length=100, null=True, verbose_name='Должность')),
],
options={
'verbose_name': 'Представитель',
'verbose_name_plural': 'Представители',
},
),
migrations.CreateModel(
name='School',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('address', models.CharField(max_length=300, verbose_name='Адрес')),
('director', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='outreach.representative', verbose_name='Директор')),
('institution_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='outreach.institutiontype', verbose_name='Вид учебного заведения')),
],
options={
'verbose_name': 'Школа',
'verbose_name_plural': 'Школы',
},
),
migrations.CreateModel(
name='SchoolResponsibility',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('academic_year', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.academicyear', verbose_name='Учебный год')),
('responsible', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='employees.employee', verbose_name='Ответственный сотрудник')),
('school', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='outreach.school', verbose_name='Школа')),
],
options={
'verbose_name': 'Ответственный за школу',
'verbose_name_plural': 'Ответственные за школы',
},
),
migrations.CreateModel(
name='PartnerRepresentative',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('partner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='outreach.partner', verbose_name='Партнер')),
('representative', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='outreach.representative', verbose_name='Представитель')),
],
options={
'verbose_name': 'Представитель партнера',
'verbose_name_plural': 'Представители партнеров',
'unique_together': {('representative', 'partner')},
},
),
]

View File

95
outreach/models.py Normal file
View File

@@ -0,0 +1,95 @@
from django.db import models
from core.models import ContactMixin # Предполагается, что ContactMixin находится в приложении core
from education.models import AcademicYear # Предполагается, что AcademicYear находится в приложении education
from employees.models import Employee # Предполагается, что модель Employee находится в приложении employees
## Профориентационная деятельность (название приложения: outreach)
class InstitutionType(models.Model):
name = models.CharField(max_length=50, verbose_name="Название вида учебного заведения")
def __str__(self):
return self.name
class Meta:
verbose_name = "Вид учебного заведения"
verbose_name_plural = "Виды учебных заведений"
ordering = ["name"]
class Representative(ContactMixin):
position = models.CharField(max_length=100, blank=True, null=True, verbose_name="Должность")
def __str__(self):
return f"{super().__str__()}"+(" - "+self.position if self.position else "")
class Meta:
verbose_name = "Представитель"
verbose_name_plural = "Представители"
ordering = ["position"]
class School(models.Model):
director = models.ForeignKey(
Representative, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Директор"
)
institution_type = models.ForeignKey(
InstitutionType, on_delete=models.CASCADE, verbose_name="Вид учебного заведения"
)
name = models.CharField(max_length=200, verbose_name="Название")
address = models.CharField(max_length=300, verbose_name="Адрес")
def __str__(self):
return f"{self.name} ({self.institution_type})"
class Meta:
verbose_name = "Школа"
verbose_name_plural = "Школы"
ordering = ["institution_type", "name", "address"]
class Partner(models.Model):
name = models.CharField(max_length=200, verbose_name="Название")
description = models.TextField(blank=True, null=True, verbose_name="Описание")
def __str__(self):
return self.name
class Meta:
verbose_name = "Партнер"
verbose_name_plural = "Партнеры"
ordering = ["name"]
class PartnerRepresentative(models.Model):
representative = models.OneToOneField(
Representative, on_delete=models.CASCADE, verbose_name="Представитель"
)
partner = models.ForeignKey(
Partner, on_delete=models.CASCADE, verbose_name="Партнер"
)
def __str__(self):
return f"{self.representative} - {self.partner}"
class Meta:
verbose_name = "Представитель партнера"
verbose_name_plural = "Представители партнеров"
unique_together = ("representative", "partner")
ordering = ["partner", "representative"]
class SchoolResponsibility(models.Model):
academic_year = models.ForeignKey(
AcademicYear, on_delete=models.CASCADE, verbose_name="Учебный год"
)
responsible = models.ForeignKey(
Employee, on_delete=models.CASCADE, verbose_name="Ответственный сотрудник"
)
school = models.ForeignKey(
School, on_delete=models.CASCADE, verbose_name="Школа"
)
def __str__(self):
return f"{self.responsible} - {self.school} ({self.academic_year})"
class Meta:
verbose_name = "Ответственный за школу"
verbose_name_plural = "Ответственные за школы"
ordering = ["academic_year", "responsible", "school"]

67
outreach/serializers.py Normal file
View File

@@ -0,0 +1,67 @@
from rest_framework import serializers
from .models import (
InstitutionType,
Representative,
School,
Partner,
PartnerRepresentative,
SchoolResponsibility,
)
class InstitutionTypeSerializer(serializers.ModelSerializer):
class Meta:
model = InstitutionType
fields = "__all__"
class RepresentativeSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = Representative
fields = ["id", "first_name", "last_name", "middle_name", "phone", "email", "position", "full_name"]
def get_full_name(self, obj):
return f"{obj.first_name} {obj.last_name}" + (f" {obj.middle_name}" if obj.middle_name else "")
class SchoolSerializer(serializers.ModelSerializer):
director_name = serializers.SerializerMethodField()
class Meta:
model = School
fields = ["id", "director", "institution_type", "name", "address", "director_name"]
def get_director_name(self, obj):
if obj.director:
return f"{obj.director.first_name} {obj.director.last_name}"
return None
class PartnerSerializer(serializers.ModelSerializer):
class Meta:
model = Partner
fields = "__all__"
class PartnerRepresentativeSerializer(serializers.ModelSerializer):
representative_name = serializers.SerializerMethodField()
class Meta:
model = PartnerRepresentative
fields = ["id", "representative", "partner", "representative_name"]
def get_representative_name(self, obj):
return f"{obj.representative.first_name} {obj.representative.last_name}"
class SchoolResponsibilitySerializer(serializers.ModelSerializer):
responsible_name = serializers.SerializerMethodField()
class Meta:
model = SchoolResponsibility
fields = ["id", "academic_year", "responsible", "school", "responsible_name"]
def get_responsible_name(self, obj):
return f"{obj.responsible.account.first_name} {obj.responsible.account.last_name}"

3
outreach/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

22
outreach/urls.py Normal file
View File

@@ -0,0 +1,22 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
InstitutionTypeViewSet,
RepresentativeViewSet,
SchoolViewSet,
PartnerViewSet,
PartnerRepresentativeViewSet,
SchoolResponsibilityViewSet,
)
router = DefaultRouter()
router.register(r"institution-types", InstitutionTypeViewSet, basename="institution-type")
router.register(r"representatives", RepresentativeViewSet, basename="representative")
router.register(r"schools", SchoolViewSet, basename="school")
router.register(r"partners", PartnerViewSet, basename="partner")
router.register(r"partner-representatives", PartnerRepresentativeViewSet, basename="partner-representative")
router.register(r"school-responsibilities", SchoolResponsibilityViewSet, basename="school-responsibility")
urlpatterns = [
path("", include(router.urls)),
]

73
outreach/views.py Normal file
View File

@@ -0,0 +1,73 @@
from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from .models import (
InstitutionType,
Representative,
School,
Partner,
PartnerRepresentative,
SchoolResponsibility,
)
from .serializers import (
InstitutionTypeSerializer,
RepresentativeSerializer,
SchoolSerializer,
PartnerSerializer,
PartnerRepresentativeSerializer,
SchoolResponsibilitySerializer,
)
from .filters import (
SchoolFilter,
SchoolResponsibilityFilter,
) # Фильтры будут добавлены позже
class InstitutionTypeViewSet(viewsets.ModelViewSet):
queryset = InstitutionType.objects.all()
serializer_class = InstitutionTypeSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name"]
ordering_fields = ["name"]
class RepresentativeViewSet(viewsets.ModelViewSet):
queryset = Representative.objects.all()
serializer_class = RepresentativeSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["first_name", "last_name", "position"]
ordering_fields = ["position"]
class SchoolViewSet(viewsets.ModelViewSet):
queryset = School.objects.all()
serializer_class = SchoolSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = SchoolFilter
search_fields = ["name", "address"]
ordering_fields = ["institution_type", "name"]
class PartnerViewSet(viewsets.ModelViewSet):
queryset = Partner.objects.all()
serializer_class = PartnerSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name", "description"]
ordering_fields = ["name"]
class PartnerRepresentativeViewSet(viewsets.ModelViewSet):
queryset = PartnerRepresentative.objects.all()
serializer_class = PartnerRepresentativeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["representative", "partner"]
search_fields = ["representative__first_name", "partner__name"]
ordering_fields = ["partner", "representative"]
class SchoolResponsibilityViewSet(viewsets.ModelViewSet):
queryset = SchoolResponsibility.objects.all()
serializer_class = SchoolResponsibilitySerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = SchoolResponsibilityFilter
search_fields = ["school__name", "responsible__account__first_name"]
ordering_fields = ["academic_year", "responsible", "school"]

View File

@@ -1 +1,13 @@
django~=4.2.21
django-cors-headers
djangorestframework
drf-yasg
Pillow
requests
django-autoslug
transliterate
pytest-django
pytest
django-filter
djangorestframework-simplejwt
djoser

0
surveys/__init__.py Normal file
View File

104
surveys/admin.py Normal file
View File

@@ -0,0 +1,104 @@
from django.contrib import admin
from .models import (
Survey, ScoreVariable, Statement, QuestionPair, AgreementQuestion,
EducationalInstitution, Profession, Specialty, ProfessionSpecialtyLink, ProfessionTestResult
)
class ScoreVariableInline(admin.TabularInline):
model = ScoreVariable
extra = 1
class QuestionPairInline(admin.TabularInline):
model = QuestionPair
extra = 1
autocomplete_fields = ('statement_a', 'statement_b')
class AgreementQuestionInline(admin.TabularInline):
model = AgreementQuestion
extra = 1
autocomplete_fields = ('score_variable',)
@admin.register(Survey)
class SurveyAdmin(admin.ModelAdmin):
list_display = ('title',)
search_fields = ('title',)
inlines = [ScoreVariableInline, QuestionPairInline, AgreementQuestionInline]
@admin.register(ScoreVariable)
class ScoreVariableAdmin(admin.ModelAdmin):
list_display = ('title', 'survey', 'name')
list_filter = ('survey',)
search_fields = ('title', 'name', 'description')
autocomplete_fields = ('survey',)
@admin.register(Statement)
class StatementAdmin(admin.ModelAdmin):
list_display = ('statement', 'score_variable')
list_filter = ('score_variable__survey',)
search_fields = ('statement', 'score_variable__title')
autocomplete_fields = ('score_variable',)
@admin.register(QuestionPair)
class QuestionPairAdmin(admin.ModelAdmin):
list_display = ('survey', 'statement_a', 'statement_b')
list_filter = ('survey',)
search_fields = ('statement_a__statement', 'statement_b__statement')
autocomplete_fields = ('survey', 'statement_a', 'statement_b')
@admin.register(AgreementQuestion)
class AgreementQuestionAdmin(admin.ModelAdmin):
list_display = ('question', 'survey', 'score_variable')
list_filter = ('survey',)
search_fields = ('question', 'score_variable__title')
autocomplete_fields = ('survey', 'score_variable')
@admin.register(EducationalInstitution)
class EducationalInstitutionAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
@admin.register(Profession)
class ProfessionAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name', 'description')
class ProfessionSpecialtyLinkInline(admin.TabularInline):
model = ProfessionSpecialtyLink
extra = 1
autocomplete_fields = ('specialty',)
@admin.register(Specialty)
class SpecialtyAdmin(admin.ModelAdmin):
list_display = ('name', 'institution', 'link')
list_filter = ('institution',)
search_fields = ('name', 'institution__name')
inlines = [ProfessionSpecialtyLinkInline]
autocomplete_fields = ('institution',)
@admin.register(ProfessionSpecialtyLink)
class ProfessionSpecialtyLinkAdmin(admin.ModelAdmin):
list_display = ('profession', 'specialty')
list_filter = ('profession',)
search_fields = ('profession__name', 'specialty__name')
autocomplete_fields = ('profession', 'specialty')
@admin.register(ProfessionTestResult)
class ProfessionTestResultAdmin(admin.ModelAdmin):
list_display = ('profession', 'survey_score_variable')
list_filter = ('profession',)
search_fields = ('profession__name', 'survey_score_variable__title')
autocomplete_fields = ('profession', 'survey_score_variable')

7
surveys/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class SurveysConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'surveys'
verbose_name = "Анкетирование и Тестирование"

19
surveys/filters.py Normal file
View File

@@ -0,0 +1,19 @@
import django_filters
from .models import QuestionPair, Specialty
class QuestionPairFilter(django_filters.FilterSet):
survey_title = django_filters.CharFilter(field_name="survey__title", lookup_expr="icontains")
statement_a_text = django_filters.CharFilter(field_name="statement_a__statement", lookup_expr="icontains")
statement_b_text = django_filters.CharFilter(field_name="statement_b__statement", lookup_expr="icontains")
class Meta:
model = QuestionPair
fields = ["survey_title", "statement_a_text", "statement_b_text"]
class SpecialtyFilter(django_filters.FilterSet):
institution_name = django_filters.CharFilter(field_name="institution__name", lookup_expr="icontains")
class Meta:
model = Specialty
fields = ["name", "institution_name"]

View File

@@ -0,0 +1,143 @@
# Generated by Django 4.2.21 on 2025-05-18 18:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='EducationalInstitution',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256, verbose_name='Название учреждения')),
],
options={
'verbose_name': 'Образовательное учреждение',
'verbose_name_plural': 'Образовательные учреждения',
},
),
migrations.CreateModel(
name='Profession',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256, verbose_name='Название профессии')),
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
],
options={
'verbose_name': 'Профессия',
'verbose_name_plural': 'Профессии',
},
),
migrations.CreateModel(
name='ScoreVariable',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, verbose_name='Имя переменной')),
('title', models.CharField(max_length=70, verbose_name='Название переменной')),
('description', models.TextField(verbose_name='Описание')),
],
options={
'verbose_name': 'Переменная оценки',
'verbose_name_plural': 'Переменные оценки',
},
),
migrations.CreateModel(
name='Survey',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=70, verbose_name='Название опроса')),
],
options={
'verbose_name': 'Опрос',
'verbose_name_plural': 'Опросы',
},
),
migrations.CreateModel(
name='Statement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('statement', models.CharField(max_length=256, verbose_name='Утверждение')),
('score_variable', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='surveys.scorevariable', verbose_name='Переменная оценки')),
],
options={
'verbose_name': 'Утверждение',
'verbose_name_plural': 'Утверждения',
},
),
migrations.CreateModel(
name='Specialty',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256, verbose_name='Название специальности')),
('link', models.URLField(blank=True, null=True, verbose_name='Ссылка')),
('institution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.educationalinstitution', verbose_name='Образовательное учреждение')),
],
options={
'verbose_name': 'Специальность',
'verbose_name_plural': 'Специальности',
},
),
migrations.AddField(
model_name='scorevariable',
name='survey',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.survey', verbose_name='Опрос'),
),
migrations.CreateModel(
name='QuestionPair',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('statement_a', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='pair_a', to='surveys.statement', verbose_name='Утверждение A')),
('statement_b', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='pair_b', to='surveys.statement', verbose_name='Утверждение B')),
('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.survey', verbose_name='Опрос')),
],
options={
'verbose_name': 'Пара вопросов',
'verbose_name_plural': 'Пары вопросов',
},
),
migrations.CreateModel(
name='ProfessionTestResult',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('profession', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.profession', verbose_name='Профессия')),
('survey_score_variable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.scorevariable', verbose_name='Переменная оценки')),
],
options={
'verbose_name': 'Результат теста по профессии',
'verbose_name_plural': 'Результаты тестов по профессиям',
},
),
migrations.CreateModel(
name='AgreementQuestion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.CharField(max_length=256, verbose_name='Вопрос')),
('score_variable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.scorevariable', verbose_name='Переменная оценки')),
('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.survey', verbose_name='Опрос')),
],
options={
'verbose_name': 'Вопрос на согласие',
'verbose_name_plural': 'Вопросы на согласие',
},
),
migrations.CreateModel(
name='ProfessionSpecialtyLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('profession', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.profession', verbose_name='Профессия')),
('specialty', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.specialty', verbose_name='Специальность')),
],
options={
'verbose_name': 'Связь между профессией и специальностью',
'verbose_name_plural': 'Связи между профессиями и специальностями',
'unique_together': {('profession', 'specialty')},
},
),
]

View File

161
surveys/models.py Normal file
View File

@@ -0,0 +1,161 @@
from django.db import models
## Модуль Анкетирование (название приложения: surveys)
class Survey(models.Model):
title = models.CharField(max_length=70, verbose_name="Название опроса")
def __str__(self):
return self.title
class Meta:
verbose_name = "Опрос"
verbose_name_plural = "Опросы"
ordering = ["title"]
class ScoreVariable(models.Model):
name = models.CharField(max_length=30, verbose_name="Имя переменной")
title = models.CharField(max_length=70, verbose_name="Название переменной")
description = models.TextField(verbose_name="Описание")
survey = models.ForeignKey(
Survey, on_delete=models.CASCADE, verbose_name="Опрос"
)
def __str__(self):
return f"{self.title} ({self.survey})"
class Meta:
verbose_name = "Переменная оценки"
verbose_name_plural = "Переменные оценки"
ordering = ["survey", "name", "title"]
class Statement(models.Model):
statement = models.CharField(max_length=256, verbose_name="Утверждение")
score_variable = models.ForeignKey(
ScoreVariable, on_delete=models.SET_NULL, blank=True, null=True, verbose_name="Переменная оценки"
)
def __str__(self):
return self.statement
class Meta:
verbose_name = "Утверждение"
verbose_name_plural = "Утверждения"
ordering = ["statement", "score_variable"]
class QuestionPair(models.Model):
survey = models.ForeignKey(
Survey, on_delete=models.CASCADE, verbose_name="Опрос"
)
statement_a = models.OneToOneField(
Statement, on_delete=models.CASCADE, related_name="pair_a", verbose_name="Утверждение A"
)
statement_b = models.OneToOneField(
Statement, on_delete=models.CASCADE, related_name="pair_b", verbose_name="Утверждение B"
)
def __str__(self):
return f"Пара: {self.statement_a} - {self.statement_b}"
class Meta:
verbose_name = "Пара вопросов"
verbose_name_plural = "Пары вопросов"
ordering = ["survey", "statement_a", "statement_b"]
class AgreementQuestion(models.Model):
survey = models.ForeignKey(
Survey, on_delete=models.CASCADE, verbose_name="Опрос"
)
question = models.CharField(max_length=256, verbose_name="Вопрос")
score_variable = models.ForeignKey(
ScoreVariable, on_delete=models.CASCADE, verbose_name="Переменная оценки"
)
def __str__(self):
return self.question
class Meta:
verbose_name = "Вопрос на согласие"
verbose_name_plural = "Вопросы на согласие"
ordering = ["survey", "question"]
class EducationalInstitution(models.Model):
name = models.CharField(max_length=256, verbose_name="Название учреждения")
def __str__(self):
return self.name
class Meta:
verbose_name = "Образовательное учреждение"
verbose_name_plural = "Образовательные учреждения"
ordering = ["name"]
class Profession(models.Model):
name = models.CharField(max_length=256, verbose_name="Название профессии")
description = models.TextField(blank=True, null=True, verbose_name="Описание")
def __str__(self):
return self.name
class Meta:
verbose_name = "Профессия"
verbose_name_plural = "Профессии"
ordering = ["name"]
class Specialty(models.Model):
name = models.CharField(max_length=256, verbose_name="Название специальности")
institution = models.ForeignKey(
EducationalInstitution, on_delete=models.CASCADE, verbose_name="Образовательное учреждение"
)
link = models.URLField(verbose_name="Ссылка", null=True, blank=True)
def __str__(self):
return f"{self.name} ({self.institution})"
class Meta:
verbose_name = "Специальность"
verbose_name_plural = "Специальности"
ordering = ["name", "institution"]
class ProfessionSpecialtyLink(models.Model):
profession = models.ForeignKey(
Profession, on_delete=models.CASCADE, verbose_name="Профессия"
)
specialty = models.ForeignKey(
Specialty, on_delete=models.CASCADE, verbose_name="Специальность"
)
def __str__(self):
return f"{self.profession} - {self.specialty}"
class Meta:
verbose_name = "Связь между профессией и специальностью"
verbose_name_plural = "Связи между профессиями и специальностями"
unique_together = ("profession", "specialty")
ordering = ["profession", "specialty"]
class ProfessionTestResult(models.Model):
profession = models.ForeignKey(
Profession, on_delete=models.CASCADE, verbose_name="Профессия"
)
survey_score_variable = models.ForeignKey(
ScoreVariable, on_delete=models.CASCADE, verbose_name="Переменная оценки"
)
def __str__(self):
return f"{self.profession} - {self.survey_score_variable}"
class Meta:
verbose_name = "Результат теста по профессии"
verbose_name_plural = "Результаты тестов по профессиям"
ordering = ["profession", "survey_score_variable"]

81
surveys/serializers.py Normal file
View File

@@ -0,0 +1,81 @@
from rest_framework import serializers
from .models import (
Survey,
ScoreVariable,
Statement,
QuestionPair,
AgreementQuestion,
EducationalInstitution,
Profession,
Specialty,
ProfessionSpecialtyLink,
ProfessionTestResult,
)
class SurveySerializer(serializers.ModelSerializer):
class Meta:
model = Survey
fields = "__all__"
class ScoreVariableSerializer(serializers.ModelSerializer):
class Meta:
model = ScoreVariable
fields = "__all__"
class StatementSerializer(serializers.ModelSerializer):
class Meta:
model = Statement
fields = "__all__"
class QuestionPairSerializer(serializers.ModelSerializer):
statement_a_text = serializers.SerializerMethodField()
statement_b_text = serializers.SerializerMethodField()
class Meta:
model = QuestionPair
fields = ["id", "survey", "statement_a", "statement_b", "statement_a_text", "statement_b_text"]
def get_statement_a_text(self, obj):
return obj.statement_a.statement
def get_statement_b_text(self, obj):
return obj.statement_b.statement
class AgreementQuestionSerializer(serializers.ModelSerializer):
class Meta:
model = AgreementQuestion
fields = "__all__"
class EducationalInstitutionSerializer(serializers.ModelSerializer):
class Meta:
model = EducationalInstitution
fields = "__all__"
class ProfessionSerializer(serializers.ModelSerializer):
class Meta:
model = Profession
fields = "__all__"
class SurveySpecialtySerializer(serializers.ModelSerializer):
class Meta:
model = Specialty
fields = "__all__"
class ProfessionSpecialtyLinkSerializer(serializers.ModelSerializer):
class Meta:
model = ProfessionSpecialtyLink
fields = "__all__"
class ProfessionTestResultSerializer(serializers.ModelSerializer):
class Meta:
model = ProfessionTestResult
fields = "__all__"

3
surveys/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

30
surveys/urls.py Normal file
View File

@@ -0,0 +1,30 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
SurveyViewSet,
ScoreVariableViewSet,
StatementViewSet,
QuestionPairViewSet,
AgreementQuestionViewSet,
EducationalInstitutionViewSet,
ProfessionViewSet,
SpecialtyViewSet,
ProfessionSpecialtyLinkViewSet,
ProfessionTestResultViewSet,
)
router = DefaultRouter()
router.register(r"surveys", SurveyViewSet, basename="survey")
router.register(r"score-variables", ScoreVariableViewSet, basename="score-variable")
router.register(r"statements", StatementViewSet, basename="statement")
router.register(r"question-pairs", QuestionPairViewSet, basename="question-pair")
router.register(r"agreement-questions", AgreementQuestionViewSet, basename="agreement-question")
router.register(r"institutions", EducationalInstitutionViewSet, basename="institution")
router.register(r"professions", ProfessionViewSet, basename="profession")
router.register(r"specialties", SpecialtyViewSet, basename="specialty")
router.register(r"profession-specialty-links", ProfessionSpecialtyLinkViewSet, basename="profession-specialty-link")
router.register(r"profession-test-results", ProfessionTestResultViewSet, basename="profession-test-result")
urlpatterns = [
path("", include(router.urls)),
]

117
surveys/views.py Normal file
View File

@@ -0,0 +1,117 @@
from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from .models import (
Survey,
ScoreVariable,
Statement,
QuestionPair,
AgreementQuestion,
EducationalInstitution,
Profession,
Specialty,
ProfessionSpecialtyLink,
ProfessionTestResult,
)
from .serializers import (
SurveySerializer,
ScoreVariableSerializer,
StatementSerializer,
QuestionPairSerializer,
AgreementQuestionSerializer,
EducationalInstitutionSerializer,
ProfessionSerializer,
SurveySpecialtySerializer,
ProfessionSpecialtyLinkSerializer,
ProfessionTestResultSerializer,
)
from .filters import (
QuestionPairFilter,
SpecialtyFilter,
) # Фильтры будут добавлены позже
class SurveyViewSet(viewsets.ModelViewSet):
queryset = Survey.objects.all()
serializer_class = SurveySerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["title"]
ordering_fields = ["title"]
class ScoreVariableViewSet(viewsets.ModelViewSet):
queryset = ScoreVariable.objects.all()
serializer_class = ScoreVariableSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["survey"]
search_fields = ["name", "title"]
ordering_fields = ["survey", "name"]
class StatementViewSet(viewsets.ModelViewSet):
queryset = Statement.objects.all()
serializer_class = StatementSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["score_variable"]
search_fields = ["statement"]
ordering_fields = ["statement"]
class QuestionPairViewSet(viewsets.ModelViewSet):
queryset = QuestionPair.objects.all()
serializer_class = QuestionPairSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = QuestionPairFilter
search_fields = ["statement_a__statement", "statement_b__statement"]
ordering_fields = ["survey", "statement_a", "statement_b"]
class AgreementQuestionViewSet(viewsets.ModelViewSet):
queryset = AgreementQuestion.objects.all()
serializer_class = AgreementQuestionSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["survey", "score_variable"]
search_fields = ["question"]
ordering_fields = ["survey", "question"]
class EducationalInstitutionViewSet(viewsets.ModelViewSet):
queryset = EducationalInstitution.objects.all()
serializer_class = EducationalInstitutionSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name"]
ordering_fields = ["name"]
class ProfessionViewSet(viewsets.ModelViewSet):
queryset = Profession.objects.all()
serializer_class = ProfessionSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name"]
ordering_fields = ["name"]
class SpecialtyViewSet(viewsets.ModelViewSet):
queryset = Specialty.objects.all()
serializer_class = SurveySpecialtySerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = SpecialtyFilter
search_fields = ["name", "institution__name"]
ordering_fields = ["name", "institution"]
class ProfessionSpecialtyLinkViewSet(viewsets.ModelViewSet):
queryset = ProfessionSpecialtyLink.objects.all()
serializer_class = ProfessionSpecialtyLinkSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["profession", "specialty"]
search_fields = ["profession__name", "specialty__name"]
ordering_fields = ["profession", "specialty"]
class ProfessionTestResultViewSet(viewsets.ModelViewSet):
queryset = ProfessionTestResult.objects.all()
serializer_class = ProfessionTestResultSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ["profession", "survey_score_variable"]
search_fields = ["profession__name", "survey_score_variable__title"]
ordering_fields = ["profession", "survey_score_variable"]

View File

@@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -39,13 +40,26 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'corsheaders',
'rest_framework',
"django_filters",
'drf_yasg',
'djoser',
'rest_framework_simplejwt',
"core.apps.CoreConfig",
'users.apps.UsersConfig',
"employees.apps.EmployeesConfig",
"education.apps.EducationConfig",
"events.apps.EventsConfig",
"outreach.apps.OutreachConfig",
"surveys.apps.SurveysConfig",
"frontend.apps.FrontendConfig"
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
@@ -143,4 +157,49 @@ MEDIA_ROOT = BASE_DIR / 'media'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = "users.UserAccount"
AUTH_USER_MODEL = "users.Account"
CORS_ORIGIN_WHITELIST = ['http://localhost:8000',
'http://localhost:3000']
CSRF_TRUSTED_ORIGINS = ['http://localhost:8000',
'http://localhost:3000']
CORS_ALLOW_CREDENTIALS = True
REST_FRAMEWORK = {
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_VERSION": "v1",
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication", # Сессионная аутентификация
),
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
}
DJOSER = {
"LOGIN_FIELD": "login", # Используем поле login вместо username
"USER_CREATE_PASSWORD_RETYPE": True,
"SET_PASSWORD_RETYPE": True,
"PASSWORD_RESET_CONFIRM_URL": "password/reset/confirm/{uid}/{token}",
"USERNAME_RESET_CONFIRM_URL": "username/reset/confirm/{uid}/{token}",
"ACTIVATION_URL": "activate/{uid}/{token}",
"SEND_ACTIVATION_EMAIL": False, # Отключаем отправку email, если не нужно
"SERIALIZERS": {
"user_create": "users.serializers.AccountSerializer",
"user": "users.serializers.AccountSerializer",
},
}

View File

@@ -14,9 +14,42 @@ 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.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path
from django.urls import path, include
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
schema_view = get_schema_view(
openapi.Info(
title="ProFi API",
default_version='v1',
description="API for ProFi (v1)",
contact=openapi.Contact(email="support@novelplus.ru"),
),
public=True,
permission_classes=[permissions.AllowAny],
)
urlpatterns = [
path('admin/', admin.site.urls),
]
path('api/v1/education/', include("education.urls")),
path('api/v1/employees/', include("employees.urls")),
path('api/v1/events/', include("events.urls")),
path('api/v1/outreach/', include("outreach.urls")),
path('api/v1/surveys/', include("surveys.urls")),
path('api/v1/users/', include("users.urls")),
path('docs/swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('docs/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('docs/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -1,3 +1,61 @@
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.utils.translation import gettext_lazy as _
from .models import Account, Role
class AccountChangeForm(UserChangeForm):
class Meta:
model = Account
fields = '__all__'
class AccountCreationForm(UserCreationForm):
class Meta:
model = Account
fields = ('login',)
@admin.register(Account)
class AccountAdmin(BaseUserAdmin):
form = AccountChangeForm
add_form = AccountCreationForm
list_display = ('login', 'full_name', 'role', 'is_staff', 'is_active')
list_filter = ('is_staff', 'is_active', 'role')
search_fields = ('login', 'first_name', 'last_name', 'middle_name', 'phone', 'email')
ordering = ('login',)
filter_horizontal = ('groups', 'user_permissions')
fieldsets = (
(None, {'fields': ('login', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'middle_name', 'phone', 'email')}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
}),
(_('Role'), {'fields': ('role',)}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('login', 'password1', 'password2'),
}),
)
@admin.display(description=_('ФИО'))
def full_name(self, obj):
return str(obj)
@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
# Register your models here.

View File

@@ -4,3 +4,4 @@ from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'
verbose_name = "Пользователи"

View File

@@ -1,4 +1,4 @@
# Generated by Django 4.2.21 on 2025-05-14 16:31
# Generated by Django 4.2.21 on 2025-05-18 18:21
from django.db import migrations, models
import django.db.models.deletion
@@ -17,35 +17,35 @@ class Migration(migrations.Migration):
name='Role',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True, verbose_name='Название роли')),
('name', models.CharField(max_length=100, verbose_name='Название роли')),
],
options={
'verbose_name': 'Роль',
'verbose_name_plural': 'Роли',
'verbose_name': 'Роль пользователя',
'verbose_name_plural': 'Роли пользователей',
},
),
migrations.CreateModel(
name='UserAccount',
name='Account',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(max_length=100, verbose_name='Имя')),
('last_name', models.CharField(max_length=100, verbose_name='Фамилия')),
('last_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Фамилия')),
('middle_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='Отчество')),
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Телефон')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Почта')),
('username', models.CharField(max_length=150, unique=True, verbose_name='Имя пользователя')),
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
('login', models.CharField(max_length=150, unique=True, verbose_name='Логин')),
('is_staff', models.BooleanField(default=False, verbose_name='Сотрудник')),
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
('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')),
('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.role', verbose_name='Роль')),
('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.role', verbose_name='Роль пользователя')),
('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')),
],
options={
'verbose_name': 'Пользователь',
'verbose_name_plural': 'Пользователи',
'verbose_name': 'Аккаунт',
'verbose_name_plural': 'Аккаунты',
},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.21 on 2025-05-18 18:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='account',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.role', unique=True, verbose_name='Роль пользователя'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.21 on 2025-05-18 18:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0002_alter_account_role'),
]
operations = [
migrations.AlterField(
model_name='account',
name='role',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.role', verbose_name='Роль пользователя'),
),
]

View File

@@ -1,48 +1,54 @@
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models
from core.mixins import ContactMixin
from core.models import ContactMixin
## Пользователи
class Role(models.Model):
name = models.CharField(max_length=20, unique=True, verbose_name='Название роли')
name = models.CharField(max_length=100, verbose_name="Название роли")
def __str__(self):
return self.name
class Meta:
verbose_name = 'Роль'
verbose_name_plural = 'Роли'
verbose_name = "Роль пользователя"
verbose_name_plural = "Роли пользователей"
class UserAccountManager(BaseUserManager):
def create_user(self, username, password=None, role=None, **extra_fields):
if not username:
raise ValueError('Имя пользователя должно быть указано')
user = self.model(username=username, role=role, **extra_fields)
class AccountManager(BaseUserManager):
def create_user(self, login, password=None, **extra_fields):
if not login:
raise ValueError("Login must be set")
user = self.model(login=login, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, password=None, **extra_fields):
admin_role, _ = Role.objects.get_or_create(name='admin')
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self.create_user(username, password, role=admin_role, **extra_fields)
def create_superuser(self, login, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
return self.create_user(login, password, **extra_fields)
class UserAccount(ContactMixin, AbstractBaseUser, PermissionsMixin):
username = models.CharField(max_length=150, unique=True, verbose_name='Имя пользователя')
role = models.ForeignKey(Role, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Роль')
is_active = models.BooleanField(default=True, verbose_name='Активен')
is_staff = models.BooleanField(default=False, verbose_name='Сотрудник')
objects = UserAccountManager()
class Account(ContactMixin, AbstractBaseUser, PermissionsMixin):
login = models.CharField(max_length=150, unique=True, verbose_name="Логин")
role = models.ForeignKey(
Role, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Роль пользователя"
)
is_staff = models.BooleanField(default=False, verbose_name="Сотрудник")
is_active = models.BooleanField(default=True, verbose_name="Активен")
USERNAME_FIELD = 'username'
USERNAME_FIELD = "login"
REQUIRED_FIELDS = []
objects = AccountManager()
def __str__(self):
return self.username+" ("+ContactMixin.__str__(self)+")"
return super().__str__() or self.login
class Meta:
verbose_name = 'Пользователь'
verbose_name_plural = 'Пользователи'
verbose_name = "Аккаунт"
verbose_name_plural = "Аккаунты"

22
users/serializers.py Normal file
View File

@@ -0,0 +1,22 @@
from rest_framework import serializers
from djoser.serializers import UserCreateSerializer
from .models import Account, Role
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
fields = "__all__"
class AccountSerializer(UserCreateSerializer):
role = serializers.PrimaryKeyRelatedField(queryset=Role.objects.all(), required=False)
class Meta(UserCreateSerializer.Meta):
model = Account
fields = ["id", "login", "role", "is_staff", "is_active", "first_name", "last_name", "middle_name", "phone", "email"]
def create(self, validated_data):
user = super().create(validated_data)
user.set_password(validated_data["password"])
user.save()
return user

13
users/urls.py Normal file
View File

@@ -0,0 +1,13 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import AccountViewSet, RoleViewSet
router = DefaultRouter()
router.register(r"users", AccountViewSet, basename="user")
router.register(r"roles", RoleViewSet, basename="role")
urlpatterns = [
path("", include(router.urls)),
path("auth/", include("djoser.urls")),
path("auth/", include("djoser.urls.jwt")),
]

View File

@@ -1,3 +1,15 @@
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from djoser.views import UserViewSet as DjoserUserViewSet
from .models import Account, Role
from .serializers import AccountSerializer, RoleSerializer
# Create your views here.
class AccountViewSet(DjoserUserViewSet):
queryset = Account.objects.all()
serializer_class = AccountSerializer
permission_classes = [IsAuthenticated]
class RoleViewSet(viewsets.ModelViewSet):
queryset = Role.objects.all()
serializer_class = RoleSerializer