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.
Files changed (96) hide show
  1. apps/__init__.py +1 -0
  2. apps/admissions/__init__.py +0 -0
  3. apps/admissions/admin.py +28 -0
  4. apps/admissions/apps.py +6 -0
  5. apps/admissions/migrations/0001_initial.py +80 -0
  6. apps/admissions/migrations/__init__.py +0 -0
  7. apps/admissions/models.py +105 -0
  8. apps/admissions/serializers.py +28 -0
  9. apps/admissions/tests.py +29 -0
  10. apps/admissions/urls.py +16 -0
  11. apps/admissions/views.py +30 -0
  12. apps/core/__init__.py +0 -0
  13. apps/core/admin.py +17 -0
  14. apps/core/apps.py +6 -0
  15. apps/core/cli.py +1341 -0
  16. apps/core/compliance.py +236 -0
  17. apps/core/management/__init__.py +0 -0
  18. apps/core/management/commands/__init__.py +0 -0
  19. apps/core/management/commands/seed_school_data.py +281 -0
  20. apps/core/management/commands/wait_for_db.py +26 -0
  21. apps/core/migrations/0001_initial.py +55 -0
  22. apps/core/migrations/0002_alter_schoolprofile_options.py +17 -0
  23. apps/core/migrations/__init__.py +0 -0
  24. apps/core/models.py +134 -0
  25. apps/core/serializers.py +18 -0
  26. apps/core/templatetags/__init__.py +1 -0
  27. apps/core/templatetags/compliance_tags.py +13 -0
  28. apps/core/tests.py +123 -0
  29. apps/core/urls.py +14 -0
  30. apps/core/views.py +130 -0
  31. apps/gallery/__init__.py +0 -0
  32. apps/gallery/admin.py +18 -0
  33. apps/gallery/apps.py +6 -0
  34. apps/gallery/migrations/0001_initial.py +46 -0
  35. apps/gallery/migrations/__init__.py +0 -0
  36. apps/gallery/models.py +48 -0
  37. apps/gallery/serializers.py +14 -0
  38. apps/gallery/tests.py +31 -0
  39. apps/gallery/urls.py +11 -0
  40. apps/gallery/views.py +15 -0
  41. apps/legal/__init__.py +0 -0
  42. apps/legal/admin.py +31 -0
  43. apps/legal/apps.py +6 -0
  44. apps/legal/migrations/0001_initial.py +105 -0
  45. apps/legal/migrations/__init__.py +0 -0
  46. apps/legal/models.py +172 -0
  47. apps/legal/serializers.py +43 -0
  48. apps/legal/tests.py +42 -0
  49. apps/legal/urls.py +17 -0
  50. apps/legal/views.py +33 -0
  51. apps/news/__init__.py +0 -0
  52. apps/news/admin.py +13 -0
  53. apps/news/apps.py +6 -0
  54. apps/news/migrations/0001_initial.py +46 -0
  55. apps/news/migrations/__init__.py +0 -0
  56. apps/news/models.py +55 -0
  57. apps/news/serializers.py +14 -0
  58. apps/news/tests.py +37 -0
  59. apps/news/urls.py +11 -0
  60. apps/news/views.py +15 -0
  61. apps/nutrition/__init__.py +0 -0
  62. apps/nutrition/admin.py +12 -0
  63. apps/nutrition/apps.py +6 -0
  64. apps/nutrition/migrations/0001_initial.py +47 -0
  65. apps/nutrition/migrations/0002_alter_cateringprovider_options.py +17 -0
  66. apps/nutrition/migrations/__init__.py +0 -0
  67. apps/nutrition/models.py +53 -0
  68. apps/nutrition/serializers.py +17 -0
  69. apps/nutrition/tests.py +41 -0
  70. apps/nutrition/urls.py +13 -0
  71. apps/nutrition/views.py +13 -0
  72. apps/safety/__init__.py +0 -0
  73. apps/safety/admin.py +21 -0
  74. apps/safety/apps.py +6 -0
  75. apps/safety/migrations/0001_initial.py +48 -0
  76. apps/safety/migrations/__init__.py +0 -0
  77. apps/safety/models.py +70 -0
  78. apps/safety/serializers.py +17 -0
  79. apps/safety/tests.py +20 -0
  80. apps/safety/urls.py +11 -0
  81. apps/safety/views.py +17 -0
  82. apps/schedule/__init__.py +0 -0
  83. apps/schedule/admin.py +12 -0
  84. apps/schedule/apps.py +6 -0
  85. apps/schedule/migrations/0001_initial.py +49 -0
  86. apps/schedule/migrations/__init__.py +0 -0
  87. apps/schedule/models.py +60 -0
  88. apps/schedule/serializers.py +15 -0
  89. apps/schedule/tests.py +51 -0
  90. apps/schedule/urls.py +11 -0
  91. apps/schedule/views.py +15 -0
  92. django_school_framework-0.2.2.dist-info/METADATA +53 -0
  93. django_school_framework-0.2.2.dist-info/RECORD +96 -0
  94. django_school_framework-0.2.2.dist-info/WHEEL +5 -0
  95. django_school_framework-0.2.2.dist-info/entry_points.txt +2 -0
  96. 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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+ class LegalConfig(AppConfig):
4
+ default_auto_field = 'django.db.models.BigAutoField'
5
+ name = 'apps.legal'
6
+ verbose_name = 'Прозорість та обов’язкові документи (ст. 30 ЗУ «Про освіту»)'
@@ -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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+ class NewsConfig(AppConfig):
4
+ default_auto_field = 'django.db.models.BigAutoField'
5
+ name = 'apps.news'
6
+ verbose_name = 'Новини та оголошення закладу'
@@ -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
@@ -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
+ ]