Django: generator, čuvanje i brisanje logoa

Lekcija 8 od 9 · Python web: Django projekat

Spajamo sve: generator iz frontend projekta postaje Django forma, prijavljen korisnik čuva logo u bazu, na strani „Moji logoi“ vidi sve svoje logoe nacrtane na serveru i može da ih briše – ali samo svoje.

Pre nego što počneš: potrebno predznanje – prijava i zaštićene strane, generator logoa (frontend).
Kako da učiš: radi u svom projektu korak po korak i posle svake lekcije proveri da li server radi; zadatak na kraju prvo uradi sam/a. Kod je za Django 5.2 (LTS) i Python 3.12+; svaki deo upiši u fajl naveden u komentaru na vrhu primera.

ModelForm: forma iz modela

# logoi/forms.py
from django import forms
from .models import Logo


class LogoForma(forms.ModelForm):
    class Meta:
        model = Logo
        fields = ["naziv", "slogan", "boja", "oblik", "font"]
        widgets = {
            "naziv": forms.TextInput(attrs={"class": "form-control", "maxlength": 20}),
            "slogan": forms.TextInput(attrs={"class": "form-control", "maxlength": 30}),
            "boja": forms.TextInput(attrs={"type": "color", "class": "form-control form-control-color w-100"}),
            "oblik": forms.Select(attrs={"class": "form-select"}),
            "font": forms.Select(attrs={"class": "form-select"}),
        }

ModelForm pravi polja forme iz polja modela: choices postaju padajuće liste, validator boje proverava unos, a max_length dužinu. Pomoću widgets poljima dajemo Bootstrap klase i tip color za izbor boje.

Pogledi

# logoi/views.py (konačna verzija – lekcija 8)
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import redirect_to_login
from django.shortcuts import get_object_or_404, redirect, render

from .forms import LogoForma
from .models import Logo


def naslovna(request):
    return render(request, "logoi/naslovna.html")


def generator(request):
    if request.method == "POST":
        if not request.user.is_authenticated:                 # čuvanje samo za prijavljene
            return redirect_to_login(request.get_full_path())
        forma = LogoForma(request.POST, auto_id="%s")
        if forma.is_valid():
            logo = forma.save(commit=False)                   # objekat još nije u bazi
            logo.vlasnik = request.user                       # vlasnika određuje server, ne forma
            logo.save()
            messages.success(request, f"Logo „{logo.naziv}“ je sačuvan.")
            return redirect("moji_logoi")
    else:
        naziv = request.GET.get("naziv", "Logo")[:20]
        forma = LogoForma(initial={"naziv": naziv}, auto_id="%s")
    return render(request, "logoi/generator.html", {"forma": forma})


@login_required
def moji_logoi(request):
    logoi = request.user.logoi.all()                          # related_name="logoi" iz modela
    return render(request, "logoi/moji_logoi.html", {"logoi": logoi})


@login_required
def obrisi(request, pk):
    logo = get_object_or_404(Logo, pk=pk, vlasnik=request.user)   # tuđi logo -> 404
    if request.method == "POST":
        logo.delete()
        messages.info(request, "Logo je obrisan.")
        return redirect("moji_logoi")
    return render(request, "logoi/obrisi.html", {"logo": logo})
# logoi/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("", views.naslovna, name="naslovna"),
    path("generator/", views.generator, name="generator"),
    path("moji-logoi/", views.moji_logoi, name="moji_logoi"),            # lekcija 8
    path("moji-logoi/<int:pk>/obrisi/", views.obrisi, name="obrisi"),   # lekcija 8
]
DetaljZašto
auto_id="%s"polja dobijaju id naziv, boja… (umesto id_naziv) – isti kao u generator.js, pa JavaScript radi bez izmena
save(commit=False) + logo.vlasnik = request.uservlasnik se ne bira u formi – inače bi korisnik mogao da sačuva logo „na tuđe ime“
redirect_to_loginneprijavljen korisnik koji klikne „Sačuvaj“ ide na prijavu, pa se vraća na generator
get_object_or_404(Logo, pk=pk, vlasnik=request.user)tuđi logo daje grešku 404, kao da ne postoji
brisanje samo na POSTGET prikazuje pitanje „Obriši?“, a briše tek potvrda – link sam ne briše ništa

