Moduły, algorytmy i wzorce projektowe¶
Sekcja przechodzi przez siedem kluczowych ścieżek kodu, które najlepiej wyjaśniają architekturę aplikacji. Pełny kod żyje w repozytorium — tu pokazujemy wzorce i decyzje.
Struktura projektu¶
config/ — settings.py, urls.py, wsgi.py
accounts/ — User, formularze auth, e-maile aktywacyjne
core/ — strona główna i dashboard
movies/ — katalog, oceny, komentarze, statusy, integracja TMDB
templates/ — wszystkie szablony DTL (auth, movies, partials, e-maile)
static/ — CSS, JS, ikony
tests/ — e2e/ (Playwright), perf/ (locust)
1. Rejestracja z weryfikacją e-mail — accounts/views.py:RegisterView¶
Wzorzec: Form + post-save side effect w form_valid.
class RegisterView(FormView):
template_name = "accounts/register.html"
form_class = RegisterForm
success_url = reverse_lazy("accounts:activation_sent")
def form_valid(self, form):
user = form.save() # is_active=False
try:
send_activation_email(user)
messages.success(self.request, "Konto zostało utworzone...")
except Exception:
logger.exception(...)
messages.warning(self.request, "Wysyłka e-maila się nie powiodła...")
return super().form_valid(form)
Decyzja: wysyłka maila NIE jest atomiczna z zapisem usera. Jeżeli
SMTP padnie, konto powstaje, użytkownik dostaje komunikat ostrzegawczy
i może użyć /auth/resend-activation/.
2. Generowanie i weryfikacja tokenu aktywacyjnego — accounts/utils.py, views.ActivateAccountView¶
Wzorzec: stateless token Django (default_token_generator). Brak
osobnej tabeli ActivationToken — token deterministycznie generowany z
kombinacji user.pk + user.password + last_login + timestamp.
def send_activation_email(user: User) -> None:
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = default_token_generator.make_token(user)
activation_path = reverse("accounts:activate", kwargs={"uidb64": uid, "token": token})
activation_url = f"{settings.APP_BASE_URL}{activation_path}"
...
Token unieważnia się po pierwszej zmianie hasła (zmienia się hash) i
po PASSWORD_RESET_TIMEOUT (24 h).
3. Model danych — movies/models.py¶
Najistotniejsze decyzje:
UserMovieStatuszamiast osobnych tabel watchlist/watched. Polestatusprzełącza stan,UniqueConstraint(user, movie)blokuje duplikaty.Rating.scorejakoDecimalField(max_digits=2, decimal_places=1)+MinValueValidator(0.5)+MaxValueValidator(5.0)+CheckConstraint. Krok 0,5 pilnowany przez walidator formularza.- Cache aggregates —
Movie.average_ratingiMovie.ratings_countaktualizowane w warstwie serwisowej, NIE przez signals. Świadoma decyzja: signals są niewidoczne i utrudniają testowanie. Comment.toxicity_score+STATUS_CHOICESzflagged/hidden— schemat gotowy, widok publiczny już teraz filtruje nastatus='visible'.MovieCreditjako through-table m2m Person↔Movie z dodatkowymi atrybutami (credit_type,character,order). Indeks(movie, credit_type, order)daje od razu posortowaną obsadę bez sortowania w Pythonie.
4. Service layer — movies/services.py (ok. 1000 linii)¶
Wzorzec: Service layer / Application service. Views są cienkie, serwisy zawierają:
- Transakcyjność — operacje wieloetapowe (
upsert_rating,set_movie_status,delete_own_comment) opakowane wtransaction.atomic. - Cache-first dla TMDB —
fetch_and_cache_movie(tmdb_id)zagląda najpierw do bazy, potem do TMDB, potem zapisuje:
def fetch_and_cache_movie(tmdb_id: int) -> Movie | None:
if not (movie := Movie.objects.filter(tmdb_id=tmdb_id).first()):
... # try TMDB, persist
if _credits_stale(movie):
backfill_credits(movie) # MovieCredit + Person
return movie
- Refresh aggregates po każdej operacji ratingowej:
@transaction.atomic
def upsert_rating(user, movie, score: Decimal) -> Rating:
rating, _ = Rating.objects.update_or_create(
user=user, movie=movie, defaults={"score": score}
)
_refresh_movie_aggregates(movie)
return rating
def _refresh_movie_aggregates(movie: Movie) -> None:
aggr = movie.ratings.aggregate(avg=Avg("score"), n=Count("id"))
Movie.objects.filter(pk=movie.pk).update(
average_rating=aggr["avg"] or Decimal("0.00"),
ratings_count=aggr["n"] or 0,
)
- Visible comments query używa indeksu
(movie, status, -created_at):
def visible_comments_for(movie: Movie) -> QuerySet[Comment]:
return movie.comments.filter(status=Comment.VISIBLE).select_related("user").order_by("-created_at")
5. Klient TMDB — movies/tmdb.py¶
Wzorzec: Adapter + typed payloads (Pydantic-podobne TypedDict-y TmdbMovieSummary, TmdbMovieDetail, TmdbCredits).
httpx.Clientz timeoutem konfigurowalnym przez ENV.- Centralne wyjątki:
TmdbApiError,TmdbConfigError(separate config-vs-runtime errors). - Wszystkie zapytania dodają
language=pl-PL(lub wartość zesettings.TMDB_LANGUAGE). append_to_responseużywane do pobrania szczegółów + credits w jednym GET (/movie/<id>?append_to_response=credits).
6. Class-based vs function-based views — movies/views.py¶
Decyzja stylu: CBV dla widoków renderujących templatkę, FBV dla akcji POST modyfikujących stan.
class MovieListView(TemplateView): # renderuje listę
template_name = "movies/list.html"
def get_context_data(self, **kwargs): ...
@login_required
@require_POST
def update_movie_rating(request, tmdb_id: int): # POST endpoint
movie = get_object_or_404(Movie, tmdb_id=tmdb_id)
...
Powód: CBV dają darmowo get_context_data, mixiny (LoginRequiredMixin).
FBV są krótsze i czytelniejsze dla wąskich akcji, gdzie cały handler to
walidacja + 1 wywołanie serwisowe.
7. Konfiguracja środowiskowa — config/settings.py¶
Wzorzec: 12-factor — wszystkie różnice dev/prod sterowane zmiennymi środowiskowymi z bezpiecznymi domyślnymi.
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
DEBUG = env_bool("DJANGO_DEBUG", True)
DATABASES = {"default": dj_database_url.config(default=None) or _SQLITE}
if not DEBUG: # production hardening
SECURE_SSL_REDIRECT = env_bool("SECURE_SSL_REDIRECT", True)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = int(os.getenv("SECURE_HSTS_SECONDS", "31536000"))
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = "same-origin"
X_FRAME_OPTIONS = "DENY"
Decyzja: harden tylko gdy DEBUG=False, by lokalny runserver
działał bez SSL.
Komendy administracyjne¶
W movies/management/commands/:
| Komenda | Cel |
|---|---|
sync_tmdb_genres |
Jednorazowy import 19 kanonicznych gatunków TMDB |
sync_tmdb_popular --pages N |
Pobranie N stron popularnych filmów (po 20 sztuk) |
backfill_credits |
Uzupełnienie obsady i reżyserii dla istniejących filmów (gdy Person/MovieCredit zostały dodane po Movie) |
normalize_genres |
Naprawa polskich nazw gatunków (gdyby TMDB zwróciło angielskie etykiety) |
Pełna instrukcja użycia: Podręcznik administratora.
Wzorce projektowe — podsumowanie¶
| Wzorzec | Lokalizacja |
|---|---|
| Service layer | movies/services.py, accounts/utils.py |
| Adapter | movies/tmdb.py (Aster ↔ TMDB) |
| Form template method (Django) | RegisterForm.save(), LoginForm.clean() |
| Through-table (m2m z atrybutami) | MovieCredit |
| Cache-first / lazy loading | fetch_and_cache_movie |
| Stateless token | default_token_generator w aktywacji i password reset |
| 12-factor configuration | config/settings.py |
| Progressive enhancement | toggle gatunków, modal oceny — działają bez JS w trybie minimalnym |