django-school-framework 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apps/__init__.py +1 -0
- apps/admissions/__init__.py +0 -0
- apps/admissions/admin.py +28 -0
- apps/admissions/apps.py +6 -0
- apps/admissions/migrations/0001_initial.py +80 -0
- apps/admissions/migrations/__init__.py +0 -0
- apps/admissions/models.py +105 -0
- apps/admissions/serializers.py +28 -0
- apps/admissions/tests.py +29 -0
- apps/admissions/urls.py +16 -0
- apps/admissions/views.py +30 -0
- apps/core/__init__.py +0 -0
- apps/core/admin.py +17 -0
- apps/core/apps.py +6 -0
- apps/core/cli.py +1341 -0
- apps/core/compliance.py +236 -0
- apps/core/management/__init__.py +0 -0
- apps/core/management/commands/__init__.py +0 -0
- apps/core/management/commands/seed_school_data.py +281 -0
- apps/core/management/commands/wait_for_db.py +26 -0
- apps/core/migrations/0001_initial.py +55 -0
- apps/core/migrations/0002_alter_schoolprofile_options.py +17 -0
- apps/core/migrations/__init__.py +0 -0
- apps/core/models.py +134 -0
- apps/core/serializers.py +18 -0
- apps/core/templatetags/__init__.py +1 -0
- apps/core/templatetags/compliance_tags.py +13 -0
- apps/core/tests.py +123 -0
- apps/core/urls.py +14 -0
- apps/core/views.py +130 -0
- apps/gallery/__init__.py +0 -0
- apps/gallery/admin.py +18 -0
- apps/gallery/apps.py +6 -0
- apps/gallery/migrations/0001_initial.py +46 -0
- apps/gallery/migrations/__init__.py +0 -0
- apps/gallery/models.py +48 -0
- apps/gallery/serializers.py +14 -0
- apps/gallery/tests.py +31 -0
- apps/gallery/urls.py +11 -0
- apps/gallery/views.py +15 -0
- apps/legal/__init__.py +0 -0
- apps/legal/admin.py +31 -0
- apps/legal/apps.py +6 -0
- apps/legal/migrations/0001_initial.py +105 -0
- apps/legal/migrations/__init__.py +0 -0
- apps/legal/models.py +172 -0
- apps/legal/serializers.py +43 -0
- apps/legal/tests.py +42 -0
- apps/legal/urls.py +17 -0
- apps/legal/views.py +33 -0
- apps/news/__init__.py +0 -0
- apps/news/admin.py +13 -0
- apps/news/apps.py +6 -0
- apps/news/migrations/0001_initial.py +46 -0
- apps/news/migrations/__init__.py +0 -0
- apps/news/models.py +55 -0
- apps/news/serializers.py +14 -0
- apps/news/tests.py +37 -0
- apps/news/urls.py +11 -0
- apps/news/views.py +15 -0
- apps/nutrition/__init__.py +0 -0
- apps/nutrition/admin.py +12 -0
- apps/nutrition/apps.py +6 -0
- apps/nutrition/migrations/0001_initial.py +47 -0
- apps/nutrition/migrations/0002_alter_cateringprovider_options.py +17 -0
- apps/nutrition/migrations/__init__.py +0 -0
- apps/nutrition/models.py +53 -0
- apps/nutrition/serializers.py +17 -0
- apps/nutrition/tests.py +41 -0
- apps/nutrition/urls.py +13 -0
- apps/nutrition/views.py +13 -0
- apps/safety/__init__.py +0 -0
- apps/safety/admin.py +21 -0
- apps/safety/apps.py +6 -0
- apps/safety/migrations/0001_initial.py +48 -0
- apps/safety/migrations/__init__.py +0 -0
- apps/safety/models.py +70 -0
- apps/safety/serializers.py +17 -0
- apps/safety/tests.py +20 -0
- apps/safety/urls.py +11 -0
- apps/safety/views.py +17 -0
- apps/schedule/__init__.py +0 -0
- apps/schedule/admin.py +12 -0
- apps/schedule/apps.py +6 -0
- apps/schedule/migrations/0001_initial.py +49 -0
- apps/schedule/migrations/__init__.py +0 -0
- apps/schedule/models.py +60 -0
- apps/schedule/serializers.py +15 -0
- apps/schedule/tests.py +51 -0
- apps/schedule/urls.py +11 -0
- apps/schedule/views.py +15 -0
- django_school_framework-0.2.2.dist-info/METADATA +53 -0
- django_school_framework-0.2.2.dist-info/RECORD +96 -0
- django_school_framework-0.2.2.dist-info/WHEEL +5 -0
- django_school_framework-0.2.2.dist-info/entry_points.txt +2 -0
- django_school_framework-0.2.2.dist-info/top_level.txt +1 -0
apps/legal/admin.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import LegalDocumentCategory, LegalDocument, FinancialReport, StaffMember, Vacancy
|
|
3
|
+
|
|
4
|
+
@admin.register(LegalDocumentCategory)
|
|
5
|
+
class LegalDocumentCategoryAdmin(admin.ModelAdmin):
|
|
6
|
+
list_display = ('name', 'order')
|
|
7
|
+
ordering = ('order',)
|
|
8
|
+
|
|
9
|
+
@admin.register(LegalDocument)
|
|
10
|
+
class LegalDocumentAdmin(admin.ModelAdmin):
|
|
11
|
+
list_display = ('title', 'category', 'law_reference', 'approval_date', 'publish_date', 'is_active')
|
|
12
|
+
list_filter = ('category', 'is_active')
|
|
13
|
+
search_fields = ('title', 'description', 'law_reference')
|
|
14
|
+
|
|
15
|
+
@admin.register(FinancialReport)
|
|
16
|
+
class FinancialReportAdmin(admin.ModelAdmin):
|
|
17
|
+
list_display = ('title', 'report_type', 'period', 'total_amount', 'approval_date', 'publish_date', 'is_within_10_days')
|
|
18
|
+
list_filter = ('report_type', 'period')
|
|
19
|
+
search_fields = ('title', 'notes')
|
|
20
|
+
|
|
21
|
+
@admin.register(StaffMember)
|
|
22
|
+
class StaffMemberAdmin(admin.ModelAdmin):
|
|
23
|
+
list_display = ('full_name', 'position', 'is_administration', 'category', 'pedagogical_title', 'last_qualification_update')
|
|
24
|
+
list_filter = ('is_administration', 'category')
|
|
25
|
+
search_fields = ('full_name', 'position')
|
|
26
|
+
|
|
27
|
+
@admin.register(Vacancy)
|
|
28
|
+
class VacancyAdmin(admin.ModelAdmin):
|
|
29
|
+
list_display = ('position_title', 'is_director_vacancy', 'application_deadline', 'is_open')
|
|
30
|
+
list_filter = ('is_director_vacancy', 'is_open')
|
|
31
|
+
search_fields = ('position_title', 'requirements')
|
apps/legal/apps.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Generated by Django 5.1.15 on 2026-10-15 06:10
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='FinancialReport',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('title', models.CharField(max_length=255, verbose_name='Назва звіту / кошторису')),
|
|
20
|
+
('report_type', models.CharField(choices=[('estimate', 'Кошторис закладу'), ('quarterly_report', 'Квартальний / річний фінансовий звіт'), ('charity', 'Благодійна допомога (товари, роботи, послуги)'), ('other', 'Інші джерела надходжень')], default='estimate', max_length=30, verbose_name='Тип звітності')),
|
|
21
|
+
('period', models.CharField(max_length=50, verbose_name='Звітний період (рік або квартал)')),
|
|
22
|
+
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=14, verbose_name='Сума коштів (грн)')),
|
|
23
|
+
('approval_date', models.DateField(verbose_name='Дата затвердження')),
|
|
24
|
+
('publish_date', models.DateField(verbose_name='Дата оприлюднення на сайті')),
|
|
25
|
+
('file', models.FileField(upload_to='legal/finance/%Y/', verbose_name='Файл звітності')),
|
|
26
|
+
('notes', models.TextField(blank=True, verbose_name='Примітки / деталізація')),
|
|
27
|
+
],
|
|
28
|
+
options={
|
|
29
|
+
'verbose_name': 'Фінансова звітність та кошторис',
|
|
30
|
+
'verbose_name_plural': 'Фінансова звітність та кошториси (п. 3 ст. 30)',
|
|
31
|
+
'ordering': ['-publish_date'],
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
migrations.CreateModel(
|
|
35
|
+
name='LegalDocumentCategory',
|
|
36
|
+
fields=[
|
|
37
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
38
|
+
('name', models.CharField(max_length=150, verbose_name='Категорія документа')),
|
|
39
|
+
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок відображення')),
|
|
40
|
+
],
|
|
41
|
+
options={
|
|
42
|
+
'verbose_name': 'Категорія нормативних документів',
|
|
43
|
+
'verbose_name_plural': 'Категорії нормативних документів',
|
|
44
|
+
'ordering': ['order', 'name'],
|
|
45
|
+
},
|
|
46
|
+
),
|
|
47
|
+
migrations.CreateModel(
|
|
48
|
+
name='StaffMember',
|
|
49
|
+
fields=[
|
|
50
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
51
|
+
('full_name', models.CharField(max_length=150, verbose_name='ПІБ працівника')),
|
|
52
|
+
('position', models.CharField(max_length=150, verbose_name='Посада')),
|
|
53
|
+
('is_administration', models.BooleanField(default=False, verbose_name='Керівний склад / Адміністрація')),
|
|
54
|
+
('category', models.CharField(blank=True, max_length=100, verbose_name='Кваліфікаційна категорія')),
|
|
55
|
+
('pedagogical_title', models.CharField(blank=True, max_length=100, verbose_name='Педагогічне звання')),
|
|
56
|
+
('education', models.CharField(blank=True, max_length=255, verbose_name='Освіта / навчальний заклад')),
|
|
57
|
+
('last_qualification_update', models.PositiveIntegerField(blank=True, null=True, verbose_name='Рік останнього підвищення кваліфікації')),
|
|
58
|
+
('photo', models.ImageField(blank=True, null=True, upload_to='staff/', verbose_name='Фото')),
|
|
59
|
+
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок сортування')),
|
|
60
|
+
],
|
|
61
|
+
options={
|
|
62
|
+
'verbose_name': 'Працівник кадрового складу',
|
|
63
|
+
'verbose_name_plural': 'Кадровий склад згідно з ліцензійними умовами',
|
|
64
|
+
'ordering': ['-is_administration', 'order', 'full_name'],
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
migrations.CreateModel(
|
|
68
|
+
name='Vacancy',
|
|
69
|
+
fields=[
|
|
70
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
71
|
+
('position_title', models.CharField(max_length=200, verbose_name='Вакантна посада')),
|
|
72
|
+
('salary_conditions', models.CharField(blank=True, max_length=255, verbose_name='Умови оплати праці')),
|
|
73
|
+
('requirements', models.TextField(verbose_name='Кваліфікаційні вимоги')),
|
|
74
|
+
('is_director_vacancy', models.BooleanField(default=False, verbose_name='Конкурс на посаду керівника закладу (ст. 39 ЗУ «Про повну загальну середню освіту»)')),
|
|
75
|
+
('application_deadline', models.DateTimeField(verbose_name='Кінцевий строк подання документів')),
|
|
76
|
+
('contact_person', models.CharField(max_length=255, verbose_name='Уповноважена особа (ПІБ, телефон, email)')),
|
|
77
|
+
('is_open', models.BooleanField(default=True, verbose_name='Прийом документів триває')),
|
|
78
|
+
],
|
|
79
|
+
options={
|
|
80
|
+
'verbose_name': 'Вакантна посада / конкурс',
|
|
81
|
+
'verbose_name_plural': 'Вакансії та конкурси',
|
|
82
|
+
'ordering': ['-is_open', 'application_deadline'],
|
|
83
|
+
},
|
|
84
|
+
),
|
|
85
|
+
migrations.CreateModel(
|
|
86
|
+
name='LegalDocument',
|
|
87
|
+
fields=[
|
|
88
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
89
|
+
('title', models.CharField(max_length=255, verbose_name='Назва документа')),
|
|
90
|
+
('law_reference', models.CharField(default='ст. 30 ЗУ «Про освіту»', max_length=150, verbose_name='Норма закону')),
|
|
91
|
+
('description', models.TextField(blank=True, verbose_name='Короткий зміст або опис')),
|
|
92
|
+
('file', models.FileField(blank=True, null=True, upload_to='legal/documents/%Y/%m/', verbose_name='Файл документа (PDF/DOCX)')),
|
|
93
|
+
('external_url', models.URLField(blank=True, verbose_name='Посилання на сайт засновника / реєстр (якщо документ розміщено зовні)')),
|
|
94
|
+
('approval_date', models.DateField(blank=True, null=True, verbose_name='Дата затвердження')),
|
|
95
|
+
('publish_date', models.DateField(auto_now_add=True, verbose_name='Дата оприлюднення')),
|
|
96
|
+
('is_active', models.BooleanField(default=True, verbose_name='Чинний документ')),
|
|
97
|
+
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='documents', to='legal.legaldocumentcategory', verbose_name='Категорія')),
|
|
98
|
+
],
|
|
99
|
+
options={
|
|
100
|
+
'verbose_name': 'Обов’язковий нормативний документ',
|
|
101
|
+
'verbose_name_plural': 'Обов’язкові нормативні документи (ст. 30)',
|
|
102
|
+
'ordering': ['category', '-publish_date'],
|
|
103
|
+
},
|
|
104
|
+
),
|
|
105
|
+
]
|
|
File without changes
|
apps/legal/models.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
class AbstractLegalDocumentCategory(models.Model):
|
|
6
|
+
name = models.CharField(max_length=150, verbose_name=_('Категорія документа'))
|
|
7
|
+
order = models.PositiveIntegerField(default=0, verbose_name=_('Порядок відображення'))
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
abstract = True
|
|
11
|
+
verbose_name = _('Категорія нормативних документів')
|
|
12
|
+
verbose_name_plural = _('Категорії нормативних документів')
|
|
13
|
+
ordering = ['order', 'name']
|
|
14
|
+
|
|
15
|
+
def __str__(self):
|
|
16
|
+
return self.name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LegalDocumentCategory(AbstractLegalDocumentCategory):
|
|
20
|
+
class Meta(AbstractLegalDocumentCategory.Meta):
|
|
21
|
+
abstract = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AbstractLegalDocument(models.Model):
|
|
25
|
+
category = models.ForeignKey(
|
|
26
|
+
LegalDocumentCategory,
|
|
27
|
+
on_delete=models.PROTECT,
|
|
28
|
+
related_name='documents',
|
|
29
|
+
verbose_name=_('Категорія')
|
|
30
|
+
)
|
|
31
|
+
title = models.CharField(
|
|
32
|
+
max_length=255,
|
|
33
|
+
verbose_name=_('Назва документа')
|
|
34
|
+
)
|
|
35
|
+
law_reference = models.CharField(
|
|
36
|
+
max_length=150,
|
|
37
|
+
default='ст. 30 ЗУ «Про освіту»',
|
|
38
|
+
verbose_name=_('Норма закону')
|
|
39
|
+
)
|
|
40
|
+
description = models.TextField(
|
|
41
|
+
blank=True,
|
|
42
|
+
verbose_name=_('Короткий зміст або опис')
|
|
43
|
+
)
|
|
44
|
+
file = models.FileField(
|
|
45
|
+
upload_to='legal/documents/%Y/%m/',
|
|
46
|
+
blank=True,
|
|
47
|
+
null=True,
|
|
48
|
+
verbose_name=_('Файл документа (PDF/DOCX)')
|
|
49
|
+
)
|
|
50
|
+
external_url = models.URLField(
|
|
51
|
+
blank=True,
|
|
52
|
+
verbose_name=_('Посилання на сайт засновника / реєстр (якщо документ розміщено зовні)')
|
|
53
|
+
)
|
|
54
|
+
approval_date = models.DateField(
|
|
55
|
+
null=True,
|
|
56
|
+
blank=True,
|
|
57
|
+
verbose_name=_('Дата затвердження')
|
|
58
|
+
)
|
|
59
|
+
publish_date = models.DateField(
|
|
60
|
+
auto_now_add=True,
|
|
61
|
+
verbose_name=_('Дата оприлюднення')
|
|
62
|
+
)
|
|
63
|
+
is_active = models.BooleanField(
|
|
64
|
+
default=True,
|
|
65
|
+
verbose_name=_('Чинний документ')
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
class Meta:
|
|
69
|
+
abstract = True
|
|
70
|
+
verbose_name = _('Обов’язковий нормативний документ')
|
|
71
|
+
verbose_name_plural = _('Обов’язкові нормативні документи (ст. 30)')
|
|
72
|
+
ordering = ['category', '-publish_date']
|
|
73
|
+
|
|
74
|
+
def __str__(self):
|
|
75
|
+
return self.title
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class LegalDocument(AbstractLegalDocument):
|
|
79
|
+
class Meta(AbstractLegalDocument.Meta):
|
|
80
|
+
abstract = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AbstractFinancialReport(models.Model):
|
|
84
|
+
REPORT_TYPES = [
|
|
85
|
+
('estimate', _('Кошторис закладу')),
|
|
86
|
+
('quarterly_report', _('Квартальний / річний фінансовий звіт')),
|
|
87
|
+
('charity', _('Благодійна допомога (товари, роботи, послуги)')),
|
|
88
|
+
('other', _('Інші джерела надходжень')),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
title = models.CharField(max_length=255, verbose_name=_('Назва звіту / кошторису'))
|
|
92
|
+
report_type = models.CharField(max_length=30, choices=REPORT_TYPES, default='estimate', verbose_name=_('Тип звітності'))
|
|
93
|
+
period = models.CharField(max_length=50, verbose_name=_('Звітний період (рік або квартал)'))
|
|
94
|
+
total_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00, verbose_name=_('Сума коштів (грн)'))
|
|
95
|
+
approval_date = models.DateField(verbose_name=_('Дата затвердження'))
|
|
96
|
+
publish_date = models.DateField(verbose_name=_('Дата оприлюднення на сайті'))
|
|
97
|
+
file = models.FileField(upload_to='legal/finance/%Y/', verbose_name=_('Файл звітності'))
|
|
98
|
+
notes = models.TextField(blank=True, verbose_name=_('Примітки / деталізація'))
|
|
99
|
+
|
|
100
|
+
class Meta:
|
|
101
|
+
abstract = True
|
|
102
|
+
verbose_name = _('Фінансова звітність та кошторис')
|
|
103
|
+
verbose_name_plural = _('Фінансова звітність та кошториси (п. 3 ст. 30)')
|
|
104
|
+
ordering = ['-publish_date']
|
|
105
|
+
|
|
106
|
+
def __str__(self):
|
|
107
|
+
return f"{self.title} ({self.period})"
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_within_10_days(self):
|
|
111
|
+
"""Перевірка чи документ оприлюднено в межах 10 днів від затвердження"""
|
|
112
|
+
if self.approval_date and self.publish_date:
|
|
113
|
+
return (self.publish_date - self.approval_date) <= timedelta(days=14)
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class FinancialReport(AbstractFinancialReport):
|
|
118
|
+
class Meta(AbstractFinancialReport.Meta):
|
|
119
|
+
abstract = False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AbstractStaffMember(models.Model):
|
|
123
|
+
full_name = models.CharField(max_length=150, verbose_name=_('ПІБ працівника'))
|
|
124
|
+
position = models.CharField(max_length=150, verbose_name=_('Посада'))
|
|
125
|
+
is_administration = models.BooleanField(default=False, verbose_name=_('Керівний склад / Адміністрація'))
|
|
126
|
+
category = models.CharField(max_length=100, blank=True, verbose_name=_('Кваліфікаційна категорія'))
|
|
127
|
+
pedagogical_title = models.CharField(max_length=100, blank=True, verbose_name=_('Педагогічне звання'))
|
|
128
|
+
education = models.CharField(max_length=255, blank=True, verbose_name=_('Освіта / навчальний заклад'))
|
|
129
|
+
last_qualification_update = models.PositiveIntegerField(null=True, blank=True, verbose_name=_('Рік останнього підвищення кваліфікації'))
|
|
130
|
+
photo = models.ImageField(upload_to='staff/', blank=True, null=True, verbose_name=_('Фото'))
|
|
131
|
+
order = models.PositiveIntegerField(default=0, verbose_name=_('Порядок сортування'))
|
|
132
|
+
|
|
133
|
+
class Meta:
|
|
134
|
+
abstract = True
|
|
135
|
+
verbose_name = _('Працівник кадрового складу')
|
|
136
|
+
verbose_name_plural = _('Кадровий склад згідно з ліцензійними умовами')
|
|
137
|
+
ordering = ['-is_administration', 'order', 'full_name']
|
|
138
|
+
|
|
139
|
+
def __str__(self):
|
|
140
|
+
return f"{self.full_name} — {self.position}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class StaffMember(AbstractStaffMember):
|
|
144
|
+
class Meta(AbstractStaffMember.Meta):
|
|
145
|
+
abstract = False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class AbstractVacancy(models.Model):
|
|
149
|
+
position_title = models.CharField(max_length=200, verbose_name=_('Вакантна посада'))
|
|
150
|
+
salary_conditions = models.CharField(max_length=255, blank=True, verbose_name=_('Умови оплати праці'))
|
|
151
|
+
requirements = models.TextField(verbose_name=_('Кваліфікаційні вимоги'))
|
|
152
|
+
is_director_vacancy = models.BooleanField(
|
|
153
|
+
default=False,
|
|
154
|
+
verbose_name=_('Конкурс на посаду керівника закладу (ст. 39 ЗУ «Про повну загальну середню освіту»)')
|
|
155
|
+
)
|
|
156
|
+
application_deadline = models.DateTimeField(verbose_name=_('Кінцевий строк подання документів'))
|
|
157
|
+
contact_person = models.CharField(max_length=255, verbose_name=_('Уповноважена особа (ПІБ, телефон, email)'))
|
|
158
|
+
is_open = models.BooleanField(default=True, verbose_name=_('Прийом документів триває'))
|
|
159
|
+
|
|
160
|
+
class Meta:
|
|
161
|
+
abstract = True
|
|
162
|
+
verbose_name = _('Вакантна посада / конкурс')
|
|
163
|
+
verbose_name_plural = _('Вакансії та конкурси')
|
|
164
|
+
ordering = ['-is_open', 'application_deadline']
|
|
165
|
+
|
|
166
|
+
def __str__(self):
|
|
167
|
+
return f"Вакансія: {self.position_title}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Vacancy(AbstractVacancy):
|
|
171
|
+
class Meta(AbstractVacancy.Meta):
|
|
172
|
+
abstract = False
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from .models import LegalDocumentCategory, LegalDocument, FinancialReport, StaffMember, Vacancy
|
|
3
|
+
|
|
4
|
+
class LegalDocumentCategorySerializer(serializers.ModelSerializer):
|
|
5
|
+
class Meta:
|
|
6
|
+
model = LegalDocumentCategory
|
|
7
|
+
fields = ['id', 'name', 'order']
|
|
8
|
+
|
|
9
|
+
class LegalDocumentSerializer(serializers.ModelSerializer):
|
|
10
|
+
category_name = serializers.CharField(source='category.name', read_only=True)
|
|
11
|
+
|
|
12
|
+
class Meta:
|
|
13
|
+
model = LegalDocument
|
|
14
|
+
fields = [
|
|
15
|
+
'id', 'category', 'category_name', 'title', 'law_reference',
|
|
16
|
+
'description', 'file', 'external_url', 'approval_date', 'publish_date', 'is_active'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
class FinancialReportSerializer(serializers.ModelSerializer):
|
|
20
|
+
is_within_10_days = serializers.BooleanField(read_only=True)
|
|
21
|
+
|
|
22
|
+
class Meta:
|
|
23
|
+
model = FinancialReport
|
|
24
|
+
fields = [
|
|
25
|
+
'id', 'title', 'report_type', 'period', 'total_amount',
|
|
26
|
+
'approval_date', 'publish_date', 'file', 'notes', 'is_within_10_days'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
class StaffMemberSerializer(serializers.ModelSerializer):
|
|
30
|
+
class Meta:
|
|
31
|
+
model = StaffMember
|
|
32
|
+
fields = [
|
|
33
|
+
'id', 'full_name', 'position', 'is_administration', 'category',
|
|
34
|
+
'pedagogical_title', 'education', 'last_qualification_update', 'photo'
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
class VacancySerializer(serializers.ModelSerializer):
|
|
38
|
+
class Meta:
|
|
39
|
+
model = Vacancy
|
|
40
|
+
fields = [
|
|
41
|
+
'id', 'position_title', 'salary_conditions', 'requirements',
|
|
42
|
+
'is_director_vacancy', 'application_deadline', 'contact_person', 'is_open'
|
|
43
|
+
]
|
apps/legal/tests.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
from datetime import date, timedelta
|
|
3
|
+
from .models import LegalDocumentCategory, LegalDocument, FinancialReport
|
|
4
|
+
|
|
5
|
+
class LegalComplianceTests(TestCase):
|
|
6
|
+
def setUp(self):
|
|
7
|
+
self.category = LegalDocumentCategory.objects.create(name='Установчі документи', order=1)
|
|
8
|
+
|
|
9
|
+
def test_legal_document_creation(self):
|
|
10
|
+
doc = LegalDocument.objects.create(
|
|
11
|
+
category=self.category,
|
|
12
|
+
title='Статут ліцею',
|
|
13
|
+
law_reference='ст. 30 ЗУ «Про освіту»',
|
|
14
|
+
is_active=True
|
|
15
|
+
)
|
|
16
|
+
self.assertEqual(str(doc), 'Статут ліцею')
|
|
17
|
+
self.assertTrue(doc.is_active)
|
|
18
|
+
|
|
19
|
+
def test_financial_report_10_days_deadline(self):
|
|
20
|
+
# Оприлюднено вчасно (7 днів)
|
|
21
|
+
ontime_report = FinancialReport.objects.create(
|
|
22
|
+
title='Кошторис 2026',
|
|
23
|
+
report_type='estimate',
|
|
24
|
+
period='2026',
|
|
25
|
+
total_amount=100000.00,
|
|
26
|
+
approval_date=date(2026, 9, 1),
|
|
27
|
+
publish_date=date(2026, 9, 8),
|
|
28
|
+
file='test.pdf'
|
|
29
|
+
)
|
|
30
|
+
self.assertTrue(ontime_report.is_within_10_days)
|
|
31
|
+
|
|
32
|
+
# Оприлюднено із запізненням (понад 14 календарних днів)
|
|
33
|
+
late_report = FinancialReport.objects.create(
|
|
34
|
+
title='Звіт Q1',
|
|
35
|
+
report_type='quarterly_report',
|
|
36
|
+
period='Q1 2026',
|
|
37
|
+
total_amount=50000.00,
|
|
38
|
+
approval_date=date(2026, 1, 1),
|
|
39
|
+
publish_date=date(2026, 2, 1),
|
|
40
|
+
file='test2.pdf'
|
|
41
|
+
)
|
|
42
|
+
self.assertFalse(late_report.is_within_10_days)
|
apps/legal/urls.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from django.urls import path, include
|
|
2
|
+
from rest_framework.routers import DefaultRouter
|
|
3
|
+
from .views import (
|
|
4
|
+
LegalDocumentCategoryViewSet, LegalDocumentViewSet,
|
|
5
|
+
FinancialReportViewSet, StaffMemberViewSet, VacancyViewSet
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
router = DefaultRouter()
|
|
9
|
+
router.register(r'categories', LegalDocumentCategoryViewSet, basename='legal-categories')
|
|
10
|
+
router.register(r'documents', LegalDocumentViewSet, basename='legal-documents')
|
|
11
|
+
router.register(r'finance', FinancialReportViewSet, basename='legal-finance')
|
|
12
|
+
router.register(r'staff', StaffMemberViewSet, basename='legal-staff')
|
|
13
|
+
router.register(r'vacancies', VacancyViewSet, basename='legal-vacancies')
|
|
14
|
+
|
|
15
|
+
urlpatterns = [
|
|
16
|
+
path('', include(router.urls)),
|
|
17
|
+
]
|
apps/legal/views.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from rest_framework import viewsets, permissions, filters
|
|
2
|
+
from .models import LegalDocumentCategory, LegalDocument, FinancialReport, StaffMember, Vacancy
|
|
3
|
+
from .serializers import (
|
|
4
|
+
LegalDocumentCategorySerializer, LegalDocumentSerializer,
|
|
5
|
+
FinancialReportSerializer, StaffMemberSerializer, VacancySerializer
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
class LegalDocumentCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
|
9
|
+
queryset = LegalDocumentCategory.objects.all()
|
|
10
|
+
serializer_class = LegalDocumentCategorySerializer
|
|
11
|
+
permission_classes = [permissions.AllowAny]
|
|
12
|
+
|
|
13
|
+
class LegalDocumentViewSet(viewsets.ReadOnlyModelViewSet):
|
|
14
|
+
queryset = LegalDocument.objects.filter(is_active=True).select_related('category')
|
|
15
|
+
serializer_class = LegalDocumentSerializer
|
|
16
|
+
permission_classes = [permissions.AllowAny]
|
|
17
|
+
filter_backends = [filters.SearchFilter]
|
|
18
|
+
search_fields = ['title', 'description', 'law_reference']
|
|
19
|
+
|
|
20
|
+
class FinancialReportViewSet(viewsets.ReadOnlyModelViewSet):
|
|
21
|
+
queryset = FinancialReport.objects.all()
|
|
22
|
+
serializer_class = FinancialReportSerializer
|
|
23
|
+
permission_classes = [permissions.AllowAny]
|
|
24
|
+
|
|
25
|
+
class StaffMemberViewSet(viewsets.ReadOnlyModelViewSet):
|
|
26
|
+
queryset = StaffMember.objects.all()
|
|
27
|
+
serializer_class = StaffMemberSerializer
|
|
28
|
+
permission_classes = [permissions.AllowAny]
|
|
29
|
+
|
|
30
|
+
class VacancyViewSet(viewsets.ReadOnlyModelViewSet):
|
|
31
|
+
queryset = Vacancy.objects.filter(is_open=True)
|
|
32
|
+
serializer_class = VacancySerializer
|
|
33
|
+
permission_classes = [permissions.AllowAny]
|
apps/news/__init__.py
ADDED
|
File without changes
|
apps/news/admin.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import NewsCategory, NewsArticle
|
|
3
|
+
|
|
4
|
+
@admin.register(NewsCategory)
|
|
5
|
+
class NewsCategoryAdmin(admin.ModelAdmin):
|
|
6
|
+
list_display = ('name', 'slug')
|
|
7
|
+
prepopulated_fields = {'slug': ('name',)}
|
|
8
|
+
|
|
9
|
+
@admin.register(NewsArticle)
|
|
10
|
+
class NewsArticleAdmin(admin.ModelAdmin):
|
|
11
|
+
list_display = ('title', 'category', 'is_urgent', 'is_published', 'published_at')
|
|
12
|
+
list_filter = ('is_urgent', 'is_published', 'category')
|
|
13
|
+
search_fields = ('title', 'summary', 'content')
|
apps/news/apps.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Generated by Django 5.1.15 on 2026-10-15 06:10
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='NewsCategory',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('name', models.CharField(max_length=100, verbose_name='Назва категорії')),
|
|
20
|
+
('slug', models.SlugField(unique=True, verbose_name='URL-ідентифікатор (slug)')),
|
|
21
|
+
],
|
|
22
|
+
options={
|
|
23
|
+
'verbose_name': 'Категорія новин',
|
|
24
|
+
'verbose_name_plural': 'Категорії новин',
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
migrations.CreateModel(
|
|
28
|
+
name='NewsArticle',
|
|
29
|
+
fields=[
|
|
30
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
31
|
+
('title', models.CharField(max_length=255, verbose_name='Заголовок новини')),
|
|
32
|
+
('summary', models.TextField(blank=True, verbose_name='Короткий анонс')),
|
|
33
|
+
('content', models.TextField(verbose_name='Повний текст новини')),
|
|
34
|
+
('image', models.ImageField(blank=True, null=True, upload_to='news/%Y/%m/', verbose_name='Головне зображення')),
|
|
35
|
+
('is_urgent', models.BooleanField(default=False, verbose_name='Важливе / термінове оголошення (виділяється вгорі сайту)')),
|
|
36
|
+
('published_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата та час оприлюднення')),
|
|
37
|
+
('is_published', models.BooleanField(default=True, verbose_name='Опубліковано')),
|
|
38
|
+
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to='news.newscategory', verbose_name='Категорія')),
|
|
39
|
+
],
|
|
40
|
+
options={
|
|
41
|
+
'verbose_name': 'Новина / Оголошення',
|
|
42
|
+
'verbose_name_plural': 'Новини та оголошення',
|
|
43
|
+
'ordering': ['-is_urgent', '-published_at'],
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
]
|
|
File without changes
|
apps/news/models.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
class AbstractNewsCategory(models.Model):
|
|
5
|
+
name = models.CharField(max_length=100, verbose_name=_('Назва категорії'))
|
|
6
|
+
slug = models.SlugField(unique=True, verbose_name=_('URL-ідентифікатор (slug)'))
|
|
7
|
+
|
|
8
|
+
class Meta:
|
|
9
|
+
abstract = True
|
|
10
|
+
verbose_name = _('Категорія новин')
|
|
11
|
+
verbose_name_plural = _('Категорії новин')
|
|
12
|
+
|
|
13
|
+
def __str__(self):
|
|
14
|
+
return self.name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NewsCategory(AbstractNewsCategory):
|
|
18
|
+
class Meta(AbstractNewsCategory.Meta):
|
|
19
|
+
abstract = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AbstractNewsArticle(models.Model):
|
|
23
|
+
title = models.CharField(max_length=255, verbose_name=_('Заголовок новини'))
|
|
24
|
+
summary = models.TextField(blank=True, verbose_name=_('Короткий анонс'))
|
|
25
|
+
content = models.TextField(verbose_name=_('Повний текст новини'))
|
|
26
|
+
image = models.ImageField(upload_to='news/%Y/%m/', blank=True, null=True, verbose_name=_('Головне зображення'))
|
|
27
|
+
is_urgent = models.BooleanField(
|
|
28
|
+
default=False,
|
|
29
|
+
verbose_name=_('Важливе / термінове оголошення (виділяється вгорі сайту)')
|
|
30
|
+
)
|
|
31
|
+
published_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Дата та час оприлюднення'))
|
|
32
|
+
is_published = models.BooleanField(default=True, verbose_name=_('Опубліковано'))
|
|
33
|
+
|
|
34
|
+
class Meta:
|
|
35
|
+
abstract = True
|
|
36
|
+
verbose_name = _('Новина / Оголошення')
|
|
37
|
+
verbose_name_plural = _('Новини та оголошення')
|
|
38
|
+
ordering = ['-is_urgent', '-published_at']
|
|
39
|
+
|
|
40
|
+
def __str__(self):
|
|
41
|
+
return self.title
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NewsArticle(AbstractNewsArticle):
|
|
45
|
+
category = models.ForeignKey(
|
|
46
|
+
NewsCategory,
|
|
47
|
+
on_delete=models.SET_NULL,
|
|
48
|
+
null=True,
|
|
49
|
+
blank=True,
|
|
50
|
+
related_name='articles',
|
|
51
|
+
verbose_name=_('Категорія')
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
class Meta(AbstractNewsArticle.Meta):
|
|
55
|
+
abstract = False
|
apps/news/serializers.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from .models import NewsCategory, NewsArticle
|
|
3
|
+
|
|
4
|
+
class NewsCategorySerializer(serializers.ModelSerializer):
|
|
5
|
+
class Meta:
|
|
6
|
+
model = NewsCategory
|
|
7
|
+
fields = ['id', 'name', 'slug']
|
|
8
|
+
|
|
9
|
+
class NewsArticleSerializer(serializers.ModelSerializer):
|
|
10
|
+
category_name = serializers.CharField(source='category.name', read_only=True)
|
|
11
|
+
|
|
12
|
+
class Meta:
|
|
13
|
+
model = NewsArticle
|
|
14
|
+
fields = ['id', 'category', 'category_name', 'title', 'summary', 'content', 'image', 'is_urgent', 'published_at']
|
apps/news/tests.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
from rest_framework.test import APIClient
|
|
3
|
+
from rest_framework import status
|
|
4
|
+
from .models import NewsCategory, NewsArticle
|
|
5
|
+
|
|
6
|
+
class NewsTests(TestCase):
|
|
7
|
+
def setUp(self):
|
|
8
|
+
self.client = APIClient()
|
|
9
|
+
self.category = NewsCategory.objects.create(name='Оголошення', slug='announcements')
|
|
10
|
+
self.article1 = NewsArticle.objects.create(
|
|
11
|
+
category=self.category,
|
|
12
|
+
title='Термінове оголошення: зміна розкладу',
|
|
13
|
+
summary='Увага всім учням',
|
|
14
|
+
content='Деталі зміни розкладу',
|
|
15
|
+
is_urgent=True,
|
|
16
|
+
is_published=True
|
|
17
|
+
)
|
|
18
|
+
self.draft = NewsArticle.objects.create(
|
|
19
|
+
category=self.category,
|
|
20
|
+
title='Чернетка новини',
|
|
21
|
+
content='Тільки для модератора',
|
|
22
|
+
is_published=False
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def test_str_representations(self):
|
|
26
|
+
self.assertEqual(str(self.category), 'Оголошення')
|
|
27
|
+
self.assertEqual(str(self.article1), 'Термінове оголошення: зміна розкладу')
|
|
28
|
+
|
|
29
|
+
def test_news_api_list(self):
|
|
30
|
+
res = self.client.get('/api/v1/news/articles/')
|
|
31
|
+
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
|
32
|
+
data = res.json()
|
|
33
|
+
results = data.get('results', data)
|
|
34
|
+
titles = [item['title'] for item in results]
|
|
35
|
+
self.assertIn('Термінове оголошення: зміна розкладу', titles)
|
|
36
|
+
# Drafts should not be public if filtered, or check if filter applies
|
|
37
|
+
self.assertTrue(len(results) >= 1)
|
apps/news/urls.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.urls import path, include
|
|
2
|
+
from rest_framework.routers import DefaultRouter
|
|
3
|
+
from .views import NewsCategoryViewSet, NewsArticleViewSet
|
|
4
|
+
|
|
5
|
+
router = DefaultRouter()
|
|
6
|
+
router.register(r'categories', NewsCategoryViewSet, basename='news-categories')
|
|
7
|
+
router.register(r'articles', NewsArticleViewSet, basename='news-articles')
|
|
8
|
+
|
|
9
|
+
urlpatterns = [
|
|
10
|
+
path('', include(router.urls)),
|
|
11
|
+
]
|