Šabloni: generator, moji logoi, brisanje

{% extends "base.html" %}
{% load static %}

{% block naslov %}Generator – Logo Kreator{% endblock %}

{% block sadrzaj %}
<div class="container py-4">
  <h1 class="h3 mb-4">Generator logoa</h1>
  <div class="row g-4">
    <form id="forma" class="col-md-5" method="post">
      {% csrf_token %}
      {% for polje in forma %}
        <div class="mb-3">
          <label for="{{ polje.id_for_label }}" class="form-label">{{ polje.label }}</label>
          {{ polje }}
          {% for greska in polje.errors %}<div class="text-danger small">{{ greska }}</div>{% endfor %}
        </div>
      {% endfor %}
      {% if user.is_authenticated %}
        <button type="submit" class="btn btn-danger">Sačuvaj u „Moje logoe“</button>
      {% else %}
        <p class="small text-body-secondary"><a href="{% url 'login' %}?next={{ request.get_full_path|urlencode }}">Prijavi se</a> da bi čuvao/la logoe.</p>
      {% endif %}
      <button type="button" id="preuzmiSvg" class="btn btn-outline-danger">Preuzmi SVG</button>
      <button type="button" id="preuzmiPng" class="btn btn-outline-danger">Preuzmi PNG</button>
    </form>
    <div class="col-md-7 text-center">
      <div id="prikaz" class="p-3 bg-white rounded shadow-sm"></div>
    </div>
  </div>
</div>
{% endblock %}

{% block skripte %}
<script src="{% static 'logoi/js/generator.js' %}"></script>
{% endblock %}

JavaScript iz lekcije Generator logoa ostaje skoro isti:

