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“.
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.
# 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.
# 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
]
| Detalj | Zaš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.user | vlasnik se ne bira u formi – inače bi korisnik mogao da sačuva logo „na tuđe ime“ |
redirect_to_login | neprijavljen 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 POST | GET prikazuje pitanje „Obriši?“, a briše tek potvrda – link sam ne briše ništa |
{% 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.
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.
Za svaki zadatak postoji primer, uputstvo i rešenje.
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“.
fields forme – postavi ga u pogledu.request.user; samo pk u adresi nije zaštita.naziv, slogan, boja, oblik, font.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)?