Yaliyomo

Wiki 16: Django Sehemu ya 2 — Forms, CRUD, Class-Based Views, Authentication na Mradi wa Mwisho

Mwezi 4 | Lengo kuu: Mwanafunzi aongeze uwezo wa kuandika data kupitia forms (CRUD), atumie class-based views, aweke login/logout/usajili, kisha ajenge mradi wa mwisho unaounganisha kila kitu — na kufunga kozi.


Maandalizi ya Mwalimu (soma kabla ya wiki kuanza)


Malengo ya Wiki (mwisho wa wiki mwanafunzi ataweza...)

  1. Kutengeneza ModelForm na kuthibitisha data (validation) kwa clean_<field>.
  2. Kuandika views za CRUD (Create, Read, Update, Delete) kwa function-based views, pamoja na {% csrf_token %}.
  3. Kuonyesha ujumbe kwa mtumiaji kwa messages framework.
  4. Kubadilisha views kuwa class-based views: ListView, DetailView, CreateView, UpdateView, DeleteView.
  5. Kuweka login, logout (POST), na usajili (signup) kwa UserCreationForm.
  6. Kulinda kurasa kwa @login_required / LoginRequiredMixin na kuruhusu mwandishi tu kuhariri post yake.
  7. Kujenga mradi kamili wa Django (Duka/Blog) kwa kujitegemea.
  8. Kueleza hatua kuu za kuweka (deploy) Django mtandaoni (PythonAnywhere/Render).
  9. Kujua njia za kuendelea kujifunza baada ya kozi.

Kipindi 1 — ModelForms, CRUD na Messages

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani ya Wiki 15 (Mapishi)
  2. HTML forms, GET vs POST, CSRF — kwa nini?
  3. ModelForm na validation
  4. Views za Create na Update (pattern ya GET/POST)
  5. Delete na ukurasa wa kuthibitisha; messages
  6. Zoezi darasani
  7. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

GET vs POST:

  • GET = kuomba/kusoma ukurasa (kufungua link, kutafuta ?q=). Haipaswi kubadilisha data.
  • POST = kutuma data inayobadilisha kitu (kuongeza, kuhariri, kufuta).

CSRF (Cross-Site Request Forgery) — shambulio ambapo website mbaya inamfanya browser yako itume POST kwenye site uliyoingia (mf. "futa post zangu"). Django inazuia kwa token ya siri inayowekwa kwenye kila form: {% csrf_token %}. Ukisahau → 403 Forbidden.

ModelForm — form inayotengenezwa moja kwa moja kutoka kwenye model. Unasema model na fields; Django inatengeneza inputs, inathibitisha aina za data (mf. kichwa si zaidi ya herufi 200), na form.save() inahifadhi kwenye database. Validation ya ziada: method clean_<jina_la_field> inayorudisha thamani safi au kurusha ValidationError.

Pattern ya view ya form (ikariri — ni ile ile kila mahali):

text
Kama request ni POST:
    form = Form(request.POST)          # jaza form kwa data iliyotumwa
    kama form ni sahihi (is_valid):
        hifadhi -> ujumbe -> redirect   # "Post/Redirect/Get" — refresh haitatuma tena
    (vinginevyo: tunaanguka chini na kuonyesha form pamoja na makosa)
Vinginevyo (GET):
    form = Form()                       # form tupu
render template pamoja na form

Kwa nini redirect baada ya POST? Mtumiaji akibonyeza F5 baada ya POST bila redirect, browser itatuma POST tena → post mbili zinazofanana. Redirect inazuia hilo.

form.save(commit=False) — tengeneza object bila kuhifadhi bado, ili tuongeze vitu visivyo kwenye form (mf. mwandishi = request.user), kisha save().

Update = ile ile, lakini PostForm(request.POST, instance=post) — form inajazwa na data ya post iliyopo na save() inasasisha badala ya kutengeneza mpya.

Delete — tunauliza "Una uhakika?" kwa GET (ukurasa wa kuthibitisha), na tunafuta kwa POST tu.

Messages framework — ujumbe wa mara moja ("Post imehifadhiwa!") unaoonyeshwa kwenye ukurasa unaofuata, kisha unapotea. messages.success(request, "..."), kisha kwenye base.html tunaonyesha {% for message in messages %}.

Mwandishi wa post: Post inahitaji mwandishi. Kwa leo tutaingia (login) kupitia ukurasa wa admin (session ile ile inatumika kwenye site yote), na kutumia @login_required. Kwa muda tutaweka LOGIN_URL = "admin:login"; Kipindi 2 tutatengeneza ukurasa wetu wa login.

Sema hivi kwa mwanafunzi

"GET ni kama kusoma tangazo ubaoni. POST ni kama kupeleka barua ofisini kuomba kitu kibadilishwe. Hutaki mtu abadilishe kitu kwa kusoma tu."

"CSRF token ni kama muhuri wa ofisi kwenye barua. Barua isiyo na muhuri wetu — hata ikionekana halali — tunaikataa."

"ModelForm ni kama fomu ya benki iliyochapishwa kulingana na mfumo wa benki — haikuruhusu kuandika herufi kwenye nafasi ya namba ya akaunti."

"Redirect baada ya POST ni kama kupewa risiti na kuambiwa 'nenda dirisha jingine'. Hukai pale ukibonyeza 'tuma' tena na tena."

Andika code hii live (share screen)

Hatua 1: Form — tengeneza blog/forms.py

python
from django import forms

from .models import Post


class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["kichwa", "kundi", "maudhui", "imechapishwa"]
        labels = {
            "kichwa": "Kichwa cha post",
            "kundi": "Kundi",
            "maudhui": "Maudhui",
            "imechapishwa": "Chapisha sasa?",
        }
        widgets = {
            "maudhui": forms.Textarea(attrs={"rows": 8}),
        }

    def clean_kichwa(self):
        kichwa = self.cleaned_data["kichwa"].strip()
        if len(kichwa) < 5:
            raise forms.ValidationError("Kichwa kiwe na herufi angalau 5.")
        return kichwa

Hatua 2: Settings (muda) — config/settings.py (chini kabisa)

python
LOGIN_URL = "admin:login"     # kwa leo tu; Jumatano tutabadilisha

Hatua 3: Views za CRUD — blog/views.py (ongeza/rekebisha)

python
from datetime import date

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render

from .forms import PostForm
from .models import Post


def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    # Rasimu (haijachapishwa) inaonekana kwa mwandishi wake tu
    if not post.imechapishwa and post.mwandishi != request.user:
        raise Http404("Post haipo")
    return render(request, "blog/post_detail.html", {"post": post})


