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/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
@@ -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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+ class NutritionConfig(AppConfig):
4
+ default_auto_field = 'django.db.models.BigAutoField'
5
+ name = 'apps.nutrition'
6
+ verbose_name = 'Харчування та щоденне меню'
@@ -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
@@ -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
+ ]
@@ -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
+ ]
@@ -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]
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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+ class SafetyConfig(AppConfig):
4
+ default_auto_field = 'django.db.models.BigAutoField'
5
+ name = 'apps.safety'
6
+ verbose_name = 'Безпека та протидія булінгу'
@@ -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,6 @@
1
+ from django.apps import AppConfig
2
+
3
+ class ScheduleConfig(AppConfig):
4
+ default_auto_field = 'django.db.models.BigAutoField'
5
+ name = 'apps.schedule'
6
+ verbose_name = 'Розклад занять та дзвінків'
@@ -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
@@ -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']