django-school-framework 0.2.2__tar.gz → 0.2.4__tar.gz

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 (103) hide show
  1. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/PKG-INFO +1 -1
  2. django_school_framework-0.2.4/apps/core/apps.py +20 -0
  3. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/cli.py +211 -51
  4. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/serializers.py +2 -1
  5. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/models.py +1 -0
  6. django_school_framework-0.2.4/apps/safety/tests.py +41 -0
  7. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/views.py +1 -0
  8. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/django_school_framework.egg-info/PKG-INFO +1 -1
  9. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/pyproject.toml +1 -1
  10. django_school_framework-0.2.2/apps/core/apps.py +0 -6
  11. django_school_framework-0.2.2/apps/safety/tests.py +0 -20
  12. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/README.md +0 -0
  13. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/__init__.py +0 -0
  14. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/__init__.py +0 -0
  15. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/admin.py +0 -0
  16. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/apps.py +0 -0
  17. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/migrations/0001_initial.py +0 -0
  18. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/migrations/__init__.py +0 -0
  19. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/models.py +0 -0
  20. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/serializers.py +0 -0
  21. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/tests.py +0 -0
  22. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/urls.py +0 -0
  23. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/admissions/views.py +0 -0
  24. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/__init__.py +0 -0
  25. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/admin.py +0 -0
  26. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/compliance.py +0 -0
  27. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/management/__init__.py +0 -0
  28. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/management/commands/__init__.py +0 -0
  29. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/management/commands/seed_school_data.py +0 -0
  30. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/management/commands/wait_for_db.py +0 -0
  31. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/migrations/0001_initial.py +0 -0
  32. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/migrations/0002_alter_schoolprofile_options.py +0 -0
  33. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/migrations/__init__.py +0 -0
  34. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/models.py +0 -0
  35. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/serializers.py +0 -0
  36. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/templatetags/__init__.py +0 -0
  37. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/templatetags/compliance_tags.py +0 -0
  38. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/tests.py +0 -0
  39. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/urls.py +0 -0
  40. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/core/views.py +0 -0
  41. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/__init__.py +0 -0
  42. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/admin.py +0 -0
  43. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/apps.py +0 -0
  44. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/migrations/0001_initial.py +0 -0
  45. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/migrations/__init__.py +0 -0
  46. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/models.py +0 -0
  47. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/serializers.py +0 -0
  48. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/tests.py +0 -0
  49. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/urls.py +0 -0
  50. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/gallery/views.py +0 -0
  51. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/__init__.py +0 -0
  52. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/admin.py +0 -0
  53. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/apps.py +0 -0
  54. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/migrations/0001_initial.py +0 -0
  55. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/migrations/__init__.py +0 -0
  56. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/models.py +0 -0
  57. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/tests.py +0 -0
  58. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/urls.py +0 -0
  59. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/legal/views.py +0 -0
  60. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/__init__.py +0 -0
  61. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/admin.py +0 -0
  62. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/apps.py +0 -0
  63. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/migrations/0001_initial.py +0 -0
  64. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/migrations/__init__.py +0 -0
  65. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/models.py +0 -0
  66. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/serializers.py +0 -0
  67. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/tests.py +0 -0
  68. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/urls.py +0 -0
  69. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/news/views.py +0 -0
  70. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/__init__.py +0 -0
  71. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/admin.py +0 -0
  72. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/apps.py +0 -0
  73. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/migrations/0001_initial.py +0 -0
  74. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/migrations/0002_alter_cateringprovider_options.py +0 -0
  75. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/migrations/__init__.py +0 -0
  76. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/models.py +0 -0
  77. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/serializers.py +0 -0
  78. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/tests.py +0 -0
  79. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/urls.py +0 -0
  80. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/nutrition/views.py +0 -0
  81. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/__init__.py +0 -0
  82. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/admin.py +0 -0
  83. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/apps.py +0 -0
  84. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/migrations/0001_initial.py +0 -0
  85. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/migrations/__init__.py +0 -0
  86. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/serializers.py +0 -0
  87. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/safety/urls.py +0 -0
  88. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/__init__.py +0 -0
  89. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/admin.py +0 -0
  90. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/apps.py +0 -0
  91. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/migrations/0001_initial.py +0 -0
  92. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/migrations/__init__.py +0 -0
  93. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/models.py +0 -0
  94. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/serializers.py +0 -0
  95. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/tests.py +0 -0
  96. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/urls.py +0 -0
  97. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/apps/schedule/views.py +0 -0
  98. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/django_school_framework.egg-info/SOURCES.txt +0 -0
  99. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/django_school_framework.egg-info/dependency_links.txt +0 -0
  100. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/django_school_framework.egg-info/entry_points.txt +0 -0
  101. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/django_school_framework.egg-info/requires.txt +0 -0
  102. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/django_school_framework.egg-info/top_level.txt +0 -0
  103. {django_school_framework-0.2.2 → django_school_framework-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-school-framework
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Відкритий веб-фреймворк для офіційних сайтів закладів загальної середньої освіти України (ст. 30 ЗУ «Про освіту»)
5
5
  License: MIT
6
6
  Classifier: Framework :: Django
@@ -0,0 +1,20 @@
1
+ from django.apps import AppConfig
2
+
3
+ class CoreConfig(AppConfig):
4
+ default_auto_field = 'django.db.models.BigAutoField'
5
+ name = 'apps.core'
6
+ verbose_name = 'Загальні відомості та налаштування школи'
7
+
8
+ def ready(self):
9
+ import sys
10
+ if sys.platform == 'win32':
11
+ if hasattr(sys.stdout, 'reconfigure'):
12
+ try:
13
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
14
+ except Exception:
15
+ pass
16
+ if hasattr(sys.stderr, 'reconfigure'):
17
+ try:
18
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
19
+ except Exception:
20
+ pass
@@ -9,6 +9,33 @@ import os
9
9
  import argparse
10
10
  from pathlib import Path
11
11
 
12
+ # Windows UTF-8 console & ANSI color configuration
13
+ def configure_windows_encoding():
14
+ """Налаштування UTF-8 кодування для Windows консолі та системного виводу."""
15
+ if sys.platform == "win32":
16
+ try:
17
+ import os
18
+ os.system("") # Вмикає підтримку ANSI escape-послідовностей у Windows 10/11
19
+ except Exception:
20
+ pass
21
+ if hasattr(sys.stdout, "reconfigure"):
22
+ try:
23
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
24
+ except Exception:
25
+ pass
26
+ if hasattr(sys.stderr, "reconfigure"):
27
+ try:
28
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
29
+ except Exception:
30
+ pass
31
+ if hasattr(sys.stdin, "reconfigure"):
32
+ try:
33
+ sys.stdin.reconfigure(encoding="utf-8", errors="replace")
34
+ except Exception:
35
+ pass
36
+
37
+ configure_windows_encoding()
38
+
12
39
  # ANSI colors
13
40
  GREEN = "\033[92m"
14
41
  YELLOW = "\033[93m"
@@ -64,6 +91,18 @@ import os
64
91
  import sys
65
92
 
66
93
  def main():
94
+ if sys.platform == 'win32':
95
+ if hasattr(sys.stdout, 'reconfigure'):
96
+ try:
97
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
98
+ except Exception:
99
+ pass
100
+ if hasattr(sys.stderr, 'reconfigure'):
101
+ try:
102
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
103
+ except Exception:
104
+ pass
105
+
67
106
  os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
68
107
  try:
69
108
  from django.core.management import execute_from_command_line
@@ -76,8 +115,11 @@ def main():
76
115
  if __name__ == '__main__':
77
116
  main()
78
117
  """
79
- (output_dir / "manage.py").write_text(manage_py_content)
80
- (output_dir / "manage.py").chmod(0o755)
118
+ (output_dir / "manage.py").write_text(manage_py_content, encoding="utf-8")
119
+ try:
120
+ (output_dir / "manage.py").chmod(0o755)
121
+ except OSError:
122
+ pass
81
123
 
82
124
  # 2. config/settings.py
83
125
  settings_content = f"""import os
@@ -148,7 +190,7 @@ WSGI_APPLICATION = 'config.wsgi.application'
148
190
  DATABASES = {{
149
191
  'default': {{
150
192
  'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'),
151
- 'NAME': BASE_DIR / 'db.sqlite3' if os.environ.get('DB_ENGINE', '').endswith('sqlite3') else os.environ.get('DB_NAME', '{project_name}_db'),
193
+ 'NAME': BASE_DIR / 'db.sqlite3' if 'sqlite' in os.environ.get('DB_ENGINE', 'sqlite') else os.environ.get('DB_NAME', '{project_name}_db'),
152
194
  }}
153
195
  }}