@login_required
def post_create(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.mwandishi = request.user
            post.save()
            messages.success(request, f'Post "{post.kichwa}" imeongezwa!')
            return redirect(post)          # inatumia post.get_absolute_url()
    else:
        form = PostForm()
    return render(request, "blog/post_form.html", {"form": form})


@login_required
def post_update(request, pk):
    # Mwandishi tu ndiye anapata post yake; wengine 404
    post = get_object_or_404(Post, pk=pk, mwandishi=request.user)
    if request.method == "POST":
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            messages.success(request, "Post imesasishwa.")
            return redirect(post)
    else:
        form = PostForm(instance=post)
    return render(request, "blog/post_form.html", {"form": form, "post": post})


@login_required
def post_delete(request, pk):
    post = get_object_or_404(Post, pk=pk, mwandishi=request.user)
    if request.method == "POST":
        kichwa = post.kichwa
        post.delete()
        messages.success(request, f'Post "{kichwa}" imefutwa.')
        return redirect("blog:nyumbani")
    return render(request, "blog/post_confirm_delete.html", {"post": post})

Hatua 4: URLs — blog/urls.py

python
from django.urls import path
from . import views

app_name = "blog"

urlpatterns = [
    path("", views.nyumbani, name="nyumbani"),
    path("kuhusu/", views.kuhusu, name="kuhusu"),
    path("post/mpya/", views.post_create, name="post_create"),
    path("post/<int:pk>/", views.post_detail, name="post_detail"),
    path("post/<int:pk>/hariri/", views.post_update, name="post_update"),
    path("post/<int:pk>/futa/", views.post_delete, name="post_delete"),
]

Hatua 5: Templates

blog/templates/blog/post_form.html (moja kwa kuongeza NA kuhariri):

html
{% extends "blog/base.html" %}

{% block title %}{% if form.instance.pk %}Hariri post{% else %}Post mpya{% endif %}{% endblock %}

{% block content %}
    <h2>{% if form.instance.pk %}Hariri: {{ form.instance.kichwa }}{% else %}Andika post mpya{% endif %}</h2>

    <form method="post" novalidate>
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">Hifadhi</button>
        <a href="{% url 'blog:nyumbani' %}">Ghairi</a>
    </form>
{% endblock %}

novalidate inazima ukaguzi wa browser ili tuone validation ya Django darasani. Kwenye mradi halisi unaweza kuiondoa.

blog/templates/blog/post_confirm_delete.html:

html
{% extends "blog/base.html" %}

{% block title %}Futa post{% endblock %}

{% block content %}
    <h2>Una uhakika unataka kufuta "{{ post.kichwa }}"?</h2>
    <p>Kitendo hiki hakiwezi kurudishwa.</p>
    <form method="post">
        {% csrf_token %}
        <button type="submit" style="background:#c62828; color:white;">Ndiyo, futa</button>
        <a href="{{ post.get_absolute_url }}">Hapana, rudi</a>
    </form>
{% endblock %}

Kwenye post_detail.html, ongeza chini ya </article>:

html
    {% if user == post.mwandishi %}
        <p>
            <a href="{% url 'blog:post_update' post.pk %}">✏️ Hariri</a> |
            <a href="{% url 'blog:post_delete' post.pk %}">🗑️ Futa</a>
        </p>
    {% endif %}

Kwenye base.html, ongeza kwenye <nav> link ya post mpya, na juu ya {% block content %} onyesha messages:

html
        <nav>
            <a href="{% url 'blog:nyumbani' %}">Nyumbani</a>
            <a href="{% url 'blog:kuhusu' %}">Kuhusu</a>
            <a href="{% url 'blog:post_create' %}">+ Post mpya</a>
        </nav>
html
    <main>
        {% if messages %}
            {% for message in messages %}
                <div class="ujumbe ujumbe-{{ message.tags }}">{{ message }}</div>
            {% endfor %}
        {% endif %}

        {% block content %}
        {% endblock %}
    </main>

style.css ongeza:

css
.ujumbe { padding: 10px; border-radius: 6px; margin: 12px 0; }
.ujumbe-success { background: #c8e6c9; }
.ujumbe-error { background: #ffcdd2; }
.errorlist { color: #c62828; }

Hatua 6: Jaribu

  1. python manage.py runserver, fungua http://127.0.0.1:8000/post/mpya/ ukiwa hujaingia → unapelekwa kwenye login ya admin → ingia → unarudishwa kwenye form (?next=).
  2. Andika kichwa "Hi" → "Kichwa kiwe na herufi angalau 5." (validation yetu).
  3. Andika post sahihi → ujumbe wa kijani "Post ... imeongezwa!" kwenye ukurasa wa post.
  4. Hariri → futa → ujumbe.
  5. Refresh baada ya kuhifadhi → hakuna post mbili (shukrani kwa redirect).
  6. Kwa makusudi ondoa {% csrf_token %} → tuma → 403 Forbidden (CSRF verification failed). Rudisha.

Maswali ya kumuuliza mwanafunzi

  1. Swali: Kwa nini tunafuta kwa POST na si kwa link ya GET? Jibu: GET haipaswi kubadilisha data; link inaweza kufunguliwa kwa bahati mbaya au na roboti; POST inalindwa na CSRF.
  2. Swali: commit=False inatumika lini? Jibu: Tunapohitaji kuongeza data isiyotoka kwenye form (mf. mwandishi) kabla ya kuhifadhi.
  3. Swali: Tofauti ya PostForm(request.POST) na PostForm(request.POST, instance=post)? Jibu: Ya kwanza inatengeneza post mpya; ya pili inasasisha post iliyopo.
  4. Swali: Kwa nini get_object_or_404(Post, pk=pk, mwandishi=request.user)? Jibu: Ili mtu asiweze kuhariri/kufuta post ya mwingine kwa kubadilisha namba kwenye URL.
  5. Swali: Ujumbe wa messages unaonekana mara ngapi? Jibu: Mara moja tu — kwenye ukurasa unaofuata, kisha unapotea.

Makosa ya kawaida

Kosa Ujumbe Suluhisho
Kusahau {% csrf_token %} 403 Forbidden — CSRF verification failed. Request aborted. Weka {% csrf_token %} ndani ya kila <form method="post">
form.save() bila mwandishi IntegrityError: NOT NULL constraint failed: blog_post.mwandishi_id save(commit=False), weka mwandishi, kisha save()
Kusahau method="post" kwenye form Data inaenda kwenye URL (?kichwa=...), hakuna kinachohifadhiwa <form method="post">
Kurudisha render badala ya redirect baada ya kuhifadhi Refresh inatengeneza nakala return redirect(...)
Hujaingia (login) Unapelekwa login / ?next= Ingia kupitia /admin/
post/mpya/ imewekwa BAADA ya post/<int:pk>/? (Hapa haina tatizo kwa sababu "mpya" si int) — ila kwa <str:...> ingegongana Weka paths maalum kabla ya zenye vigezo vya jumla
Messages hazionekani (hakuna error) Ongeza loop ya messages kwenye base.html

Zoezi darasani

Zoezi 1.1: Ongeza validation kwenye PostForm: maudhui yawe na angalau maneno 10 (kidokezo: len(maudhui.split())).

Zoezi 1.2: Tengeneza KundiForm na view kundi_create (/kundi/mpya/, login inahitajika) ya kuongeza kundi jipya. Jina la kundi lisirudiwe — tazama kinachotokea ukijaribu jina lililopo (unique).

Zoezi 1.3: Kwenye ukurasa wa post moja, ongeza form ya kuongeza maoni (model Maoni ya Wiki 15) — mtu yeyote (hata bila login) anaweza kutoa maoni.

Jibu
python
# blog/forms.py (ongeza)
from .models import Kundi, Maoni


class PostForm(forms.ModelForm):
    # ... Meta na clean_kichwa kama juu ...

    def clean_maudhui(self):                       # 1.1
        maudhui = self.cleaned_data["maudhui"]
        if len(maudhui.split()) < 10:
            raise forms.ValidationError("Maudhui yawe na angalau maneno 10.")
        return maudhui


class KundiForm(forms.ModelForm):                  # 1.2
    class Meta:
        model = Kundi
        fields = ["jina"]


class MaoniForm(forms.ModelForm):                  # 1.3
    class Meta:
        model = Maoni
        fields = ["jina", "ujumbe"]
        widgets = {"ujumbe": forms.Textarea(attrs={"rows": 3})}
python
# blog/views.py
from .forms import KundiForm, MaoniForm, PostForm


@login_required
def kundi_create(request):
    form = KundiForm(request.POST or None)
    if request.method == "POST" and form.is_valid():
        kundi = form.save()
        messages.success(request, f'Kundi "{kundi}" limeongezwa.')
        return redirect("blog:nyumbani")
    return render(request, "blog/kundi_form.html", {"form": form})
# Jina lililopo -> form inaonyesha "Kundi with this Jina already exists." (au kwa Kiswahili) — unique inathibitishwa na ModelForm yenyewe.


def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if not post.imechapishwa and post.mwandishi != request.user:
        raise Http404("Post haipo")
    if request.method == "POST":
        maoni_form = MaoniForm(request.POST)
        if maoni_form.is_valid():
            maoni = maoni_form.save(commit=False)
            maoni.post = post
            maoni.save()
            messages.success(request, "Asante kwa maoni yako!")
            return redirect(post)
    else:
        maoni_form = MaoniForm()
    return render(request, "blog/post_detail.html", {"post": post, "maoni_form": maoni_form})
python
# blog/urls.py
    path("kundi/mpya/", views.kundi_create, name="kundi_create"),
html
<!-- blog/templates/blog/kundi_form.html -->
{% extends "blog/base.html" %}
{% block content %}
<h2>Kundi jipya</h2>
<form method="post">{% csrf_token %}{{ form.as_p }}<button type="submit">Hifadhi</button></form>
{% endblock %}

<!-- post_detail.html — chini ya orodha ya maoni -->
<h4>Toa maoni</h4>
<form method="post">
    {% csrf_token %}
    {{ maoni_form.as_p }}
    <button type="submit">Tuma</button>
</form>

Kipindi 2 — Class-Based Views na Authentication

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani (Kipindi 1)
  2. Class-based views: ListView, DetailView, CreateView, UpdateView, DeleteView
  3. Mixins: LoginRequiredMixin, UserPassesTestMixin, SuccessMessageMixin
  4. Auth: login, logout (POST!), usajili (signup)
  5. Zoezi darasani
  6. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

Class-based views (CBVs). Ulivyoona Kipindi 1, views za CRUD zinarudiwa sana: "pata object au 404, onyesha form, kama POST na sahihi hifadhi na redirect". Django imeandika views hizi tayari kama classes — tunarithi (inheritance — ile ile ya OOP tuliyojifunza!) na kubadilisha vipande tu.

CBV Kazi Template ya default Context
ListView orodha blog/post_list.html object_list, post_list (+ page_obj ukiweka paginate_by)
DetailView object moja (kwa pk) blog/post_detail.html object, post
CreateView form ya kuongeza blog/post_form.html form
UpdateView form ya kuhariri blog/post_form.html form, object, post
DeleteView kuthibitisha + kufuta blog/post_confirm_delete.html object, post

Habari njema: templates tulizotengeneza Kipindi 1 zina majina hayo hayo — zitafanya kazi bila kubadilishwa!

Methods tunazobadilisha mara nyingi:

  • get_queryset() — badilisha data inayochukuliwa (mf. zilizochapishwa tu, utafutaji).
  • get_context_data() — ongeza vitu kwenye context.
  • form_valid(form) — fanya kitu kabla ya kuhifadhi (mf. weka mwandishi).
  • success_url — wapi pa kwenda baada ya kufanikiwa. Kama haipo, CreateView/UpdateView zinatumia get_absolute_url() ya model. Kwa DeleteView lazima uiweke; tunatumia reverse_lazy (si reverse) kwa sababu URLs hazijapakiwa bado wakati class inaposomwa.
  • URLs: views.PostListView.as_view() — as_view() inageuza class kuwa function ambayo Django inaweza kuita.

Mixins — vipande vidogo vya tabia tunavyoongeza kwa kurithi zaidi ya class moja (multiple inheritance). Viwekwe kushoto kwa View kuu:

  • LoginRequiredMixin — kama @login_required kwa CBVs.
  • UserPassesTestMixin — tunaandika test_func(); ikirudisha False → 403 Forbidden.
  • SuccessMessageMixin — success_message = "..." inaongeza ujumbe wa messages yenyewe.

Authentication ya Django iko tayari (django.contrib.auth):

  • path("accounts/", include("django.contrib.auth.urls")) inaleta URLs: login, logout, password_change, password_reset... (majina ya URL ni login, logout — bila namespace).
  • Tunahitaji template tu: registration/login.html (Django inaitafuta kwa jina hilo).
  • Settings: LOGIN_URL = "login", LOGIN_REDIRECT_URL (baada ya kuingia), LOGOUT_REDIRECT_URL (baada ya kutoka).
  • Django 5: logout ni POST tu. Tunaweka form ndogo yenye kitufe "Toka" na {% csrf_token %}.
  • Usajili: Django haina view ya signup, lakini ina UserCreationForm (username + password mara mbili, ikithibitisha nguvu ya password). Tunaitumia na CreateView, kisha tunamwingiza mtumiaji kwa login(request, user).
  • Kwenye templates, user ipo kila mahali: {% if user.is_authenticated %}Habari {{ user.username }}{% endif %}.
  • Passwords zinahifadhiwa kama hash (si maandishi wazi) — Django inafanya yenyewe.

Templates za project nzima: Tutaweka registration/ kwenye folda templates/ ya project (si ndani ya app), na kuiambia Django kupitia TEMPLATES[0]["DIRS"] = [BASE_DIR / "templates"].

Sema hivi kwa mwanafunzi

"Function-based views ni kama kupika chakula kuanzia mwanzo kila siku. Class-based views ni kama kununua chapati za kupasha — zimeshatengenezwa, wewe unaongeza mchuzi wako tu (unabadilisha methods unazohitaji)."

"Mixin ni kama kuongeza 'extras' kwenye oda: ListView + LoginRequired + SuccessMessage. Kila kimoja kinaongeza uwezo mmoja."

"Login ni kama kuonyesha kitambulisho getini. Baada ya hapo unapewa kibandiko (session cookie) — huulizwi tena kila mlango ndani."

"Kuhusu logout kuwa POST: kama logout ingekuwa link ya kawaida, website mbaya ingeweza kuweka picha yenye link hiyo na kukutoa kila unapoifungua. Django 5 imefunga mlango huo."

Andika code hii live (share screen)

Hatua 1: Settings — config/settings.py

python
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],          # <- ongeza hii
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

# Chini kabisa — badilisha LOGIN_URL ya Jumatatu
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "blog:nyumbani"
LOGOUT_REDIRECT_URL = "blog:nyumbani"

Hatua 2: URLs za auth — config/urls.py

python
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")),
    path("", include("blog.urls")),
]

Hatua 3: CBVs — andika upya blog/views.py

python
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import Q
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView

from .forms import PostForm
from .models import Post


class PostListView(ListView):
    model = Post
    template_name = "blog/post_list.html"
    context_object_name = "posts"
    paginate_by = 5

    def get_queryset(self):
        qs = Post.objects.filter(imechapishwa=True).select_related("mwandishi", "kundi")
        swali = self.request.GET.get("q", "").strip()
        if swali:
            qs = qs.filter(Q(kichwa__icontains=swali) | Q(maudhui__icontains=swali))
        return qs

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["swali"] = self.request.GET.get("q", "")
        return context


class PostDetailView(DetailView):
    model = Post

    def get_queryset(self):
        # Zilizochapishwa kwa wote; rasimu kwa mwandishi wake tu
        if self.request.user.is_authenticated:
            return Post.objects.filter(Q(imechapishwa=True) | Q(mwandishi=self.request.user))
        return Post.objects.filter(imechapishwa=True)


class PostCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
    model = Post
    form_class = PostForm
    success_message = 'Post "%(kichwa)s" imeongezwa!'

    def form_valid(self, form):
        form.instance.mwandishi = self.request.user
        return super().form_valid(form)


class MwandishiPekeeMixin(UserPassesTestMixin):
    """Ruhusu mwandishi wa post tu."""
    def test_func(self):
        return self.get_object().mwandishi == self.request.user


class PostUpdateView(LoginRequiredMixin, MwandishiPekeeMixin, SuccessMessageMixin, UpdateView):
    model = Post
    form_class = PostForm
    success_message = "Post imesasishwa."


class PostDeleteView(LoginRequiredMixin, MwandishiPekeeMixin, SuccessMessageMixin, DeleteView):
    model = Post
    success_url = reverse_lazy("blog:nyumbani")
    success_message = "Post imefutwa."


class JisajiliView(CreateView):
    form_class = UserCreationForm
    template_name = "registration/jisajili.html"
    success_url = reverse_lazy("blog:nyumbani")

    def form_valid(self, form):
        response = super().form_valid(form)      # inahifadhi user -> self.object
        login(self.request, self.object)         # mwingize moja kwa moja
        return response


def kuhusu(request):
    return render(request, "blog/kuhusu.html", {"mwalimu": "Jina la Mwalimu"})

Eleza: "Angalia jinsi PostCreateView ni mistari 6 tu — ile function ya Kipindi 1 ilikuwa 13."

Hatua 4: URLs — blog/urls.py

python
from django.urls import path
from . import views

app_name = "blog"

urlpatterns = [
    path("", views.PostListView.as_view(), name="nyumbani"),
    path("kuhusu/", views.kuhusu, name="kuhusu"),
    path("jisajili/", views.JisajiliView.as_view(), name="jisajili"),
    path("post/mpya/", views.PostCreateView.as_view(), name="post_create"),
    path("post/<int:pk>/", views.PostDetailView.as_view(), name="post_detail"),
    path("post/<int:pk>/hariri/", views.PostUpdateView.as_view(), name="post_update"),
    path("post/<int:pk>/futa/", views.PostDeleteView.as_view(), name="post_delete"),
]

Hatua 5: Template ya orodha yenye pagination — blog/templates/blog/post_list.html

html
{% extends "blog/base.html" %}

{% block title %}Posts | Blog Yetu{% endblock %}

{% block content %}
    <form method="get">
        <input type="text" name="q" value="{{ swali }}" placeholder="Tafuta...">
        <button type="submit">Tafuta</button>
    </form>

    {% for post in posts %}
        <article>
            <h3><a href="{{ post.get_absolute_url }}">{{ post.kichwa }}</a></h3>
            <small>na {{ post.mwandishi.username }} | {{ post.tarehe|date:"d/m/Y" }}
                {% if post.kundi %}| {{ post.kundi }}{% endif %}</small>
            <p>{{ post.maudhui|truncatewords:25 }}</p>
        </article>
    {% empty %}
        <p>Hakuna posts.</p>
    {% endfor %}

    {% if is_paginated %}
        <p>
            {% if page_obj.has_previous %}
                <a href="?page={{ page_obj.previous_page_number }}{% if swali %}&q={{ swali|urlencode }}{% endif %}">&laquo; Nyuma</a>
            {% endif %}
            Ukurasa {{ page_obj.number }} kati ya {{ page_obj.paginator.num_pages }}
            {% if page_obj.has_next %}
                <a href="?page={{ page_obj.next_page_number }}{% if swali %}&q={{ swali|urlencode }}{% endif %}">Mbele &raquo;</a>
            {% endif %}
        </p>
    {% endif %}
{% endblock %}

Hatua 6: Templates za auth — tengeneza folda templates/registration/ kwenye mzizi wa project (pembeni ya manage.py).

templates/registration/login.html:

html
{% extends "blog/base.html" %}

{% block title %}Ingia{% endblock %}

{% block content %}
    <h2>Ingia</h2>
    {% if form.errors %}
        <p class="errorlist">Jina au nenosiri si sahihi. Jaribu tena.</p>
    {% endif %}
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="hidden" name="next" value="{{ next }}">
        <button type="submit">Ingia</button>
    </form>
    <p>Huna akaunti? <a href="{% url 'blog:jisajili' %}">Jisajili hapa</a></p>
{% endblock %}

templates/registration/jisajili.html:

html
{% extends "blog/base.html" %}

{% block title %}Jisajili{% endblock %}

{% block content %}
    <h2>Fungua akaunti</h2>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">Jisajili</button>
    </form>
{% endblock %}

Hatua 7: Menyu inayojua mtumiaji — base.html, badilisha <nav>:

html
        <nav>
            <a href="{% url 'blog:nyumbani' %}">Nyumbani</a>
            <a href="{% url 'blog:kuhusu' %}">Kuhusu</a>
            {% if user.is_authenticated %}
                <a href="{% url 'blog:post_create' %}">+ Post mpya</a>
                <span>Habari, {{ user.username }}!</span>
                <form method="post" action="{% url 'logout' %}" style="display:inline;">
                    {% csrf_token %}
                    <button type="submit">Toka</button>
                </form>
            {% else %}
                <a href="{% url 'login' %}">Ingia</a>
                <a href="{% url 'blog:jisajili' %}">Jisajili</a>
            {% endif %}
        </nav>

Hatua 8: Jaribu kila kitu

  1. Toka → "Ingia" na "Jisajili" zinaonekana.
  2. Jisajili kama juma (password kama Python2027!) → unaingizwa moja kwa moja.
  3. Tengeneza post kama juma → ujumbe wa kijani.
  4. Toka, ingia kama superuser, fungua /post/<pk ya post ya juma>/hariri/ → 403 Forbidden (MwandishiPekeeMixin inafanya kazi).
  5. Ukiwa umetoka, fungua /post/mpya/ → unapelekwa /accounts/login/?next=/post/mpya/ → ukiingia unarudishwa kwenye form.
  6. Ongeza posts 6+ → pagination inaonekana.

Hatua 9: Tests za auth — blog/tests.py (ongeza)

python
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse

from .models import Post


class AuthTests(TestCase):
    def setUp(self):
        self.asha = User.objects.create_user("asha", password="Siri-12345")
        self.juma = User.objects.create_user("juma", password="Siri-12345")
        self.post = Post.objects.create(kichwa="Post ya Asha", maudhui="maudhui", mwandishi=self.asha)

    def test_post_mpya_inahitaji_login(self):
        r = self.client.get(reverse("blog:post_create"))
        self.assertRedirects(r, f"{reverse('login')}?next={reverse('blog:post_create')}")

    def test_kuunda_post_ukiwa_umeingia(self):
        self.client.login(username="asha", password="Siri-12345")
        r = self.client.post(reverse("blog:post_create"), {
            "kichwa": "Post mpya kabisa", "maudhui": "Haya ni maudhui", "imechapishwa": True,
        })
        self.assertEqual(r.status_code, 302)
        self.assertTrue(Post.objects.filter(kichwa="Post mpya kabisa", mwandishi=self.asha).exists())

    def test_mtu_mwingine_hawezi_kuhariri(self):
        self.client.login(username="juma", password="Siri-12345")
        r = self.client.get(reverse("blog:post_update", args=[self.post.pk]))
        self.assertEqual(r.status_code, 403)

    def test_logout_ni_post(self):
        self.client.login(username="asha", password="Siri-12345")
        self.assertEqual(self.client.get(reverse("logout")).status_code, 405)
        self.assertEqual(self.client.post(reverse("logout")).status_code, 302)
powershell
python manage.py test blog

Maswali ya kumuuliza mwanafunzi

  1. Swali: Faida na hasara ya CBVs? Jibu: Faida: code fupi, inarudia pattern za kawaida. Hasara: "uchawi" uliofichika — vigumu kuelewa mwanzoni; ukihitaji kitu kisicho cha kawaida, function view inaweza kuwa rahisi zaidi.
  2. Swali: Kwa nini reverse_lazy kwenye success_url na si reverse? Jibu: Class inasomwa wakati URLs bado hazijapakiwa; reverse_lazy inasubiri mpaka itakapohitajika.
  3. Swali: Kwa nini LoginRequiredMixin iwe kushoto kabisa? Jibu: Mixins zinatekelezwa kutoka kushoto; tunataka ukaguzi wa login ufanyike kwanza.
  4. Swali: Juma akijaribu kuhariri post ya Asha anapata nini, na kwa nini? Jibu: 403 — test_func ya MwandishiPekeeMixin inarudisha False.
  5. Swali: Kwa nini logout ni POST kwenye Django 5? Jibu: Kuzuia site nyingine kukutoa kwa link/picha (CSRF); POST inahitaji token.
  6. Swali: Je, Django inahifadhi passwords kama zilivyo? Jibu: Hapana — inahifadhi hash, hata admin haoni password halisi.

Makosa ya kawaida

Kosa Ujumbe Suluhisho
Logout kwa link <a> 405 Method Not Allowed (na kwenye terminal: Method Not Allowed (GET): /accounts/logout/) Tumia <form method="post" action="{% url 'logout' %}"> na {% csrf_token %}
Template ya login haipatikani TemplateDoesNotExist: registration/login.html Weka templates/registration/login.html na "DIRS": [BASE_DIR / "templates"]
Kusahau .as_view() TypeError: View.__init__() takes 1 positional argument but 2 were given views.PostListView.as_view()
reverse badala ya reverse_lazy kwenye class ImproperlyConfigured: The included URLconf ... does not appear to have any patterns (circular import) Tumia reverse_lazy
DeleteView bila success_url ImproperlyConfigured: No URL to redirect to. Provide a success_url. Weka success_url = reverse_lazy(...)
CreateView bila get_absolute_url wala success_url ImproperlyConfigured: No URL to redirect to. Either provide a url or define a get_absolute_url method on the Model. Ongeza get_absolute_url kwenye model (tunayo)
Mixins mwisho badala ya mwanzo Ukaguzi wa login haufanyiki class X(LoginRequiredMixin, ..., CreateView)
Kutumia {% url 'blog:login' %} NoReverseMatch URLs za auth hazina namespace: {% url 'login' %}

Zoezi darasani

Zoezi 2.1: Tengeneza ukurasa /posts-zangu/ (PostZanguView, ListView, login inahitajika) unaoonyesha posts zote za mtumiaji aliyeingia — zikiwemo rasimu — na alama "(rasimu)" kwa zisizochapishwa.

Zoezi 2.2: Tengeneza /mwandishi/<str:username>/ unaoonyesha posts zilizochapishwa za mwandishi huyo (404 kama mwandishi hayupo — kidokezo: get_object_or_404(User, username=...) ndani ya get_queryset).

Zoezi 2.3: Badilisha JisajiliView iongeze ujumbe wa messages "Karibu, ! Akaunti yako imefunguliwa."

Jibu
python
# blog/views.py (ongeza)
from django.contrib import messages
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404


class PostZanguView(LoginRequiredMixin, ListView):          # 2.1
    template_name = "blog/posts_zangu.html"
    context_object_name = "posts"

    def get_queryset(self):
        return Post.objects.filter(mwandishi=self.request.user)


class PostZaMwandishiView(ListView):                        # 2.2
    template_name = "blog/post_list.html"
    context_object_name = "posts"
    paginate_by = 5

    def get_queryset(self):
        self.mwandishi = get_object_or_404(User, username=self.kwargs["username"])
        return Post.objects.filter(mwandishi=self.mwandishi, imechapishwa=True)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["mwandishi"] = self.mwandishi
        return context


class JisajiliView(CreateView):                              # 2.3
    form_class = UserCreationForm
    template_name = "registration/jisajili.html"
    success_url = reverse_lazy("blog:nyumbani")

    def form_valid(self, form):
        response = super().form_valid(form)
        login(self.request, self.object)
        messages.success(self.request, f"Karibu, {self.object.username}! Akaunti yako imefunguliwa.")
        return response
python
# blog/urls.py
    path("posts-zangu/", views.PostZanguView.as_view(), name="posts_zangu"),
    path("mwandishi/<str:username>/", views.PostZaMwandishiView.as_view(), name="mwandishi"),
html
<!-- blog/templates/blog/posts_zangu.html -->
{% extends "blog/base.html" %}
{% block title %}Posts zangu{% endblock %}
{% block content %}
<h2>Posts zangu ({{ posts|length }})</h2>
<ul>
{% for post in posts %}
    <li><a href="{{ post.get_absolute_url }}">{{ post.kichwa }}</a>
        {% if not post.imechapishwa %}<em>(rasimu)</em>{% endif %}
        — <a href="{% url 'blog:post_update' post.pk %}">hariri</a></li>
{% empty %}
    <li>Bado hujaandika post. <a href="{% url 'blog:post_create' %}">Anza sasa</a></li>
{% endfor %}
</ul>
{% endblock %}

Kwenye post_list.html unaweza kuongeza juu: {% if mwandishi %}<h2>Posts za {{ mwandishi.username }}</h2>{% endif %}, na kwenye kila post: <a href="{% url 'blog:mwandishi' post.mwandishi.username %}">{{ post.mwandishi.username }}</a>.


Kipindi 3 — Mradi wa Mwisho, Deployment na Kufunga Kozi

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani (Kipindi 2)
  2. Maelezo ya Mradi wa Mwisho: "Duka Letu" (au kuendeleza Blog)
  3. Kujenga mradi — mwanafunzi anaandika, mwalimu anaongoza
  4. Deployment: settings za production + hatua za PythonAnywhere (na Render kwa ufupi)
  5. Muhtasari wa kozi nzima + nini cha kujifunza baadaye
  6. Tathmini ya mwisho (rubric) na checklist ya cheti
  7. Shukrani, maswali ya mwisho, tarehe ya kuwasilisha mradi

Elezo kwa mwalimu (soma kwanza wewe)

Kipindi hiki ni tofauti. Lengo si kufundisha dhana mpya nyingi, bali mwanafunzi aonyeshe anaweza kuunganisha alichojifunza. Wewe ni kocha: muulize "hatua inayofuata ni ipi?" kabla ya kumwambia. Kipindi kimoja hakitoshi kumaliza mradi wote — atamaliza nyumbani na kuwasilisha siku mtakayokubaliana.

Mradi wa Mwisho — "Duka Letu" (app mpya duka ndani ya wiki15_django, au project mpya): Mahitaji ya chini (lazima):

  1. Model Bidhaa: jina, maelezo, bei (DecimalField), idadi (stoo), aina (choices: chakula/vinywaji/vifaa/nguo), muuzaji (FK user), tarehe.
  2. Admin yenye list_display, search_fields, list_filter.
  3. Orodha ya bidhaa (ListView) yenye utafutaji ?q= na kuchuja kwa aina ?aina=, pagination.
  4. Ukurasa wa bidhaa moja (DetailView); "Imeisha stoo" kama idadi == 0.
  5. Kuongeza/kuhariri/kufuta bidhaa (CreateView/UpdateView/DeleteView) — muuzaji aliyeingia tu, na mwenye bidhaa tu ndiye anahariri/futa.
  6. Login/logout/jisajili (zilizopo kutoka blog zinatumika).
  7. Messages baada ya kila kitendo.
  8. Template inheritance + CSS.
  9. Tests angalau 4.
  10. README.md + code kwenye GitHub (bila db.sqlite3, venv/, .env).

Ziada (kwa alama za juu — chagua 1 au zaidi):

  • Kikapu rahisi (session) au model Agizo (order).
  • Kitufe "Tengeneza maelezo kwa AI" kinachotumia Claude API (Wiki 14) kuandika maelezo ya bidhaa.
  • Endpoint ya JSON (JsonResponse) ya bidhaa zote.
  • Kuwekwa mtandaoni (PythonAnywhere).

Deployment kwa ufupi — dhana kuu:

  • runserver ni ya maendeleo tu. Mtandaoni tunatumia server ya WSGI (PythonAnywhere inaishughulikia; Render inatumia gunicorn).
  • DEBUG = False kwenye production — vinginevyo watu wanaona code na siri zako kwenye kurasa za error.
  • SECRET_KEY isomwe kutoka environment variable, si kwenye code.
  • ALLOWED_HOSTS — majina ya domain yanayoruhusiwa (mf. jinalako.pythonanywhere.com).
  • Static files: Ukiwa na DEBUG=False, Django haitoi CSS yenyewe. Tunaweka STATIC_ROOT, tunaendesha python manage.py collectstatic (inakusanya CSS zote kwenye folda moja), kisha server ya mtandaoni inazitoa.
  • python manage.py check --deploy — Django inakuambia mambo ya usalama uliyosahau.

Sema hivi kwa mwanafunzi

"Katika kozi hii ulikuwa unajifunza kutumia vifaa: nyundo, msumeno, randa. Leo unajenga kiti chako mwenyewe. Mimi nipo pembeni kama fundi mkuu — lakini mikono ni yako."

"Kuweka website mtandaoni ni kama kuhamisha duka kutoka nyumbani (localhost) kwenda sokoni Kariakoo. Unahitaji kufunga milango (DEBUG=False), kuficha funguo (SECRET_KEY), na kubandika jina la duka (ALLOWED_HOSTS)."

"Kumaliza kozi si mwisho — ni leseni ya kuanza kujifunza peke yako. Waandaaji programu wazuri hujifunza kitu kipya kila wiki."

Andika code hii live (share screen)

Mbinu: Mpe mwanafunzi control — yeye ashare screen yake na aandike; wewe uongoze kwa maswali. Kama muda unabana, wewe andika models.py na views.py haraka, yeye afanye templates na tests nyumbani. Suluhisho kamili liko chini.

Hatua 1: App na model

powershell
python manage.py startapp duka

Ongeza "duka" kwenye INSTALLED_APPS, na kwenye config/urls.py: path("duka/", include("duka.urls")), (juu ya path("", include("blog.urls"))).

duka/models.py:

python
from django.conf import settings
from django.db import models
from django.urls import reverse


class Bidhaa(models.Model):
    class Aina(models.TextChoices):
        CHAKULA = "chakula", "Chakula"
        VINYWAJI = "vinywaji", "Vinywaji"
        VIFAA = "vifaa", "Vifaa vya nyumbani"
        NGUO = "nguo", "Nguo"

    jina = models.CharField(max_length=120)
    maelezo = models.TextField(blank=True)
    bei = models.DecimalField(max_digits=10, decimal_places=2)
    idadi = models.PositiveIntegerField(default=0)
    aina = models.CharField(max_length=20, choices=Aina.choices, default=Aina.CHAKULA)
    muuzaji = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
                                related_name="bidhaa")
    tarehe = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name_plural = "Bidhaa"
        ordering = ["-tarehe"]

    def __str__(self):
        return self.jina

    def get_absolute_url(self):
        return reverse("duka:detail", args=[self.pk])

    @property
    def ipo_stoo(self):
        return self.idadi > 0
powershell
python manage.py makemigrations duka
python manage.py migrate

Hatua 2: Deployment — settings za production (onyesha live) — config/settings.py

python
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-tu-badilisha-kwenye-production")
DEBUG = os.environ.get("DJANGO_DEBUG", "1") == "1"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",")

