### 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 ![Screenshot (1503)](https://hackmd.io/_uploads/HJEd18mvWl.png) ![Screenshot (1504)](https://hackmd.io/_uploads/S11MS8mDZl.png) ### 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==/