154
196
 
@@ -176,18 +218,23 @@ if DEBUG:
176
218
  CSRF_TRUSTED_ORIGINS.append(f'http://localhost:{{port}}')
177
219
  CSRF_TRUSTED_ORIGINS.append(f'http://127.0.0.1:{{port}}')
178
220
  """
179
- (output_dir / "config" / "settings.py").write_text(settings_content)
221
+ (output_dir / "config" / "settings.py").write_text(settings_content, encoding="utf-8")
180
222
 
181
223
  # 3. config/urls.py
182
224
  urls_content = """from django.contrib import admin
183
225
  from django.urls import path, include
184
226
  from django.conf import settings
227
+ from django.conf.urls.static import static
185
228
  from django.views.generic import RedirectView
229
+ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
186
230
  from apps.core.views import PublicSchoolHomeView
187
231
 
188
232
  urlpatterns = [
189
233
  path('', PublicSchoolHomeView.as_view(), name='home'),
190
234
  path('admin/', admin.site.urls),
235
+ path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
236
+ path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
237
+ path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
191
238
  path('api/v1/core/', include('apps.core.urls')),
192
239
  path('api/v1/legal/', include('apps.legal.urls')),
193
240
  path('api/v1/admissions/', include('apps.admissions.urls')),
@@ -200,8 +247,9 @@ urlpatterns = [
200
247
 
201
248
  if settings.DEBUG:
202
249
  urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
250
+ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
203
251
  """
204
- (output_dir / "config" / "urls.py").write_text(urls_content)
252
+ (output_dir / "config" / "urls.py").write_text(urls_content, encoding="utf-8")
205
253
 
206
254
  # 4. config/__init__.py, wsgi.py, asgi.py
207
255
  init_content = """# Django School Framework Project
@@ -224,25 +272,25 @@ try:
224
272
  except Exception:
225
273
  pass
226
274
  """
227
- (output_dir / "config" / "__init__.py").write_text(init_content)
275
+ (output_dir / "config" / "__init__.py").write_text(init_content, encoding="utf-8")
228
276
 
229
277
  wsgi_content = """import os
230
278
  from django.core.wsgi import get_wsgi_application
231
279
  os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
232
280
  application = get_wsgi_application()
233
281
  """
234
- (output_dir / "config" / "wsgi.py").write_text(wsgi_content)
282
+ (output_dir / "config" / "wsgi.py").write_text(wsgi_content, encoding="utf-8")
235
283
 
236
284
  # 5. .env.example
237
285
  env_example = f"""DEBUG=True
238
286
  DJANGO_SECRET_KEY=change-me-in-production
239
287
  DB_ENGINE=django.db.backends.sqlite3
240
- DB_NAME={project_name}_db
288
+ DB_NAME=db.sqlite3
241
289
  DB_HOST_PORT=3307
242
290
  BACKEND_HOST_PORT=8000
243
291
  FRONTEND_HOST_PORT=5173
244
292
  """
245
- (output_dir / ".env.example").write_text(env_example)
293
+ (output_dir / ".env.example").write_text(env_example, encoding="utf-8")
246
294
 
247
295
  # 6. README.md
248
296
  readme_content = f"""# {project_name} — Офіційний вебсайт закладу освіти
@@ -269,7 +317,7 @@ school-admin check-compliance
269
317
  python manage.py runserver
270
318
  ```
271
319
  """
272
- (output_dir / "README.md").write_text(readme_content)
320
+ (output_dir / "README.md").write_text(readme_content, encoding="utf-8")
273
321
 
274
322
  print(f"\n{GREEN}{BOLD}✓ Проєкт закладу освіти успішно створено у {output_dir}!{RESET}\n")
275
323
  print(f"Наступні кроки:")
@@ -439,7 +487,7 @@ def cmd_create_frontend(args):
439
487
  }
440
488
  }
441
489
  """
442
- (target_dir / "package.json").write_text(pkg_json)
490
+ (target_dir / "package.json").write_text(pkg_json, encoding="utf-8")
443
491
 
444
492
  # 2. vite.config.ts
445
493
  backend_url = args.api_url or "http://127.0.0.1:8000"
@@ -474,7 +522,7 @@ export default defineConfig({{
474
522
  }},
475
523
  }});
476
524
  """
477
- (target_dir / "vite.config.ts").write_text(vite_conf)
525
+ (target_dir / "vite.config.ts").write_text(vite_conf, encoding="utf-8")
478
526
 
479
527
  # 3. index.html
480
528
  index_html = """<!doctype html>
@@ -490,7 +538,7 @@ export default defineConfig({{
490
538
  </body>
491
539
  </html>
492
540
  """
493
- (target_dir / "index.html").write_text(index_html)
541
+ (target_dir / "index.html").write_text(index_html, encoding="utf-8")
494
542
 
495
543
  # 4. tsconfig.json
496
544
  tsconfig = """{
@@ -511,7 +559,7 @@ export default defineConfig({{
511
559
  "include": ["src"]
512
560
  }
513
561
  """
514
- (target_dir / "tsconfig.json").write_text(tsconfig)
562
+ (target_dir / "tsconfig.json").write_text(tsconfig, encoding="utf-8")
515
563
 
516
564
  # 5. tailwind & postcss
517
565
  tailwind_conf = """/** @type {import('tailwindcss').Config} */
@@ -523,7 +571,7 @@ export default {
523
571
  plugins: [],
524
572
  };
525
573
  """
526
- (target_dir / "tailwind.config.js").write_text(tailwind_conf)
574
+ (target_dir / "tailwind.config.js").write_text(tailwind_conf, encoding="utf-8")
527
575
 
528
576
  postcss_conf = """export default {
529
577
  plugins: {
@@ -532,7 +580,7 @@ export default {
532
580
  },
533
581
  };
534
582
  """
535
- (target_dir / "postcss.config.js").write_text(postcss_conf)
583
+ (target_dir / "postcss.config.js").write_text(postcss_conf, encoding="utf-8")
536
584
 
537
585
  # 6. src/index.css
538
586
  index_css = """@tailwind base;
@@ -551,7 +599,7 @@ body.contrast-bw {
551
599
  color: #000000 !important;
552
600
  }
553
601
  """
554
- (src_dir / "index.css").write_text(index_css)
602
+ (src_dir / "index.css").write_text(index_css, encoding="utf-8")
555
603
 
556
604
  # 7. src/main.tsx
557
605
  main_tsx = """import React from 'react';
@@ -565,7 +613,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
565
613
  </React.StrictMode>
566
614
  );
567
615
  """
568
- (src_dir / "main.tsx").write_text(main_tsx)
616
+ (src_dir / "main.tsx").write_text(main_tsx, encoding="utf-8")
569
617
 
570
618
  # 8. src/App.tsx
571
619
  app_tsx = """import React, { useState, useEffect } from 'react';
@@ -590,10 +638,12 @@ export default function App() {
590
638
  const [newsList, setNewsList] = useState<any[]>([]);
591
639
  const [selectedNews, setSelectedNews] = useState<any | null>(null);
592
640
  const [bells, setBells] = useState<any[]>([]);
641
+ const [cateringProvider, setCateringProvider] = useState<any | null>(null);
593
642
 
594
643
  // Bullying report form
595
644
  const [incidentText, setIncidentText] = useState('');
596
645
  const [reporterRole, setReporterRole] = useState('student');
646
+ const [reportStatus, setReportStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
597
647
  const [reportFeedback, setReportFeedback] = useState<string | null>(null);
598
648
 
599
649
  useEffect(() => {
@@ -633,6 +683,14 @@ export default function App() {
633
683
  .then(d => setMenus(d.results || d))
634
684
  .catch(() => {});
635
685
 
686
+ fetch('/api/v1/nutrition/providers/')
687
+ .then(r => r.json())
688
+ .then(d => {
689
+ const items = d.results || d;
690
+ if (items.length > 0) setCateringProvider(items[0]);
691
+ })
692
+ .catch(() => {});
693
+
636
694
  fetch('/api/v1/safety/policy/')
637
695
  .then(r => r.json())
638
696
  .then(d => {
@@ -666,9 +724,17 @@ export default function App() {
666
724
  const handleReportSubmit = (e: React.FormEvent) => {
667
725
  e.preventDefault();
668
726
  if (!incidentText.trim()) return;
727
+ setReportStatus('loading');
728
+ setReportFeedback(null);
729
+
730
+ const csrfMatch = document.cookie.match(/csrftoken=([^;]+)/);
731
+ const csrfToken = csrfMatch ? csrfMatch[1] : '';
732
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
733
+ if (csrfToken) headers['X-CSRFToken'] = csrfToken;
734
+
669
735
  fetch('/api/v1/safety/reports/', {
670
736
  method: 'POST',
671
- headers: { 'Content-Type': 'application/json' },
737
+ headers,
672
738
  body: JSON.stringify({
673
739
  reporter_role: reporterRole,
674
740
  incident_description: incidentText,
@@ -676,15 +742,17 @@ export default function App() {
676
742
  })
677
743
  .then(res => {
678
744
  if (res.ok) {
745
+ setReportStatus('success');
679
746
  setReportFeedback('Дякуємо! Ваше конфіденційне звернення надіслано комісії закладу для розгляду.');
680
747
  setIncidentText('');
681
748
  } else {
749
+ setReportStatus('error');
682
750
  setReportFeedback('Помилка при відправці. Будь ласка, зверніться до чергового адміністратора.');
683
751
  }
684
752
  })
685
753
  .catch(() => {
686
- setReportFeedback('Звернення зафіксовано. Дякуємо за небайдужість.');
687
- setIncidentText('');
754
+ setReportStatus('error');
755
+ setReportFeedback('Помилка з\'єднання. Будь ласка, зверніться до чергового адміністратора.');
688
756
  });
689
757
  };
690
758
 
@@ -804,18 +872,19 @@ export default function App() {
804
872
  <div>
805
873
  <div className="font-semibold text-slate-900">{doc.title}</div>
806
874
  <div className="text-xs text-slate-500">
807
- Категорія: {doc.category} &bull; Оприлюднено: {doc.published_date || doc.updated_at?.split('T')[0]}
875
+ {doc.category_name && <span>Категорія: {doc.category_name} &bull; </span>}
876
+ Оприлюднено: {doc.publish_date || doc.published_date || doc.approval_date || ''}
808
877
  </div>
809
878
  </div>
810
879
  </div>
811
- {doc.file && (
880
+ {(doc.file || doc.external_url) && (
812
881
  <a
813
- href={doc.file}
882
+ href={doc.file || doc.external_url}
814
883
  target="_blank"
815
884
  rel="noreferrer"
816
885
  className="px-3 py-1.5 text-xs font-semibold text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg flex items-center gap-1"
817
886
  >
818
- <Download className="w-3.5 h-3.5" /> Завантажити
887
+ <Download className="w-3.5 h-3.5" /> {doc.file ? 'Завантажити' : 'Відкрити'}
819
888
  </a>
820
889
  )}
821
890
  </div>
@@ -848,8 +917,7 @@ export default function App() {
848
917
  <tr className="border-b border-slate-200 text-slate-500 text-xs uppercase">
849
918
  <th className="py-3 px-2">Звітний період</th>
850
919
  <th className="py-3 px-2">Тип звіту</th>
851
- <th className="py-3 px-2">Надходження (грн)</th>
852
- <th className="py-3 px-2">Видатки (грн)</th>
920
+ <th className="py-3 px-2">Сума коштів (грн)</th>
853
921
  <th className="py-3 px-2">Контроль 10 днів</th>
854
922
  <th className="py-3 px-2 text-right">Файл</th>
855
923
  </tr>
@@ -857,10 +925,9 @@ export default function App() {
857
925
  <tbody className="divide-y divide-slate-100">
858
926
  {finances.map((f: any) => (
859
927
  <tr key={f.id} className="hover:bg-slate-50">
860
- <td className="py-3 px-2 font-bold text-slate-900">{f.year} рік</td>
861
- <td className="py-3 px-2">{f.report_type}</td>
862
- <td className="py-3 px-2 font-mono text-emerald-700">{Number(f.total_revenue || 0).toLocaleString()} ₴</td>
863
- <td className="py-3 px-2 font-mono text-slate-800">{Number(f.total_expenses || 0).toLocaleString()} ₴</td>
928
+ <td className="py-3 px-2 font-bold text-slate-900">{f.period || f.title}</td>
929
+ <td className="py-3 px-2">{f.report_type_display || f.report_type}</td>
930
+ <td className="py-3 px-2 font-mono text-emerald-700">{Number(f.total_amount || 0).toLocaleString()} ₴</td>
864
931
  <td className="py-3 px-2">
865
932
  {f.is_within_10_days ? (
866
933
  <span className="inline-flex items-center gap-1 text-xs text-emerald-700 bg-emerald-50 px-2 py-0.5 rounded font-medium">
@@ -873,8 +940,8 @@ export default function App() {
873
940
  )}
874
941
  </td>
875
942
  <td className="py-3 px-2 text-right">
876
- {f.document_file && (
877
- <a href={f.document_file} target="_blank" rel="noreferrer" className="text-blue-600 hover:underline text-xs font-semibold">
943
+ {(f.file || f.document_file) && (
944
+ <a href={f.file || f.document_file} target="_blank" rel="noreferrer" className="text-blue-600 hover:underline text-xs font-semibold">
878
945
  PDF
879
946
  </a>
880
947
  )}
@@ -904,10 +971,10 @@ export default function App() {
904
971
  ) : (
905
972
  capacities.map((c: any) => (
906
973
  <div key={c.id} className="p-3 flex justify-between items-center text-sm">
907
- <span className="font-semibold text-slate-800">{c.grade_level}-й клас</span>
908
- <span className="text-xs text-slate-500">Фактично: {c.actual_students}</span>
974
+ <span className="font-semibold text-slate-800">{c.grade_name || (c.grade_level ? `${c.grade_level}-й клас` : 'Клас')}</span>
975
+ <span className="text-xs text-slate-500">Фактично: {c.enrolled_count ?? c.actual_students ?? 0}</span>
909
976
  <span className="font-bold text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded text-xs">
910
- Вільних: {c.available_seats || (c.max_capacity - c.actual_students)}
977
+ Вільних: {c.available_slots ?? (c.total_capacity && c.enrolled_count !== undefined ? c.total_capacity - c.enrolled_count : 0)}
911
978
  </span>
912
979
  </div>
913
980
  ))
@@ -938,23 +1005,77 @@ export default function App() {
938
1005
 
939
1006
  {activeTab === 'nutrition' && (
940
1007
  <div>
941
- <h2 className="text-xl font-bold text-slate-900 mb-2">Харчування здобувачів освіти</h2>
942
- <p className="text-slate-500 text-sm mb-6">Щоденне меню відповідно до Постанови КМУ № 305 та стандартів НАССР</p>
1008
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-2 mb-6">
1009
+ <div>
1010
+ <h2 className="text-xl font-bold text-slate-900 mb-1">Харчування здобувачів освіти</h2>
1011
+ <p className="text-slate-500 text-sm">Щоденне меню відповідно до Постанови КМУ № 305 та стандартів НАССР</p>
1012
+ </div>
1013
+ {cateringProvider && (
1014
+ <div className="text-xs bg-amber-50 text-amber-900 border border-amber-200 rounded-lg p-2.5">
1015
+ <span className="font-semibold">Постачальник:</span> {cateringProvider.company_name}
1016
+ {cateringProvider.phone && <span className="ml-2 font-mono">тел. {cateringProvider.phone}</span>}
1017
+ </div>
1018
+ )}
1019
+ </div>
943
1020
 
944
1021
  {menus.length === 0 ? (
945
- <div className="p-8 text-center text-slate-500 bg-slate-50 rounded-xl">
1022
+ <div className="p-8 text-center text-slate-500 bg-slate-50 rounded-xl border border-slate-200">
946
1023
  Меню на поточний тиждень завантажується черговою медсестрою/оператором харчування.
947
1024
  </div>
948
1025
  ) : (
949
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1026
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
950
1027
  {menus.map((m: any) => (
951
- <div key={m.id} className="p-4 rounded-xl border border-slate-200 bg-slate-50">
952
- <div className="flex justify-between items-center mb-2">
953
- <span className="font-bold text-slate-900">{m.menu_date}</span>
954
- <span className="text-xs px-2 py-0.5 bg-blue-100 text-blue-800 rounded font-semibold">{m.age_group}</span>
1028
+ <div key={m.id} className="p-5 rounded-2xl border border-slate-200 bg-white shadow-sm flex flex-col justify-between">
1029
+ <div>
1030
+ <div className="flex justify-between items-center pb-3 mb-3 border-b border-slate-100">
1031
+ <span className="font-bold text-base text-slate-900">{m.menu_date}</span>
1032
+ <span className="text-xs px-2.5 py-1 bg-blue-50 text-blue-700 rounded-full font-semibold border border-blue-100">
1033
+ {m.age_group_display || m.age_group}
1034
+ </span>
1035
+ </div>
1036
+
1037
+ <div className="space-y-3">
1038
+ {m.breakfast && (
1039
+ <div>
1040
+ <div className="text-xs font-bold text-amber-700 uppercase tracking-wide mb-1">🍳 Сніданок (страви, вихід у грамах):</div>
1041
+ <div className="text-xs text-slate-800 bg-slate-50 p-2.5 rounded-lg border border-slate-100 whitespace-pre-line leading-relaxed">
1042
+ {m.breakfast}
1043
+ </div>
1044
+ </div>
1045
+ )}
1046
+
1047
+ {m.lunch && (
1048
+ <div>
1049
+ <div className="text-xs font-bold text-emerald-700 uppercase tracking-wide mb-1">🍲 Обід (страви, вихід у грамах):</div>
1050
+ <div className="text-xs text-slate-800 bg-slate-50 p-2.5 rounded-lg border border-slate-100 whitespace-pre-line leading-relaxed">
1051
+ {m.lunch}
1052
+ </div>
1053
+ </div>
1054
+ )}
1055
+
1056
+ {m.snack && (
1057
+ <div>
1058
+ <div className="text-xs font-bold text-violet-700 uppercase tracking-wide mb-1">🍎 Полуденок / підвечірок:</div>
1059
+ <div className="text-xs text-slate-800 bg-slate-50 p-2.5 rounded-lg border border-slate-100 whitespace-pre-line leading-relaxed">
1060
+ {m.snack}
1061
+ </div>
1062
+ </div>
1063
+ )}
1064
+ </div>
955
1065
  </div>
956
- <div className="text-xs text-slate-600 mb-2"><strong>Прийом їжі:</strong> {m.meal_type}</div>
957
- <div className="text-sm text-slate-800 bg-white p-3 rounded-lg border border-slate-200">{m.dishes_description || m.items}</div>
1066
+
1067
+ {m.photo && (
1068
+ <div className="mt-4 pt-3 border-t border-slate-100">
1069
+ <a
1070
+ href={m.photo}
1071
+ target="_blank"
1072
+ rel="noreferrer"
1073
+ className="inline-flex items-center gap-1.5 text-xs font-semibold text-blue-600 hover:text-blue-800 hover:underline"
1074
+ >
1075
+ 📷 Переглянути фото затвердженого меню з печаткою &rarr;
1076
+ </a>
1077
+ </div>
1078
+ )}
958
1079
  </div>
959
1080
  ))}
960
1081
  </div>
@@ -975,8 +1096,38 @@ export default function App() {
975
1096
  <div className="p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm text-slate-700">
976
1097
  <p className="font-semibold text-slate-900 mb-2">{policy?.title || 'План заходів щодо запобігання та протидії булінгу'}</p>
977
1098
  <p className="text-xs text-slate-600 leading-relaxed mb-4">
978
- {policy?.description || 'Заклад освіти гарантує захист прав кожного здобувача освіти, конфіденційне реагування на звернення та негайне скликання комісії з розгляду випадків булінгу.'}
1099
+ {policy?.academic_year ? `Навчальний рік: ${policy.academic_year}. ` : ''}
1100
+ Заклад освіти гарантує захист прав кожного здобувача освіти, конфіденційне реагування на звернення та негайне скликання комісії з розгляду випадків булінгу.
979
1101
  </p>
1102
+
1103
+ {policy && (
1104
+ <div className="space-y-2 pt-2 border-t border-slate-200">
1105
+ {policy.action_plan_doc && (
1106
+ <a href={policy.action_plan_doc} target="_blank" rel="noreferrer" className="flex items-center justify-between p-2.5 bg-white rounded-lg border border-slate-200 text-xs font-semibold text-blue-700 hover:bg-blue-50">
1107
+ <span>📄 План заходів з протидії булінгу</span>
1108
+ <Download className="w-3.5 h-3.5" />
1109
+ </a>
1110
+ )}
1111
+ {policy.behavior_rules_doc && (
1112
+ <a href={policy.behavior_rules_doc} target="_blank" rel="noreferrer" className="flex items-center justify-between p-2.5 bg-white rounded-lg border border-slate-200 text-xs font-semibold text-blue-700 hover:bg-blue-50">
1113
+ <span>📄 Правила поведінки здобувача освіти</span>
1114
+ <Download className="w-3.5 h-3.5" />
1115
+ </a>
1116
+ )}
1117
+ {policy.application_procedure_doc && (
1118
+ <a href={policy.application_procedure_doc} target="_blank" rel="noreferrer" className="flex items-center justify-between p-2.5 bg-white rounded-lg border border-slate-200 text-xs font-semibold text-blue-700 hover:bg-blue-50">
1119
+ <span>📄 Порядок подання та розгляду заяв про булінг</span>
1120
+ <Download className="w-3.5 h-3.5" />
1121
+ </a>
1122
+ )}
1123
+ {policy.reaction_procedure_doc && (
1124
+ <a href={policy.reaction_procedure_doc} target="_blank" rel="noreferrer" className="flex items-center justify-between p-2.5 bg-white rounded-lg border border-slate-200 text-xs font-semibold text-blue-700 hover:bg-blue-50">
1125
+ <span>📄 Порядок реагування на доведені випадки булінгу</span>
1126
+ <Download className="w-3.5 h-3.5" />
1127
+ </a>
1128
+ )}
1129
+ </div>
1130
+ )}
980
1131
  </div>
981
1132
  </div>
982
1133
 
@@ -994,6 +1145,7 @@ export default function App() {
994
1145
  <option value="parent">Батько / Мати / Опікун</option>
995
1146
  <option value="teacher">Вчитель / Працівник</option>
996
1147
  <option value="witness">Свідок події</option>
1148
+ <option value="anonymous">Анонімно</option>
997
1149
  </select>
998
1150
  </div>
999
1151
  <div>
@@ -1009,18 +1161,26 @@ export default function App() {
1009
1161
  </div>
1010
1162
  <button
1011
1163
  type="submit"
1012
- className="w-full py-2 bg-emerald-700 text-white rounded-lg text-xs font-semibold hover:bg-emerald-800 flex items-center justify-center gap-1.5"
1164
+ disabled={reportStatus === 'loading'}
1165
+ className="w-full py-2 bg-emerald-700 text-white rounded-lg text-xs font-semibold hover:bg-emerald-800 flex items-center justify-center gap-1.5 disabled:opacity-50"
1013
1166
  >
1014
- <Send className="w-3.5 h-3.5" /> Надіслати конфіденційно
1167
+ <Send className="w-3.5 h-3.5" /> {reportStatus === 'loading' ? 'Надсилання...' : 'Надіслати конфіденційно'}
1015
1168
  </button>
1016
1169
  {reportFeedback && (
1017
- <div className="p-2.5 bg-emerald-50 text-emerald-800 border border-emerald-200 rounded-lg text-xs">
1170
+ <div className={`p-3 rounded-lg text-xs border leading-relaxed ${
1171
+ reportStatus === 'success'
1172
+ ? 'bg-emerald-50 text-emerald-800 border-emerald-200'
1173
+ : 'bg-rose-50 text-rose-800 border-rose-200'
1174
+ }`}>
1018
1175
  {reportFeedback}
1019
1176
  </div>
1020
1177
  )}
1021
1178
  </form>
1022
1179
  </div>
1023
1180
  </div>
1181
+ </div>
1182
+ )}
1183
+
1024
1184
  {activeTab === 'gallery' && (
1025
1185
  <div>
1026
1186
  <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-6">
@@ -1269,7 +1429,7 @@ export default function App() {
1269
1429
  );
1270
1430
  }
1271
1431
  """
1272
- (src_dir / "App.tsx").write_text(app_tsx)
1432
+ (src_dir / "App.tsx").write_text(app_tsx, encoding="utf-8")
1273
1433
 
1274
1434
  print(f"\n{GREEN}{BOLD}✓ Готовий React-фронтенд успішно створено у {target_dir}!{RESET}\n")
1275
1435
  print(f"Наступні кроки для запуску сайту:")
@@ -18,11 +18,12 @@ class LegalDocumentSerializer(serializers.ModelSerializer):
18
18
 
19
19
  class FinancialReportSerializer(serializers.ModelSerializer):
20
20
  is_within_10_days = serializers.BooleanField(read_only=True)
21
+ report_type_display = serializers.CharField(source='get_report_type_display', read_only=True)
21
22
 
22
23
  class Meta:
23
24
  model = FinancialReport
24
25
  fields = [
25
- 'id', 'title', 'report_type', 'period', 'total_amount',
26
+ 'id', 'title', 'report_type', 'report_type_display', 'period', 'total_amount',
26
27
  'approval_date', 'publish_date', 'file', 'notes', 'is_within_10_days'
27
28
  ]
28
29
 
@@ -41,6 +41,7 @@ class AbstractConfidentialReport(models.Model):
41
41
  ('student', _('Учень / учениця')),
42
42
  ('parent', _('Батьки / законні представники')),
43
43
  ('teacher', _('Педагогічний працівник')),
44
+ ('witness', _('Свідок події')),
44
45
  ('anonymous', _('Анонімно')),
45
46
  ]
46
47
 
@@ -0,0 +1,41 @@
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
+ def test_confidential_report_by_witness(self):
20
+ """Перевірка ролі 'свідок події' при надсиланні заяви"""
21
+ payload = {
22
+ 'reporter_role': 'witness',
23
+ 'incident_description': 'Помічено булінг на шкільному подвір\'ї.',
24
+ }
25
+ response = self.client.post('/api/v1/safety/reports/', payload, format='json')
26
+ self.assertEqual(response.status_code, 201)
27
+ report = ConfidentialReport.objects.get(reporter_role='witness')
28
+ self.assertEqual(report.reporter_role, 'witness')
29
+
30
+ def test_confidential_report_with_logged_in_session(self):
31
+ """Перевірка, що користувач із активною сесією (наприклад адмін) може відправити форму без CSRF помилки"""
32
+ from django.contrib.auth.models import User
33
+ user = User.objects.create_user(username='staff', password='pwd')
34
+ client = APIClient(enforce_csrf_checks=True)
35
+ client.force_login(user)
36
+ payload = {
37
+ 'reporter_role': 'student',
38
+ 'incident_description': 'Звернення від учня під час сесії.',
39
+ }
40
+ response = client.post('/api/v1/safety/reports/', payload, format='json')
41
+ self.assertEqual(response.status_code, 201)
@@ -15,3 +15,4 @@ class ConfidentialReportViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet
15
15
  queryset = ConfidentialReport.objects.all()
16
16
  serializer_class = ConfidentialReportSerializer
17
17
  permission_classes = [permissions.AllowAny]
18
+ authentication_classes = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-school-framework
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Відкритий веб-фреймворк для офіційних сайтів закладів загальної середньої освіти України (ст. 30 ЗУ «Про освіту»)
5
5
  License: MIT
6
6
  Classifier: Framework :: Django
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "django-school-framework"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Відкритий веб-фреймворк для офіційних сайтів закладів загальної середньої освіти України (ст. 30 ЗУ «Про освіту»)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +0,0 @@
1
- from django.apps import AppConfig
2
-
3
- class CoreConfig(AppConfig):
4
- default_auto_field = 'django.db.models.BigAutoField'
5
- name = 'apps.core'
6
- verbose_name = 'Загальні відомості та налаштування школи'
@@ -1,20 +0,0 @@
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)