# ... (sehemu nyingine kama zilivyo) ...

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"     # collectstatic inakusanya hapa

Tengeneza SECRET_KEY mpya na ujaribu:

powershell
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
$env:DJANGO_DEBUG = "0"
python manage.py check --deploy
python manage.py collectstatic
$env:DJANGO_DEBUG = "1"

check --deploy itatoa maonyo (warnings) kadhaa kuhusu HTTPS (SECURE_SSL_REDIRECT, SESSION_COOKIE_SECURE...). Eleza: "Haya ni mapendekezo ya usalama — kwa mradi wa kujifunzia ni sawa kuyasoma tu; kwa mradi wa wateja halisi tunayafanyia kazi."
Ongeza staticfiles/ kwenye .gitignore.

Hatua 3: PythonAnywhere (eleza kwa kuonyesha dashboard — usitumie muda mwingi)

  1. Weka code kwenye GitHub: pip freeze > requirements.txt, git push.
  2. PythonAnywhere → fungua akaunti ya bure → Consoles → Bash:

    bash
    git clone https://github.com/jinalako/wiki15_django.git
    cd wiki15_django
    python3.12 -m venv venv
    source venv/bin/activate
    pip install -r requirements.txt
    python manage.py migrate
    python manage.py createsuperuser
    python manage.py collectstatic
    

    Angalia: kwenye Linux ni source venv/bin/activate (si .\venv\Scripts\Activate.ps1 ya Windows).

  3. Web tab → Add a new web app → Manual configuration → Python 3.12.

  4. Weka Virtualenv: /home/jinalako/wiki15_django/venv.
  5. Hariri WSGI configuration file: weka njia ya project na DJANGO_SETTINGS_MODULE = "config.settings", pamoja na environment variables (DJANGO_SECRET_KEY, DJANGO_DEBUG=0, DJANGO_ALLOWED_HOSTS=jinalako.pythonanywhere.com).
  6. Static files: URL /static/ → Directory /home/jinalako/wiki15_django/staticfiles.
  7. Bonyeza Reload → fungua https://jinalako.pythonanywhere.com. 🎉

