Glowify adalah proyek Django sederhana sebagai tugas semester 3 mata kuliah Pemrograman Berbasis Platform oleh Adelya Amanda (2306165616). Proyek ini di buat dengan sistem operasi Windows.
- Membuat sebuah repository lokal yang sudah terkoneksi dengan repository Github bernama
glowify - Di direktori
glowify, membuat virtual environment Python baru dengan command:python -m venv env
- Menyalakan virtual environment Python baru dengan command:
env\Scripts\activate
- Mempersiapkan
requirements.txtdi direktori yang sama, berisi daftar modul requirements:django gunicorn whitenoise psycopg2-binary requests urllib3
- Meng-install modul requirements dengan pip:
python -m pip install -r requirements.txt
- Membuat proyek Django baru dengan command:
django-admin startproject glowify . - Mengubah ALLOWED_HOSTS di file
settings.pyagar dapat berjalan di localhost dengan menambahkan:... ALLOWED_HOSTS = ["localhost", "127.0.0.1"] ...
- Membuat aplikasi bernama main dengan command:
python manage.py startapp main
- Menambahkan nama aplikasi ke
INSTALLED_APPSpada filesettings.pydi direktoriglowifyINSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'main' ]
- Me-routing url pada file
urls.pydi direktoriglowifysehingga isi fileurls.pymenjadi:from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('main.urls')), ]
- Mengubah
models.pydenganclass Product, lalu isi dengan datatype sesuai kriteria:from django.db import models class Product(models.Model): name = models.CharField(max_length=255) price = models.IntegerField() description = models.TextField() volume = models.IntegerField() @property def price_per_ml(self): return self.price / self.volume if self.volume > 0 else 0
- Melakukan migrasi dengan command:
python manage.py makemigrations python manage.py migrate
- Membuat direktori template di aplikasi main, lalu isi dengan
index.htmluntuk laman main:<h1>Glowify your skin!</h1> <h5>Name: </h5> <p>{{ name }}<p> <h5>NPM: </h5> <p>{{ npm }}<p> <h5>Class: </h5> <p>{{ class }}<p>
- Menambahkan fungsi untuk me-render laman main pada file
views.py:from django.shortcuts import render def show_main(request): context = { 'npm' : '2306165616', 'name': 'Adelya Amanda', 'class': 'PBP A' } return render(request, "main.html", context)
- Melakukan routing pada aplikasi
mainpada fileurls.pydi direktori main:from django.urls import path from main.views import show_main app_name = 'main' urlpatterns = [ path('', show_main, name='show_main'), ]
- Test aplikasi pada localhost dengan command:
python manage.py runserver
- Melakukan deploy app ke Pacil Web Server (PWS). Project telah dideploy di PWS melalui [ http://adelya-amanda-glowify.pbp.cs.ui.ac.id/ ]
Ketika user mengirimkan request melalui browser, request tersebut pada pertama kali akan diterima oleh urls.py. File ini berfungsi untuk menentukan URL mana yang cocok dengan request yang dikirim, kemudian mengarahkannya ke fungsi view yang sesuai di views.py. Setiap URL telah dipetakan ke fungsi tertentu di views.py.
Di dalam views.py, logic web app dijalankan, seperti memvalidasi data, mengambil informasi dari database, atau memproses input dari user. Jika perlu berinteraksi dengan database, views.py akan menggunakan models.py -- representasi data yang disimpan dalam database. Setelah data diambil atau diproses, view akan menggunakan file template HTML dari folder templates untuk menyajikan tampilan yang user lihat. Akhirnya, halaman HTML yang dirender tersebut dikirim kembali ke browser user sebagai response. Dengan demikian, urls.py, views.py, models.py, dan template HTML bekerja sama untuk memproses request & response dalam aplikasi Django.
Git adalah alat version control yang umum digunakan dalam software development. Fungsinya untuk membantu developer melacak perubahan pada kode secara terorganisir. Dengan Git, setiap perubahan dalam proyek akan tersimpan di dalam repositori, sehingga developer dapat melihat riwayat perubahan, membandingkan versi kode yang berbeda, melakukan eksperimen pada kodenya, dan mengembalikan kode ke versi sebelumnya jika diperlukan. Fitur ini membuat kolaborasi antar tim menjadi mlebih mudah, karena setiap anggota tim dapat bekerja pada versi kode yang sama tanpa harus menyatukan perubahan secara manual.
Fitur unggulan lainnya dari Git adalah kemampuan untuk membuat cabang (branch) dari proyek utama. Dengan branch, developer dapat bekerja pada fitur atau perbaikan bug secara terpisah tanpa mengganggu kode utama yang sudah diyakini stabil. Setelah pekerjaan pada cabang selesai, cabang tersebut bisa digabungkan kembali ke proyek utama. Dengan git, setiap orang dapat fokus pada bagian mereka tanpa memengaruhi pekerjaan orang lain.
Dari pengalaman saya menggunakan framework full-stack seperti Node.js, saya merasa Django lebih menawarkan kepraktisan. Salah satu alasan utama adalah karena Django menggunakan Python, yang sering menjadi bahasa pertama bagi banyak orang yang ingin belajar pemrograman. Dengan Python yang terkenal readable seperti pseudocode dan juga beginner-friendly, Django menjadi pilihan yang natural bagi pemula yang ingin mencoba terjun ke web development
Selain itu, setup untuk membuat aplikasi web penuh di Django relatif jauh lebih cepat dibandingkan Node.js. Django sudah menyediakan banyak fitur bawaan, seperti sistem autentikasi, routing, dan manajemen database, sehingga developer dapat langsung fokus pada logika tanpa harus menyiapkan banyak komponen dari awal. Django juga menggabungkan back-end dan front-end dalam satu framework, sehingga tidak perlu menggunakan dua framework terpisah untuk full-stack development. Poin ini menghemat waktu dan tenaga dalam hal setup, terutama bagi yang baru memulai belajar software development.
Model dalam Django adalah representasi data yang ada di dalam database. Model biasanya berbentuk kelas Python yang mendefinisikan struktur data relasional, seperti kolom, tipe data, dan aturan-aturan terkait datanya. Model berfungsi sebagai penghubung antara logika web app dengan data yang disimpan di database.
Django menggunakan ORM (Object Relational Mapping) karena model memetakan objek Python (seperti kelas dan konsep OOP) dengan tabel di database. ORM ini berbeda dengan pengalaman saya menggunakan bahasa dan framework lain, di mana untuk berinteraksi dengan database saya harus menulis perintah SQL secara langsung untuk melakukan operasi CRUD. Django dapat melakukan manipulasi database menggunakan kode Python tanpa perlu menulis query SQL secara manual. ORM membantu proses pengelolaan data tanpa perlu memahami detail bahasa SQL.
-
Membuat
forms.pydi direktorimaindengan isi:from django import forms from .models import Product class ProductForm(forms.ModelForm): class Meta: model = Product fields = ['name', 'price', 'description', 'volume']
-
Menambahkan fungsi
create_productuntuk menambha entri database diviews.pydi direktori main:def create_product(request): form = ProductForm(request.POST or None) if form.is_valid(): form.save() return redirect('main:show_main') context = {'form': form} return render(request, "create_product.html", context)
-
Mengimplementasikan form yang sudah dibuat ke dalam laman baru dengan template html yang baru
create_product.html:{% extends 'base.html' %} {% block content %} <h1>Add new product!</h1> <form method="POST"> {% csrf_token %} <table> {{form.as_table}} <tr> <td></td> <td> <input type="submit" value="Add product" /> </td> </tr> </table> </form> {% endblock %} -
Routing URL ke page yang sesuai di
urls.pypada adirektorimain:urlpatterns = [ ... path('create-product', create_product, name='create_product'), ... ]
-
Menambahkan folder
templatesdi direktori utama danbase.htmlsebagai base dari halaman-halaman lainnya. -
Menambahkan lokasi folder
templateskesettings.pydi direktoriglowify:... 'DIRS': [BASE_DIR / 'templates'], ...
-
Mengimplementasikan database ke dalam halaman utama
main.html, yang juga menjadi perpanjangan daribase.htmldi direktori utama:{% if not products %} <p>Product not found T_T</p> {% else %} <table> <tr> <th>Product Name</th> <th>Price</th> <th>Description</th> <th>Volume (ml)</th> </tr> {% for product in products %} <tr> <td>{{ product.name }}</td> <td>{{ product.price }}</td> <td>{{ product.description }}</td> <td>{{ product.volume }}</td> </tr> {% endfor %} </table> {% endif %} <br /> <a href="{% url 'main:create_product' %}"> <button>Add new product!</button> </a> -
Menambahkan fungsi-fungsi yang diperlukan untuk menampilkan JSON dan XML, baik secara keseluruhan ataupun berdasarkan PK entri database yang sesuai di
views.py:def show_xml(request): data = Product.objects.all() return HttpResponse(serializers.serialize('xml', data), content_type='application/xml') def show_json(request): data = Product.objects.all() return HttpResponse(serializers.serialize('json', data), content_type='application/json') def show_xml_by_id(request, id): data = Product.objects.filter(pk=id) return HttpResponse(serializers.serialize('xml', data), content_type='application/xml') def show_json_by_id(request, id): data = Product.objects.filter(pk=id) return HttpResponse(serializers.serialize('json', data), content_type='application/json')
-
Melakukan routing kembali URL yang sesuai di file
urls.py:
urlpatterns = [
...
path('xml/', show_xml, name='show_xml'),
path('json/', show_json, name='show_json'),
path('xml/<str:id>', show_xml_by_id, name='show_xml_by_id'),
path('json/<str:id>', show_json_by_id, name='show_json_by_id')
]- Melakukan test aplikasi pada localhost, cek apakah ada error. Cek juga endpoint yang sesuai, baik tidak ataupun menggunakan PK:
python manage.py runserverMisalkan untuk user e-commerce glowify, user akan expect untuk mendapatkan lists dari produk terbaru dan melihat stok barang yang tersedia. Misalkan saya, sebagai salah satu seller dari glowify, tentu saya ingin menuliskan deskripsi produk up-to-date agar pembeli saya mendapatkan review produk yang paling relevan, atau terus mengupdate harga dari produk yang saya jual seiring waktu. Saya juga perlu mengupdate stok yang tersedia di inventori saya. Dengan demikian, data delivery berguna untuk user experience. User memerlukan update data yang dinamis, jadi data perlu diperbaharui secara terus menerus, menyanggupi request yang masuk.
Melalui data delivery, setiap produk yang saya update di glowify telah memiliki identitas (ID) nya sendiri. Namun saya dapat merasa nyaman sebagai penjual, karena tahu ID nya bukanlah semata desimal yang mudah dienumerate, tapi sebuah unique ID yang dipastikan berbeda untuk setiap barang yang saya jual. Dengan demikian, data delivery juga berfungsi sebagai data protection.
Meskipun keduanya merupakan form of data delivery yang paling umum digunakan, saya bisa mengerti mengapa JSON lebih populer. Secara awam, JSON akan jauh lebih mudah dipahami karena hanya berupa key and value pair, juga lebih 'sedikit' untuk ditulis (less verbose). Dengan penulisan yang lebih sedikit (simple) tetapi merepresentasikan data delivery yang sama, tentu JSON akan lebih menjadi pilihan. Selain itu, JSON juga didukung oleh Javascript, yang merupakan 'most used web programming language' (Statista, 2024). Ketika pertama kali mempelajari web development, saya juga lebih dulu diperkenalkan dengan Javascript, sehingga jauh lebih familiar untuk menggunakan JSON. Ketika mempelajari RESTful APIs pun, JSON akan digunakan untuk data interchange, ditambah JSON didukung oleh built-in method yang berguna seperti JSON.parse() dan JSON.stringify() yang dapat langsung mentranslasi data dalam bentuk objek. Bandingkan dengan XML yang perlu berhadapan dengan parsing text secara manual. JSON juga mensupport penggunaan array, tidak dengan XML. Tetapi, semua kembali ke konteks penggunaan. Bagaimanapun, XML juga memberikan hierarki yang lebih jelas dan descriptive.
is_valid() merupakan built-in method dari Django. Ketika memanggil is_valid(), Django akan melakukan validasi untuk setiap field di form. Validasi berupa: apakah tipe dari field sesuai dengan yang didefinisikan di models.py, apakah semua field terisi, ataupun custom logic lainnya.
Mengapa membutuhkan method is_valid()? Jika validasi gagal, yang berarti data yang diinput user tidak seusai kriteria, field akan me-return false dan memunculkan pesan error, sehingga user dapat langsung menyadari kesalahan dari input dan segera membetulkan pengisian input fieldnya. Jika is_valid() me-return true, form akan emiliki cleaned_data yang dapat digunakan untuk logika selanjutnya dalam aplikasi Django.
Jika tidak melalui validasi, data yang masuk ke database dengan tipe IntegerField bisa saja berupa alphabet, sehingga database (umumnya SQL) akan segera mereturn error, yang berujung rumit jika harus membetulkannya manual dari sisi database. Sehingga, dengan method is_valid(), ketika saya mencoba mengisi field price dan volume dengan alphabet, tulisannya tidak akan bisa muncul, akibat saya telah mendefine field form tersebut dengan Integer.
CSRF merupakan jenis serangan di mana attacker melakukan tindakan yang tidak sah, atas nama pengguna yang sah, tanpa sepengetahuan mereka. Tindakan yang tidak sah dalam konteks aplikasi bisa berupa mengubah kata sandi, mengirim pesan atas nama pengguna yang sah, melakukan transaksi tidak sah, hingga mengubah data. Dalam konteks glowify, serangan bisa berupa penghapusan produk atau perubahan informasi mengenai produk yang dijual.
Contoh mekanismenya, attacker dapat membuat form tersembunyi yang mengirimkan POST request ke aplikasi Django saya, glowify. Pengguna glowify yang mengira mereka sedang mengisi form di situs glowify asli, akan mengisi data penting mengenai pembelian mereka pada form palsu, lalu submit form tersebut. Submit request akan tampak sah bagi glowify dikarenakan berasal dari browser pengguna yang sah.
csrf_token membantu menghasilkan token unik yang disertakan dalam form. Token ini yang hanya akan lolos oleh verifikasi server saat form disubmit, jika token tersebut sesuai dengan yang dihasilkan server. Sehingga, hanya form sah glowify yang akan diterima oleh server.
- Buat fungsi
registerdiviews.pyuntuk menangani registrasi pengguna baru menggunakanUserCreationForm. - Tambahkan halaman
register.htmldi dalammain/templatesuntuk form pendaftaran. - Tambahkan routing untuk
registerdiurls.py.
# views.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib import messages
def register(request):
form = UserCreationForm()
if request.method == "POST":
form = UserCreationForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Your account has been successfully created!')
return redirect('main:login')
return render(request, 'register.html', {'form': form})<!-- register.html -->
{% extends 'base.html' %}
{% block content %}
<div class="login">
<h1>Register</h1>
<form method="POST">{% csrf_token %}
<table>{{ form.as_table }}</table>
<input type="submit" name="submit" value="Daftar" />
</form>
</div>
{% endblock %}# urls.py
from main.views import register
urlpatterns = [
...
path('register/', register, name='register'),
]- Buat fungsi
login_useruntuk menangani autentikasi pengguna. Setelah berhasil login, set cookielast_login. - Buat halaman
login.html. - Tambahkan routing untuk login di
urls.py.
# views.py
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate, login
import datetime
def login_user(request):
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
response = HttpResponseRedirect(reverse("main:show_main"))
response.set_cookie('last_login', str(datetime.datetime.now()))
return response
else:
form = AuthenticationForm()
return render(request, 'login.html', {'form': form})<!-- login.html -->
{% extends 'base.html' %}
{% block content %}
<div class="login">
<h1>Back to Glowify!</h1>
<form method="POST">{% csrf_token %}
<table>{{ form.as_table }}</table>
<input type="submit" value="Login" />
</form>
<a href="{% url 'main:register' %}">Register Now</a>
</div>
{% endblock %}# urls.py
urlpatterns = [
...
path('login/', login_user, name='login'),
]- Buat fungsi
logout_useruntuk logout dan menghapus cookielast_login. - Tambahkan routing untuk logout di
urls.py.
# views.py
from django.contrib.auth import logout
def logout_user(request):
logout(request)
response = HttpResponseRedirect(reverse('main:login'))
response.delete_cookie('last_login')
return response<!-- main.html -->
<a href="{% url 'main:logout' %}">
<button>Logout</button>
</a># urls.py
from main.views import logout_user
urlpatterns = [
...
path('logout/', logout_user, name='logout'),
]Tambahkan decorator login_required untuk membatasi akses ke halaman main hanya untuk pengguna yang sudah login.
# views.py
from django.contrib.auth.decorators import login_required
@login_required(login_url='/login')
def show_main(request):
products = Product.objects.filter(user=request.user)
context = {
'name': request.user.username,
'npm': '2306165616',
'class': 'PBP-A',
'products': products,
'last_login': request.COOKIES.get('last_login'),
}
return render(request, 'main.html', context)Sesuaikan model Product untuk terhubung dengan User melalui ForeignKey, dan pastikan setiap produk terhubung ke pengguna yang membuatnya.
# models.py
from django.db import models
from django.contrib.auth.models import User
import uuid
class Product(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=255)
price = models.IntegerField()
description = models.TextField()
volume = models.IntegerField()
@property
def price_per_ml(self):
return self.price / self.volume if self.volume > 0 else 0Tambahkan logika untuk menyimpan produk baru yang terkait dengan pengguna yang sedang login.
# views.py
@login_required(login_url='/login')
def create_product(request):
form = ProductForm(request.POST or None)
if form.is_valid() and request.method == "POST":
product = form.save(commit=False)
product.user = request.user
product.save()
return redirect('main:show_main')
return render(request, 'create_product.html', {'form': form})Membuat dan menerapkan migrasi:
python manage.py makemigrations
python manage.py migrateTambahkan pengecekan environment production dengan menggunakan variabel os.
# settings.py
import os
PRODUCTION = os.getenv("PRODUCTION", False)
DEBUG = not PRODUCTIONCek apakah semuanya aman dengan menjalankan perintah:
python manage.py runserveraman:D
Dalam views.py di direktori main, terdapat penggunaan dari kedua fungsi tersebut.
- Merupakan
functiondi Django yang membantu routing menjadi lebih sederhana, dikarenakan fungsi ini langsung menerima berbagai jenis argumen seperti nama view hingga objek model. Detail URL akan diurus oleh Django.def register(request): form = UserCreationForm() if request.method == "POST": form = UserCreationForm(request.POST) if form.is_valid(): form.save() messages.success(request, 'Your account has been successfully created!') return redirect('main:login') ...
- Pada function
register, digunakanredirect()dikarenakan saya hanya ingin langsung mengarahkan user untuk ke halaman login setelah selesai mendaftarkan akunnya. Saya tidak perlu memberikan detail URL tertentu yang tepat atau mekanisme tambahan. Kode akan jadi lebih mudah dibaca, karena fungsionalitas yang saya butuhkan hanya switch laman saja.
- Merupakan
kelasdi Django untuk mengembalikan respons HTTP yang juga mengarahkan pengguna ke URL tertentu secara manual. Diperlukan pemberian URL tujuan sebagai argumen. Penggunaan kelas ini membantu kontrol lebih terhadap respons HTTP, terutama jika perlu melakukan mekanisme tambahan sebelum mengirimkan respons. Jadi, yang dilakukan tidak hanya sekedar switch laman.
def login_user(request):
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
response = HttpResponseRedirect(reverse("main:show_main"))
response.set_cookie('last_login', str(datetime.datetime.now()))
return response
...- Pada function
login_user, digunakanHttpResponseRedirect()dikarenakan saya ingin menambahkan cookie ke dalam respons sebelum mengembalikannya. Dengan demikian, terjadi modifikasi response sebelum dikirim balik ke pengguna (dikembalikan ke lamanshow_main), dilihat dari bagaimana kelas dipanggil dengan cara disimpan ke variabelresponseterlebih dahulu.
- Menambahkan ForeignKey ke dalam model
Productagar setiap produk akan terkait dengan satu pengguna dari modelUserbawaan Django. Setiap entri produk yang dibuat pengguna akan disimpan dengan informasi siapa yang membuatnya. Jika pengguna dihapus, produk yang terkait juga akan dihapus secara otomatis melaluion_delete=models.CASCADE
from django.contrib.auth.models import User
class Product(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
...- Tentu best practice e-commerce adalah menampilkan produk sesuai dengan pengguna yang sedang login. Sehingga di bagian
view, gunakan filter untuk menampilkan produk yang hanya dimiliki oleh pengguna yang terautentikasi. Padashow_main, filter produk denganrequest.user.
@login_required(login_url='/login')
def show_main(request):
products = Product.objects.filter(user=request.user)
...- Saat pengguna membuat produk baru, produk akan otomatis terkait dengan pengguna yang sedang login dikarenakan telah ditambahkan properti
userpada instance produk sebelum disimpan.
def create_product(request):
form = ProductForm(request.POST or None)
if form.is_valid() and request.method == "POST":
product = form.save(commit=False)
product.user = request.user # Menghubungkan produk dengan user
product.save() # Baru simpan
return redirect('main:show_main')
context = {'form': form}
return render(request, "create_product.html", context)- Autentikasi adalah proses memverifikasi identitas user. Proses ini memastikan bahwa pengguna yang mencoba mengakses aplikasi sesuai dengan 'field pengguna' yang diklaim. Proses ini biasanya meminta username dan password. Saat pengguna berhasil login, berarti proses autentikasi berhasil. Otorisasi adalah proses memberi izin, 'apa saja yang boleh dilakukan oleh pengguna yang sudah terautentikasi'? Otorisasi menentukan halaman atau data apa saja yang bisa diakses oleh pengguna tersebut.
- Misal, saya pernah mendaftar ke
glowifydengan username bizarrebeam. Autentikasi akan menanyakan, 'apakah ini benar-benar bizarrebeam?'. Otorisasi, di lain sisi, akan menanyakan, 'apakah bizarrebeam berhak untuk mengakses halaman admin ecommerce, atau mengedit data produk?' - Django menangani autentikasi dengan modul
django.contrib.auth. Django telah menyediakan form login bawaan yang menangani pengecekan kredensial pengguna. Padaviews.pysaya:
form = AuthenticationForm(data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)- Django menangani otorisasi dengan decorator seperti
@login_required, sehingga hanya pengguna yang telah login yang bisa mengakses halaman tertentu. Padaviews.pysaya:
@login_required(login_url='/login')
def show_main(request):4. Bagaimana Django mengingat pengguna yang telah login, kegunaan lain dari cookies, apakah semua cookies aman digunakan
- Setelah pengguna login, Django menggunakan sesi untuk mengingat pengguna tersebut. Informasi sesi disimpan di server. Browser pengguna hanya menyimpan session ID dalam bentuk cookie. Saat pengguna melakukan permintaan (request) ke server, browser mengirimkan session ID ini, dan Django akan mencocokkannya dengan informasi yang ada di server untuk mengidentifikasi pengguna. Proses ini terjadi setiap kali pengguna membuka halaman baru tanpa perlu login ulang.
- Setelah login, Django mengirim cookie dengan session ID ke browser. Django mengakses informasi sesi menggunakan cookie ini. Jika pengguna membuka halaman lain di situs, browser akan mengirim cookie ini dalam setiap permintaan, sehingga Django bisa mengenali pengguna yang telah login.
- Django secara default akan mengingat pengguna selama sesi berlangsung. Jika pengguna menutup browser atau jika durasi sesi habis, pengguna harus login kembali. Namun, Django juga bisa dikonfigurasi untuk mengingat pengguna lebih lama, misalnya dengan fitur "remember me" yang membuat sesi bertahan lebih lama.
- Cookie umum diminta untuk menyimpan preferensi pengguna, seperti bahasa yang dipilih, light/dark mode.
- Cooies dapat digunakan untuk melacak aktivitas pengguna di situs web, misal mengingat halaman yang dikunjungi, atau produk yang ditambahkan ke keranjang belanja (dalam konteks ecommerce).
- Third-party cookies dapat digunakan layanan iklan untuk menampilkan iklan yang lebih personalize. Cookies ini digunakan untuk menargetkan iklan berdasarkan perilaku pengguna di internet.
- Cookies juga dapat digunakan untuk mengumpulkan data statistik tentang kunjungan pengguna, lalu digunakan untuk menganalisis performa situs web.
Ada beberapa hal yang harus diperhatikan, sebab belum tentu semua cookies aman jika tidak dikonfigurasi dengan baik.
- Cookies yang mengandung informasi penting, seperti session ID, harus memiliki atribut HttpOnly. Atribut ini mencegah cookies tersebut diakses oleh JavaScript, sehingga mengurangi risiko serangan cross-site scripting (XSS).
- Cookies yang dikirim melalui koneksi yang aman (HTTPS) harus diberi atribut
Secureyang memastikan cookies hanya dikirim melalui koneksi yang terenkripsi. Tanpa atribut ini, cookies bisa dicuri jika pengguna mengakses situs melalui koneksi yang tidak aman. - Atribut
SameSitemencegah cookies dikirimkan dalam permintaan lintas situs, yang melindungi dari serangan cross-site request forgery (CSRF), seperti yang telah dijelaskan pada README Tugas 3. - Cookies pihak ketiga yang digunakan untuk iklan atau pelacakan sering kali dianggap 'invasif' karena melacak aktivitas pengguna di berbagai situs, sehingga menjadi concern privasi. Beberapa browser telah mulai memblokir third-party cookies secara default.
-
File
base.htmldi folder templates di root project:- Menambahkan tag
<meta name="viewport">a - Menambahkan script CDN Tailwind di bagian
<head>.
<head> {% block meta %} <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1"> {% endblock meta %} <script src="https://cdn.tailwindcss.com"></script> </head>
- Menambahkan tag
-
File
views.pydi subdirektori main:- Menambahkan fungsi
edit_product.
def edit_product(request, id): product = Product.objects.get(pk=id) form = ProductForm(request.POST or None, request.FILES or None, instance=product) if form.is_valid() and request.method == "POST": form.save() return HttpResponseRedirect(reverse('main:show_main')) context = {'form': form} return render(request, "edit_product.html", context)
- Menambahkan fungsi
-
Membuka
urls.pydi subdirektori main:- Menambahkan path URL untuk fungsi
edit_product.
from main.views import delete_product urlpatterns = [ # URL patterns lainnya path('edit-product/<uuid:id>', edit_product, name='edit_product'), ]
- Menambahkan path URL untuk fungsi
-
Membuka
urls.pydi subdirektori main:- Menambahkan path URL untuk fungsi
delete_product.
from main.views import delete_product urlpatterns = [ # URL patterns lainnya path('delete-product/<uuid:id>', delete_product, name='delete_product'), ]
- Menambahkan path URL untuk fungsi
-
Membuka
main.htmldi subdirektori templates:- Menambahkan tabel untuk menampilkan daftar produk.
- Menambahkan tombol untuk mengedit dan menghapus produk.
<table> <tr> <th>Product Name</th> <th>Price</th> <th>Description</th> <th>Volume (ml)</th> <th>Image</th> <th>Actions</th> </tr> {% for product in products %} <tr> <td>{{ product.name }}</td> <td>{{ product.price }}</td> <td>{{ product.description }}</td> <td>{{ product.volume }}</td> <td> {% if product.image %} <img src="{{ product.image.url }}" alt="{{ product.name }}" style="max-width: 100px; max-height: 100px;"> {% else %} <p>No image available</p> {% endif %} </td> <td> <a href="{% url 'main:edit_product' product.pk %}"> <button>Edit</button> </a> <a href="{% url 'main:delete_product' product.pk %}"> <button>Delete</button> </a> </td> </tr> {% endfor %} </table>
-
Styling di
base.html**:- Menambahkan script CDN Tailwind dan menghubungkannya dengan
global.css.
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> {% block meta %} {% endblock meta %} <script src="https://cdn.tailwindcss.com"></script> <link rel="stylesheet" href="{% static 'css/global.css' %}"/> </head> <body> {% block content %} {% endblock content %} </body> </html> - Menambahkan script CDN Tailwind dan menghubungkannya dengan
-
Styling di
global.css:- Menambahkan custom styling di
global.css
- Menambahkan custom styling di
-
Menambahkan styling di page lainnya:
login.htmlregister.htmlmain.htmlnavbar.htmlfooter.htmlcard_product.htmladd_product.htmledit_product.html
- Membuat
navbar.htmldi folder templates
{% load static %}
<nav class="bg-[#333333] fixed top-0 left-0 z-40 w-screen">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14 sm:h-16 md:h-20">
<!-- Logo Title -->
<div class="flex items-center">
<a href="{% url 'main:show_main' %}">
<img src="{% static 'image/glowify-logo.png' %}" alt="glowify logo" class="h-4 sm:h-5 md:h-6">
</a>
</div>
<!-- Navbar Links -->
<div class="hidden md:flex items-center space-x-8">
<a href="{% url 'main:show_main' %}" class="text-[#F5F3EE] hover:opacity-60">Home</a>
<a href="#" class="text-[#F5F3EE] hover:opacity-60">Products</a>
<a href="#" class="text-[#F5F3EE] hover:opacity-60">Categories</a>
<a href="#" class="text-[#F5F3EE] hover:opacity-60">Cart</a>
<!-- Logout Button -->
<a href="{% url 'main:logout' %}" class="text-center border border-[#F5F3EE] hover:bg-gray-200 hover:text-[#333333] text-[#F5F3EE] font-bold py-2 px-4 rounded transition duration-300">
Logout
</a>
</div>
<!-- Mobile Menu Button -->
<div class="md:hidden flex items-center">
<button class="mobile-menu-button">
<svg class="w-5 h-5 sm:w-6 sm:h-6 text-[#F5F3EE]" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div class="mobile-menu hidden md:hidden px-4 w-full">
<div class="py-2 space-y-1">
<a href="#" class="block text-[#F5F3EE] hover:opacity-60 py-2">Home</a>
<a href="#" class="block text-[#F5F3EE] hover:opacity-60 py-2">Products</a>
<a href="#" class="block text-[#F5F3EE] hover:opacity-60 py-2">Categories</a>
<a href="#" class="block text-[#F5F3EE] hover:opacity-60 py-2">Cart</a>
<!-- Logout Button for Mobile -->
<a href="{% url 'main:logout' %}" class="block text-center border border-[#F5F3EE] hover:bg-gray-200 hover:text-[#333333] text-[#F5F3EE] font-bold py-2 px-4 rounded transition duration-300 mt-2">
Logout
</a>
</div>
</div>
</nav>
<script>
const btn = document.querySelector("button.mobile-menu-button");
const menu = document.querySelector(".mobile-menu");
btn.addEventListener("click", () => {
menu.classList.toggle("hidden");
});
</script>-
Di file
settings.py:- Menambahkan middleware WhiteNoise.
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # Tambahkan tepat di bawah SecurityMiddleware # Middleware lainnya ]
-
Konfigurasi Static Files di
settings.py:STATIC_URL = '/static/' if DEBUG: STATICFILES_DIRS = [ BASE_DIR / 'static' # merujuk ke /static root project pada mode development ] else: STATIC_ROOT = BASE_DIR / 'static' # merujuk ke /static root project pada mode production
- Membuat
footer.htmldi folder templates:
{% load static %}
<footer class="bg-[#333333] text-[#F5F3EE] py-6 sm:py-8 md:py-10 text-xs sm:text-sm md:text-base">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col space-y-4 sm:space-y-6 md:space-y-8">
<!-- Logo -->
<div class="flex justify-start">
<img src="{% static 'image/glowify-logo.png' %}" alt="glowify logo" class="h-4 sm:h-5 md:h-6">
</div>
<!-- Divider -->
<div class="w-full border-t border-[#F5F3EE]"></div>
<!-- Links and Copyright -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center w-full space-y-2 sm:space-y-0">
<!-- Copyright -->
<div class="text-left">
<p class="text-xs sm:text-sm md:text-base">© 2024, bizarrebeams.</p>
</div>
<!-- Links -->
<div class="flex flex-col sm:flex-row space-y-1 sm:space-y-0 sm:space-x-3 md:space-x-4 text-left sm:text-right">
<a href="#" class="hover:underline text-xs sm:text-sm md:text-base">Privacy Policy</a>
<a href="#" class="hover:underline text-xs sm:text-sm md:text-base">Terms of Service</a>
</div>
</div>
</div>
</div>
</footer>- Membuat
card_product.htmldi folder templates:
{% load humanize %}
<div class="bg-[#F5F5F5] p-4 sm:p-5 md:p-6 rounded shadow-md">
<!-- Product Image -->
<div class="bg-[#E7E5E0] p-3 sm:p-4 rounded">
<img src="{{ product.image.url }}" class="mx-auto object-cover h-36 sm:h-44 md:h-52 w-auto" alt="{{ product.name }}">
</div>
<!-- Product Details -->
<div class="text-left mt-3 sm:mt-4">
<h3 class="text-sm sm:text-base md:text-lg text-[#333333] mb-1 sm:mb-2">{{ product.name }}</h3>
<p class="text-xs sm:text-sm md:text-base text-[#818181] mb-2 sm:mb-3">{{ product.description }}</p>
<p class="text-base sm:text-lg md:text-xl font-bold text-[#333333] inline-block mb-1 sm:mb-2">Rp{{ product.price|intcomma }}</p>
<p class="text-xxs sm:text-xs md:text-sm text-[#818181] inline-block ml-1 mb-1 sm:mb-2"> /{{ product.volume }} mL</p>
</div>
<!-- Actions -->
<div class="mt-2 sm:mt-3 md:mt-4 flex justify-end space-x-2 sm:space-x-3 md:space-x-4">
<a href="{% url 'main:edit_product' product.pk %}">
<button class="bg-[#333333] text-white py-1 sm:py-1.5 md:py-2 px-2 sm:px-3 md:px-4 text-[10px] sm:text-xs md:text-sm rounded hover:bg-[#818181] hover:text-black transition duration-300">EDIT</button>
</a>
<a href="{% url 'main:delete_product' product.pk %}">
<button class="bg-[#F5F5F5] text-[#333333] border border-[#333333] py-1 sm:py-1.5 md:py-2 px-2 sm:px-3 md:px-4 text-[10px] sm:text-xs md:text-sm rounded hover:bg-[#818181] hover:text-black transition duration-300">DELETE</button>
</a>
</div>
</div>- Membuat
register.htmldi folder templates:
{% extends 'base.html' %}
{% load static %}
{% block meta %}
<title>Register</title>
{% endblock meta %}
{% block content %}
<div class="flex h-screen">
<!-- Left side with image and text -->
<div class="w-1/2 relative hidden sm:block">
<img src="{% static 'image/login-photo.png' %}" alt="Register Photo" class="object-cover w-full h-full">
<div class="absolute inset-0 flex justify-center items-end pb-4 sm:pb-8 md:pb-16 lg:pb-24">
<p class="text-white text-lg sm:text-xl md:text-2xl lg:text-3xl text-center drop-shadow-md">
Discover your radiant glow.<br>One product at a time.
</p>
</div>
</div>
<!-- Right side with register form -->
<div class="w-full sm:w-1/2 flex flex-col justify-center items-center bg-[#F5F3EE] p-4 sm:p-8 md:p-16 lg:p-24">
<!-- Logo -->
<img src="{% static 'image/glowify-logo-black.png' %}" alt="Glowify Logo" class="mb-4 sm:mb-8 md:mb-16 lg:mb-24 h-6 sm:h-7 md:h-8 w-auto">
<!-- Title -->
<h1 class="text-[#333333] text-xl sm:text-2xl md:text-3xl font-semibold mb-4 sm:mb-6 md:mb-8">Create your account</h1>
<!-- Register Form -->
<form method="POST" action="" class="w-full max-w-xs sm:max-w-sm">
{% csrf_token %}
<div class="mb-3 sm:mb-4">
<label for="id_username" class="block text-[#333333] text-xs sm:text-sm md:text-base mb-1 sm:mb-2">Username</label>
<input id="id_username" name="username" type="text" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10 text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="Enter your username">
</div>
<div class="mb-3 sm:mb-4">
<label for="id_password1" class="block text-[#333333] text-xs sm:text-sm md:text-base mb-1 sm:mb-2">Password</label>
<input id="id_password1" name="password1" type="password" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10 text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="Enter your password">
</div>
<div class="mb-3 sm:mb-4">
<label for="id_password2" class="block text-[#333333] text-xs sm:text-sm md:text-base mb-1 sm:mb-2">Confirm Password</label>
<input id="id_password2" name="password2" type="password" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10 text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="Confirm your password">
</div>
<div class="mb-4 sm:mb-6 md:mb-8">
<input class="btn login_btn w-full bg-[#333333] text-[#F5F3EE] py-1 sm:py-2 md:py-3 px-4 rounded transition duration-300 hover:opacity-80 text-xs sm:text-sm md:text-base cursor-pointer" type="submit" value="Register" />
</div>
</form>
<!-- Login Link -->
<p class="text-xs sm:text-sm md:text-base text-[#333333]">Already have an account?
<a href="{% url 'main:login' %}" class="font-bold hover:opacity-80">Login here</a>
</p>
</div>
</div>
{% endblock content %}- Membuat
edit_product.htmldi folder templates:
{% extends 'base.html' %}
{% load static %}
{% block content %}
{% include 'navbar.html' %}
<div class="bg-[#F5F3EE] min-h-screen flex items-center justify-center py-10 sm:py-16 md:py-20 px-10 sm:px-6 lg:px-8">
<div class="w-full max-w-sm sm:max-w-md md:max-w-lg lg:max-w-xl bg-white shadow-md rounded-md p-4 sm:p-6 md:p-8 my-6 sm:my-8 md:my-12">
<h1 class="text-xl sm:text-2xl md:text-3xl font-semibold text-center text-[#333333] mb-4 sm:mb-6 md:mb-8">Edit product</h1>
<form method="POST" enctype="multipart/form-data" class="space-y-3 sm:space-y-4 md:space-y-5">
{% csrf_token %}
<div>
<label for="id_name" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Product name</label>
<input id="id_name" name="name" type="text" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="E.g. COSRX Snail Mucin" value="{{ form.name.value }}">
</div>
<div>
<label for="id_price" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Price</label>
<input id="id_price" name="price" type="text" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="In Rupiah, e.g. 150000" value="{{ form.price.value }}">
</div>
<div>
<label for="id_description" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Description</label>
<textarea id="id_description" name="description" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="E.g. An essence to calm down the skin.">{{ form.description.value }}</textarea>
</div>
<div>
<label for="id_volume" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Volume</label>
<input id="id_volume" name="volume" type="text" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="In ml, e.g. 150" value="{{ form.volume.value }}">
</div>
<div>
<label for="id_image" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Product Image</label>
<input id="id_image" name="image" type="file" class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border border-gray-300 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 text-xs sm:text-sm md:text-base bg-[#E7E5E0]">
</div>
<div>
<input type="submit" value="Edit product" class="w-full bg-[#333333] text-[#F5F3EE] py-2 sm:py-2.5 md:py-3 px-4 rounded transition duration-300 hover:bg-gray-200 hover:text-black text-xs sm:text-sm md:text-base cursor-pointer mt-2 sm:mt-3 md:mt-4">
</div>
</form>
</div>
</div>
{% include 'footer.html' %}
{% endblock content %}- Membuat
create_product.htmldi folder templates:
{% extends 'base.html' %}
{% load static %}
{% block content %}
{% include 'navbar.html' %}
<div class="bg-[#F5F3EE] min-h-screen flex items-center justify-center py-10 sm:py-16 md:py-20 px-10 sm:px-6 lg:px-8">
<div class="w-full max-w-sm sm:max-w-md md:max-w-lg lg:max-w-xl bg-white shadow-md rounded-md p-4 sm:p-6 md:p-8 my-6 sm:my-8 md:my-12">
<h1 class="text-xl sm:text-2xl md:text-3xl font-semibold text-center text-[#333333] mb-4 sm:mb-6 md:mb-8">Add a new product</h1>
<form method="POST" enctype="multipart/form-data" class="space-y-3 sm:space-y-4 md:space-y-5">
{% csrf_token %}
<div>
<label for="id_name" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Product name</label>
<input id="id_name" name="name" type="text" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="E.g. COSRX Snail Mucin">
</div>
<div>
<label for="id_price" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Price</label>
<input id="id_price" name="price" type="text" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="In Rupiah, e.g. 150000">
</div>
<div>
<label for="id_description" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Description</label>
<textarea id="id_description" name="description" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="E.g. An essence to calm down the skin."></textarea>
</div>
<div>
<label for="id_volume" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Volume</label>
<input id="id_volume" name="volume" type="text" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border placeholder-[#818181] text-gray-900 focus:outline-none focus:ring-[#333333] focus:border-[#333333] focus:z-10text-xs sm:text-sm md:text-base bg-[#E7E5E0]" placeholder="In ml, e.g. 150">
</div>
<div>
<label for="id_image" class="block text-xs sm:text-sm md:text-base text-[#333333] mb-1 sm:mb-2">Product Image</label>
<input id="id_image" name="image" type="file" required class="appearance-none rounded-md relative block w-full px-2 sm:px-3 py-1.5 sm:py-2 md:py-3 border border-gray-300 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 text-xs sm:text-sm md:text-base bg-[#E7E5E0]">
</div>
<div>
<input type="submit" value="Add product" class="w-full bg-[#333333] text-[#F5F3EE] py-2 sm:py-2.5 md:py-3 px-4 rounded transition duration-300 hover:bg-gray-200 hover:text-black text-xs sm:text-sm md:text-base cursor-pointer mt-2 sm:mt-3 md:mt-4">
</div>
</form>
</div>
</div>
{% include 'footer.html' %}
{% endblock content %}-
Inline Styles
- CSS yang ditulis langsung pada atribut
styledi elemen HTML memiliki prioritas tertinggi.<p style="color: red;">This is a paragraph.</p>
- CSS yang ditulis langsung pada atribut
-
ID Selectors
- Selector yang menggunakan ID memiliki prioritas lebih tinggi dibandingkan class, attribute, dan type selectors.
#myId { color: blue; }
- Selector yang menggunakan ID memiliki prioritas lebih tinggi dibandingkan class, attribute, dan type selectors.
-
Class, Attribute, dan Pseudo-class Selectors
- Selector yang menggunakan class, attribute, atau pseudo-class memiliki prioritas lebih tinggi dibandingkan type selectors.
.myClass { color: green; } [type="text"] { color: yellow; } :hover { color: orange; }
- Selector yang menggunakan class, attribute, atau pseudo-class memiliki prioritas lebih tinggi dibandingkan type selectors.
-
Type Selectors dan Pseudo-element Selectors
- Selector yang menggunakan type (tag) atau pseudo-element memiliki prioritas lebih rendah dibandingkan ID, class, dan attribute selectors.
p { color: purple; } ::before { content: "Prefix"; }
- Selector yang menggunakan type (tag) atau pseudo-element memiliki prioritas lebih rendah dibandingkan ID, class, dan attribute selectors.
-
Universal Selector
- Selector yang menggunakan
*memiliki prioritas paling rendah.* { color: black; }
- Selector yang menggunakan
-
Important Rule
- Deklarasi yang menggunakan
!importantakan mengesampingkan semua aturan lainnya, kecuali aturan lain yang juga menggunakan!important. - Contoh:
p { color: blue !important; }
- Deklarasi yang menggunakan
Mengapa penting? Misalkan saya tidak menerapkan responsive design pada glowify saya:
-
Design
glowifypada tab
(masih boleh lah ya.... sesek dikit) -
Design
glowifypada mobile:
(sesek bgt..... apa yang mau diliat)
- Memberi pengalaman pengguna yang lebih baik
- Website dapat diakses dari berbagai jenis perangkat, mau layarnya 'besar' ataupun lebih kecil
- Hanya membuat satu website, tetapi sudah diset agar mampu menghandle display design untuk berbagai perangkat (dibandingkan membuat website untuk tipe desktop, tab, dan hp....). Pengelolaan website akan lebih mudah.
- SIAKNG :D
Baru tampilan website saja, login formnya menyediakan banyak empty space yang tidak terisi. Empty space umum di login form, namun akan lebih baik jika main utility webistenya (form) diletakkan di tengah sebagai center of attention.
Untuk mobile screen size, pengguna harus zoom page secara manual dikarenakan teks sangat kecil.
Banyak space kosong tersedia di bawah, padahal design bisa dibuat agar layoutnya menyesuaikan mobile screen size
-
Margin
- Ruang di luar elemen yang memisahkan elemen tersebut dari elemen lain di sekitarnya. Margin tidak memiliki warna atau gaya, hanya ruang kosong.
- Digunakan untuk mengatur jarak antara elemen-elemen di halaman web.
.example { margin: 20px; /* Memberikan jarak 20px di semua sisi elemen */ }
-
Border
- Garis yang mengelilingi elemen. Border dapat memiliki warna, ketebalan, dan gaya (seperti solid, dashed, atau dotted).
- Digunakan untuk memberikan batas visual pada elemen, sehingga elemen tersebut lebih menonjol atau terpisah dari elemen lain.
.example { border: 2px solid black; /* Memberikan border hitam solid dengan ketebalan 2px */ }
-
Padding
- Ruang di dalam elemen yang memisahkan konten elemen dari border elemen tersebut. Padding tidak memiliki warna atau gaya, hanya ruang kosong.
- Digunakan untuk memberikan ruang di dalam elemen, sehingga konten tidak terlalu dekat dengan border.
.example { padding: 10px; /* Memberikan ruang 10px di dalam elemen di semua sisi */ }
Adalah model layout satu dimensi yang digunakan untuk mengatur tata letak elemen dalam satu baris atau satu kolom. Flexbox umum digunakan untuk membua layout yang responsif (mampu adjust di beda breakpoint ukuran screen) dikarenakan konsepnya berupa 'flexible box'.
Flexbox berguna untuk:
- Mengatur elemen dalam satu baris (horizontal) atau adlam satu kolom (vertikal)
- Membantu pengaturan spasi antara elemen dan perataan elemen di dalam container
- Membantu elemen untuk menyesuaikan ukuran dan posisi berdasarkan ukuran container
Adalah model layout dua dimensi yang digunakan untuk mengatur tata letak elemen dalam baris dan kolom. Grid Layout sangat berguna untuk membuat tata letak yang lebih terstruktur.
Grid berguna untuk:
- Mengatur elemen dalam baris dan kolom dengan design yang lebih 'terstruktur' dikarenakan memiliki grid lines
- Mengatur ukuran dan posisi elemen dengan lebih presisi.
- Menambahkan pesan error untuk menampilkan validasi kesalahan pada form login.
{% if form.errors %}
<div class="text-red-500 text-xs mt-1">
{{ form.errors }}
</div>
{% endif %}- Membuat view baru
create_product_ajaxuntuk menangani permintaan POST menggunakan AJAX.
@csrf_exempt
@require_POST
def create_product_ajax(request):
form = ProductForm(request.POST, request.FILES)
if form.is_valid():
product = form.save(commit=False)
product.user = request.user
product.save()
return JsonResponse({
"message": "Product created successfully",
"product": {
"id": str(product.id),
"name": product.name,
"price": product.price,
"description": product.description,
"volume": product.volume,
"image": product.image.url if product.image else None
}
}, status=201)
else:
return JsonResponse({"errors": form.errors}, status=400)- Menambahkan path baru untuk view
create_product_ajax.
urlpatterns = [
path('create-product-ajax', create_product_ajax, name='create_product_ajax'),
]- Membuat fungsi
refreshProductsuntuk mengambil dan menampilkan data produk menggunakan AJAX GET.
<script>
async function getProducts() {
return fetch("{% url 'main:show_json' %}").then((res) => res.json());
}
async function refreshProducts() {
const productCardsContainer = document.getElementById("product_cards");
const products = await getProducts();
let htmlString = "";
if (products.length === 0) {
htmlString = `
<div class="col-span-full text-center py-8 sm:py-10 md:py-12">
<h3 class="text-lg sm:text-xl md:text-2xl font-semibold text-[#333333]">We couldn't find anything.</h3>
<p class="text-base sm:text-lg md:text-xl text-[#333333] mt-2">Drop yours here, one product at a time.</p>
</div>
`;
} else {
products.forEach((product) => {
const name = DOMPurify.sanitize(product.fields.name);
const description = DOMPurify.sanitize(product.fields.description);
const price = DOMPurify.sanitize(product.fields.price.toString());
const volume = DOMPurify.sanitize(product.fields.volume.toString());
const image = product.fields.image ? DOMPurify.sanitize(product.fields.image) : '{% static "image.png" %}';
htmlString += `
<div class="bg-[#F5F5F5] p-4 sm:p-5 md:p-6 rounded shadow-md">
<div class="bg-[#E7E5E0] p-3 sm:p-4 rounded">
<img src="${image}" class="mx-auto object-cover h-36 sm:h-44 md:h-52 w-auto" alt="${name}">
</div>
<div class="text-left mt-3 sm:mt-4">
<h3 class="text-sm sm:text-base md:text-lg text-[#333333] mb-1 sm:mb-2">${name}</h3>
<p class="text-xs sm:text-sm md:text-base text-[#818181] mb-2 sm:mb-3">${description}</p>
<p class="text-base sm:text-lg md:text-xl font-bold text-[#333333] inline-block mb-1 sm:mb-2">Rp${parseInt(price).toLocaleString()}</p>
<p class="text-xxs sm:text-xs md:text-sm text-[#818181] inline-block ml-1 mb-1 sm:mb-2"> /${volume} mL</p>
</div>
<div class="mt-2 sm:mt-3 md:mt-4 flex justify-end space-x-2 sm:space-x-3 md:space-x-4">
<a href="/edit-product/${product.pk}">
<button class="bg-[#333333] text-white py-1 sm:py-1.5 md:py-2 px-2 sm:px-3 md:px-4 text-[10px] sm:text-xs md:text-sm rounded hover:bg-[#818181] hover:text-black transition duration-300">EDIT</button>
</a>
<a href="/delete/${product.pk}">
<button class="bg-[#F5F5F5] text-[#333333] border border-[#333333] py-1 sm:py-1.5 md:py-2 px-2 sm:px-3 md:px-4 text-[10px] sm:text-xs md:text-sm rounded hover:bg-[#818181] hover:text-black transition duration-300">DELETE</button>
</a>
</div>
</div>
`;
});
}
productCardsContainer.innerHTML = htmlString;
}
refreshProducts();
</script>- Menambahkan modal form untuk menambahkan produk baru menggunakan AJAX POST.
<!-- Modal -->
<div id="crudModal" tabindex="-1" aria-hidden="true" class="hidden fixed inset-0 z-50 w-full flex items-center justify-center bg-gray-800 bg-opacity-50 overflow-x-hidden overflow-y-auto transition-opacity duration-300 ease-out">
<div id="crudModalContent" class="relative bg-white rounded-lg shadow-lg w-5/6 sm:w-3/4 md:w-1/2 lg:w-1/3 mx-4 sm:mx-0 transform scale-95 opacity-0 transition-transform transition-opacity duration-300 ease-out">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 border-b rounded-t">
<h3 class="text-xl font-semibold text-gray-900">
Add New Product
</h3>
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" id="closeModalBtn">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="px-6 py-4 space-y-6 form-style">
<form id="productForm">
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700">Product name</label>
<input type="text" id="name" name="name" class="mt-1 block w-full border border-gray-300 rounded-md p-2" placeholder="E.g. COSRX Snail Mucin" required>
</div>
<div class="mb-4">
<label for="price" class="block text-sm font-medium text-gray-700">Price</label>
<input type="text" id="price" name="price" class="mt-1 block w-full border border-gray-300 rounded-md p-2" placeholder="In Rupiah, e.g. 150000" required>
</div>
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<textarea id="description" name="description" rows="3" class="mt-1 block w-full h-52 resize-none border border-gray-300 rounded-md p-2" placeholder="E.g. An essence to calm down the skin." required></textarea>
</div>
<div class="mb-4">
<label for="volume" class="block text-sm font-medium text-gray-700">Volume</label>
<input type="text" id="volume" name="volume" class="mt-1 block w-full border border-gray-300 rounded-md p-2" placeholder="In ml, e.g. 150" required>
</div>
<div class="mb-4">
<label for="image" class="block text-sm font-medium text-gray-700">Product Image</label>
<input type="file" id="image" name="image" class="mt-1 block w-full border border-gray-300 rounded-md p-2" required>
</div>
</form>
</div>
<!-- Modal footer -->
<div class="flex flex-col space-y-2 md:flex-row md:space-y-0 md:space-x-2 p-6 border-t border-gray-200 rounded-b justify-center md:justify-end">
<button type="button" class="bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg" id="cancelButton">Cancel</button>
<button type="submit" id="submitProduct" form="productForm" class="bg-[#333333] hover:bg-gray-200 hover:text-black text-white font-bold py-2 px-4 rounded-lg">Save</button>
</div>
</div>
</div>
<script>
const modal = document.getElementById('crudModal');
const modalContent = document.getElementById('crudModalContent');
function showModal() {
modal.classList.remove('hidden');
setTimeout(() => {
modalContent.classList.remove('opacity-0', 'scale-95');
modalContent.classList.add('opacity-100', 'scale-100');
}, 50);
}
function hideModal() {
modalContent.classList.remove('opacity-100', 'scale-100');
modalContent.classList.add('opacity-0', 'scale-95');
setTimeout(() => {
modal.classList.add('hidden');
}, 150);
}
document.getElementById("cancelButton").addEventListener("click", hideModal);
document.getElementById("closeModalBtn").addEventListener("click", hideModal);
document.getElementById("productForm").addEventListener("submit", async function(event) {
event.preventDefault();
const formData = new FormData(this);
const response = await fetch("{% url 'main:create_product_ajax' %}", {
method: "POST",
body: formData,
headers: {
"X-CSRFToken": "{{ csrf_token }}",
},
});
if (response.ok) {
hideModal();
refreshProducts();
document.getElementById("productForm").reset();
} else {
const data = await response.json();
const errors = data.errors;
for (const [field, messages] of Object.entries(errors)) {
const input = document.getElementById(field);
const errorContainer = document.createElement('p');
errorContainer.className = 'text-red-500 text-xs mt-1';
errorContainer.innerText = messages.join(', ');
input.parentNode.appendChild(errorContainer);
}
}
});
</script>- Menambahkan fungsi untuk menangani pengiriman form menggunakan AJAX POST.
<script>
document.getElementById("productForm").addEventListener("submit", async function(event) {
event.preventDefault();
const formData = new FormData(this);
const response = await fetch("{% url 'main:create_product_ajax' %}", {
method: "POST",
body: formData,
headers: {
"X-CSRFToken": "{{ csrf_token }}",
},
});
if (response.ok) {
hideModal();
refreshProducts();
document.getElementById("productForm").reset();
} else {
const data = await response.json();
const errors = data.errors;
for (const [field, messages] of Object.entries(errors)) {
const input = document.getElementById(field);
const errorContainer = document.createElement('p');
errorContainer.className = 'text-red-500 text-xs mt-1';
errorContainer.innerText = messages.join(', ');
input.parentNode.appendChild(errorContainer);
}
}
});
</script>- Menggunakan DOMPurify untuk membersihkan data yang diambil dari server sebelum ditampilkan di halaman.
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.7/dist/purify.min.js"></script>
<script>
async function refreshProducts() {
const productCardsContainer = document.getElementById("product_cards");
const products = await getProducts();
let htmlString = "";
if (products.length === 0) {
htmlString = `
<div class="col-span-full text-center py-8 sm:py-10 md:py-12">
<h3 class="text-lg sm:text-xl md:text-2xl font-semibold text-[#333333]">We couldn't find anything.</h3>
<p class="text-base sm:text-lg md:text-xl text-[#333333] mt-2">Drop yours here, one product at a time.</p>
</div>
`;
} else {
products.forEach((product) => {
const name = DOMPurify.sanitize(product.fields.name);
const description = DOMPurify.sanitize(product.fields.description);
const price = DOMPurify.sanitize(product.fields.price.toString());
const volume = DOMPurify.sanitize(product.fields.volume.toString());
const image = product.fields.image ? DOMPurify.sanitize(product.fields.image) : '{% static "image.png" %}';
htmlString += `
<div class="bg-[#F5F5F5] p-4 sm:p-5 md:p-6 rounded shadow-md">
<div class="bg-[#E7E5E0] p-3 sm:p-4 rounded">
<img src="${image}" class="mx-auto object-cover h-36 sm:h-44 md:h-52 w-auto" alt="${name}">
</div>
<div class="text-left mt-3 sm:mt-4">
<h3 class="text-sm sm:text-base md:text-lg text-[#333333] mb-1 sm:mb-2">${name}</h3>
<p class="text-xs sm:text-sm md:text-base text-[#818181] mb-2 sm:mb-3">${description}</p>
<p class="text-base sm:text-lg md:text-xl font-bold text-[#333333] inline-block mb-1 sm:mb-2">Rp${parseInt(price).toLocaleString()}</p>
<p class="text-xxs sm:text-xs md:text-sm text-[#818181] inline-block ml-1 mb-1 sm:mb-2"> /${volume} mL</p>
</div>
<div class="mt-2 sm:mt-3 md:mt-4 flex justify-end space-x-2 sm:space-x-3 md:space-x-4">
<a href="/edit-product/${product.pk}">
<button class="bg-[#333333] text-white py-1 sm:py-1.5 md:py-2 px-2 sm:px-3 md:px-4 text-[10px] sm:text-xs md:text-sm rounded hover:bg-[#818181] hover:text-black transition duration-300">EDIT</button>
</a>
<a href="/delete/${product.pk}">
<button class="bg-[#F5F5F5] text-[#333333] border border-[#333333] py-1 sm:py-1.5 md:py-2 px-2 sm:px-3 md:px-4 text-[10px] sm:text-xs md:text-sm rounded hover:bg-[#818181] hover:text-black transition duration-300">DELETE</button>
</a>
</div>
</div>
`;
});
}
productCardsContainer.innerHTML = htmlString;
}
refreshProducts();
</script>- Menggunakan DOMPurify untuk membersihkan data yang diambil dari server sebelum ditampilkan di halaman.
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.7/dist/purify.min.js"></script>
<script>
async function getProducts() {
return fetch("{% url 'main:show_json' %}").then((res) => res.json());
}
async function refreshProducts() {
const productCardsContainer = document.getElementById("product_cards");
const products = await getProducts();
let htmlString = "";
if (products.length === 0) {
htmlString = `
<div class="col-span-full text-center py-8 sm:py-10 md:py-12">
<h3 class="text-lg sm:text-xl md:text-2xl font-semibold text-[#333333]">We couldn't find anything.</h3>
<p class="text-base sm:text-lg md:text-xl text-[#333333] mt-2">Drop yours here, one product at a time.</p>
</div>
`;
} else {
products.forEach((product) => {
const name = DOMPurify.sanitize(product.fields.name);
const description = DOMPurify.sanitize(product.fields.description);
const price = DOMPurify.sanitize(product.fields.price.toString());
const volume = DOMPurify.sanitize(product.fields.volume.toString());
const image = DOMPurify.sanitize(product.fields.image || '{% static "image.png" %}');
...
</script>-
Membuat halaman web menjadi lebih interaktif. Misalnya, di
glowify, JS digunakan untuk membuka dan menutup modal form saat menambahkan produk baru.<script> function showModal() { document.getElementById('crudModal').classList.remove('hidden'); } function hideModal() { document.getElementById('crudModal').classList.add('hidden'); } </script>
-
AJAX (Asynchronous JavaScript and XML) membantu mengambil dan mengirim data ke server tanpa perlu me-refresh halaman.
<script> async function refreshProducts() { const response = await fetch('/path/to/api'); const products = await response.json(); // Menampilkan produk di halaman } </script>
-
JS membantu validasi form di sisi klien sebelum data dikirim ke server. Di
glowify, dapat dipastikan dulu bahwa semua field diisi dengan benar sebelum mengirim data produk baru.<script> document.getElementById("productForm").addEventListener("submit", function(event) { event.preventDefault(); // Validasi form di sini // Jika valid, kirim data ke server }); </script>
-
JS membuat pengalaman pengguna yang lebih responsif. Misalnya, di
glowify, setelah produk baru ditambahkan, daftar produk diperbarui secara otomatis tanpa perlu me-refresh halaman. Dari sisi penggunaan akan terasa lebih responsif dan mudah digunakan.
Fungsi adalah untuk menunggu hingga permintaan HTTP selesai dan responsnya diterima sebelum melanjutkan eksekusi kode berikutnya. Dalam glowify, misalnya saat mengambil data produk dari server, gunakan await agar JavaScript menunggu hingga data produk benar-benar diterima sebelum mencoba memproses dan menampilkan data tersebut di halaman.
Jika tidak menggunakan await, maka JavaScript tidak akan menunggu respons dari fetch() dan langsung melanjutkan eksekusi kode berikutnya. Program berpotensi mencoba memproses data yang belum diterima, yang akan menyebabkan kesalahan atau data yang tidak lengkap ditampilkan. Misalnya, di glowify, jika tidak menggunakan await saat mengambil data produk, program mencoba menampilkan daftar produk sebelum data tersebut benar-benar tersedia, sehingga halaman bisa menampilkan data yang kosong atau tidak lengkap.
await memastikan bahwa data produk sudah siap sebelum ditampilkan, sehingga pengalaman pengguna menjadi lebih baik.
Dikarenakan decorator ini menonaktifkan perlindungan CSRF (Cross-Site Request Forgery) untuk view tersebut. Dalam glowify, misalnya saat membuat view untuk menambahkan produk baru menggunakan AJAX POST, gunakan csrf_exempt agar permintaan AJAX dari klien dapat diterima oleh server tanpa memerlukan token CSRF.
CSRF adalah jenis serangan di mana penyerang dapat membuat permintaan yang tidak sah atas nama pengguna yang sah (seperti penjelasan pada Tugas 2). Django secara default melindungi aplikasi dari serangan ini dengan memeriksa token CSRF pada setiap permintaan POST. Namun, ketika menggunakan AJAX untuk mengirim data, token CSRF mungkin tidak selalu tersedia atau dikirim dengan benar, terutama jika tidak dikonfigurasi dengan benar di JavaScript.
csrf_exemptmenonaktifkan pemeriksaan token CSRF untuk view tertentu, sehingga permintaan AJAX dapat diterima tanpa masalah. Menonaktifkan perlindungan CSRF dapat membuka celah keamanan jika tidak digunakan dengan hati-hati. Oleh karena itu, perlu dipastikan bahwa view yang menggunakan csrf_exempt hanya menerima permintaan dari sumber yang tepercaya.
Mengapa pembersihan data input pengguna dilakukan di belakang (backend) juga, tidak dilakukan di frontend saja?
Untuk memastikan bahwa data yang diterima oleh server adalah valid dan aman. Dalam glowify, pembersihan data digunakan untuk mencegah serangan seperti Cross-Site Scripting (XSS) seperti yang dijelaskan pada tutorial. tidak hanya sekedar tampilan frontend, tetapi juga memastikan bahwa data yang disimpan di database (sisi yang diurusi backend) adalah data yang sesuai dengan yang diharapkan.
Misalnya, ketika pengguna mengirimkan data produk baru melalui form, data tersebut akan diproses oleh view di backend. Di sini, Django Forms digunakan untuk memvalidasi dan membersihkan data input. Django Forms secara otomatis akan memeriksa apakah data yang dimasukkan sesuai dengan tipe data yang diharapkan dan akan membersihkan data dari karakter-karakter yang tidak diinginkan.
Contoh pada glowify adalah ketika pengguna menambahkan produk baru melalui form. Data yang dikirimkan oleh pengguna akan diterima oleh view create_product_ajax. Di view ini, ProductForm akan memvalidasi dan membersihkan data. Jika data valid, produk akan disimpan ke dalam database. Jika tidak, pesan error akan dikembalikan ke pengguna.
@csrf_exempt
@require_POST
def create_product_ajax(request):
form = ProductForm(request.POST, request.FILES)
if form.is_valid():
product = form.save(commit=False)
product.user = request.user
product.save()
return JsonResponse({"message": "Product created successfully"}, status=201)
else:
return JsonResponse({"errors": form.errors}, status=400)












