fd79513ce2
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s
Trzeci z pięciu kroków. Zmieniłem jego zakres wobec planu i warto wiedzieć dlaczego: pierwotnie miała to być deduplikacja pięciu kopii link_crypto.py, ale po zrobieniu PR 2 widać, że astroklienta blokuje co innego — ekrany, których nie ma mieć, siedzą wewnątrz jednego main.py. Deduplikacja kryptografii jest realnym długiem, ale niczego nie blokuje. PODZIAŁ. main.py (1023 linie) rozpadł się na podstawa.py (wspólne obiekty i pomocnicy), jedenaście modułów w app/ekrany/ i main.py, który jest już samym ZŁOŻENIEM: lista importów JEST definicją produktu. Podział zrobiony mechanicznie, z osobnym sprawdzeniem, że żadna sekcja nie wołała pomocnika z innej (nie wołała). KATALOG Z REJESTRACJI. Dotąd wszystkie funkcje były wypisane w features.py, więc obraz produktu, który części z nich nie ma, i tak niósł ich nazwy — spis funkcji, których nie ma jak włączyć. Teraz ekran zgłasza siebie, swoje trasy i swoje zasoby przy imporcie własnego modułu, a features.py nie wymienia ani jednego ekranu. Kolejność w nawigacji jest jawna (`kolejnosc`), żeby nie rządziła nią kolejność importów. To samo dotyczy nawigacji administratora: odsyłacz do ekranu kont był wpisany na sztywno w base.html, więc w węższym produkcie zostawał martwy link i nazwa ekranu, którego nie ma. EKSPORT JAKO MODUŁ. Zgodnie z ustaleniem eksport jest funkcją administracyjną, więc musi dać się usunąć. app/moduly/eksport/ zabiera arkusz, trasę PDF-a i akcję formularza. „Można, ale nie temu kontu" i „nie ma takiej możliwości" to dwie różne gwarancje, a eksport wynosi najwięcej treści baz naraz. MOST ODKRYWA MODUŁY. Skoro modułów jest więcej niż jeden, most nie może ich znać z nazwy — nazwa nieobecnego modułu jechałaby do obrazu, w którym go nie ma. Przechodzi więc po podkatalogach app/moduly/ i pyta każdy, co wnosi. Katalog generowania przeniesiony z app/dodatki na app/moduly/dodatki. ZNALEZIONE PRZY OKAZJI. Po wydzieleniu eksportu okazało się, że jego ścieżki SZCZĘŚLIWEJ nie sprawdzał żaden test — badano wyłącznie odmowę dla konta bez uprawnienia. Moduł dostaje zależności z wywołania montującego, więc brak jednej z nich wyszedłby dopiero przy pierwszym kliknięciu. Dopisany test funkcjonalny (realny arkusz, sprawdzany aż do nagłówka ZIP-a) i brakujące zależności. Cztery komentarze w plikach współdzielonych wymieniały zakładkę „Skompiluj", w tym wheelzoom.js ze wzmianką o „przyszłej zakładce" — to samo zgłoszenie, które audyt podnosił wcześniej. TEST ZŁOŻENIA. Buduje węższy produkt NAPRAWDĘ: kopiuje drzewo, usuwa cztery ekrany i oba moduły, uruchamia aplikację w OSOBNYM PROCESIE (importy są zapamiętywane, więc sprawdzanie tego w procesie, który moduł już zaimportował, dawałoby wynik fałszywie pozytywny) i sprawdza, że wstaje, że zachowane ekrany oddają 200, że usunięte oddają 404 (nie 403 i nie 500), że katalog opisuje ten obraz, i że w nawigacji nie ma martwych odsyłaczy. Napisałem najpierw ostrzejszy test — „nazwa ekranu nie pada poza jego modułem" — i go wyrzuciłem: zgłaszał wzmianki o Horoskopie w plikach współdzielonych, choć astroklient Horoskop MA. Ślad ma znaczenie wyłącznie wobec konkretnego złożenia, więc sprawdzenie należy do produktu, nie do mechanizmu. Testy: presentation 368, logic 342, data 37, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
165 lines
7.3 KiB
Python
165 lines
7.3 KiB
Python
"""Pliki baz: wybór, wgrywanie, archiwizacja i reguły przyjmowania (DAN-27)."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import File, Form, Request, UploadFile
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
import base64
|
|
import httpx
|
|
|
|
from app.features import Feature
|
|
from app.podstawa import (app, templates, logic, features, files_state, _perms, _logic_error)
|
|
|
|
# ---------------- Pliki baz (DAN-27) ----------------
|
|
# Trzy poziomy dostępu, opisane w features.ROUTES:
|
|
# „files" — widzi listę i decyduje, z czego program korzysta,
|
|
# „files_input" — dokłada wgrywanie i ARCHIWIZACJĘ (plik zostaje zamrożony
|
|
# ze znacznikiem czasu, znika tylko z użytku),
|
|
# administrator — kasowanie, przywracanie z archiwum i REGUŁY WALIDACJI.
|
|
#
|
|
# Kwarantanna (plik wgrany, ale odrzucony przez walidację) jest odsiewana W WARSTWIE
|
|
# DANYCH przy `for_admin=False`. Nie filtrujemy jej tutaj ani w szablonie: gdyby
|
|
# takie pliki dochodziły do przeglądarki, wystarczyłby podgląd źródła, żeby poznać
|
|
# reguły — a te ma znać wyłącznie administrator.
|
|
|
|
def _files_context(request: Request, error: str = "", done: str = "") -> dict:
|
|
is_admin = features.ADMIN in _perms(request)
|
|
try:
|
|
data = logic.files_list(for_admin=is_admin)
|
|
except httpx.HTTPError as e:
|
|
return {"files": [], "rules": {}, "is_admin": is_admin,
|
|
"error": _logic_error(e), "done": ""}
|
|
return {"files": data.get("files") or [], "rules": data.get("rules") or {},
|
|
"is_admin": is_admin, "error": error, "done": done}
|
|
|
|
|
|
@app.get("/files", response_class=HTMLResponse)
|
|
def files_view(request: Request, error: str = "", done: str = ""):
|
|
return templates.TemplateResponse(request, "files.html",
|
|
_files_context(request, error, done))
|
|
|
|
|
|
def _files_redirect(error: str = "", done: str = "") -> RedirectResponse:
|
|
from urllib.parse import urlencode
|
|
|
|
q = urlencode({k: v for k, v in (("error", error), ("done", done)) if v})
|
|
return RedirectResponse(f"/files{'?' + q if q else ''}", status_code=303)
|
|
|
|
|
|
def _who(request: Request) -> str:
|
|
return getattr(request.state, "user", "") or "-"
|
|
|
|
|
|
@app.post("/files/use")
|
|
def files_use(request: Request, path: str = Form(...), use: str = Form("")):
|
|
"""Włącza albo odstawia bazę. Włączenie przechodzi przez bramkę walidacji
|
|
w warstwie danych — odmowa wraca BEZ POWODU, bo powód zdradzałby reguły."""
|
|
want = files_state.ACTIVE if use.strip().lower() in {"1", "true", "on", "tak"} \
|
|
else files_state.READY
|
|
try:
|
|
logic.files_status(path, want, by=_who(request))
|
|
except httpx.HTTPStatusError as e:
|
|
detail = _http_detail(e)
|
|
if features.ADMIN not in _perms(request):
|
|
detail = "Tego pliku nie da się teraz włączyć do użytku."
|
|
return _files_redirect(error=detail)
|
|
except httpx.HTTPError as e:
|
|
return _files_redirect(error=_logic_error(e))
|
|
return _files_redirect(done="Zapisano." if want == files_state.READY
|
|
else "Plik włączony do użytku.")
|
|
|
|
|
|
@app.post("/files/upload")
|
|
async def files_upload(request: Request, upload: UploadFile = File(...)):
|
|
"""Wgranie nowej bazy. Plik zostaje NIEZALEŻNIE od wyniku walidacji —
|
|
nie tracimy niczego, co ktoś wgrał."""
|
|
raw = await upload.read()
|
|
if not raw:
|
|
return _files_redirect(error="Pusty plik.")
|
|
try:
|
|
out = logic.files_upload(upload.filename or "plik.xlsx",
|
|
base64.b64encode(raw).decode("ascii"), by=_who(request))
|
|
except httpx.HTTPError as e:
|
|
return _files_redirect(error=_logic_error(e))
|
|
# DOKŁADNIE ten sam komunikat niezależnie od wyniku sprawdzenia. Dwa różne
|
|
# komunikaty były wyrocznią: wystarczyło wgrywać spreparowane pliki i czytać
|
|
# odpowiedź, żeby odgadnąć reguły, które ma znać wyłącznie administrator.
|
|
# Poprzednia treść mówiła przy tym wprost, że istnieje „administrator", który
|
|
# musi plik „zatwierdzić" — czyli że konto wgrywającego jest ograniczone
|
|
# i że plik przechodzi przez jakąś bramkę.
|
|
return _files_redirect(done=f"Wgrano „{out.get('name')}”.")
|
|
|
|
|
|
@app.post("/files/archive")
|
|
def files_archive(request: Request, path: str = Form(...)):
|
|
"""Archiwizacja: plik ZOSTAJE, zamrożony, ze znacznikiem czasu — znika tylko
|
|
z użytku. To najdalej idąca operacja dostępna osobie wgrywającej dane."""
|
|
try:
|
|
logic.files_status(path, files_state.ARCHIVED, by=_who(request))
|
|
except httpx.HTTPError as e:
|
|
return _files_redirect(error=_logic_error(e))
|
|
return _files_redirect(done=f"Zarchiwizowano „{path}”. Plik został zachowany.")
|
|
|
|
|
|
@app.post("/files/restore")
|
|
def files_restore(request: Request, path: str = Form(...)):
|
|
try:
|
|
logic.files_status(path, files_state.READY, by=_who(request))
|
|
except httpx.HTTPError as e:
|
|
return _files_redirect(error=_logic_error(e))
|
|
return _files_redirect(done=f"Przywrócono „{path}” z archiwum.")
|
|
|
|
|
|
@app.post("/files/delete")
|
|
def files_delete(request: Request, path: str = Form(...)):
|
|
try:
|
|
logic.files_delete(path)
|
|
except httpx.HTTPError as e:
|
|
return _files_redirect(error=_logic_error(e))
|
|
return _files_redirect(done=f"Skasowano „{path}” bezpowrotnie.")
|
|
|
|
|
|
@app.post("/files/rules")
|
|
def files_rules(request: Request, extensions: str = Form(".xlsx"),
|
|
max_size_mb: float = Form(50), min_rows: int = Form(1),
|
|
required_columns: str = Form(""),
|
|
reject_duplicate_content: str = Form("")):
|
|
rules = {
|
|
"extensions": [e.strip() for e in extensions.split(",") if e.strip()],
|
|
"max_size_mb": max_size_mb,
|
|
"min_rows": min_rows,
|
|
"required_columns": [c.strip() for c in required_columns.split(",") if c.strip()],
|
|
"reject_duplicate_content": reject_duplicate_content.strip().lower()
|
|
in {"1", "true", "on", "tak"},
|
|
}
|
|
try:
|
|
logic.files_rules(rules)
|
|
except httpx.HTTPError as e:
|
|
return _files_redirect(error=_logic_error(e))
|
|
return _files_redirect(done="Zapisano reguły walidacji.")
|
|
|
|
|
|
def _http_detail(e: httpx.HTTPStatusError) -> str:
|
|
try:
|
|
return str(e.response.json().get("detail") or e)
|
|
except Exception: # noqa: BLE001
|
|
return str(e)
|
|
|
|
features.zarejestruj(
|
|
ekran=Feature("files", "Pliki", "Wybór plików, z których korzysta program.",
|
|
"Ekrany", "/files", kolejnosc=70),
|
|
# Kasowanie, przywracanie i REGUŁY PRZYJMOWANIA to wyłącznie administrator —
|
|
# o istnieniu reguł nikt poza nim nie ma skąd wiedzieć (DAN-27).
|
|
trasy={("GET", "/files"): "files", ("POST", "/files/use"): "files",
|
|
("POST", "/files/upload"): "files_input",
|
|
("POST", "/files/archive"): "files_input",
|
|
("POST", "/files/restore"): features.ADMIN,
|
|
("POST", "/files/delete"): features.ADMIN,
|
|
("POST", "/files/rules"): features.ADMIN},
|
|
zasoby={"s-files.css": "files"},
|
|
)
|
|
features.zarejestruj(rozszerzenie=Feature(
|
|
"files_input", "Wgrywanie i archiwizacja plików",
|
|
"Dodawanie nowych plików i wycofywanie ich z użytku (plik zostaje, zamrożony, "
|
|
"ze znacznikiem czasu). Kasować może wyłącznie administrator.",
|
|
"Rozszerzenia", kolejnosc=40))
|