Render (kwa ufupi): inaunganisha na GitHub moja kwa moja; unahitaji gunicorn (gunicorn config.wsgi), whitenoise kwa static files, na kwa kawaida PostgreSQL badala ya SQLite (kwa sababu diski ya Render ya bure inafutwa kila deploy). Ni hatua inayofuata baada ya PythonAnywhere.

Muhtasari wa kozi (sema hivi)

"Tumetoka mbali! Hebu tuangalie safari yetu:"

Kipindi cha kozi Tulichojifunza
Wiki 1–12 Python ya msingi, OOP, Git; FastAPI, Pydantic, SQLModel/SQLite, JWT auth, pytest; NumPy, pandas, matplotlib, Jupyter
Wiki 13 Machine Learning (scikit-learn) na kutoa model kupitia FastAPI
Wiki 14 Generative AI (Claude API), chatbot ya Kiswahili, AI endpoint
Wiki 15–16 Django: models, templates, admin, forms, auth, Mradi wa Mwisho

Nini cha kujifunza baadaye (chagua njia moja, usijaribu zote kwa wakati mmoja):

  • Njia ya Web Developer: HTML/CSS/JavaScript zaidi → Django REST Framework → htmx au React → PostgreSQL → Docker → deployment ya kweli (Render, Railway, VPS).
  • Njia ya Data/ML: Takwimu (statistics) → Kaggle competitions → feature engineering → deep learning (PyTorch).
  • Njia ya AI Engineer: Prompt engineering zaidi → RAG (kuuliza nyaraka zako) → AI agents na tool use → evaluation ya models.
  • Kila njia: Jenga portfolio — miradi 3 kwenye GitHub yenye README nzuri; changia open source; andika blog kuhusu unachojifunza (unaweza kutumia blog yako ya Django!).
  • Rasilimali: Django Girls Tutorial (https://tutorial.djangogirls.org), Real Python (https://realpython.com), Kaggle Learn (https://www.kaggle.com/learn), docs rasmi za Python (https://docs.python.org/3/).

Tathmini ya mwisho (rubric ya Mradi wa Mwisho — alama 100)

Kipengele Alama Kigezo
Models na migrations 10 Bidhaa sahihi, __str__, get_absolute_url, migrations zipo
Admin 5 Imesajiliwa na list_display, search_fields, list_filter
Orodha + utafutaji + kuchuja + pagination 15 Inafanya kazi bila errors
CRUD (create/update/delete) 20 Forms zenye validation, CSRF, redirect, messages
Auth na ruhusa 15 Login inahitajika; mwenye bidhaa tu anahariri/futa (403 kwa wengine)
Templates na muonekano 10 Inheritance, menyu inajua mtumiaji, CSS
Tests 10 Angalau 4 zinapita (python manage.py test)
Git + README 5 Commits zenye maana; .gitignore sahihi; README inaeleza jinsi ya kuendesha
Uwasilishaji (presentation) 10 Anaeleza code yake na anajibu maswali
Ziada (AI, kikapu, deployment, JSON API) hadi +10

Pendekezo: Alama ≥ 60 = amefaulu kozi. ≥ 85 = "Kwa heshima" (with distinction).

Maswali ya kumuuliza mwanafunzi

  1. Swali: Kwa nini DEBUG = False kwenye production? Jibu: Kurasa za error za DEBUG zinaonyesha code, settings na taarifa za siri kwa yeyote.
  2. Swali: collectstatic inafanya nini? Jibu: Inakusanya static files zote (za apps zote na admin) kwenye STATIC_ROOT ili server ya mtandaoni izitoe.
  3. Swali: Kwa nini SECRET_KEY kutoka environment variable? Jibu: Ili isiwe kwenye code/GitHub; kila mazingira yanaweza kuwa na yake.
  4. Swali: Kwenye mradi wa duka, ni wapi tunaweka ukaguzi kwamba mwenye bidhaa tu anaweza kuihariri? Jibu: UserPassesTestMixin.test_func (au get_queryset iliyochujwa kwa muuzaji=request.user).
  5. Swali: Ni dhana gani kutoka kozi hii utakayoitumia zaidi? (Swali la kutafakari — hakuna jibu moja sahihi.)

Makosa ya kawaida

Kosa Ujumbe Suluhisho
DEBUG=False bila ALLOWED_HOSTS CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False. Weka ALLOWED_HOSTS
CSS haionekani mtandaoni Ukurasa bila mtindo STATIC_ROOT + collectstatic + mapping ya /static/ kwenye Web tab
collectstatic bila STATIC_ROOT ImproperlyConfigured: You're using the staticfiles app without having set the STATIC_ROOT setting to a filesystem path. Weka STATIC_ROOT = BASE_DIR / "staticfiles"
PythonAnywhere "Something went wrong" Angalia error log kwenye Web tab Mara nyingi: njia ya virtualenv au WSGI si sahihi; DisallowedHost → ALLOWED_HOSTS
DecimalField bila max_digits SystemCheckError ... DecimalFields must define a 'max_digits' attribute. DecimalField(max_digits=10, decimal_places=2)
Kusahau migrations kwenye server no such table: duka_bidhaa python manage.py migrate kwenye Bash ya PythonAnywhere
db.sqlite3 imecommitiwa Data ya majaribio/passwords zinavuja Ongeza kwenye .gitignore, git rm --cached db.sqlite3

Zoezi darasani

Zoezi 3.1 (Mradi wa Mwisho — sehemu ya darasani): Kamilisha model Bidhaa, admin, BidhaaListView (utafutaji + aina), BidhaaDetailView, BidhaaCreateView na templates zake. (Update/Delete, tests na README — nyumbani.)

Jibu (suluhisho kamili la Mradi wa Mwisho)
python
# duka/admin.py
from django.contrib import admin
from .models import Bidhaa


@admin.register(Bidhaa)
class BidhaaAdmin(admin.ModelAdmin):
    list_display = ["jina", "aina", "bei", "idadi", "muuzaji", "tarehe"]
    list_filter = ["aina", "tarehe"]
    search_fields = ["jina", "maelezo"]
    list_editable = ["bei", "idadi"]
python
# duka/forms.py
from django import forms
from .models import Bidhaa


class BidhaaForm(forms.ModelForm):
    class Meta:
        model = Bidhaa
        fields = ["jina", "aina", "bei", "idadi", "maelezo"]
        widgets = {"maelezo": forms.Textarea(attrs={"rows": 4})}

    def clean_bei(self):
        bei = self.cleaned_data["bei"]
        if bei <= 0:
            raise forms.ValidationError("Bei lazima iwe zaidi ya sifuri.")
        return bei
python
# duka/views.py
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import Q
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView

from .forms import BidhaaForm
from .models import Bidhaa


class BidhaaListView(ListView):
    model = Bidhaa
    template_name = "duka/bidhaa_list.html"
    context_object_name = "bidhaa_zote"
    paginate_by = 6

    def get_queryset(self):
        qs = Bidhaa.objects.select_related("muuzaji")
        q = self.request.GET.get("q", "").strip()
        aina = self.request.GET.get("aina", "")
        if q:
            qs = qs.filter(Q(jina__icontains=q) | Q(maelezo__icontains=q))
        if aina:
            qs = qs.filter(aina=aina)
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["q"] = self.request.GET.get("q", "")
        ctx["aina_iliyochaguliwa"] = self.request.GET.get("aina", "")
        ctx["aina_zote"] = Bidhaa.Aina.choices
        return ctx


class BidhaaDetailView(DetailView):
    model = Bidhaa
    template_name = "duka/bidhaa_detail.html"
    context_object_name = "bidhaa"


class BidhaaCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
    model = Bidhaa
    form_class = BidhaaForm
    template_name = "duka/bidhaa_form.html"
    success_message = 'Bidhaa "%(jina)s" imeongezwa!'

    def form_valid(self, form):
        form.instance.muuzaji = self.request.user
        return super().form_valid(form)


class MuuzajiPekeeMixin(UserPassesTestMixin):
    def test_func(self):
        return self.get_object().muuzaji == self.request.user


class BidhaaUpdateView(LoginRequiredMixin, MuuzajiPekeeMixin, SuccessMessageMixin, UpdateView):
    model = Bidhaa
    form_class = BidhaaForm
    template_name = "duka/bidhaa_form.html"
    success_message = "Bidhaa imesasishwa."


class BidhaaDeleteView(LoginRequiredMixin, MuuzajiPekeeMixin, SuccessMessageMixin, DeleteView):
    model = Bidhaa
    template_name = "duka/bidhaa_confirm_delete.html"
    context_object_name = "bidhaa"
    success_url = reverse_lazy("duka:orodha")
    success_message = "Bidhaa imefutwa."
python
# duka/urls.py
from django.urls import path
from . import views

app_name = "duka"

urlpatterns = [
    path("", views.BidhaaListView.as_view(), name="orodha"),
    path("mpya/", views.BidhaaCreateView.as_view(), name="ongeza"),
    path("<int:pk>/", views.BidhaaDetailView.as_view(), name="detail"),
    path("<int:pk>/hariri/", views.BidhaaUpdateView.as_view(), name="hariri"),
    path("<int:pk>/futa/", views.BidhaaDeleteView.as_view(), name="futa"),
]
html
<!-- duka/templates/duka/bidhaa_list.html -->
{% extends "blog/base.html" %}
{% load humanize %}
{% block title %}Duka Letu{% endblock %}
{% block content %}
<h2>🛒 Duka Letu</h2>
<form method="get">
    <input type="text" name="q" value="{{ q }}" placeholder="Tafuta bidhaa...">
    <select name="aina">
        <option value="">Aina zote</option>
        {% for thamani, jina in aina_zote %}
            <option value="{{ thamani }}" {% if thamani == aina_iliyochaguliwa %}selected{% endif %}>{{ jina }}</option>
        {% endfor %}
    </select>
    <button type="submit">Chuja</button>
</form>
{% if user.is_authenticated %}<p><a href="{% url 'duka:ongeza' %}">+ Ongeza bidhaa</a></p>{% endif %}

{% for b in bidhaa_zote %}
    <div style="border:1px solid #ddd; border-radius:8px; padding:10px; margin:8px 0;">
        <h3><a href="{{ b.get_absolute_url }}">{{ b.jina }}</a></h3>
        <p>TSh {{ b.bei|floatformat:0|intcomma }} | {{ b.get_aina_display }}
           | {% if b.ipo_stoo %}Stoo: {{ b.idadi }}{% else %}<strong style="color:#c62828;">Imeisha stoo</strong>{% endif %}</p>
    </div>
{% empty %}
    <p>Hakuna bidhaa zinazolingana.</p>
{% endfor %}

{% if is_paginated %}
<p>
    {% if page_obj.has_previous %}<a href="?page={{ page_obj.previous_page_number }}&q={{ q|urlencode }}&aina={{ aina_iliyochaguliwa }}">&laquo; Nyuma</a>{% endif %}
    Ukurasa {{ page_obj.number }}/{{ page_obj.paginator.num_pages }}
    {% if page_obj.has_next %}<a href="?page={{ page_obj.next_page_number }}&q={{ q|urlencode }}&aina={{ aina_iliyochaguliwa }}">Mbele &raquo;</a>{% endif %}
</p>
{% endif %}
{% endblock %}
html
<!-- duka/templates/duka/bidhaa_detail.html -->
{% extends "blog/base.html" %}
{% load humanize %}
{% block title %}{{ bidhaa.jina }}{% endblock %}
{% block content %}
<h2>{{ bidhaa.jina }}</h2>
<p><strong>Bei:</strong> TSh {{ bidhaa.bei|floatformat:0|intcomma }}</p>
<p><strong>Aina:</strong> {{ bidhaa.get_aina_display }}</p>
<p><strong>Stoo:</strong> {% if bidhaa.ipo_stoo %}{{ bidhaa.idadi }}{% else %}Imeisha stoo{% endif %}</p>
<p><strong>Muuzaji:</strong> {{ bidhaa.muuzaji.username }}</p>
<div>{{ bidhaa.maelezo|default:"Hakuna maelezo."|linebreaks }}</div>
{% if user == bidhaa.muuzaji %}
    <p><a href="{% url 'duka:hariri' bidhaa.pk %}">✏️ Hariri</a> | <a href="{% url 'duka:futa' bidhaa.pk %}">🗑️ Futa</a></p>
{% endif %}
<p><a href="{% url 'duka:orodha' %}">&larr; Rudi dukani</a></p>
{% endblock %}
html
<!-- duka/templates/duka/bidhaa_form.html -->
{% extends "blog/base.html" %}
{% block title %}{% if form.instance.pk %}Hariri bidhaa{% else %}Bidhaa mpya{% endif %}{% endblock %}
{% block content %}
<h2>{% if form.instance.pk %}Hariri: {{ form.instance.jina }}{% else %}Ongeza bidhaa{% endif %}</h2>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Hifadhi</button>
    <a href="{% url 'duka:orodha' %}">Ghairi</a>
</form>
{% endblock %}
html
<!-- duka/templates/duka/bidhaa_confirm_delete.html -->
{% extends "blog/base.html" %}
{% block content %}
<h2>Futa "{{ bidhaa.jina }}"?</h2>
<form method="post">
    {% csrf_token %}
    <button type="submit">Ndiyo, futa</button>
    <a href="{{ bidhaa.get_absolute_url }}">Hapana</a>
</form>
{% endblock %}
python
# duka/tests.py
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse

from .models import Bidhaa


class DukaTests(TestCase):
    def setUp(self):
        self.neema = User.objects.create_user("neema", password="Siri-12345")
        self.baraka = User.objects.create_user("baraka", password="Siri-12345")
        self.sukari = Bidhaa.objects.create(jina="Sukari", bei=3200, idadi=10,
                                            aina="chakula", muuzaji=self.neema)
        self.soda = Bidhaa.objects.create(jina="Soda", bei=1000, idadi=0,
                                          aina="vinywaji", muuzaji=self.neema)

    def test_orodha_na_utafutaji(self):
        r = self.client.get(reverse("duka:orodha"), {"q": "suk"})
        self.assertContains(r, "Sukari")
        self.assertNotContains(r, "Soda")

    def test_kuchuja_kwa_aina(self):
        r = self.client.get(reverse("duka:orodha"), {"aina": "vinywaji"})
        self.assertContains(r, "Soda")
        self.assertNotContains(r, "Sukari")

    def test_imeisha_stoo(self):
        r = self.client.get(reverse("duka:detail", args=[self.soda.pk]))
        self.assertContains(r, "Imeisha stoo")

    def test_kuongeza_kunahitaji_login(self):
        r = self.client.get(reverse("duka:ongeza"))
        self.assertEqual(r.status_code, 302)

    def test_kuongeza_bidhaa(self):
        self.client.login(username="baraka", password="Siri-12345")
        r = self.client.post(reverse("duka:ongeza"), {
            "jina": "Mchele", "aina": "chakula", "bei": "2800", "idadi": 5, "maelezo": "",
        })
        self.assertEqual(r.status_code, 302)
        self.assertEqual(Bidhaa.objects.get(jina="Mchele").muuzaji, self.baraka)

    def test_bei_hasi_inakataliwa(self):
        self.client.login(username="baraka", password="Siri-12345")
        r = self.client.post(reverse("duka:ongeza"), {
            "jina": "Kitu", "aina": "vifaa", "bei": "-5", "idadi": 1,
        })
        self.assertEqual(r.status_code, 200)            # form inaonyeshwa tena na kosa
        self.assertFalse(Bidhaa.objects.filter(jina="Kitu").exists())

    def test_mwingine_hawezi_kufuta(self):
        self.client.login(username="baraka", password="Siri-12345")
        r = self.client.post(reverse("duka:futa", args=[self.sukari.pk]))
        self.assertEqual(r.status_code, 403)
        self.assertTrue(Bidhaa.objects.filter(pk=self.sukari.pk).exists())

Kumbuka: {% load humanize %} inahitaji "django.contrib.humanize" kwenye INSTALLED_APPS (tuliiongeza Wiki 15). Ongeza link ya "Duka" kwenye menyu ya base.html: <a href="{% url 'duka:orodha' %}">Duka</a>.


Kazi ya Nyumbani

(Inatakiwa: siku ya kuwasilisha mradi wa mwisho)

  1. Kamilisha Mradi wa Mwisho "Duka Letu" (mahitaji yote 10 ya lazima hapo juu).
  2. Chagua angalau ziada moja: (a) kitufe cha AI kinachotengeneza maelezo ya bidhaa kwa Claude, (b) endpoint ya JSON /duka/api/bidhaa/, (c) kuweka mtandaoni PythonAnywhere.
  3. Andika README.md (mradi ni nini, jinsi ya kuendesha, picha 1–2 za skrini).
  4. Andaa uwasilishaji mfupi: onyesha site ikifanya kazi, eleza sehemu moja ya code unayoijivunia, na changamoto moja uliyoishinda.
Jibu (ziada a na b)
python
# (b) duka/views.py — JSON endpoint
from django.http import JsonResponse


def bidhaa_json(request):
    data = [
        {"id": b.pk, "jina": b.jina, "bei": float(b.bei), "idadi": b.idadi,
         "aina": b.aina, "muuzaji": b.muuzaji.username}
        for b in Bidhaa.objects.select_related("muuzaji")
    ]
    return JsonResponse({"bidhaa": data})

# duka/urls.py
#    path("api/bidhaa/", views.bidhaa_json, name="api_bidhaa"),
python
# (a) duka/ai.py — tumia Claude kuandika maelezo (pip install anthropic python-dotenv)
import anthropic
from dotenv import load_dotenv

load_dotenv()                     # ANTHROPIC_API_KEY kutoka .env (pembeni ya manage.py)
_client = anthropic.Anthropic()


def tengeneza_maelezo(jina: str, aina: str) -> str:
    r = _client.messages.create(
        model="claude-haiku-5-5",
        max_tokens=1024,
        output_config={"effort": "low"},
        system="Wewe ni mwandishi wa matangazo ya duka la Tanzania. Andika kwa Kiswahili, sentensi 2-3, bila kubuni bei.",
        messages=[{"role": "user", "content": f"Andika maelezo mafupi ya kuvutia ya bidhaa: {jina} (aina: {aina})."}],
    )
    return "".join(b.text for b in r.content if b.type == "text").strip()
python
# duka/views.py — view ya kitufe (POST tu, muuzaji tu)
import anthropic
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from django.views.decorators.http import require_POST

from .ai import tengeneza_maelezo


@login_required
@require_POST
def maelezo_ai(request, pk):
    bidhaa = get_object_or_404(Bidhaa, pk=pk, muuzaji=request.user)
    try:
        bidhaa.maelezo = tengeneza_maelezo(bidhaa.jina, bidhaa.get_aina_display())
        bidhaa.save()
        messages.success(request, "Maelezo yametengenezwa na AI. Yasome na uyahariri kama inahitajika.")
    except anthropic.APIError:
        messages.error(request, "Huduma ya AI haipatikani kwa sasa. Jaribu baadaye.")
    return redirect(bidhaa)

# duka/urls.py
#    path("<int:pk>/maelezo-ai/", views.maelezo_ai, name="maelezo_ai"),
html
<!-- bidhaa_detail.html, ndani ya {% if user == bidhaa.muuzaji %} -->
<form method="post" action="{% url 'duka:maelezo_ai' bidhaa.pk %}">
    {% csrf_token %}
    <button type="submit">✨ Tengeneza maelezo kwa AI</button>
</form>

Kumbuka: .env iko kwenye .gitignore; kwenye PythonAnywhere weka ANTHROPIC_API_KEY kwenye WSGI file au .env ya server.


Ukiulizwa swali usilolijua

Sema: "Sijui jibu kamili sasa hivi — tuliandike, nitalitafuta na kukujibu kipindi kijacho (au kwa ujumbe)." Kwa Django, tafuta "django 5.2". Ukurasa wa "Classy Class-Based Views" unaonyesha methods zote za kila CBV — msaada mkubwa.


Checklist ya mwisho wa wiki

  • Anatengeneza ModelForm yenye validation ya ziada (clean_<field>)
  • Anaandika views za create/update/delete (pattern ya GET/POST + redirect) na anaeleza CSRF
  • Anatumia messages na zinaonekana kwenye base.html
  • Anabadilisha views kuwa ListView/DetailView/CreateView/UpdateView/DeleteView
  • Anatumia LoginRequiredMixin, UserPassesTestMixin, SuccessMessageMixin
  • Login, logout (POST) na jisajili vinafanya kazi; menyu inabadilika kulingana na mtumiaji
  • Mwenye post/bidhaa tu ndiye anahariri/futa (403 kwa wengine)
  • Anaeleza DEBUG, SECRET_KEY, ALLOWED_HOSTS, collectstatic kwa deployment
  • Mradi wa Mwisho umeanzishwa na tarehe ya uwasilishaji imekubaliwa

Checklist ya cheti (mwisho wa kozi)

Vigezo hivi ni pendekezo — vibadilishe kulingana na sheria za shule yako.

  • Amehudhuria angalau 80% ya vipindi (vipindi 39 kati ya 48)
  • Amewasilisha kazi za nyumbani za angalau wiki 13 kati ya 16
  • Ana GitHub yenye miradi: FastAPI, ML/AI API (Wiki 13–14), Django (Wiki 15–16)
  • Mradi wa Mwisho umepata alama ≥ 60 kwenye rubric
  • Amewasilisha mradi kwa mdomo na kujibu maswali
  • Amejaza fomu ya maoni ya kozi (feedback)
  • Cheti kimeandaliwa (jina kamili sahihi, alama/heshima)