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/news/views.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from rest_framework import viewsets, permissions, filters
|
|
2
|
+
from .models import NewsCategory, NewsArticle
|
|
3
|
+
from .serializers import NewsCategorySerializer, NewsArticleSerializer
|
|
4
|
+
|
|
5
|
+
class NewsCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
|
6
|
+
queryset = NewsCategory.objects.all()
|
|
7
|
+
serializer_class = NewsCategorySerializer
|
|
8
|
+
permission_classes = [permissions.AllowAny]
|
|
9
|
+
|
|
10
|
+
class NewsArticleViewSet(viewsets.ReadOnlyModelViewSet):
|
|
11
|
+
queryset = NewsArticle.objects.filter(is_published=True).select_related('category')
|
|
12
|
+
serializer_class = NewsArticleSerializer
|
|
13
|
+
permission_classes = [permissions.AllowAny]
|
|
14
|
+
filter_backends = [filters.SearchFilter]
|
|
15
|
+
search_fields = ['title', 'summary', 'content']
|
|
File without changes
|
apps/nutrition/admin.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import CateringProvider, DailyMenu
|
|
3
|
+
|
|
4
|
+
@admin.register(CateringProvider)
|
|
5
|
+
class CateringProviderAdmin(admin.ModelAdmin):
|
|
6
|
+
list_display = ('company_name', 'edrpou', 'contract_number', 'contact_person', 'phone')
|
|
7
|
+
|
|
8
|
+
@admin.register(DailyMenu)
|
|
9
|
+
class DailyMenuAdmin(admin.ModelAdmin):
|
|
10
|
+
list_display = ('menu_date', 'age_group')
|
|
11
|
+
list_filter = ('age_group', 'menu_date')
|
|
12
|
+
date_hierarchy = 'menu_date'
|
apps/nutrition/apps.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Generated by Django 5.1.15 on 2026-10-15 06:10
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
initial = True
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name='CateringProvider',
|
|
16
|
+
fields=[
|
|
17
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
18
|
+
('company_name', models.CharField(max_length=255, verbose_name='Організація, що надає послуги харчування')),
|
|
19
|
+
('edrpou', models.CharField(blank=True, max_length=10, verbose_name='Код ЄДРПОУ постачальника')),
|
|
20
|
+
('contract_number', models.CharField(max_length=100, verbose_name='Номер та дата договору')),
|
|
21
|
+
('contact_person', models.CharField(max_length=150, verbose_name='Відповідальна особа / технолог')),
|
|
22
|
+
('phone', models.CharField(max_length=50, verbose_name='Телефон для скарг та пропозицій')),
|
|
23
|
+
],
|
|
24
|
+
options={
|
|
25
|
+
'verbose_name': 'Організація харчування',
|
|
26
|
+
'verbose_name_plural': 'Організація харчування',
|
|
27
|
+
},
|
|
28
|
+
),
|
|
29
|
+
migrations.CreateModel(
|
|
30
|
+
name='DailyMenu',
|
|
31
|
+
fields=[
|
|
32
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
33
|
+
('menu_date', models.DateField(verbose_name='Дата меню')),
|
|
34
|
+
('age_group', models.CharField(choices=[('1_4', '1-4 класи (початкова школа)'), ('5_9', '5-9 класи (базова школа)'), ('10_11', '10-11 класи (старша школа)')], default='1_4', max_length=20, verbose_name='Вікова група')),
|
|
35
|
+
('breakfast', models.TextField(blank=True, verbose_name='Сніданок (страви, вихід у грамах)')),
|
|
36
|
+
('lunch', models.TextField(blank=True, verbose_name='Обід (страви, вихід у грамах)')),
|
|
37
|
+
('snack', models.TextField(blank=True, verbose_name='Полуденок / підвечірок')),
|
|
38
|
+
('photo', models.ImageField(blank=True, null=True, upload_to='nutrition/menus/%Y/%m/', verbose_name='Фото меню з печаткою')),
|
|
39
|
+
],
|
|
40
|
+
options={
|
|
41
|
+
'verbose_name': 'Щоденне меню',
|
|
42
|
+
'verbose_name_plural': 'Щоденні меню харчування',
|
|
43
|
+
'ordering': ['-menu_date', 'age_group'],
|
|
44
|
+
'unique_together': {('menu_date', 'age_group')},
|
|
45
|
+
},
|
|
46
|
+
),
|
|
47
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 5.1.15 on 2026-10-15 06:17
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('nutrition', '0001_initial'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterModelOptions(
|
|
14
|
+
name='cateringprovider',
|
|
15
|
+
options={'ordering': ['id'], 'verbose_name': 'Організація харчування', 'verbose_name_plural': 'Організація харчування'},
|
|
16
|
+
),
|
|
17
|
+
]
|
|
File without changes
|
apps/nutrition/models.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
class AbstractCateringProvider(models.Model):
|
|
5
|
+
company_name = models.CharField(max_length=255, verbose_name=_('Організація, що надає послуги харчування'))
|
|
6
|
+
edrpou = models.CharField(max_length=10, blank=True, verbose_name=_('Код ЄДРПОУ постачальника'))
|
|
7
|
+
contract_number = models.CharField(max_length=100, verbose_name=_('Номер та дата договору'))
|
|
8
|
+
contact_person = models.CharField(max_length=150, verbose_name=_('Відповідальна особа / технолог'))
|
|
9
|
+
phone = models.CharField(max_length=50, verbose_name=_('Телефон для скарг та пропозицій'))
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
abstract = True
|
|
13
|
+
verbose_name = _('Організація харчування')
|
|
14
|
+
verbose_name_plural = _('Організація харчування')
|
|
15
|
+
ordering = ['id']
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
return self.company_name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CateringProvider(AbstractCateringProvider):
|
|
22
|
+
class Meta(AbstractCateringProvider.Meta):
|
|
23
|
+
abstract = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AbstractDailyMenu(models.Model):
|
|
27
|
+
AGE_GROUPS = [
|
|
28
|
+
('1_4', _('1-4 класи (початкова школа)')),
|
|
29
|
+
('5_9', _('5-9 класи (базова школа)')),
|
|
30
|
+
('10_11', _('10-11 класи (старша школа)')),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
menu_date = models.DateField(verbose_name=_('Дата меню'))
|
|
34
|
+
age_group = models.CharField(max_length=20, choices=AGE_GROUPS, default='1_4', verbose_name=_('Вікова група'))
|
|
35
|
+
breakfast = models.TextField(blank=True, verbose_name=_('Сніданок (страви, вихід у грамах)'))
|
|
36
|
+
lunch = models.TextField(blank=True, verbose_name=_('Обід (страви, вихід у грамах)'))
|
|
37
|
+
snack = models.TextField(blank=True, verbose_name=_('Полуденок / підвечірок'))
|
|
38
|
+
photo = models.ImageField(upload_to='nutrition/menus/%Y/%m/', blank=True, null=True, verbose_name=_('Фото меню з печаткою'))
|
|
39
|
+
|
|
40
|
+
class Meta:
|
|
41
|
+
abstract = True
|
|
42
|
+
verbose_name = _('Щоденне меню')
|
|
43
|
+
verbose_name_plural = _('Щоденні меню харчування')
|
|
44
|
+
ordering = ['-menu_date', 'age_group']
|
|
45
|
+
|
|
46
|
+
def __str__(self):
|
|
47
|
+
return f"Меню на {self.menu_date} ({self.get_age_group_display()})"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DailyMenu(AbstractDailyMenu):
|
|
51
|
+
class Meta(AbstractDailyMenu.Meta):
|
|
52
|
+
abstract = False
|
|
53
|
+
unique_together = ('menu_date', 'age_group')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from .models import CateringProvider, DailyMenu
|
|
3
|
+
|
|
4
|
+
class CateringProviderSerializer(serializers.ModelSerializer):
|
|
5
|
+
class Meta:
|
|
6
|
+
model = CateringProvider
|
|
7
|
+
fields = ['id', 'company_name', 'edrpou', 'contract_number', 'contact_person', 'phone']
|
|
8
|
+
|
|
9
|
+
class DailyMenuSerializer(serializers.ModelSerializer):
|
|
10
|
+
age_group_display = serializers.CharField(source='get_age_group_display', read_only=True)
|
|
11
|
+
|
|
12
|
+
class Meta:
|
|
13
|
+
model = DailyMenu
|
|
14
|
+
fields = [
|
|
15
|
+
'id', 'menu_date', 'age_group', 'age_group_display',
|
|
16
|
+
'breakfast', 'lunch', 'snack', 'photo'
|
|
17
|
+
]
|
apps/nutrition/tests.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from django.test import TestCase
|
|
3
|
+
from django.db import IntegrityError
|
|
4
|
+
from rest_framework.test import APIClient
|
|
5
|
+
from rest_framework import status
|
|
6
|
+
from .models import CateringProvider, DailyMenu
|
|
7
|
+
|
|
8
|
+
class NutritionTests(TestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
self.client = APIClient()
|
|
11
|
+
self.provider = CateringProvider.objects.create(
|
|
12
|
+
company_name='ТОВ "Якісне харчування"',
|
|
13
|
+
edrpou='87654321',
|
|
14
|
+
contract_number='№ 12-2026',
|
|
15
|
+
contact_person='Шевченко Ірина',
|
|
16
|
+
phone='+380 44 999-88-77'
|
|
17
|
+
)
|
|
18
|
+
self.menu = DailyMenu.objects.create(
|
|
19
|
+
menu_date=date.today(),
|
|
20
|
+
age_group='1_4',
|
|
21
|
+
breakfast='Каша з молоком',
|
|
22
|
+
lunch='Суп та пюре з рибою'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def test_provider_str(self):
|
|
26
|
+
self.assertEqual(str(self.provider), 'ТОВ "Якісне харчування"')
|
|
27
|
+
|
|
28
|
+
def test_daily_menu_unique_per_day_and_age_group(self):
|
|
29
|
+
with self.assertRaises(IntegrityError):
|
|
30
|
+
DailyMenu.objects.create(
|
|
31
|
+
menu_date=date.today(),
|
|
32
|
+
age_group='1_4',
|
|
33
|
+
breakfast='Дубльоване меню'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def test_nutrition_apis(self):
|
|
37
|
+
res_provider = self.client.get('/api/v1/nutrition/providers/')
|
|
38
|
+
self.assertEqual(res_provider.status_code, status.HTTP_200_OK)
|
|
39
|
+
|
|
40
|
+
res_menu = self.client.get('/api/v1/nutrition/daily-menus/')
|
|
41
|
+
self.assertEqual(res_menu.status_code, status.HTTP_200_OK)
|
apps/nutrition/urls.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from django.urls import path, include
|
|
2
|
+
from rest_framework.routers import DefaultRouter
|
|
3
|
+
from .views import CateringProviderViewSet, DailyMenuViewSet
|
|
4
|
+
|
|
5
|
+
router = DefaultRouter()
|
|
6
|
+
router.register(r'providers', CateringProviderViewSet, basename='nutrition-providers')
|
|
7
|
+
router.register(r'provider', CateringProviderViewSet, basename='nutrition-provider')
|
|
8
|
+
router.register(r'daily-menus', DailyMenuViewSet, basename='nutrition-daily-menus')
|
|
9
|
+
router.register(r'menu', DailyMenuViewSet, basename='nutrition-menu')
|
|
10
|
+
|
|
11
|
+
urlpatterns = [
|
|
12
|
+
path('', include(router.urls)),
|
|
13
|
+
]
|
apps/nutrition/views.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from rest_framework import viewsets, permissions
|
|
2
|
+
from .models import CateringProvider, DailyMenu
|
|
3
|
+
from .serializers import CateringProviderSerializer, DailyMenuSerializer
|
|
4
|
+
|
|
5
|
+
class CateringProviderViewSet(viewsets.ReadOnlyModelViewSet):
|
|
6
|
+
queryset = CateringProvider.objects.all()
|
|
7
|
+
serializer_class = CateringProviderSerializer
|
|
8
|
+
permission_classes = [permissions.AllowAny]
|
|
9
|
+
|
|
10
|
+
class DailyMenuViewSet(viewsets.ReadOnlyModelViewSet):
|
|
11
|
+
queryset = DailyMenu.objects.all()
|
|
12
|
+
serializer_class = DailyMenuSerializer
|
|
13
|
+
permission_classes = [permissions.AllowAny]
|
apps/safety/__init__.py
ADDED
|
File without changes
|
apps/safety/admin.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import AntiBullyingPolicy, ConfidentialReport
|
|
3
|
+
|
|
4
|
+
@admin.register(AntiBullyingPolicy)
|
|
5
|
+
class AntiBullyingPolicyAdmin(admin.ModelAdmin):
|
|
6
|
+
list_display = ('title', 'academic_year', 'is_active')
|
|
7
|
+
list_filter = ('is_active', 'academic_year')
|
|
8
|
+
|
|
9
|
+
@admin.register(ConfidentialReport)
|
|
10
|
+
class ConfidentialReportAdmin(admin.ModelAdmin):
|
|
11
|
+
list_display = ('id', 'reporter_role', 'created_at', 'is_reviewed')
|
|
12
|
+
list_filter = ('reporter_role', 'is_reviewed', 'created_at')
|
|
13
|
+
readonly_fields = ('reporter_role', 'incident_description', 'contact_feedback', 'created_at')
|
|
14
|
+
fieldsets = (
|
|
15
|
+
('Дані звернення (Конфіденційно)', {
|
|
16
|
+
'fields': ('reporter_role', 'incident_description', 'contact_feedback', 'created_at')
|
|
17
|
+
}),
|
|
18
|
+
('Опрацювання комісією закладу освіти', {
|
|
19
|
+
'fields': ('is_reviewed', 'review_notes')
|
|
20
|
+
}),
|
|
21
|
+
)
|
apps/safety/apps.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Generated by Django 5.1.15 on 2026-10-15 06:10
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
initial = True
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name='AntiBullyingPolicy',
|
|
16
|
+
fields=[
|
|
17
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
18
|
+
('title', models.CharField(max_length=255, verbose_name='Назва політики / наказу')),
|
|
19
|
+
('academic_year', models.CharField(max_length=50, verbose_name='Навчальний рік (наприклад: 2026/2027)')),
|
|
20
|
+
('behavior_rules_doc', models.FileField(upload_to='safety/rules/', verbose_name='Правила поведінки здобувача освіти в закладі')),
|
|
21
|
+
('application_procedure_doc', models.FileField(upload_to='safety/procedures/', verbose_name='Порядок подання та розгляду заяв про випадки булінгу (з конфіденційністю)')),
|
|
22
|
+
('reaction_procedure_doc', models.FileField(upload_to='safety/procedures/', verbose_name='Порядок реагування на доведені випадки булінгу та відповідальність')),
|
|
23
|
+
('action_plan_doc', models.FileField(upload_to='safety/plans/', verbose_name='План заходів щодо запобігання та протидії булінгу')),
|
|
24
|
+
('is_active', models.BooleanField(default=True, verbose_name='Чинна редакція')),
|
|
25
|
+
],
|
|
26
|
+
options={
|
|
27
|
+
'verbose_name': 'Політика та план протидії булінгу',
|
|
28
|
+
'verbose_name_plural': 'Політики та плани протидії булінгу',
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
migrations.CreateModel(
|
|
32
|
+
name='ConfidentialReport',
|
|
33
|
+
fields=[
|
|
34
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
35
|
+
('reporter_role', models.CharField(choices=[('student', 'Учень / учениця'), ('parent', 'Батьки / законні представники'), ('teacher', 'Педагогічний працівник'), ('anonymous', 'Анонімно')], default='anonymous', max_length=20, verbose_name='Статус заявника')),
|
|
36
|
+
('incident_description', models.TextField(verbose_name='Опис фактів та обставин випадку булінгу')),
|
|
37
|
+
('contact_feedback', models.CharField(blank=True, max_length=150, verbose_name='Контакт для зворотного зв’язку (email або телефон, за бажанням)')),
|
|
38
|
+
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата та час надсилання')),
|
|
39
|
+
('is_reviewed', models.BooleanField(default=False, verbose_name='Опрацьовано комісією закладу')),
|
|
40
|
+
('review_notes', models.TextField(blank=True, verbose_name='Службові примітки комісії')),
|
|
41
|
+
],
|
|
42
|
+
options={
|
|
43
|
+
'verbose_name': 'Конфіденційне звернення про булінг',
|
|
44
|
+
'verbose_name_plural': 'Конфіденційні звернення про булінг (конфіденційно)',
|
|
45
|
+
'ordering': ['-created_at'],
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
]
|
|
File without changes
|
apps/safety/models.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
class AbstractAntiBullyingPolicy(models.Model):
|
|
5
|
+
title = models.CharField(max_length=255, verbose_name=_('Назва політики / наказу'))
|
|
6
|
+
academic_year = models.CharField(max_length=50, verbose_name=_('Навчальний рік (наприклад: 2026/2027)'))
|
|
7
|
+
behavior_rules_doc = models.FileField(
|
|
8
|
+
upload_to='safety/rules/',
|
|
9
|
+
verbose_name=_('Правила поведінки здобувача освіти в закладі')
|
|
10
|
+
)
|
|
11
|
+
application_procedure_doc = models.FileField(
|
|
12
|
+
upload_to='safety/procedures/',
|
|
13
|
+
verbose_name=_('Порядок подання та розгляду заяв про випадки булінгу (з конфіденційністю)')
|
|
14
|
+
)
|
|
15
|
+
reaction_procedure_doc = models.FileField(
|
|
16
|
+
upload_to='safety/procedures/',
|
|
17
|
+
verbose_name=_('Порядок реагування на доведені випадки булінгу та відповідальність')
|
|
18
|
+
)
|
|
19
|
+
action_plan_doc = models.FileField(
|
|
20
|
+
upload_to='safety/plans/',
|
|
21
|
+
verbose_name=_('План заходів щодо запобігання та протидії булінгу')
|
|
22
|
+
)
|
|
23
|
+
is_active = models.BooleanField(default=True, verbose_name=_('Чинна редакція'))
|
|
24
|
+
|
|
25
|
+
class Meta:
|
|
26
|
+
abstract = True
|
|
27
|
+
verbose_name = _('Політика та план протидії булінгу')
|
|
28
|
+
verbose_name_plural = _('Політики та плани протидії булінгу')
|
|
29
|
+
|
|
30
|
+
def __str__(self):
|
|
31
|
+
return f"{self.title} ({self.academic_year})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AntiBullyingPolicy(AbstractAntiBullyingPolicy):
|
|
35
|
+
class Meta(AbstractAntiBullyingPolicy.Meta):
|
|
36
|
+
abstract = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AbstractConfidentialReport(models.Model):
|
|
40
|
+
REPORTER_ROLES = [
|
|
41
|
+
('student', _('Учень / учениця')),
|
|
42
|
+
('parent', _('Батьки / законні представники')),
|
|
43
|
+
('teacher', _('Педагогічний працівник')),
|
|
44
|
+
('anonymous', _('Анонімно')),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
reporter_role = models.CharField(max_length=20, choices=REPORTER_ROLES, default='anonymous', verbose_name=_('Статус заявника'))
|
|
48
|
+
incident_description = models.TextField(verbose_name=_('Опис фактів та обставин випадку булінгу'))
|
|
49
|
+
contact_feedback = models.CharField(
|
|
50
|
+
max_length=150,
|
|
51
|
+
blank=True,
|
|
52
|
+
verbose_name=_('Контакт для зворотного зв’язку (email або телефон, за бажанням)')
|
|
53
|
+
)
|
|
54
|
+
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Дата та час надсилання'))
|
|
55
|
+
is_reviewed = models.BooleanField(default=False, verbose_name=_('Опрацьовано комісією закладу'))
|
|
56
|
+
review_notes = models.TextField(blank=True, verbose_name=_('Службові примітки комісії'))
|
|
57
|
+
|
|
58
|
+
class Meta:
|
|
59
|
+
abstract = True
|
|
60
|
+
verbose_name = _('Конфіденційне звернення про булінг')
|
|
61
|
+
verbose_name_plural = _('Конфіденційні звернення про булінг (конфіденційно)')
|
|
62
|
+
ordering = ['-created_at']
|
|
63
|
+
|
|
64
|
+
def __str__(self):
|
|
65
|
+
return f"Звернення #{self.id} ({self.get_reporter_role_display()}) від {self.created_at.strftime('%d.%m.%Y')}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ConfidentialReport(AbstractConfidentialReport):
|
|
69
|
+
class Meta(AbstractConfidentialReport.Meta):
|
|
70
|
+
abstract = False
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from .models import AntiBullyingPolicy, ConfidentialReport
|
|
3
|
+
|
|
4
|
+
class AntiBullyingPolicySerializer(serializers.ModelSerializer):
|
|
5
|
+
class Meta:
|
|
6
|
+
model = AntiBullyingPolicy
|
|
7
|
+
fields = [
|
|
8
|
+
'id', 'title', 'academic_year', 'behavior_rules_doc',
|
|
9
|
+
'application_procedure_doc', 'reaction_procedure_doc',
|
|
10
|
+
'action_plan_doc', 'is_active'
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
class ConfidentialReportSerializer(serializers.ModelSerializer):
|
|
14
|
+
class Meta:
|
|
15
|
+
model = ConfidentialReport
|
|
16
|
+
fields = ['id', 'reporter_role', 'incident_description', 'contact_feedback', 'created_at']
|
|
17
|
+
read_only_fields = ['id', 'created_at']
|
apps/safety/tests.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
from rest_framework.test import APIClient
|
|
3
|
+
from .models import ConfidentialReport
|
|
4
|
+
|
|
5
|
+
class SafetyTests(TestCase):
|
|
6
|
+
def setUp(self):
|
|
7
|
+
self.client = APIClient()
|
|
8
|
+
|
|
9
|
+
def test_confidential_report_submission(self):
|
|
10
|
+
"""Перевірка можливості анонімного надсилання заяви про булінг через API"""
|
|
11
|
+
payload = {
|
|
12
|
+
'reporter_role': 'anonymous',
|
|
13
|
+
'incident_description': 'Тестове повідомлення про факт цькування в коридорі.',
|
|
14
|
+
'contact_feedback': 'test@example.com'
|
|
15
|
+
}
|
|
16
|
+
response = self.client.post('/api/v1/safety/reports/', payload, format='json')
|
|
17
|
+
self.assertEqual(response.status_code, 201)
|
|
18
|
+
self.assertEqual(ConfidentialReport.objects.count(), 1)
|
|
19
|
+
report = ConfidentialReport.objects.first()
|
|
20
|
+
self.assertFalse(report.is_reviewed)
|
apps/safety/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 AntiBullyingPolicyViewSet, ConfidentialReportViewSet
|
|
4
|
+
|
|
5
|
+
router = DefaultRouter()
|
|
6
|
+
router.register(r'policy', AntiBullyingPolicyViewSet, basename='safety-policy')
|
|
7
|
+
router.register(r'reports', ConfidentialReportViewSet, basename='safety-reports')
|
|
8
|
+
|
|
9
|
+
urlpatterns = [
|
|
10
|
+
path('', include(router.urls)),
|
|
11
|
+
]
|
apps/safety/views.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from rest_framework import viewsets, permissions, mixins
|
|
2
|
+
from .models import AntiBullyingPolicy, ConfidentialReport
|
|
3
|
+
from .serializers import AntiBullyingPolicySerializer, ConfidentialReportSerializer
|
|
4
|
+
|
|
5
|
+
class AntiBullyingPolicyViewSet(viewsets.ReadOnlyModelViewSet):
|
|
6
|
+
queryset = AntiBullyingPolicy.objects.filter(is_active=True)
|
|
7
|
+
serializer_class = AntiBullyingPolicySerializer
|
|
8
|
+
permission_classes = [permissions.AllowAny]
|
|
9
|
+
|
|
10
|
+
class ConfidentialReportViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
|
|
11
|
+
"""
|
|
12
|
+
Дозволяє будь-якому користувачеві (учень, батьки, вчитель) конфіденційно надіслати заяву.
|
|
13
|
+
Перегляд доступний виключно в адмін-панелі уповноваженою комісією.
|
|
14
|
+
"""
|
|
15
|
+
queryset = ConfidentialReport.objects.all()
|
|
16
|
+
serializer_class = ConfidentialReportSerializer
|
|
17
|
+
permission_classes = [permissions.AllowAny]
|
|
File without changes
|
apps/schedule/admin.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import BellSchedule, LessonSchedule
|
|
3
|
+
|
|
4
|
+
@admin.register(BellSchedule)
|
|
5
|
+
class BellScheduleAdmin(admin.ModelAdmin):
|
|
6
|
+
list_display = ('lesson_number', 'start_time', 'end_time', 'break_duration')
|
|
7
|
+
|
|
8
|
+
@admin.register(LessonSchedule)
|
|
9
|
+
class LessonScheduleAdmin(admin.ModelAdmin):
|
|
10
|
+
list_display = ('grade', 'day_of_week', 'lesson_number', 'subject', 'classroom', 'teacher_name')
|
|
11
|
+
list_filter = ('grade', 'day_of_week')
|
|
12
|
+
search_fields = ('subject', 'teacher_name', 'classroom')
|
apps/schedule/apps.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
('admissions', '0001_initial'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name='BellSchedule',
|
|
18
|
+
fields=[
|
|
19
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
20
|
+
('lesson_number', models.PositiveSmallIntegerField(unique=True, verbose_name='Номер уроку')),
|
|
21
|
+
('start_time', models.TimeField(verbose_name='Початок уроку')),
|
|
22
|
+
('end_time', models.TimeField(verbose_name='Кінець уроку')),
|
|
23
|
+
('break_duration', models.PositiveSmallIntegerField(default=10, verbose_name='Тривалість перерви (хв)')),
|
|
24
|
+
],
|
|
25
|
+
options={
|
|
26
|
+
'verbose_name': 'Розклад дзвінка',
|
|
27
|
+
'verbose_name_plural': 'Розклад дзвінків',
|
|
28
|
+
'ordering': ['lesson_number'],
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
migrations.CreateModel(
|
|
32
|
+
name='LessonSchedule',
|
|
33
|
+
fields=[
|
|
34
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
35
|
+
('day_of_week', models.PositiveSmallIntegerField(choices=[(1, 'Понеділок'), (2, 'Вівторок'), (3, 'Середа'), (4, 'Четвер'), (5, 'П’ятниця')], verbose_name='День тижня')),
|
|
36
|
+
('lesson_number', models.PositiveSmallIntegerField(verbose_name='Номер уроку')),
|
|
37
|
+
('subject', models.CharField(max_length=150, verbose_name='Предмет / навчальний курс')),
|
|
38
|
+
('classroom', models.CharField(blank=True, max_length=50, verbose_name='Кабінет')),
|
|
39
|
+
('teacher_name', models.CharField(blank=True, max_length=150, verbose_name='Вчитель')),
|
|
40
|
+
('grade', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='admissions.classcapacity', verbose_name='Клас')),
|
|
41
|
+
],
|
|
42
|
+
options={
|
|
43
|
+
'verbose_name': 'Урок у розкладі',
|
|
44
|
+
'verbose_name_plural': 'Розклад уроків',
|
|
45
|
+
'ordering': ['grade', 'day_of_week', 'lesson_number'],
|
|
46
|
+
'unique_together': {('grade', 'day_of_week', 'lesson_number')},
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
]
|
|
File without changes
|
apps/schedule/models.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from apps.admissions.models import ClassCapacity
|
|
4
|
+
|
|
5
|
+
class AbstractBellSchedule(models.Model):
|
|
6
|
+
lesson_number = models.PositiveSmallIntegerField(unique=True, verbose_name=_('Номер уроку'))
|
|
7
|
+
start_time = models.TimeField(verbose_name=_('Початок уроку'))
|
|
8
|
+
end_time = models.TimeField(verbose_name=_('Кінець уроку'))
|
|
9
|
+
break_duration = models.PositiveSmallIntegerField(default=10, verbose_name=_('Тривалість перерви (хв)'))
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
abstract = True
|
|
13
|
+
verbose_name = _('Розклад дзвінка')
|
|
14
|
+
verbose_name_plural = _('Розклад дзвінків')
|
|
15
|
+
ordering = ['lesson_number']
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
return f"{self.lesson_number} урок: {self.start_time.strftime('%H:%M')} - {self.end_time.strftime('%H:%M')}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BellSchedule(AbstractBellSchedule):
|
|
22
|
+
class Meta(AbstractBellSchedule.Meta):
|
|
23
|
+
abstract = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AbstractLessonSchedule(models.Model):
|
|
27
|
+
DAYS_OF_WEEK = [
|
|
28
|
+
(1, _('Понеділок')),
|
|
29
|
+
(2, _('Вівторок')),
|
|
30
|
+
(3, _('Середа')),
|
|
31
|
+
(4, _('Четвер')),
|
|
32
|
+
(5, _('П’ятниця')),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
day_of_week = models.PositiveSmallIntegerField(choices=DAYS_OF_WEEK, verbose_name=_('День тижня'))
|
|
36
|
+
lesson_number = models.PositiveSmallIntegerField(verbose_name=_('Номер уроку'))
|
|
37
|
+
subject = models.CharField(max_length=150, verbose_name=_('Предмет / навчальний курс'))
|
|
38
|
+
classroom = models.CharField(max_length=50, blank=True, verbose_name=_('Кабінет'))
|
|
39
|
+
teacher_name = models.CharField(max_length=150, blank=True, verbose_name=_('Вчитель'))
|
|
40
|
+
|
|
41
|
+
class Meta:
|
|
42
|
+
abstract = True
|
|
43
|
+
verbose_name = _('Урок у розкладі')
|
|
44
|
+
verbose_name_plural = _('Розклад уроків')
|
|
45
|
+
ordering = ['day_of_week', 'lesson_number']
|
|
46
|
+
|
|
47
|
+
def __str__(self):
|
|
48
|
+
return f"{self.get_day_of_week_display()} #{self.lesson_number} {self.subject}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LessonSchedule(AbstractLessonSchedule):
|
|
52
|
+
grade = models.ForeignKey(ClassCapacity, on_delete=models.CASCADE, related_name='lessons', verbose_name=_('Клас'))
|
|
53
|
+
|
|
54
|
+
class Meta(AbstractLessonSchedule.Meta):
|
|
55
|
+
abstract = False
|
|
56
|
+
ordering = ['grade', 'day_of_week', 'lesson_number']
|
|
57
|
+
unique_together = ('grade', 'day_of_week', 'lesson_number')
|
|
58
|
+
|
|
59
|
+
def __str__(self):
|
|
60
|
+
return f"{self.grade.grade_name} | {self.get_day_of_week_display()} #{self.lesson_number} {self.subject}"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from .models import BellSchedule, LessonSchedule
|
|
3
|
+
|
|
4
|
+
class BellScheduleSerializer(serializers.ModelSerializer):
|
|
5
|
+
class Meta:
|
|
6
|
+
model = BellSchedule
|
|
7
|
+
fields = ['id', 'lesson_number', 'start_time', 'end_time', 'break_duration']
|
|
8
|
+
|
|
9
|
+
class LessonScheduleSerializer(serializers.ModelSerializer):
|
|
10
|
+
day_name = serializers.CharField(source='get_day_of_week_display', read_only=True)
|
|
11
|
+
grade_name = serializers.CharField(source='grade.grade_name', read_only=True)
|
|
12
|
+
|
|
13
|
+
class Meta:
|
|
14
|
+
model = LessonSchedule
|
|
15
|
+
fields = ['id', 'grade', 'grade_name', 'day_of_week', 'day_name', 'lesson_number', 'subject', 'classroom', 'teacher_name']
|