### Simple + Effective Validation
Saat sebuah form diproses dan method `is_valid()` dipanggil, Django akan menjalankan serangkaian validasi secara otomatis dan berurutan. Proses ini memastikan bahwa data yang disimpan ke database sudah sesuai aturan, sehingga tidak menimbulkan masalah di kemudian hari.
Validasi ini tidak hanya melindungi aplikasi dari data yang salah, tetapi juga meningkatkan kualitas data secara keseluruhan.
#### Validasi Bawaan Django pada Field
Django sudah memiliki banyak validasi bawaan berdasarkan tipe field yang digunakan. Contohnya adalah EmailField.
Ketika sebuah field didefinisikan sebagai EmailField, Django otomatis akan:
* Mengecek format email
* Menolak input yang tidak sesuai
* Menampilkan pesan error langsung pada field tersebut
**Contoh Perubahan Kode**
`email = forms.EmailField()`
#### Fungsi Perubahan
Dengan mengganti CharField menjadi EmailField, kita tidak perlu menulis logika validasi email sendiri. Django sudah mengerjakannya untuk kita.
#### Hasil Akhir
Jika pengguna memasukkan teks yang bukan email:
`Enter a valid email address.`
Pesan error ini muncul otomatis tanpa tambahan kode apa pun.
#### Validasi Kustom Menggunakan `clean_<field>`
Selain validasi bawaan, Django memungkinkan kita menambahkan validasi kustom pada level form. Ini dilakukan dengan metode `clean_<nama_field>`.
Method ini dipanggil setelah validasi bawaan, sehingga sangat cocok untuk aturan bisnis khusus.
Contoh Kode
```
def clean_name(self):
name = self.cleaned_data.get("name")
if name == "Hello":
raise forms.ValidationError("Not a valid name")
return name
```
Penjelasan Fungsi
* self.cleaned_data berisi data yang sudah lolos validasi bawaan
* Jika nilai name tidak sesuai aturan, proses dihentikan
* Django otomatis mengaitkan error ini ke field name
**Hasil Akhir**
Jika user mengetik:
`Hello`
Maka form tidak akan disimpan dan pesan error akan muncul di field name.
#### Non-Field Errors untuk Validasi Umum
Tidak semua validasi berhubungan langsung dengan satu field. Django menyediakan mekanisme non-field errors untuk kasus tersebut.
Perubahan pada Template
```
{% if form.errors.non_field_errors %}
{{ form.errors.non_field_errors }}
{% endif %}
```
**Fungsi**
* Menampilkan error yang bersifat umum
* Biasanya digunakan untuk validasi antar field atau aturan global
**Dampak**
Error tetap ditampilkan ke user tanpa harus terikat pada satu input tertentu.
**Memisahkan Validasi ke validators.py**
Ketika validasi mulai digunakan di banyak tempat (form, model, admin), maka praktik terbaik adalah memindahkannya ke file terpisah bernama validators.py.
Ini membuat kode:
* Lebih rapi
* Mudah digunakan ulang
* Lebih mudah dirawat
**Membuat Custom Validator Category**
File: validators.py
```
from django.core.exceptions import
ValidationError
CATEGORIES = ['Mexican', 'Asian', 'American', 'Whatever']
def validate_category(value):
cat = value.capitalize()
if not value in CATEGORIES and not cat in CATEGORIES:
raise ValidationError(f"{value} not a valid category")
```
Penjelasan Kode
* Validator menerima satu parameter: value
* Nilai input dibandingkan dengan daftar kategori yang diizinkan
* Kapitalisasi diperhitungkan agar input fleksibel
* Jika tidak valid, Django menghentikan proses
**Menggunakan Validator pada Form**
Validator bisa langsung ditempelkan pada field form.
Contoh Perubahan Kode
```
category = forms.CharField(
required=False,
validators=[validate_category]
)
```
Fungsi
* Validasi dijalankan saat `form.is_valid()`
* Tidak perlu menulis ulang logika di view
Hasil
Jika user memasukkan kategori yang tidak terdaftar, form akan gagal disimpan.
#### Validasi di Level Model (Pendekatan Paling Kuat)
Validasi pada model adalah pendekatan paling aman karena:
* Berlaku di semua jalur input
* Tidak bergantung pada form
* Menjaga konsistensi database
Perubahan Kode di models.py
```
category = models.CharField(
max_length=120,
null=True,
blank=True,
validators=[validate_category]
)
```
**Konsekuensi Teknis**
Karena struktur model berubah, Django memerlukan migrasi:
```
python manage.py makemigrations
python manage.py migrate
```
**Signal pre_save untuk Mengubah Data Sebelum Disimpan**
Validator hanya bertugas mengecek, bukan mengubah data.
Jika ingin mengubah nilai sebelum disimpan ke database, Django menyediakan signal.
**Kode Signal**
```
def rl_pre_save_receiver(sender, instance, *args, **kwargs):
instance.category = instance.category.capitalize()
if not instance.slug:
instance.slug = unique_slug_generator(instance)
pre_save.connect(rl_pre_save_receiver, sender=RestaurantLocation)
```
Fungsi
* Menyeragamkan format kategori
* Menghasilkan slug otomatis sebelum data masuk database
**Hasil Akhir**
Input:
`asian`
**Disimpan sebagai:**
`Asian`
### Letting Users own Data
#### Menambahkan Relasi User ke Model Restaurant
Untuk mengaitkan restoran dengan pengguna, Django menyediakan mekanisme ForeignKey. ForeignKey memungkinkan satu user memiliki banyak restaurant (one-to-many).
Perubahan penting dilakukan di `models.py`:
```
from django.conf import settings
User = settings.AUTH_USER_MODEL
class RestaurantLocation(models.Model):
owner = models.ForeignKey(User)
name = models.CharField(max_length=120)
location = models.CharField(max_length=120, null=True, blank=True)
category = models.CharField(max_length=120, null=True, blank=True)
```
Perubahan ini:
* Menambahkan field owner
* Menghubungkan RestaurantLocation ke user yang login
* Menggunakan settings.AUTH_USER_MODEL agar aman jika suatu saat user model diganti
#### Dampak Migrasi dan Default Value
Karena model sudah memiliki data sebelumnya, Django akan meminta default value saat migrasi. Ini terjadi karena field owner tidak boleh kosong.
Django akan menyimpan default user (misalnya user dengan id=1) untuk semua data lama. Setelah migrasi:
* Semua restoran lama memiliki owner
* Field owner tidak lagi kosong
* Struktur database konsisten
#### Mengambil Data dari Sisi User (Reverse Relation)
ForeignKey menciptakan relasi dua arah. Dari user, kita bisa mengambil semua restoran miliknya tanpa mengimpor model RestaurantLocation.
Contoh di Django shell:
```
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.get(id=1)
user.restaurantlocation_set.all()
```
Penjelasan:
* restaurantlocation_set adalah reverse relation default
* Django otomatis membuatnya dari nama model
* Menghasilkan queryset semua restoran milik user tersebut
Query ini bisa difilter seperti biasa:
`user.restaurantlocation_set.filter(category__iexact="Mexican")`
#### Mengambil Data dari Sisi Restaurant (Forward Relation)
Sebaliknya, dari restaurant kita bisa mengakses user pemiliknya:
```
restaurant = RestaurantLocation.objects.first()
restaurant.owner
```
Hasilnya adalah instance user, bukan sekadar ID. Dari sini kita bisa mengakses:
* username
* email
* status user
* relasi lain milik user tersebut
Ini menunjukkan bahwa relasi ForeignKey benar-benar dua arah.
**Filter Restaurant Berdasarkan User**
Relasi ini juga memungkinkan filtering langsung dari model restaurant:
```
RestaurantLocation.objects.filter(owner__id=1)
RestaurantLocation.objects.filter(owner__username__iexact="cfe")
```
Konsep penting di sini:
* owner__field berarti menelusuri field pada model yang direferensikan
* Django ORM secara otomatis melakukan JOIN database
### Letting Users own Data
#### Foreign Key sebagai Penanda Kepemilikan
Untuk menghubungkan data restoran dengan user, Django menyediakan ForeignKey.
ForeignKey ini bekerja seperti penanda kepemilikan di database.
Begitu relasi dibuat, setiap restoran akan selalu memiliki satu pemilik, sampai relasi tersebut diubah.
Dengan relasi ini, kita bisa memfilter data, sehingga setiap user hanya melihat data miliknya sendiri.
#### Perubahan pada Model
**Bagian yang ditambahkan:**
```
from django.conf import settings
User = settings.AUTH_USER_MODEL
class RestaurantLocation(models.Model):
owner = models.ForeignKey(User)
```
Penjelasan:
* `settings.AUTH_USER_MODEL` digunakan agar aplikasi tetap aman jika suatu saat kita memakai custom user model.
* `owner = models.ForeignKey(User)` berarti satu user bisa memiliki banyak restoran, dan setiap restoran hanya dimiliki oleh satu user.
**Hasil akhirnya:**
Setiap restoran di database sekarang memiliki kolom owner yang menunjuk ke user tertentu.
**Dampak Saat Migrasi Database**
Karena field owner tidak boleh kosong, Django akan meminta nilai default untuk data lama.
Nilai ini digunakan agar semua data yang sudah ada tetap valid setelah migrasi.
Setelah proses migrasi selesai, seluruh restoran lama otomatis terhubung dengan satu user default.
**Mengambil Data dari Sisi User**
```
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.get(id=1)
user.restaurantlocation_set.all()
```
Kode ini berarti: ambil semua restoran yang dimiliki oleh user tersebut.
Django otomatis membuat relasi balik dengan format` <model>_set`.
Kita juga bisa memfilter:
```
user.restaurantlocation_set.filter(category__exact="Mexican")
```
**Mengambil Data dari Sisi Restaurant**
```
from restaurants.models import RestaurantLocation
RestaurantLocation.objects.filter(owner__id=1)
```
Atau berdasarkan username:
`RestaurantLocation.objects.filter(owner__username__iexact="cfe")`
Dengan cara ini, kita bisa langsung mengambil restoran berdasarkan pemiliknya.
### Associate User to Form Data in FBV
Pada tahap ini, aplikasi sudah memiliki model RestaurantLocation dengan field owner. Namun, saat membuat data baru melalui form, Django akan memunculkan error NOT NULL constraint failed jika owner_id tidak diisi. Hal ini terjadi karena field owner wajib diisi, tetapi form belum mengirimkan data tersebut.
Solusinya adalah mengisi owner langsung dari user yang sedang login melalui request.user.
#### Menambahkan Owner Secara Otomatis Saat Menyimpan
Perubahan utama dilakukan di function-based view restaurant_createview.
Di sinilah kita “menyuntikkan” user ke dalam instance sebelum data benar-benar disimpan.
```
def restaurant_createview(request):
form = RestaurantLocationCreateForm(request.POST or None)
errors = None
if form.is_valid():
if request.user.is_authenticated():
instance = form.save(commit=False)
instance.owner = request.user
instance.save()
return HttpResponseRedirect("/restaurants/")
else:
return HttpResponseRedirect("/login/")
```
Makna pentingnya:
* commit=False → membuat object tapi belum disimpan.
* instance.owner = request.user → mengisi foreign key secara manual.
* instance.save() → menyimpan data dengan owner yang valid.
Hasil akhirnya: setiap restoran yang dibuat akan otomatis dimiliki oleh user yang login.
#### Mengapa Tidak Menggunakan Default pada ForeignKey?
Pada model:
```
owner = models.ForeignKey(User)
```
Field ini tidak boleh null. Jika diberi default, semua data baru akan dimiliki user yang sama.
Padahal tujuannya adalah: setiap user punya data sendiri.
Karena itu, owner harus diisi dari request, bukan dari default.
#### Pengamanan untuk User yang Belum Login
Jika user belum login, sistem akan mengarahkan ke halaman login:
```
if not request.user.is_authenticated():
return HttpResponseRedirect("/login/")
```
Ini memastikan hanya user terautentikasi yang bisa membuat data.
### Associate User to Data in Class Based View
Bagian ini bertujuan untuk menghubungkan data restoran dengan user yang sedang login.
Setiap restoran yang dibuat harus memiliki pemilik, sehingga data tidak lagi berdiri bebas tanpa relasi user.
Awalnya fitur ini sudah ada di function-based view, lalu dipindahkan ke class-based CreateView agar lebih rapi dan konsisten dengan sistem generic view Django.
#### Masalah yang Terjadi
Saat kode dipindahkan ke CreateView, muncul error:
`request is not defined`
Hal ini terjadi karena pada class-based view, object request tidak lagi dikirim sebagai parameter fungsi.
Sebaliknya, Django menyimpannya di dalam object view dan harus diakses menggunakan:
`self.request`
#### Perubahan di RestaurantCreateView
Kode yang ditambahkan ke class:
```
class RestaurantCreateView(CreateView):
form_class = RestaurantLocationCreateForm
template_name = 'restaurants/form.html'
success_url = "/restaurants/"
def form_valid(self, form):
instance = form.save(commit=False)
instance.owner = self.request.user
return super(RestaurantCreateView, self).form_valid(form)
```
Bagian penting dari kode ini:
`instance = form.save(commit=False)`
Baris ini membuat object restoran tanpa langsung menyimpannya ke database.
Tujuannya agar kita bisa menambahkan data tambahan sebelum disimpan.
`instance.owner = self.request.user`
Baris ini mengaitkan restoran dengan user yang sedang login.
`return super(RestaurantCreateView, self).form_valid(form)`
Ini menjalankan kembali proses default Django, yaitu menyimpan data dan melakukan redirect ke success_url.
Kenapa Tidak Memanggil `instance.save()`
Di dalam CreateView, Django sudah otomatis menyimpan form ketika `form_valid()` dipanggil.
Jika kita memanggil:
`instance.save()`
lalu tetap memanggil:
`super().form_valid(form)`
maka data bisa tersimpan dua kali.
Karena itu, kita hanya memodifikasi object, lalu membiarkan Django menyelesaikan prosesnya.
#### Hubungan dengan Function-Based View
Versi function-based sebelumnya:
```
instance = form.save(commit=False)
instance.owner = request.user
instance.save()
```
Versi class-based sekarang:
```
instance = form.save(commit=False)
instance.owner = self.request.user
return super().form_valid(form)
```
Perbedaannya hanya pada cara menyimpan dan mengakses request.
Logikanya tetap sama.
### Login Required to View
Sebelumnya kita hanya memeriksa login saat form dikirim.
Sekarang kita akan mencegah user yang belum login bahkan melihat form-nya.
Django sudah menyediakan dua cara resmi untuk ini, tergantung jenis view yang kita pakai.
#### Login Required pada Function-Based View
Pada function-based view, kita menggunakan decorator.
Decorator adalah fungsi pembungkus yang menjalankan pengecekan sebelum view dijalankan.
Tambahkan import:
`from django.contrib.auth.decorators import login_required`
Lalu tempelkan tepat di atas view:
```
@login_required()
def restaurant_createview(request):
...
```
Dengan ini, jika user belum login dan membuka halaman create, Django otomatis akan mengalihkan ke halaman login.
Jika user sudah login, maka view akan berjalan normal.
#### Mengatur Halaman Tujuan Login
Secara default, Django akan mencari halaman login di:
`/accounts/login/`
Agar bisa menyesuaikan sendiri, kita bisa mengatur di settings.py:
`LOGIN_URL = '/login/'`
Sekarang semua decorator dan mixin login akan otomatis mengarah ke URL ini, kecuali jika kita override di view.
#### Login Required pada Class-Based View
Untuk class-based view, kita tidak bisa memakai decorator.
Sebagai gantinya kita menggunakan LoginRequiredMixin.
Tambahkan import:
`from django.contrib.auth.mixins import LoginRequiredMixin`
Lalu ubah class:
```
class RestaurantCreateView(LoginRequiredMixin, CreateView):
form_class = RestaurantLocationCreateForm
login_url = '/login/'
template_name = 'restaurants/form.html'
success_url = "/restaurants/"
```
Mixin ini otomatis akan memblokir user yang belum login sebelum view dipanggil.
#### Kenapa Ini Lebih Baik
Sebelumnya, pengecekan login dilakukan di dalam form_valid.
Artinya user masih bisa melihat halaman create meskipun tidak login.
Dengan decorator dan mixin, pengecekan terjadi sebelum view dijalankan, sehingga lebih aman, lebih bersih, dan lebih sedikit kode.
### LoginView
#### Kenapa Login Required Itu Penting
Ketika user belum login, Django tidak menganggapnya sebagai User model.
Ia dianggap sebagai AnonymousUser.
AnonymousUser adalah class yang berbeda dari User.
Karena itu, saat kita mencoba:
`instance.owner = self.request.user`
dan user belum login, Django akan melempar error:
`AnonymousUser must be a User instance`
Artinya Django menolak menyimpan data karena foreign key owner hanya menerima User yang valid.
Itulah alasan kenapa login required bukan sekadar tampilan, tapi perlindungan data.
#### Cara Django Menangani Login Secara Built-in
Django sudah menyediakan view login sendiri, jadi kita tidak perlu membuat form login dari nol.
Kita cukup import:
`from django.contrib.auth.views import LoginView`
Lalu tambahkan di urls.py:
```
path('login/', LoginView.as_view(), name='login'),
```
Sekarang Django tahu bahwa URL /login/ adalah halaman login resmi.
#### Kenapa Muncul Error Template Tidak Ada
Ketika LoginView dipanggil, Django mencari template default:
`registration/login.html`
Karena belum ada, maka error muncul.
Solusinya:
Buat folder:
`templates/registration/`
Lalu buat file:
`login.html`
Isi dengan:
```
{% extends "base.html" %}
{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page.</p>
{% else %}
<p>Please login to see this page.</p>
{% endif %}
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="login" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
{% endblock %}
```
Sekarang halaman login akan tampil.
#### Bagaimana Redirect Setelah Login Bisa Terjadi
Jika user membuka halaman yang dibatasi, Django akan mengarahkan ke:
`/login/?next=/restaurants/create/`
Setelah login berhasil, Django otomatis membawa user kembali ke halaman yang sebelumnya dia tuju.
Itu semua terjadi karena field tersembunyi:
`<input type="hidden" name="next" value="{{ next }}" />`
#### Hubungan Login Dengan LoginRequiredMixin
Saat kamu pakai:
`class RestaurantCreateView(LoginRequiredMixin, CreateView):`
Django menjamin bahwa:
`self.request.user`
selalu user asli, bukan AnonymousUser.
Dengan begitu:
`instance.owner = self.request.user`
aman dan tidak akan error.
### Using Reverse to Shortcut URLS
#### Kenapa Kita Perlu URL Naming dan Reverse
Saat kita menulis link seperti:
`<a href="/restaurants/create/">`
itu `hard coded`.
Jika suatu hari path diubah, semua template harus ikut diubah.
Dengan URL naming dan reverse, kita cukup mengganti di satu tempat, dan seluruh sistem otomatis ikut menyesuaikan.
Itulah tujuan utama dari sistem `URL name + reverse` di Django.
#### Memberi Nama Pada URL
Di `urls.py` utama:
url('', TemplateView.as_view(template_name='home.html'), name='home'),
url('about/', TemplateView.as_view(template_name='about.html'), name='about'),
url('contact/', TemplateView.as_view(template_name='contact.html'), name='contact'),
Sekarang setiap URL punya nama unik.
#### Menggunakan URL Name di Template
Di navbar base.html:
```
<a href="{% url 'home' %}">Home</a>
<a href="{% url 'about' %}">About</a>
<a href="{% url 'contact' %}">Contact</a>
```
Sekarang link tidak lagi bergantung pada path, tapi pada nama URL.
Kalau path berubah, navbar tidak rusak.
#### Naming Untuk Detail View
Di `restaurants/urls.py`:
`path('(?P<slug>[\w-]+)/', RestaurantDetailView.as_view(), name='detail'),`
Nama ini nantinya dipanggil oleh model dan template.
#### get_absolute_url di Model
Di `models.py`:
```
from django.core.urlresolvers import reverse
def get_absolute_url(self):
return reverse('restaurants:detail', kwargs={'slug': self.slug})
```
Sekarang setiap object Restaurant tahu sendiri ke mana URL detailnya.
#### Memakai get_absolute_url di Template
Di `restaurantslocation_list.html`:
```
<li>
<a href="{{ obj.get_absolute_url }}">{{ obj }}</a>
</li>
```
Kita tidak lagi memanggil {% url %} di sini.
Model yang menentukan link-nya sendiri.
#### Memecah URL ke App (include)
Di project/urls.py:
`url(r'^restaurants/', include('restaurants.urls', namespace='restaurants')),`
Sekarang semua URL restaurant dipindahkan ke app sendiri.
#### Kenapa Namespace Penting
Karena kita menulis:
`reverse('restaurants:detail', kwargs={'slug': self.slug})`
restaurants adalah namespace.
detail adalah nama URL.
Ini membuat sistem aman walaupun ada URL lain yang juga bernama detail di app lain.
### Menu Items App
#### Membuat App Menus
Perintah berikut digunakan untuk membuat app baru.
`python manage.py startapp menus`
Tujuannya adalah memisahkan logika menu dari restoran, sehingga sistem lebih modular dan mudah dikembangkan.
#### Menambahkan App ke Settings
Pada `settings.py`:
```
'menus',
'restaurants',
```
Langkah ini wajib agar Django mengenali model baru dan bisa memproses migrasi database.
#### Struktur Model Item
Di models.py:
```
from django.conf import settings
from django.db import models
from restaurants.models import RestaurantLocation
class Item(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
restaurant = models.ForeignKey(RestaurantLocation)
name = models.CharField(max_length=120)
contents = models.TextField(help_text='Separate each item by comma')
excludes = models.TextField(blank=True, null=True, help_text='Separate each item by comma')
public = models.BooleanField(default=True)
timestamp = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-updated', '-timestamp']
def get_contents(self):
return self.contents.split(",")
def get_excludes(self):
return self.excludes.split(",")
```
#### Fungsi dari Setiap Bagian
`user`
Menghubungkan item dengan pemiliknya.
Setiap menu selalu dimiliki oleh satu user.
`restaurant`
Menghubungkan item ke restoran tertentu.
Ini membuat satu restoran bisa punya banyak menu favorit dari user berbeda.
`name`
Nama menu atau item.
`contents`
Daftar isi atau bahan yang diinginkan.
Disimpan sebagai teks lalu dipecah menjadi list.
`excludes`
Bahan yang tidak diinginkan.
Bersifat opsional.
`public`
Menentukan apakah menu ini bisa dilihat oleh orang lain.
`timestamp` dan `updated`
Digunakan untuk pelacakan waktu dan pengurutan data.
`class Meta`
Menentukan bahwa item yang paling baru diubah akan tampil lebih dulu.
`get_contents` dan `get_excludes`
Mengubah string menjadi list agar mudah dirender di template.
Registrasi ke Admin
Di `admin.py`:
```
from django.contrib import admin
from .models import Item
admin.site.register(Item)
```
Dengan ini, admin bisa menambah, mengubah, dan menghapus menu lewat dashboard Django.
**Migrasi Database**
```
python manage.py makemigrations
python manage.py migrate
```
Tujuannya adalah membuat tabel baru berdasarkan model Item.
### Menu Items Views
Semua view dibatasi agar hanya menampilkan data milik user yang sedang login.
#### Struktur Views
Di `views.py`, kita menggunakan generic views dari Django.
`from django.views.generic import ListView, DetailView, CreateView, UpdateView`
Empat view utama dibuat:
* ItemListView
Menampilkan semua menu milik user.
* ItemDetailView
Menampilkan satu menu berdasarkan ID.
* ItemCreateView
Form untuk menambahkan menu baru.
* ItemUpdateView
Form untuk mengubah menu yang sudah ada.
Semua view menggunakan filter:
`return Item.objects.filter(user=self.request.user)`
Ini memastikan user hanya bisa melihat data miliknya sendiri.
#### Form untuk Item
Di forms.py:
```
from django import forms
from .models import Item
class ItemForm(forms.ModelForm):
class Meta:
model = Item
fields = ['restaurant', 'name', 'contents', 'excludes', 'public']
```
Form ini dipakai oleh create dan update view.
#### Relasi URL
Di menus/urls.py:
```
path('create/', ItemCreateView.as_view(), name='create'),
path('(?P<pk>\d+)/', ItemDetailView.as_view(), name='detail'),
path('', ItemListView.as_view(), name='list'),
```
Urutan ini penting agar Django memeriksa detail dulu sebelum list.
#### Integrasi ke URL Utama
Di urls.py utama:
```
path('items/', include('menus.urls', namespace='menus')),
```
Sekarang semua menu bisa diakses lewat /items/.
#### Get_absolute_url pada Model
Di models.py:
```
from django.core.urlresolvers import reverse
def get_absolute_url(self):
return reverse('menus:detail', kwargs={'pk': self.pk})
```
Ini memungkinkan kita membuat link langsung ke detail item dari template.
#### Template List
`templates/menus/item_list.html`
Menampilkan semua menu user dengan isi dan pengecualian jika ada.
```
{% for obj in object_list %}
<a href="{{ obj.get_absolute_url }}">{{ obj.name }}</a>
{% if obj.contents %}
<ul>
{% for item in obj.get_contents %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
```
ist.
#### Reusable Form Template
Form dipindahkan ke template umum form.html agar bisa dipakai oleh create dan update.
Judul diatur lewat `get_context_data`.
`context['title'] = 'Create Item'`
atau
`context['title'] = 'Update Item'`
#### Validasi User Saat Create
Di `ItemCreateView`:
```
obj = form.save(commit=False)
obj.user = self.request.user
return super(ItemCreateView, self).form_valid(form)
```
Ini memastikan setiap menu otomatis terhubung dengan user yang login.
### Limiting Form Field to QuerySet
#### Masalah Relasi Restaurant pada Item
Salah satu masalah besar yang muncul adalah ketika user membuat Item, field restaurant menampilkan semua restoran di database, termasuk milik user lain.
Padahal, secara logika, user tidak boleh mengaitkan item ke restoran yang bukan miliknya.
Masalah ini terlihat ketika satu item milik user A bisa dipasangkan ke restoran milik user B.
Hal ini menyebabkan data menjadi tidak valid dan bisa membingungkan sistem.
Solusinya adalah membatasi daftar restoran di form, agar hanya menampilkan restoran milik user yang sedang login.
#### Mengirim User ke Dalam Form
Agar form bisa mengetahui siapa user yang sedang login, kita perlu mengirim user tersebut dari view ke form.
Perubahan dilakukan di ItemCreateView dan ItemUpdateView dengan menambahkan method berikut:
```
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
```
Kode ini memastikan bahwa setiap form yang dibuat akan menerima user aktif sebagai parameter.
#### Menangkap User di Dalam Form
Sekarang form harus bisa menerima parameter user.
Kita ubah constructor (__init__) pada ItemForm.
```
def __init__(self, user=None, *args, **kwargs):
super(ItemForm, self).__init__(*args, **kwargs)
self.fields['restaurant'].queryset = RestaurantLocation.objects.filter(owner=user)
```
Sekarang field restaurant hanya akan menampilkan restoran yang dimiliki oleh user tersebut.
#### Membatasi Akses dengan LoginRequiredMixin
Agar sistem tidak error ketika user belum login, kita wajib memastikan bahwa halaman create dan update hanya bisa diakses oleh user yang sudah login.
```
from django.contrib.auth.mixins import LoginRequiredMixin
```
Kemudian pada view:
```
class ItemCreateView(LoginRequiredMixin, CreateView):
...
```
```
class ItemUpdateView(LoginRequiredMixin, UpdateView):
...
```
Ini memastikan bahwa hanya user yang terautentikasi yang bisa membuat atau mengedit item.
#### Menyaring Data Berdasarkan User
Agar user hanya bisa melihat data miliknya sendiri, kita membatasi queryset di setiap view.
```
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
```
Kode ini mencegah user melihat item milik user lain.
#### Menyimpan User Saat Item Dibuat
Ketika form disubmit, kita pastikan user otomatis tersimpan sebagai pemilik item.
```
def form_valid(self, form):
obj = form.save(commit=False)
obj.user = self.request.user
return super().form_valid(form)
```
Dengan cara ini, setiap item selalu terikat dengan user yang membuatnya.
#### Mendukung Update dengan Instance
Pada halaman edit, Django otomatis mengirimkan instance ke dalam form melalui keyword arguments.
Karena kita sudah menambahkan get_form_kwargs, maka instance tetap bekerja bersamaan dengan user.
### Personalize items, User profile view, Style profile with Bootstrap, Adding a Robust Search
#### Sistem Data Pribadi dan Publik dalam Aplikasi Menu
Pada tahap ini, sistem mulai diarahkan agar semua data restoran dan menu item bersifat personal.
Artinya, ketika user login, yang mereka lihat hanyalah data milik mereka sendiri.
Namun di saat yang sama, kita juga membangun satu fitur tambahan, yaitu public profile, agar user dapat membagikan daftar restoran dan menu favorit mereka kepada orang lain.
Dengan cara ini, aplikasi memiliki dua lapisan:
Lapisan privat untuk mengelola data
Lapisan publik untuk berbagi preferensi
#### Membatasi Data Berdasarkan User
Semua view yang menampilkan restoran dan menu item diubah agar hanya memuat data yang dimiliki user login.
Contoh filter di view:
```
def get_queryset(self):
return RestaurantLocation.objects.filter(owner=self.request.user)
```
Hal yang sama juga dilakukan pada menu item.
Dengan ini, user tidak bisa melihat atau mengedit data user lain.
#### Update View Tanpa Mengubah User
Pada halaman edit, tidak perlu mengatur ulang user, karena saat item dibuat, user sudah disimpan sebagai owner.
```
class RestaurantUpdateView(LoginRequiredMixin, UpdateView):
model = RestaurantLocation
```
Namun tetap perlu membatasi queryset agar tidak bisa mengakses data orang lain.
#### Detail dan Edit dalam Satu Halaman
Alih-alih memisahkan halaman detail dan edit, keduanya digabung.
View tetap menggunakan DetailView, tetapi juga memuat form edit.
Template menampilkan detail di atas dan form di bawahnya:
```
<h3>Make Changes</h3>
{% include "snippets/form_snippet.html" with form=form %}
```
Dengan pendekatan ini, user bisa langsung melihat data sekaligus mengeditnya tanpa berpindah halaman.
#### Snippet Form untuk Konsistensi
Agar semua form terlihat sama, dibuat satu file snippet.
```
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
```
Setiap halaman hanya tinggal memanggil:
`{% include "snippets/form_snippet.html" with form=form %}`
#### Public Profile Berbasis Username
User kini memiliki halaman publik:
`/profiles/<username>/`
View hanya mengembalikan user aktif:
```
def get_object(self):
return get_object_or_404(User, username=self.kwargs["username"], is_active=True)
```
Halaman ini bisa diakses siapa pun, bahkan tanpa login.
#### Menampilkan Relasi Restoran dan Item
Di template profil:
```
{% for rest in user.restaurantlocation_set.all %}
<h4>{{ rest.name }}</h4>
{% for item in rest.item_set.all %}
<p>{{ item.name }}</p>
{% endfor %}
{% endfor %}
```
Dengan ini, satu user bisa memperlihatkan semua restoran dan menu yang ia sukai.
#### Styling dengan Bootstrap
Struktur HTML diubah agar mendukung grid Bootstrap:
```
<div class="row">
<div class="col-sm-12 thumbnail">
<h4>{{ rest.name }}</h4>
<p>{{ rest.location }} • {{ rest.category }}</p>
</div>
</div>
```
Badge digunakan untuk menampilkan isi menu:
```
<span class="badge badge-default">{{ ingredient }}</span>
```
#### Fitur Search pada Profil
Form pencarian:
```
<form method="get">
<input type="text" name="q" placeholder="Search">
<button type="submit">Search</button>
</form>
```
Di view:
```
query = self.request.GET.get("q")
if query:
qs = qs.search(query)
```
#### Custom Model Manager untuk Search
Agar pencarian fleksibel, dibuat manager khusus:
```
class RestaurantLocationManager(models.Manager):
def search(self, query):
return self.get_queryset().filter(
Q(name__icontains=query) |
Q(category__icontains=query) |
Q(item__name__icontains=query) |
Q(item__contents__icontains=query)
).distinct()
```
Dengan ini, user bisa mencari berdasarkan:
> Nama restoran
> Kategori
> Nama menu
> Isi menu
#### Search Bisa Diakses dari Konten
Setiap ingredient dapat diklik:
`<a href="?q={{ ing }}">{{ ing }}</a>`
Ketika diklik, halaman langsung melakukan pencarian.
## Bab 5.Handling Users and Followers
### Follow Users
#### Sistem Follow User dan Feed Aktivitas
Setelah kita bisa melihat profil user lain, kebutuhan berikutnya adalah membuat sistem follow.
Tujuannya agar user bisa:
1. mengikuti user lain
2. melihat update menu mereka
3. punya tab khusus berisi orang yang diikuti
4. dan melihat siapa yang mengikuti mereka
Ini membentuk dasar fitur sosial seperti “timeline” atau “activity feed”.
**Konsep Dasar: Profile untuk Setiap User**
Profile ini menyimpan:
*user pemilik
*siapa saja yang mengikuti dia
*status aktif
*waktu dibuat dan diubah
#### Model Profile
```
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
User = settings.AUTH_USER_MODEL
class Profile(models.Model):
user = models.OneToOneField(User)
followers = models.ManyToManyField(User, related_name='is_following', blank=True)
activated = models.BooleanField(default=False)
timestamp = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.user.username
```
artinya
* profile.followers = siapa yang mengikuti profile ini
* user.is_following= siapa yang dia ikuti
#### Membuat Profile Otomatis Saat User Dibuat
Agar setiap user otomatis punya profile, kita pakai signal.
```
def post_save_user_receiver(sender, instance, created, *args, **kwargs):
if created:
profile, is_created = Profile.objects.get_or_create(user=instance)
default_user_profile = Profile.objects.get_or_create(user__id=1)[0]
default_user_profile.followers.add(instance)
profile.followers.add(default_user_profile.user)
profile.followers.add(2)
```
Artinya:
* user baru otomatis follow user default
* dan user default otomatis muncul sebagai follower mereka
Ini memberi kesan bahwa akun baru tidak kosong.
#### Reverse Relationship yang Perlu Dipahami
Dari shell:
`random_.profile.followers.all()`
Ini artinya siapa yang mengikuti user tersebut.
Sedangkan:
`random_.is_following.all()`
Ini artinya siapa yang dia ikuti.
Field `related_name='is_following'` adalah kunci untuk ini.
#### Admin Panel
```
from django.contrib import admin
from .models import Profile
admin.site.register(Profile)
```
Sekarang semua profile bisa dikelola dari admin.
#### Pengujian dari Shell
```
from django.contrib.auth import get_user_model
User = get_user_model()
random_ = User.objects.last()
random_.profile.followers.all()
random_.is_following.all()
```
Ini memastikan sistem relasi berjalan dua arah.
### Follow Button Form
#### Fitur Follow Toggle pada Profile
Fitur ini memungkinkan user mengikuti dan berhenti mengikuti user lain melalui satu tombol yang bekerja secara toggle. Saat tombol ditekan, sistem akan mengecek apakah user sudah menjadi follower atau belum. Jika sudah, maka relasi dihapus. Jika belum, maka relasi ditambahkan.
Pendekatan ini membuat interaksi sosial antar user menjadi dinamis dan konsisten tanpa perlu dua endpoint terpisah.
#### Endpoint Follow Menggunakan Class Based View
Endpoint follow dibuat sebagai class view agar mudah dikembangkan dan dibatasi hanya untuk request POST.
```
class ProfileFollowToggle(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
username_to_toggle = request.POST.get("username")
profile_, is_following = Profile.objects.toggle_follow(
request.user,
username_to_toggle
)
return redirect(f"/u/{profile_.user.username}/")
```
View ini menerima username dari form, memanggil method toggle pada model, lalu mengarahkan kembali ke halaman profile user yang dituju.
#### Routing dengan Django Modern
Endpoint follow didaftarkan menggunakan path yang lebih modern.
```
path("profile-follow/", ProfileFollowToggle.as_view(), name="follow"),
```
Endpoint ini menjadi target form yang ada pada halaman profile.
#### Logika Toggle Dipindahkan ke Model
Agar view tetap bersih, seluruh logika follow dan unfollow dipindahkan ke model melalui custom manager.
```
class ProfileManager(models.Manager):
def toggle_follow(self, request_user, username_to_toggle):
profile = self.get(user__username__iexact=username_to_toggle)
is_following = False
if request_user in profile.followers.all():
profile.followers.remove(request_user)
else:
profile.followers.add(request_user)
is_following = True
return profile, is_following
```
Fungsi ini mengecek apakah user sudah menjadi follower. Jika ya, maka relasi dihapus. Jika belum, maka relasi ditambahkan dan status diubah.
#### Menentukan Status Follow di Halaman Profile
Agar tombol dapat berubah antara Follow dan Unfollow, status ini dikirim ke template melalui context.
```
class ProfileDetailView(DetailView):
template_name = 'profiles/user.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
user = context['user']
is_following = False
if user.profile in self.request.user.is_following.all():
is_following = True
context['is_following'] = is_following
return context
```
Logika ini mengecek apakah user yang sedang login sudah mengikuti profile yang sedang ditampilkan.
#### Form Follow di Template Snippet
Form ini dapat digunakan di mana pun dengan mengirimkan username target.
```
{% if request.user.is_authenticated %}
<form class='form' method='POST' action='{% url "follow" %}'>
{% csrf_token %}
<input type='hidden' name='username' value='{{ username }}'/>
<button class='btn {% if is_following %}btn-default{% else %}btn-primary{% endif %}'>
{% if is_following %}Unfollow{% else %}Follow{% endif %}
</button>
</form>
{% endif %}
```
Tombol akan otomatis berubah berdasarkan status relasi.
#### pada Halaman Profile
Snippet form dipanggil langsung dari halaman user.
```
<p>
{% include 'profiles/snippet/follow_form.html' with username=user.username is_following=is_following %}
</p>
```
Dengan ini, setiap profile akan memiliki tombol follow yang bersifat dinamis.
### Following Home Page Feed
#### HomeView sebagai Feed Controller
Home page diubah menjadi class view yang menangani GET request dan memeriksa apakah user sudah login.
```
class HomeView(View):
def get(self, request, *args, **kwargs):
if not request.user.is_authenticated():
return render(request, "home.html", {})
user = request.user
is_following_user_ids = [x.user.id for x in user.is_following.all()]
qs = Item.objects.filter(
user__id__in=is_following_user_ids,
public=True
).order_by("-updated")[:3]
return render(request, "menus/home-feed.html", {'object_list': qs})
```
View ini mengambil seluruh profile yang diikuti, mengekstrak user id, lalu memfilter item berdasarkan user tersebut dan hanya menampilkan yang publik.
#### Integrasi HomeView ke URL Utama
Home page kini menggunakan HomeView sebagai entry point utama.
`path('', HomeView.as_view(), name='home'),`
Dengan ini, setiap user yang login akan langsung melihat feed, sedangkan user yang belum login tetap melihat halaman home statis.
#### Tampilan Feed pada Template
Template home-feed.html menerima object_list dari view dan menampilkan item beserta user, restoran, dan isi menu.
```
{% for obj in object_list %}
<div class='thumbnail'>
<h3>
<a href='{% url "profiles:detail" username=obj.user.username %}'>
{{ obj.user.username }}
</a>
</h3>
<h4>{{ obj.name }}</h4>
<p>
<a href='{% url "profiles:detail" username=obj.user.username %}?q={{ obj.restaurant.title }}'>
{{ obj.restaurant.title }}
</a> |
<a href='{% url "profiles:detail" username=obj.user.username %}?q={{ obj.restaurant.location }}'>
{{ obj.restaurant.location }}
</a> |
<a href='{% url "profiles:detail" username=obj.user.username %}?q={{ obj.restaurant.category }}'>
{{ obj.restaurant.category }}
</a>
</p>
<p>
<b>{{ obj.name }}:</b> with
{% for ing in obj.get_contents %}
<a href='{% url "profiles:detail" username=obj.user.username %}?q={{ ing }}'>
{{ ing }}
</a>
{% endfor %}
</p>
</div>
{% endfor %}
```
Setiap item dapat diklik untuk membuka profile user pemilik item, sekaligus memfilter restoran atau bahan yang berkaitan.
#### Relasi Feed dengan Sistem Follow
Feed ini sepenuhnya bergantung pada sistem follow yang telah dibuat sebelumnya. Saat user mengikuti atau berhenti mengikuti akun lain, daftar item di home feed akan langsung berubah. Jika sebuah item diubah menjadi tidak publik, item tersebut otomatis menghilang dari feed tanpa perlu logic tambahan.
Dengan struktur ini, sistem sosial, menu, dan restoran terhubung secara konsisten dan saling memperbarui.
### Register View & Activation Keys
#### Sistem Registrasi User dengan Aktivasi Email
Fitur ini memungkinkan user membuat akun, tetapi akun tersebut tidak langsung aktif. Setelah registrasi, sistem akan mengirim email berisi activation link. User harus membuka link tersebut agar akunnya aktif dan bisa login.
Sistem ini dibangun dengan memanfaatkan ModelForm, CreateView, Profile model, dan activation key acak sebagai sistem verifikasi.
#### RegisterForm sebagai Form Registrasi
Form ini berbasis `ModelForm` dari user model. Di dalamnya terdapat dua field tambahan yaitu `password1 `dan `password2` untuk validasi password.
```
class RegisterForm(forms.ModelForm):
password1 = forms.CharField(widget=forms.PasswordInput)
password2 = forms.CharField(widget=forms.PasswordInput)
```
Form ini juga memaksa email menjadi unik:
```
def clean_email(self):
email = self.cleaned_data.get("email")
if User.objects.filter(email__iexact=email).exists():
raise forms.ValidationError("Email already registered")
return email
```
Saat `save() `dipanggil, password di-hash, user dibuat non-aktif, lalu email aktivasi dikirim.
```
def save(self, commit=True):
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
user.is_active = False
user.save()
user.profile.send_activation_email()
return user
```
Intinya, proses registrasi tidak langsung mengaktifkan akun.
#### RegisterView sebagai Controller Registrasi
RegisterView menggunakan CreateView dan memanggil `RegisterForm`.
```
class RegisterView(CreateView):
form_class = RegisterForm
template_name = "registration/register.html"
success_url = "/"
```
Saat user membuka` /register/`, sistem menampilkan form.
Jika form valid, Django otomatis memanggil `form.save()`, sehingga:
1. user baru dibuat
2. password di-hash
3. user diset is_active = False
4. profile dibuat via signal
5. activation email dikirim
#### Profile Model sebagai Pusat Aktivasi
Model Profile menyimpan informasi tambahan dari user, termasuk:
* activation_key
* activated
* followers
```
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
activation_key = models.CharField(max_length=120, blank=True, null=True)
activated = models.BooleanField(default=False)
```
Saat user dibuat, signal otomatis membuat profile.
```
@receiver(post_save, sender=User)
def post_save_user_receiver(sender, instance, created, *args, **kwargs):
if created:
Profile.objects.create(user=instance)
```
Method untuk mengirim email aktivasi:
```
def send_activation_email(self):
self.activation_key = code_generator()
self.save()
path = reverse("activate", kwargs={"code": self.activation_key})
print("Activation link:", path)
```
Untuk testing, email dicetak ke terminal.
#### Generator Activation Key
Key aktivasi dibuat menggunakan fungsi berikut:
```
def code_generator(size=35, chars=string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
```
Fungsi ini menghasilkan string acak yang aman digunakan sebagai token aktivasi.
#### Aktivasi Akun Melalui URL
Saat user membuka link, sistem memanggil view ini:
```
def activate_user_view(request, code=None):
qs = Profile.objects.filter(activation_key=code)
if qs.exists():
profile = qs.first()
user = profile.user
user.is_active = True
user.save()
profile.activated = True
profile.activation_key = None
profile.save()
return redirect("/login")
```
View ini:
* mencari profile berdasarkan key
* mengaktifkan user
* menandai profile aktif
* menghapus key
* redirect ke login
Integrasi ke URL Utama
```
path('register/', RegisterView.as_view(), name='register'),
path('activate/')?P<code>[a-z0-9].*)/$', activate_user_view, name='activate'),
```
#### Integrasi Email di Settings
Email dikonfigurasi di `base.py` menggunakan SMTP Gmail untuk mengirim activation link.
## Hands On Sederhana
1. Install Python
https://www.python.org/downloads/windows/
2. Buat directory baru di windows
```
mkdir Django
```
3. Cek version Python
```
python --version
Python 3.11.1
```
4. Upgrade pip terlebih dahulu
```
-m pip install -- upgrade
```
5. cek package di python
```
pip list
Package Version
---------- -------
asgiref 3.11.0
pip 26.0
pytz 2025.2
setuptools 65.5.0
sqlparse 0.5.5
tzdata 2025.3
```
6. Buat dan aktifkan virtual environment
```
python -m venv Env
Env\scripts\activate.bat
```
7. Install Django didalam virtual environment
```
pip install Django
```
8. Cek apakah Django sudah terinstall atau belum
```
pip list
```
9. Buat project Django
```
django-admin startproject mywebsite
```
10. Buat app baru didalam project Django
```
Python manage.py startapp main
Python manage.py startapp blog
```
12. ubah beberapa hal di settings.py seperti dibawah
```
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'main',
'blog',
```
```
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
```
```
STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / "static"
]
```
13. buat views
> main/viwes.py
```
from django.shortcuts import render
def index(request):
context = {
"judul": "Kelas Terbuka",
"subjudul": "Selamat datang di Django",
}
return render(request, "index.html", context)
```
14. Buat URL App
> main/urls.py
```
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='home'),
]
```
15. Hubungkan ke URL Project
> mywebsite/urls.py
```
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('main.urls')),
]
```
16. Buat Folder Templates di dalam main lalu isi index.html
> templates/index.html
```
<!DOCTYPE html>
<html>
<head>
<title>{{ judul }}</title>
</head>
<body>
<h1>{{ judul }}</h1>
<h2>{{ subjudul }}</h2>
</body>
</html>
```
17. Isi Views Blog
> blog/views.py
```
from django.shortcuts import render
def blog(request):
context = {
"judul": "Halaman Blog",
"isi": "Ini halaman blog saya",
}
return render(request, "blog.html", context)
```
18. URL Blog
> blog/urls.py
```
from django.urls import path
from . import views
urlpatterns = [
path('', views.blog, name='blog'),
]
```
19. Hubungkan ke Project
> Hubungkan ke Project
```
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('main.urls')),
path('blog/', include('blog.urls')),
]
```
20. Buat juga folder template di blog
> templates/blog.html
```
<!DOCTYPE html>
<html>
<head>
<title>{{ judul }}</title>
</head>
<body>
<h1>{{ judul }}</h1>
<p>{{ isi }}</p>
</body>
</html>
```
21. Tambahkan folder static di mywebsite dan buat folder img lalu masukkan gambar
Hasil Project


### Link Documentasi:
Model:[https://docs.djangoproject.com/id/6.0//topics/db/models/](https://)
URL: https://docs.djangoproject.com/en/6.0/topics/http/urls/
Templates: https://docs.djangoproject.com/en/6.0/topics/templates/#the-django-template-language
Django: https://docs.djangoproject.com/en/6.0/contents/
### Link video:
CodingEnterpreeurs (YouTube): https://www.youtube.com/watch?v=yDv5FIAeyoY&list=PLEsfXFp6DpzRHiyW04co1y-CjDM1Y1sIS
CodingEnterpreeurs (Udemy): https://www.udemy.com/share/101tNe3@upr7uoIQcfXTq_rbP_fMvg1MfpDIVHaSwKbWHpp3yjGHiKbTHwq1GDnVQ99s3HQazw==/