// logoi/static/logoi/js/generator.js – izmena u odnosu na lekciju 16 kursa HTML/CSS/JS:
// polja sada pravi Django forma (id-jevi su isti zahvaljujući auto_id="%s"),
// a naziv iz adrese već je upisan u polje na serveru (initial), pa deo sa URLSearchParams više ne treba.
// Ostatak fajla (el, inicijali, nacrtaj, preuzimanje) je isti.
{# templates/logoi/_logo_svg.html – crta sačuvani logo na serveru (isti izgled kao generator.js) #}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200" width="100%" role="img" aria-label="Logo {{ logo.naziv }}">
  {% if logo.oblik == "krug" %}
    <circle cx="100" cy="100" r="70" fill="{{ logo.boja }}"/>
  {% elif logo.oblik == "kvadrat" %}
    <rect x="30" y="30" width="140" height="140" rx="24" fill="{{ logo.boja }}"/>
  {% else %}
    <polygon points="100,25 175,100 100,175 25,100" fill="{{ logo.boja }}"/>
  {% endif %}
  <text x="100" y="116" text-anchor="middle" font-size="46" font-weight="bold" font-family="{{ logo.font }}" fill="#fff">{{ logo.inicijali }}</text>
  <text x="195" y="{% if logo.slogan %}100{% else %}112{% endif %}" font-size="28" font-weight="bold" font-family="{{ logo.font }}" fill="#222">{{ logo.naziv }}</text>
  {% if logo.slogan %}
    <text x="197" y="132" font-size="16" font-family="{{ logo.font }}" fill="{{ logo.boja }}">{{ logo.slogan }}</text>
  {% endif %}
</svg>

Sačuvani logo se crta na serveru istim SVG elementima kao u generatoru. Vrednosti iz baze ({{ logo.naziv }}) Django automatski „escape“-uje, pa ni naziv sa <script> ne može da napravi štetu.

{% extends "base.html" %}

{% block naslov %}Moji logoi – Logo Kreator{% endblock %}

{% block sadrzaj %}
<div class="container py-4">
  <div class="d-flex justify-content-between align-items-center mb-3">
    <h1 class="h3 mb-0">Moji logoi ({{ logoi|length }})</h1>
    <a class="btn btn-danger" href="{% url 'generator' %}">+ Novi logo</a>
  </div>
  <div class="row g-3">
    {% for logo in logoi %}
      <div class="col-12 col-md-6 col-lg-4">
        <div class="card h-100 shadow-sm">
          <div class="card-body">{% include "logoi/_logo_svg.html" %}</div>
          <div class="card-footer d-flex justify-content-between align-items-center">
            <small class="text-body-secondary">{{ logo.napravljen|date:"d.m.Y. H:i" }}</small>
            <a class="btn btn-sm btn-outline-danger" href="{% url 'obrisi' logo.pk %}">Obriši</a>
          </div>
        </div>
      </div>
    {% empty %}
      <p>Još nemaš sačuvanih logoa. <a href="{% url 'generator' %}">Napravi prvi!</a></p>
    {% endfor %}
  </div>
</div>
{% endblock %}
{% extends "base.html" %}

{% block sadrzaj %}
<div class="container py-4" style="max-width: 520px">
  <h1 class="h4">Obriši logo „{{ logo.naziv }}“?</h1>
  <div class="my-3">{% include "logoi/_logo_svg.html" %}</div>
  <form method="post">
    {% csrf_token %}
    <button class="btn btn-danger">Da, obriši</button>
    <a class="btn btn-secondary" href="{% url 'moji_logoi' %}">Odustani</a>
  </form>
</div>
{% endblock %}

{% empty %} se prikazuje kada je lista prazna, a filter date:"d.m.Y. H:i" ispisuje datum na naš način.

Automatski testovi

Django ima ugrađen sistem testova: pokreće projekat sa privremenom, praznom bazom i proverava odgovore. Dva testa koja čuvaju najvažnija pravila:

# logoi/tests.py
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse

from .models import Logo


class LogoTestovi(TestCase):
    def setUp(self):
        self.ana = User.objects.create_user("ana", password="Lozinka.2026")
        self.marko = User.objects.create_user("marko", password="Lozinka.2026")
        self.logo = Logo.objects.create(vlasnik=self.ana, naziv="Sekcija Pupin")

    def test_inicijali(self):
        self.assertEqual(self.logo.inicijali(), "SP")

    def test_cuvanje_postavlja_vlasnika(self):
        self.client.login(username="marko", password="Lozinka.2026")
        self.client.post(reverse("generator"), {"naziv": "Moj klub", "slogan": "", "boja": "#112233",
                                                 "oblik": "krug", "font": "Arial, sans-serif"})
        self.assertEqual(Logo.objects.get(naziv="Moj klub").vlasnik, self.marko)

    def test_tudji_logo_se_ne_brise(self):
        self.client.login(username="marko", password="Lozinka.2026")
        odgovor = self.client.post(reverse("obrisi", args=[self.logo.pk]))
        self.assertEqual(odgovor.status_code, 404)
        self.assertTrue(Logo.objects.filter(pk=self.logo.pk).exists())

    def test_moji_logoi_trazi_prijavu(self):
        odgovor = self.client.get(reverse("moji_logoi"))
        self.assertEqual(odgovor.status_code, 302)          # preusmerenje na prijavu
python manage.py test

Testove pokreni posle svake veće izmene. Ako neko slučajno izbaci proveru vlasnika iz pogleda, test test_tudji_logo_se_ne_brise će odmah pasti.

Zadaci za vežbu

Za svaki zadatak postoji primer, uputstvo i rešenje.

ZADATAK 01Težina: teže

Izmena sačuvanog logoa

Dodaj stranu /moji-logoi/<pk>/izmeni/ na kojoj korisnik menja svoj logo u generatoru (forma popunjena postojećim vrednostima) i dugme „Izmeni“ na kartici u „Mojim logoima“.

Česte greške i saveti
  • Polje koje ne sme da menja korisnik (vlasnik, datum) ne stavljaj u fields forme – postavi ga u pogledu.
  • Svaki upit za tuđe podatke filtriraj po request.user; samo pk u adresi nije zaštita.
  • Ako JavaScript generatora ne radi, u F12 proveri id-jeve polja – moraju biti naziv, slogan, boja, oblik, font.

Proveri znanje

Otvori kviz

Čuvanje logoa

1. Zašto se vlasnik logoa postavlja u pogledu, a ne bira u formi?



2. Šta vraća get_object_or_404(Logo, pk=5, vlasnik=request.user) ako je logo 5 tuđi?




3. Šta radi save(commit=False)?