django-school-framework 0.2.3__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.3 → 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.3 → django_school_framework-0.2.4}/apps/core/cli.py +202 -51
  4. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/serializers.py +2 -1
  5. {django_school_framework-0.2.3 → 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.3 → django_school_framework-0.2.4}/apps/safety/views.py +1 -0
  8. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/django_school_framework.egg-info/PKG-INFO +1 -1
  9. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/pyproject.toml +1 -1
  10. django_school_framework-0.2.3/apps/core/apps.py +0 -6
  11. django_school_framework-0.2.3/apps/safety/tests.py +0 -20
  12. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/README.md +0 -0
  13. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/__init__.py +0 -0
  14. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/__init__.py +0 -0
  15. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/admin.py +0 -0
  16. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/apps.py +0 -0
  17. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/migrations/0001_initial.py +0 -0
  18. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/migrations/__init__.py +0 -0
  19. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/models.py +0 -0
  20. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/serializers.py +0 -0
  21. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/tests.py +0 -0
  22. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/urls.py +0 -0
  23. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/admissions/views.py +0 -0
  24. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/__init__.py +0 -0
  25. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/admin.py +0 -0
  26. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/compliance.py +0 -0
  27. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/management/__init__.py +0 -0
  28. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/management/commands/__init__.py +0 -0
  29. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/management/commands/seed_school_data.py +0 -0
  30. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/management/commands/wait_for_db.py +0 -0
  31. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/migrations/0001_initial.py +0 -0
  32. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/migrations/0002_alter_schoolprofile_options.py +0 -0
  33. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/migrations/__init__.py +0 -0
  34. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/models.py +0 -0
  35. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/serializers.py +0 -0
  36. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/templatetags/__init__.py +0 -0
  37. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/templatetags/compliance_tags.py +0 -0
  38. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/tests.py +0 -0
  39. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/urls.py +0 -0
  40. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/core/views.py +0 -0
  41. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/__init__.py +0 -0
  42. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/admin.py +0 -0
  43. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/apps.py +0 -0
  44. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/migrations/0001_initial.py +0 -0
  45. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/migrations/__init__.py +0 -0
  46. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/models.py +0 -0
  47. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/serializers.py +0 -0
  48. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/tests.py +0 -0
  49. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/urls.py +0 -0
  50. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/gallery/views.py +0 -0
  51. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/__init__.py +0 -0
  52. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/admin.py +0 -0
  53. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/apps.py +0 -0
  54. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/migrations/0001_initial.py +0 -0
  55. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/migrations/__init__.py +0 -0
  56. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/models.py +0 -0
  57. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/tests.py +0 -0
  58. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/urls.py +0 -0
  59. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/legal/views.py +0 -0
  60. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/__init__.py +0 -0
  61. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/admin.py +0 -0
  62. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/apps.py +0 -0
  63. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/migrations/0001_initial.py +0 -0
  64. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/migrations/__init__.py +0 -0
  65. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/models.py +0 -0
  66. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/serializers.py +0 -0
  67. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/tests.py +0 -0
  68. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/urls.py +0 -0
  69. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/news/views.py +0 -0
  70. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/__init__.py +0 -0
  71. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/admin.py +0 -0
  72. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/apps.py +0 -0
  73. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/migrations/0001_initial.py +0 -0
  74. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/migrations/0002_alter_cateringprovider_options.py +0 -0
  75. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/migrations/__init__.py +0 -0
  76. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/models.py +0 -0
  77. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/serializers.py +0 -0
  78. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/tests.py +0 -0
  79. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/urls.py +0 -0
  80. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/nutrition/views.py +0 -0
  81. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/safety/__init__.py +0 -0
  82. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/safety/admin.py +0 -0
  83. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/safety/apps.py +0 -0
  84. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/safety/migrations/0001_initial.py +0 -0
  85. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/safety/migrations/__init__.py +0 -0
  86. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/safety/serializers.py +0 -0
  87. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/safety/urls.py +0 -0
  88. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/__init__.py +0 -0
  89. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/admin.py +0 -0
  90. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/apps.py +0 -0
  91. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/migrations/0001_initial.py +0 -0
  92. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/migrations/__init__.py +0 -0
  93. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/models.py +0 -0
  94. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/serializers.py +0 -0
  95. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/tests.py +0 -0
  96. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/urls.py +0 -0
  97. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/apps/schedule/views.py +0 -0
  98. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/django_school_framework.egg-info/SOURCES.txt +0 -0
  99. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/django_school_framework.egg-info/dependency_links.txt +0 -0
  100. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/django_school_framework.egg-info/entry_points.txt +0 -0
  101. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/django_school_framework.egg-info/requires.txt +0 -0
  102. {django_school_framework-0.2.3 → django_school_framework-0.2.4}/django_school_framework.egg-info/top_level.txt +0 -0
  103. {django_school_framework-0.2.3 → 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.3
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,7 +218,7 @@ 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
@@ -207,7 +249,7 @@ if settings.DEBUG:
207
249
  urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
208
250
  urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
209
251
  """
210
- (output_dir / "config" / "urls.py").write_text(urls_content)
252
+ (output_dir / "config" / "urls.py").write_text(urls_content, encoding="utf-8")
211
253
 
212
254
  # 4. config/__init__.py, wsgi.py, asgi.py
213
255
  init_content = """# Django School Framework Project
@@ -230,25 +272,25 @@ try:
230
272
  except Exception:
231
273
  pass
232
274
  """
233
- (output_dir / "config" / "__init__.py").write_text(init_content)
275
+ (output_dir / "config" / "__init__.py").write_text(init_content, encoding="utf-8")
234
276
 
235
277
  wsgi_content = """import os
236
278
  from django.core.wsgi import get_wsgi_application
237
279
  os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
238
280
  application = get_wsgi_application()
239
281
  """
240
- (output_dir / "config" / "wsgi.py").write_text(wsgi_content)
282
+ (output_dir / "config" / "wsgi.py").write_text(wsgi_content, encoding="utf-8")
241
283
 
242
284
  # 5. .env.example
243
285
  env_example = f"""DEBUG=True
244
286
  DJANGO_SECRET_KEY=change-me-in-production
245
287
  DB_ENGINE=django.db.backends.sqlite3
246
- DB_NAME={project_name}_db
288
+ DB_NAME=db.sqlite3
247
289
  DB_HOST_PORT=3307
248
290
  BACKEND_HOST_PORT=8000
249
291
  FRONTEND_HOST_PORT=5173
250
292
  """
251
- (output_dir / ".env.example").write_text(env_example)
293
+ (output_dir / ".env.example").write_text(env_example, encoding="utf-8")
252
294
 
253
295
  # 6. README.md
254
296
  readme_content = f"""# {project_name} — Офіційний вебсайт закладу освіти
@@ -275,7 +317,7 @@ school-admin check-compliance
275
317
  python manage.py runserver
276
318
  ```
277
319
  """
278
- (output_dir / "README.md").write_text(readme_content)
320
+ (output_dir / "README.md").write_text(readme_content, encoding="utf-8")
279
321
 
280
322
  print(f"\n{GREEN}{BOLD}✓ Проєкт закладу освіти успішно створено у {output_dir}!{RESET}\n")
281
323
  print(f"Наступні кроки:")
@@ -445,7 +487,7 @@ def cmd_create_frontend(args):
445
487
  }
446
488
  }
447
489
  """
448
- (target_dir / "package.json").write_text(pkg_json)
490
+ (target_dir / "package.json").write_text(pkg_json, encoding="utf-8")
449
491
 
450
492
  # 2. vite.config.ts
451
493
  backend_url = args.api_url or "http://127.0.0.1:8000"
@@ -480,7 +522,7 @@ export default defineConfig({{
480
522
  }},
481
523
  }});
482
524
  """
483
- (target_dir / "vite.config.ts").write_text(vite_conf)
525
+ (target_dir / "vite.config.ts").write_text(vite_conf, encoding="utf-8")
484
526
 
485
527
  # 3. index.html
486
528
  index_html = """<!doctype html>
@@ -496,7 +538,7 @@ export default defineConfig({{
496
538
  </body>
497
539
  </html>
498
540
  """
499
- (target_dir / "index.html").write_text(index_html)
541
+ (target_dir / "index.html").write_text(index_html, encoding="utf-8")
500
542
 
501
543
  # 4. tsconfig.json
502
544
  tsconfig = """{
@@ -517,7 +559,7 @@ export default defineConfig({{
517
559
  "include": ["src"]
518
560
  }
519
561
  """
520
- (target_dir / "tsconfig.json").write_text(tsconfig)
562
+ (target_dir / "tsconfig.json").write_text(tsconfig, encoding="utf-8")
521
563
 
522
564
  # 5. tailwind & postcss
523
565
  tailwind_conf = """/** @type {import('tailwindcss').Config} */
@@ -529,7 +571,7 @@ export default {
529
571
  plugins: [],
530
572
  };
531
573
  """
532
- (target_dir / "tailwind.config.js").write_text(tailwind_conf)
574
+ (target_dir / "tailwind.config.js").write_text(tailwind_conf, encoding="utf-8")
533
575
 
534
576
  postcss_conf = """export default {
535
577
  plugins: {
@@ -538,7 +580,7 @@ export default {
538
580
  },
539
581
  };
540
582
  """
541
- (target_dir / "postcss.config.js").write_text(postcss_conf)
583
+ (target_dir / "postcss.config.js").write_text(postcss_conf, encoding="utf-8")
542
584
 
543
585
  # 6. src/index.css
544
586
  index_css = """@tailwind base;
@@ -557,7 +599,7 @@ body.contrast-bw {
557
599
  color: #000000 !important;
558
600
  }
559
601
  """
560
- (src_dir / "index.css").write_text(index_css)
602
+ (src_dir / "index.css").write_text(index_css, encoding="utf-8")
561
603
 
562
604
  # 7. src/main.tsx
563
605
  main_tsx = """import React from 'react';
@@ -571,7 +613,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
571
613
  </React.StrictMode>
572
614
  );
573
615
  """
574
- (src_dir / "main.tsx").write_text(main_tsx)
616
+ (src_dir / "main.tsx").write_text(main_tsx, encoding="utf-8")
575
617
 
576
618
  # 8. src/App.tsx
577
619
  app_tsx = """import React, { useState, useEffect } from 'react';
@@ -596,10 +638,12 @@ export default function App() {
596
638
  const [newsList, setNewsList] = useState<any[]>([]);
597
639
  const [selectedNews, setSelectedNews] = useState<any | null>(null);
598
640
  const [bells, setBells] = useState<any[]>([]);
641
+ const [cateringProvider, setCateringProvider] = useState<any | null>(null);
599
642
 
600
643
  // Bullying report form
601
644
  const [incidentText, setIncidentText] = useState('');
602
645
  const [reporterRole, setReporterRole] = useState('student');
646
+ const [reportStatus, setReportStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
603
647
  const [reportFeedback, setReportFeedback] = useState<string | null>(null);
604
648
 
605
649
  useEffect(() => {
@@ -639,6 +683,14 @@ export default function App() {
639
683
  .then(d => setMenus(d.results || d))
640
684
  .catch(() => {});
641
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
+
642
694
  fetch('/api/v1/safety/policy/')
643
695
  .then(r => r.json())
644
696
  .then(d => {
@@ -672,9 +724,17 @@ export default function App() {
672
724
  const handleReportSubmit = (e: React.FormEvent) => {
673
725
  e.preventDefault();
674
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
+
675
735
  fetch('/api/v1/safety/reports/', {
676
736
  method: 'POST',
677
- headers: { 'Content-Type': 'application/json' },
737
+ headers,
678
738
  body: JSON.stringify({
679
739
  reporter_role: reporterRole,
680
740
  incident_description: incidentText,
@@ -682,15 +742,17 @@ export default function App() {
682
742
  })
683
743
  .then(res => {
684
744
  if (res.ok) {
745
+ setReportStatus('success');
685
746
  setReportFeedback('Дякуємо! Ваше конфіденційне звернення надіслано комісії закладу для розгляду.');
686
747
  setIncidentText('');
687
748
  } else {
749
+ setReportStatus('error');
688
750
  setReportFeedback('Помилка при відправці. Будь ласка, зверніться до чергового адміністратора.');
689
751
  }
690
752
  })
691
753
  .catch(() => {
692
- setReportFeedback('Звернення зафіксовано. Дякуємо за небайдужість.');
693
- setIncidentText('');
754
+ setReportStatus('error');
755
+ setReportFeedback('Помилка з\'єднання. Будь ласка, зверніться до чергового адміністратора.');
694
756
  });
695
757
  };
696
758
 
@@ -810,18 +872,19 @@ export default function App() {
810
872
  <div>
811
873
  <div className="font-semibold text-slate-900">{doc.title}</div>
812
874
  <div className="text-xs text-slate-500">
813
- Категорія: {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 || ''}
814
877
  </div>
815
878
  </div>
816
879
  </div>
817
- {doc.file && (
880
+ {(doc.file || doc.external_url) && (
818
881
  <a
819
- href={doc.file}
882
+ href={doc.file || doc.external_url}
820
883
  target="_blank"
821
884
  rel="noreferrer"
822
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"
823
886
  >
824
- <Download className="w-3.5 h-3.5" /> Завантажити
887
+ <Download className="w-3.5 h-3.5" /> {doc.file ? 'Завантажити' : 'Відкрити'}
825
888
  </a>
826
889
  )}
827
890
  </div>
@@ -854,8 +917,7 @@ export default function App() {
854
917
  <tr className="border-b border-slate-200 text-slate-500 text-xs uppercase">
855
918
  <th className="py-3 px-2">Звітний період</th>
856
919
  <th className="py-3 px-2">Тип звіту</th>
857
- <th className="py-3 px-2">Надходження (грн)</th>
858
- <th className="py-3 px-2">Видатки (грн)</th>
920
+ <th className="py-3 px-2">Сума коштів (грн)</th>
859
921
  <th className="py-3 px-2">Контроль 10 днів</th>
860
922
  <th className="py-3 px-2 text-right">Файл</th>
861
923
  </tr>
@@ -863,10 +925,9 @@ export default function App() {
863
925
  <tbody className="divide-y divide-slate-100">
864
926
  {finances.map((f: any) => (
865
927
  <tr key={f.id} className="hover:bg-slate-50">
866
- <td className="py-3 px-2 font-bold text-slate-900">{f.year} рік</td>
867
- <td className="py-3 px-2">{f.report_type}</td>
868
- <td className="py-3 px-2 font-mono text-emerald-700">{Number(f.total_revenue || 0).toLocaleString()} ₴</td>
869
- <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>
870
931
  <td className="py-3 px-2">
871
932
  {f.is_within_10_days ? (
872
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">
@@ -879,8 +940,8 @@ export default function App() {
879
940
  )}
880
941
  </td>
881
942
  <td className="py-3 px-2 text-right">
882
- {f.document_file && (
883
- <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">
884
945
  PDF
885
946
  </a>
886
947
  )}
@@ -910,10 +971,10 @@ export default function App() {
910
971
  ) : (
911
972
  capacities.map((c: any) => (
912
973
  <div key={c.id} className="p-3 flex justify-between items-center text-sm">
913
- <span className="font-semibold text-slate-800">{c.grade_level}-й клас</span>
914
- <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>
915
976
  <span className="font-bold text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded text-xs">
916
- Вільних: {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)}
917
978
  </span>
918
979
  </div>
919
980
  ))
@@ -944,23 +1005,77 @@ export default function App() {
944
1005
 
945
1006
  {activeTab === 'nutrition' && (
946
1007
  <div>
947
- <h2 className="text-xl font-bold text-slate-900 mb-2">Харчування здобувачів освіти</h2>
948
- <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>
949
1020
 
950
1021
  {menus.length === 0 ? (
951
- <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">
952
1023
  Меню на поточний тиждень завантажується черговою медсестрою/оператором харчування.
953
1024
  </div>
954
1025
  ) : (
955
- <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">
956
1027
  {menus.map((m: any) => (
957
- <div key={m.id} className="p-4 rounded-xl border border-slate-200 bg-slate-50">
958
- <div className="flex justify-between items-center mb-2">
959
- <span className="font-bold text-slate-900">{m.menu_date}</span>
960
- <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>
961
1065
  </div>
962
- <div className="text-xs text-slate-600 mb-2"><strong>Прийом їжі:</strong> {m.meal_type}</div>
963
- <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
+ )}
964
1079
  </div>
965
1080
  ))}
966
1081
  </div>
@@ -981,8 +1096,38 @@ export default function App() {
981
1096
  <div className="p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm text-slate-700">
982
1097
  <p className="font-semibold text-slate-900 mb-2">{policy?.title || 'План заходів щодо запобігання та протидії булінгу'}</p>
983
1098
  <p className="text-xs text-slate-600 leading-relaxed mb-4">
984
- {policy?.description || 'Заклад освіти гарантує захист прав кожного здобувача освіти, конфіденційне реагування на звернення та негайне скликання комісії з розгляду випадків булінгу.'}
1099
+ {policy?.academic_year ? `Навчальний рік: ${policy.academic_year}. ` : ''}
1100
+ Заклад освіти гарантує захист прав кожного здобувача освіти, конфіденційне реагування на звернення та негайне скликання комісії з розгляду випадків булінгу.
985
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
+ )}
986
1131
  </div>
987
1132
  </div>
988
1133
 
@@ -1000,6 +1145,7 @@ export default function App() {
1000
1145
  <option value="parent">Батько / Мати / Опікун</option>
1001
1146
  <option value="teacher">Вчитель / Працівник</option>
1002
1147
  <option value="witness">Свідок події</option>
1148
+ <option value="anonymous">Анонімно</option>
1003
1149
  </select>
1004
1150
  </div>
1005
1151
  <div>
@@ -1015,12 +1161,17 @@ export default function App() {
1015
1161
  </div>
1016
1162
  <button
1017
1163
  type="submit"
1018
- 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"
1019
1166
  >
1020
- <Send className="w-3.5 h-3.5" /> Надіслати конфіденційно
1167
+ <Send className="w-3.5 h-3.5" /> {reportStatus === 'loading' ? 'Надсилання...' : 'Надіслати конфіденційно'}
1021
1168
  </button>
1022
1169
  {reportFeedback && (
1023
- <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
+ }`}>
1024
1175
  {reportFeedback}
1025
1176
  </div>
1026
1177
  )}
@@ -1278,7 +1429,7 @@ export default function App() {
1278
1429
  );
1279
1430
  }
1280
1431
  """
1281
- (src_dir / "App.tsx").write_text(app_tsx)
1432
+ (src_dir / "App.tsx").write_text(app_tsx, encoding="utf-8")
1282
1433
 
1283
1434
  print(f"\n{GREEN}{BOLD}✓ Готовий React-фронтенд успішно створено у {target_dir}!{RESET}\n")
1284
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.3
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.3"
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)