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)
- Hakikisha mradi wa Wiki 15 (
wiki15_django) unafanya kazi kwako na kwa mwanafunzi: appblogyenyePost,Kundi, admin,base.html, orodha na ukurasa wa post moja. Wiki hii tunaendelea na mradi huo huo. - Soma:
- Tutorial sehemu 4 (forms na generic views): https://docs.djangoproject.com/en/5.2/intro/tutorial04/
- ModelForms: https://docs.djangoproject.com/en/5.2/topics/forms/modelforms/
- Class-based views (utangulizi): https://docs.djangoproject.com/en/5.2/topics/class-based-views/
- Authentication: https://docs.djangoproject.com/en/5.2/topics/auth/default/
- Messages: https://docs.djangoproject.com/en/5.2/ref/contrib/messages/
- Deployment checklist: https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
- PythonAnywhere + Django: https://help.pythonanywhere.com/pages/DeployExistingDjangoProject/
- Jambo jipya la Django 5 unalopaswa kujua: Logout lazima iwe POST (form yenye kitufe), si link ya kawaida (GET). Link ya
<a href="/accounts/logout/">itarudisha 405 Method Not Allowed. Tutaonyesha njia sahihi. - Andaa mapema kwa Kipindi 3: Fungua akaunti ya bure ya PythonAnywhere (https://www.pythonanywhere.com — "Beginner" account) ili uweze kuonyesha dashboard. Andaa cheti (certificate) cha kumaliza kozi na fomu ya tathmini (rubric iko chini).
Malengo ya Wiki (mwisho wa wiki mwanafunzi ataweza...)
- Kutengeneza
ModelFormna kuthibitisha data (validation) kwaclean_<field>. - Kuandika views za CRUD (Create, Read, Update, Delete) kwa function-based views, pamoja na
{% csrf_token %}. - Kuonyesha ujumbe kwa mtumiaji kwa
messagesframework. - Kubadilisha views kuwa class-based views:
ListView,DetailView,CreateView,UpdateView,DeleteView. - Kuweka login, logout (POST), na usajili (signup) kwa
UserCreationForm. - Kulinda kurasa kwa
@login_required/LoginRequiredMixinna kuruhusu mwandishi tu kuhariri post yake. - Kujenga mradi kamili wa Django (Duka/Blog) kwa kujitegemea.
- Kueleza hatua kuu za kuweka (deploy) Django mtandaoni (PythonAnywhere/Render).
- Kujua njia za kuendelea kujifunza baada ya kozi.
Kipindi 1 — ModelForms, CRUD na Messages
Mtiririko wa kipindi
- Marudio ya kazi ya nyumbani ya Wiki 15 (Mapishi)
- HTML forms, GET vs POST, CSRF — kwa nini?
ModelFormna validation- Views za Create na Update (pattern ya GET/POST)
- Delete na ukurasa wa kuthibitisha; messages
- Zoezi darasani
- 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):
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
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)
LOGIN_URL = "admin:login" # kwa leo tu; Jumatano tutabadilisha
Hatua 3: Views za CRUD — blog/views.py (ongeza/rekebisha)
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
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):
{% 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 %}
novalidateinazima ukaguzi wa browser ili tuone validation ya Django darasani. Kwenye mradi halisi unaweza kuiondoa.
blog/templates/blog/post_confirm_delete.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>:
{% 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:
<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>
<main> {% if messages %} {% for message in messages %} <div class="ujumbe ujumbe-{{ message.tags }}">{{ message }}</div> {% endfor %} {% endif %} {% block content %} {% endblock %} </main>
style.css ongeza:
.ujumbe { padding: 10px; border-radius: 6px; margin: 12px 0; } .ujumbe-success { background: #c8e6c9; } .ujumbe-error { background: #ffcdd2; } .errorlist { color: #c62828; }
Hatua 6: Jaribu
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=).- Andika kichwa "Hi" → "Kichwa kiwe na herufi angalau 5." (validation yetu).
- Andika post sahihi → ujumbe wa kijani "Post ... imeongezwa!" kwenye ukurasa wa post.
- Hariri → futa → ujumbe.
- Refresh baada ya kuhifadhi → hakuna post mbili (shukrani kwa redirect).
- Kwa makusudi ondoa
{% csrf_token %}→ tuma → 403 Forbidden (CSRF verification failed). Rudisha.
Maswali ya kumuuliza mwanafunzi
- 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.
- Swali:
commit=Falseinatumika lini? Jibu: Tunapohitaji kuongeza data isiyotoka kwenye form (mf. mwandishi) kabla ya kuhifadhi. - Swali: Tofauti ya
PostForm(request.POST)naPostForm(request.POST, instance=post)? Jibu: Ya kwanza inatengeneza post mpya; ya pili inasasisha post iliyopo. - 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. - Swali: Ujumbe wa
messagesunaonekana 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
# 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})}
# 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})
# blog/urls.py path("kundi/mpya/", views.kundi_create, name="kundi_create"),
<!-- 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
- Marudio ya kazi ya nyumbani (Kipindi 1)
- Class-based views: ListView, DetailView, CreateView, UpdateView, DeleteView
- Mixins: LoginRequiredMixin, UserPassesTestMixin, SuccessMessageMixin
- Auth: login, logout (POST!), usajili (signup)
- Zoezi darasani
- 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 zinatumiaget_absolute_url()ya model. Kwa DeleteView lazima uiweke; tunatumiareverse_lazy(sireverse) 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_requiredkwa CBVs.UserPassesTestMixin— tunaandikatest_func(); ikirudishaFalse→ 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 nilogin,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 naCreateView, kisha tunamwingiza mtumiaji kwalogin(request, user). - Kwenye templates,
useripo 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
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
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
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
PostCreateViewni mistari 6 tu — ile function ya Kipindi 1 ilikuwa 13."
Hatua 4: URLs — blog/urls.py
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
{% 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 %}">« 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 »</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:
{% 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:
{% 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>:
<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
- Toka → "Ingia" na "Jisajili" zinaonekana.
- Jisajili kama
juma(password kamaPython2027!) → unaingizwa moja kwa moja. - Tengeneza post kama
juma→ ujumbe wa kijani. - Toka, ingia kama superuser, fungua
/post/<pk ya post ya juma>/hariri/→ 403 Forbidden (MwandishiPekeeMixin inafanya kazi). - Ukiwa umetoka, fungua
/post/mpya/→ unapelekwa/accounts/login/?next=/post/mpya/→ ukiingia unarudishwa kwenye form. - Ongeza posts 6+ → pagination inaonekana.
Hatua 9: Tests za auth — blog/tests.py (ongeza)
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)
python manage.py test blog
Maswali ya kumuuliza mwanafunzi
- 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.
- Swali: Kwa nini
reverse_lazykwenyesuccess_urlna sireverse? Jibu: Class inasomwa wakati URLs bado hazijapakiwa;reverse_lazyinasubiri mpaka itakapohitajika. - Swali: Kwa nini
LoginRequiredMixiniwe kushoto kabisa? Jibu: Mixins zinatekelezwa kutoka kushoto; tunataka ukaguzi wa login ufanyike kwanza. - Swali: Juma akijaribu kuhariri post ya Asha anapata nini, na kwa nini? Jibu: 403 —
test_funcyaMwandishiPekeeMixininarudishaFalse. - Swali: Kwa nini logout ni POST kwenye Django 5? Jibu: Kuzuia site nyingine kukutoa kwa link/picha (CSRF); POST inahitaji token.
- 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,
Jibu
# 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
# blog/urls.py path("posts-zangu/", views.PostZanguView.as_view(), name="posts_zangu"), path("mwandishi/<str:username>/", views.PostZaMwandishiView.as_view(), name="mwandishi"),
<!-- 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
- Marudio ya kazi ya nyumbani (Kipindi 2)
- Maelezo ya Mradi wa Mwisho: "Duka Letu" (au kuendeleza Blog)
- Kujenga mradi — mwanafunzi anaandika, mwalimu anaongoza
- Deployment: settings za production + hatua za PythonAnywhere (na Render kwa ufupi)
- Muhtasari wa kozi nzima + nini cha kujifunza baadaye
- Tathmini ya mwisho (rubric) na checklist ya cheti
- 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):
- Model
Bidhaa:jina,maelezo,bei(DecimalField),idadi(stoo),aina(choices: chakula/vinywaji/vifaa/nguo),muuzaji(FK user),tarehe. - Admin yenye
list_display,search_fields,list_filter. - Orodha ya bidhaa (ListView) yenye utafutaji
?q=na kuchuja kwa aina?aina=, pagination. - Ukurasa wa bidhaa moja (DetailView); "Imeisha stoo" kama
idadi == 0. - Kuongeza/kuhariri/kufuta bidhaa (CreateView/UpdateView/DeleteView) — muuzaji aliyeingia tu, na mwenye bidhaa tu ndiye anahariri/futa.
- Login/logout/jisajili (zilizopo kutoka blog zinatumika).
- Messages baada ya kila kitendo.
- Template inheritance + CSS.
- Tests angalau 4.
- 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
maelezoya bidhaa. - Endpoint ya JSON (
JsonResponse) ya bidhaa zote. - Kuwekwa mtandaoni (PythonAnywhere).
Deployment kwa ufupi — dhana kuu:
runserverni ya maendeleo tu. Mtandaoni tunatumia server ya WSGI (PythonAnywhere inaishughulikia; Render inatumiagunicorn).DEBUG = Falsekwenye production — vinginevyo watu wanaona code na siri zako kwenye kurasa za error.SECRET_KEYisomwe 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. TunawekaSTATIC_ROOT, tunaendeshapython 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.pynaviews.pyharaka, yeye afanye templates na tests nyumbani. Suluhisho kamili liko chini.
Hatua 1: App na model
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:
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
python manage.py makemigrations duka python manage.py migrate
Hatua 2: Deployment — settings za production (onyesha live) — config/settings.py
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:
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 --deployitatoa 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."
Ongezastaticfiles/kwenye.gitignore.
Hatua 3: PythonAnywhere (eleza kwa kuonyesha dashboard — usitumie muda mwingi)
- Weka code kwenye GitHub:
pip freeze > requirements.txt,git push. -
PythonAnywhere → fungua akaunti ya bure → Consoles → 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.ps1ya Windows). -
Web tab → Add a new web app → Manual configuration → Python 3.12.
- Weka Virtualenv:
/home/jinalako/wiki15_django/venv. - 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). - Static files: URL
/static/→ Directory/home/jinalako/wiki15_django/staticfiles. - 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
- Swali: Kwa nini
DEBUG = Falsekwenye production? Jibu: Kurasa za error za DEBUG zinaonyesha code, settings na taarifa za siri kwa yeyote. - Swali:
collectstaticinafanya nini? Jibu: Inakusanya static files zote (za apps zote na admin) kwenyeSTATIC_ROOTili server ya mtandaoni izitoe. - Swali: Kwa nini
SECRET_KEYkutoka environment variable? Jibu: Ili isiwe kwenye code/GitHub; kila mazingira yanaweza kuwa na yake. - Swali: Kwenye mradi wa duka, ni wapi tunaweka ukaguzi kwamba mwenye bidhaa tu anaweza kuihariri? Jibu:
UserPassesTestMixin.test_func(auget_querysetiliyochujwa kwamuuzaji=request.user). - 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)
# 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"]
# 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
# 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."
# 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"), ]
<!-- 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 }}">« 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 »</a>{% endif %} </p> {% endif %} {% endblock %}
<!-- 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' %}">← Rudi dukani</a></p> {% endblock %}
<!-- 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 %}
<!-- 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 %}
# 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"kwenyeINSTALLED_APPS(tuliiongeza Wiki 15). Ongeza link ya "Duka" kwenye menyu yabase.html:<a href="{% url 'duka:orodha' %}">Duka</a>.
Kazi ya Nyumbani
(Inatakiwa: siku ya kuwasilisha mradi wa mwisho)
- Kamilisha Mradi wa Mwisho "Duka Letu" (mahitaji yote 10 ya lazima hapo juu).
- 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. - Andika
README.md(mradi ni nini, jinsi ya kuendesha, picha 1–2 za skrini). - Andaa uwasilishaji mfupi: onyesha site ikifanya kazi, eleza sehemu moja ya code unayoijivunia, na changamoto moja uliyoishinda.
Jibu (ziada a na b)
# (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"),
# (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()
# 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"),
<!-- 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:
.enviko kwenye.gitignore; kwenye PythonAnywhere wekaANTHROPIC_API_KEYkwenye WSGI file au.envya server.
Ukiulizwa swali usilolijua
Sema: "Sijui jibu kamili sasa hivi — tuliandike, nitalitafuta na kukujibu kipindi kijacho (au kwa ujumbe)." Kwa Django, tafuta "django
- Django docs: https://docs.djangoproject.com/en/5.2/
- Classy CBV: https://ccbv.co.uk/
- PythonAnywhere help: https://help.pythonanywhere.com/pages/DeployExistingDjangoProject/
Checklist ya mwisho wa wiki
- Anatengeneza
ModelFormyenye validation ya ziada (clean_<field>) - Anaandika views za create/update/delete (pattern ya GET/POST + redirect) na anaeleza CSRF
- Anatumia
messagesna zinaonekana kwenyebase.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,collectstatickwa 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)