diff --git a/education/admin.py b/education/admin.py deleted file mode 100644 index 7dfd38e5e2ffdd54d7e3f244e01b920e63fd0669..0000000000000000000000000000000000000000 --- a/education/admin.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.contrib import admin -from .models import Edukasi - -admin.site.register(Edukasi) \ No newline at end of file diff --git a/education/apps.py b/education/apps.py deleted file mode 100644 index 4a92e70e534a9be8b2c5d9aa209d664f472c3273..0000000000000000000000000000000000000000 --- a/education/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class EducationConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'education' diff --git a/education/forms.py b/education/forms.py deleted file mode 100644 index 077ecfdd2ec41bc1a0ef81a85e3482d5ca0210fa..0000000000000000000000000000000000000000 --- a/education/forms.py +++ /dev/null @@ -1,24 +0,0 @@ -from django import forms -from .models import Edukasi -from .widgets import TailwindInput - -class EdukasiForm(forms.ModelForm): - class Meta: - model = Edukasi - fields = '__all__' - widgets = { - 'npm': TailwindInput(input_type="number", attrs={'placeholder': 'Enter NPM (10 digits)'}), - 'ipk': TailwindInput(input_type="number", attrs={'step': '0.01', 'placeholder': 'Enter IPK (0.00 - 4.00)'}), - 'tahun_masuk': TailwindInput(input_type="number", attrs={'min': 1900, 'max': 2024, 'placeholder': 'Enter Year of Admission'}), - 'kode_prodi': TailwindInput(input_type="text", attrs={'placeholder': 'Enter Program Code (e.g., INF12345)'}), - 'tingkat_pendidikan': forms.Select( - attrs={ - 'class': 'mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm' - } - ), - } - - labels = { - 'tingkat_pendidikan': 'Tingkat Pendidikan', - } - diff --git a/education/migrations/0001_initial.py b/education/migrations/0001_initial.py deleted file mode 100644 index 113222d7dde391f252c8df9ca6715207bfbf6e6e..0000000000000000000000000000000000000000 --- a/education/migrations/0001_initial.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-13 13:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Edukasi', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('npm', models.BigIntegerField(unique=True)), - ('ipk', models.DecimalField(decimal_places=2, max_digits=4)), - ('tahun_masuk', models.PositiveIntegerField()), - ('kode_prodi', models.CharField(max_length=8)), - ('tingkat_pendidikan', models.CharField(choices=[('S1', 'S1'), ('S2', 'S2'), ('S3', 'S3')], max_length=2)), - ], - ), - ] diff --git a/education/migrations/0002_alter_edukasi_npm.py b/education/migrations/0002_alter_edukasi_npm.py deleted file mode 100644 index f4437b7ed3925b69b10f408cddda86e23bb21517..0000000000000000000000000000000000000000 --- a/education/migrations/0002_alter_edukasi_npm.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-14 12:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('education', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='edukasi', - name='npm', - field=models.CharField(max_length=10, unique=True), - ), - ] diff --git a/education/models.py b/education/models.py deleted file mode 100644 index 10306e5e018dae30efc98b0bf7423e245c67c853..0000000000000000000000000000000000000000 --- a/education/models.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.db import models -from django.core.exceptions import ValidationError -import re - -class Edukasi(models.Model): - TINGKAT_PENDIDIKAN_CHOICES = [ - ('S1', 'S1'), - ('S2', 'S2'), - ('S3', 'S3') - ] - - npm = models.CharField(max_length=10, unique=True) # Changed to CharField with max_length of 10 - ipk = models.DecimalField(max_digits=4, decimal_places=2) - tahun_masuk = models.PositiveIntegerField() - kode_prodi = models.CharField(max_length=8) - tingkat_pendidikan = models.CharField( - max_length=2, - choices=TINGKAT_PENDIDIKAN_CHOICES - ) - - def clean(self): - # Handle None values safely - if self.npm and not len(self.npm) == 10: - raise ValidationError({'npm': "NPM harus terdiri dari 10 digit angka."}) - - if self.ipk is not None and not (0 <= self.ipk <= 4): - raise ValidationError({'ipk': "IPK harus dalam rentang 0.00 - 4.00."}) - - current_year = 2025 - - - if self.tahun_masuk is not None and not (2020 <= self.tahun_masuk <= current_year): - raise ValidationError({'tahun_masuk': f"Tahun masuk harus antara 2020 - {current_year}."}) - - if self.kode_prodi and not re.match(r'^[A-Za-z]{3}\d{5}$', self.kode_prodi): - raise ValidationError({'kode_prodi': "Kode Prodi harus diawali 3 huruf diikuti 5 angka (contoh: INF12345)."}) - - def __str__(self): - return f"{self.npm} - {self.kode_prodi} ({self.tingkat_pendidikan})" diff --git a/education/templates/education/create.html b/education/templates/education/create.html deleted file mode 100644 index 5db5021147055078abfa1c9491e52c4de5808721..0000000000000000000000000000000000000000 --- a/education/templates/education/create.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} - Create Education -{% endblock %} - -{% block content %} - <section class="w-full px-5 md:px-10"> - <div class="w-full flex items-center gap-10"> - <a href="/education"> - {% include 'components/button.html' with variant='btn-primary' text='< Back' %} - </a> - <h1 class="text-xl md:text-3xl font-bold">Create Education</h1> - </div> - <form method="post" class="space-y-4 md:w-96 "> - {% csrf_token %} - {% for field in form %} - {% if field.name == 'tingkat_pendidikan' %} - <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700">TINGKAT PENDIDIKAN</label> - {% endif %} - {{ field }} - {% if field.errors %} - <p class="text-red-500 text-xs mt-1">{{ field.errors.0 }}</p> - {% endif %} - {% endfor %} - <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Submit</button> - </form> - </section> -{% endblock %} diff --git a/education/templates/education/edit.html b/education/templates/education/edit.html deleted file mode 100644 index 294ef55104e8d91e573fca53959c7c6b87317042..0000000000000000000000000000000000000000 --- a/education/templates/education/edit.html +++ /dev/null @@ -1,32 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} - Edit Education -{% endblock %} - -{% block content %} - <section - class="w-full px-5 md:px-10" - > - <div class="w-full flex items-center gap-10"> - <a href="/education"> - {% include 'components/button.html' with variant='btn-primary' text='< Back' %} - </a> - <h1 class="text-xl md:text-3xl font-bold">Edit Education</h1> - </div> - - <form method="post" class="space-y-4 w-96"> - {% csrf_token %} - {% for field in form %} - {% if field.name == 'tingkat_pendidikan' %} - <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700">TINGKAT PENDIDIKAN</label> - {% endif %} - {{ field }} - {% if field.errors %} - <p class="text-red-500 text-xs mt-1">{{ field.errors.0 }}</p> - {% endif %} - {% endfor %} - <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Update</button> - </form> - </section> -{% endblock %} diff --git a/education/templates/education/index.html b/education/templates/education/index.html deleted file mode 100644 index a265c413474364cc1eacc80ceb816311f06d22ab..0000000000000000000000000000000000000000 --- a/education/templates/education/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} - Daftar Education -{% endblock %} - -{% block content %} - <section class="w-full px-5 md:px-10"> - <div class="w-full flex flex-col md:flex-row items-center justify-between"> - <h1 class="font-bold text-3xl mb-4 md:mb-0">Daftar Edukasi</h1> - <a href="/education/create"> - {% include 'components/button.html' with variant='btn-primary' text='+ Create' %} - </a> - </div> - <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6"> - {% for edukasi in edukasi_list %} - <div class="border p-6 rounded-lg shadow-lg bg-white"> - <h2 class="font-bold text-2xl mb-2">{{ edukasi.npm }}</h2> - <table class="text-gray-700 w-full text-sm"> - <tr> - <td class="font-normal">IPK:</td> - <td>{{ edukasi.ipk }}</td> - </tr> - <tr> - <td class="font-normal">Tahun Masuk:</td> - <td>{{ edukasi.tahun_masuk }}</td> - </tr> - <tr> - <td class="font-normal">Kode Prodi:</td> - <td>{{ edukasi.kode_prodi }}</td> - </tr> - <tr> - <td class="font-normal">Tingkat Pendidikan:</td> - <td>{{ edukasi.get_tingkat_pendidikan_display }}</td> - </tr> - </table> - <div class="flex justify-end mt-4 gap-2"> - <a href="/education/{{ edukasi.id }}/edit" class="mb-2 md:mb-0 md:mr-2"> - {% include 'components/button.html' with variant='btn-secondary' text='Edit' %} - </a> - <form action="/education/{{ edukasi.id }}/delete" method="post" onsubmit="return confirm('Are you sure you want to delete this item?');"> - {% csrf_token %} - {% include 'components/button.html' with variant='btn-danger' text='Delete' %} - </form> - </div> - </div> - {% endfor %} - </div> - </section> -{% endblock %} diff --git a/education/urls.py b/education/urls.py deleted file mode 100644 index a48fa5e741bdca051e2cf20c462b0c3088153a0c..0000000000000000000000000000000000000000 --- a/education/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path -from .views import show_all_education, show_create, show_edit, delete_education - -urlpatterns = [ - path('', show_all_education, name="education-show-all"), - path('create', show_create, name="education-create"), - path('<int:pk>/edit', show_edit, name='education-edit'), - path('<int:pk>/delete', delete_education, name='education-delete') -] diff --git a/education/views.py b/education/views.py deleted file mode 100644 index 0d9307a019cfcbbc432e2be9ace55a1190e038c0..0000000000000000000000000000000000000000 --- a/education/views.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.shortcuts import render, get_object_or_404 -from django.http import HttpRequest -from .forms import EdukasiForm -from django.shortcuts import redirect -from .models import Edukasi - -# Create your views here. -def show_all_education(request: HttpRequest): - edukasi_list = Edukasi.objects.all() - return render(request, 'education/index.html', {'edukasi_list': edukasi_list}) - -def show_create(request: HttpRequest): - if request.method == "POST": - form = EdukasiForm(request.POST) - if form.is_valid(): - form.save() - return redirect('education-show-all') # Redirect to a success page - else: - form = EdukasiForm() - - return render(request, 'education/create.html', {'form': form}) - -def show_edit(request, pk): - edukasi = get_object_or_404(Edukasi, pk=pk) # Fetch the specific record - - if request.method == "POST": - form = EdukasiForm(request.POST, instance=edukasi) - if form.is_valid(): - form.save() - return redirect('education-show-all') # Redirect to list after update - else: - form = EdukasiForm(instance=edukasi) # Populate form with existing data - - return render(request, 'education/edit.html', {'form': form, 'edukasi': edukasi}) - - -def delete_education(request, pk): - edukasi = get_object_or_404(Edukasi, pk=pk) - edukasi.delete() - return redirect('education-show-all') \ No newline at end of file diff --git a/finance/__init__.py b/finance/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/finance/admin.py b/finance/admin.py deleted file mode 100644 index 50ec5698fa264393ec7965fa4cc68640a09f7b60..0000000000000000000000000000000000000000 --- a/finance/admin.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.contrib import admin -from .models import Keuangan - -admin.site.register(Keuangan) \ No newline at end of file diff --git a/finance/forms.py b/finance/forms.py deleted file mode 100644 index c7651e8b7792fab9f84d2808392fe58e53946e55..0000000000000000000000000000000000000000 --- a/finance/forms.py +++ /dev/null @@ -1,27 +0,0 @@ -from django import forms -from .models import Keuangan -from .widgets import TailwindInput - -class KeuanganForm(forms.ModelForm): - class Meta: - model = Keuangan - fields = '__all__' - widgets = { - 'nomor_rekening': TailwindInput(input_type="text", attrs={'placeholder': 'Enter Account Number (8-10 digits)'}), - 'skor_kredit': TailwindInput(input_type="number", attrs={'min': 0, 'max': 800, 'placeholder': 'Enter Credit Score (0-800)'}), - 'nomor_kartu_atm': TailwindInput(input_type="number", attrs={'placeholder': 'Enter ATM Card Number (15-16 digits)'}), - 'status_pekerjaan': forms.Select( - attrs={ - 'class': 'mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm' - } - ), - 'kode_mata_uang': TailwindInput(input_type="text", attrs={'placeholder': 'Enter Currency Code (e.g., USD)'}), - } - - labels = { - 'nomor_rekening': 'Nomor Rekening', - 'skor_kredit': 'Skor Kredit', - 'nomor_kartu_atm': 'Nomor Kartu ATM', - 'status_pekerjaan': 'Status Pekerjaan', - 'kode_mata_uang': 'Kode Mata Uang', - } diff --git a/finance/migrations/0001_initial.py b/finance/migrations/0001_initial.py deleted file mode 100644 index fca1d1dd41c434a268b6800ed6ee199a5f35120d..0000000000000000000000000000000000000000 --- a/finance/migrations/0001_initial.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-13 13:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Keuangan', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nomor_rekening', models.CharField(max_length=10)), - ('skor_kredit', models.PositiveIntegerField()), - ('nomor_kartu_atm', models.BigIntegerField()), - ('status_pekerjaan', models.CharField(choices=[('Karyawan', 'Karyawan'), ('PNS', 'PNS'), ('Mahasiswa', 'Mahasiswa'), ('Dosen', 'Dosen')], max_length=10)), - ('kode_mata_uang', models.CharField(max_length=3)), - ], - ), - ] diff --git a/finance/migrations/__init__.py b/finance/migrations/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/finance/models.py b/finance/models.py deleted file mode 100644 index 986986e6bc93b15dc8ee35b34260b76513328986..0000000000000000000000000000000000000000 --- a/finance/models.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.db import models -from django.core.exceptions import ValidationError -import re - -class Keuangan(models.Model): - nomor_rekening = models.CharField(max_length=10) # 8-10 digit, bisa diawali 0 - skor_kredit = models.PositiveIntegerField() # Rentang 0-800 - nomor_kartu_atm = models.BigIntegerField() # 15-16 digit angka - status_pekerjaan = models.CharField( - max_length=10, - choices=[ - ('Karyawan', 'Karyawan'), - ('PNS', 'PNS'), - ('Mahasiswa', 'Mahasiswa'), - ('Dosen', 'Dosen') - ] - ) # Predefined Value - kode_mata_uang = models.CharField(max_length=3) # 3 huruf sesuai ISO 4217 - - def clean(self): - # Validasi Nomor Rekening (8-10 digit angka, bisa diawali 0) - if not re.fullmatch(r'^\d{8,10}$', self.nomor_rekening): - raise ValidationError({'nomor_rekening': "Nomor rekening harus 8-10 digit angka dan dapat diawali 0."}) - - # Validasi Skor Kredit (0-800) - if not (0 <= self.skor_kredit <= 800): - raise ValidationError({'skor_kredit': "Skor kredit harus dalam rentang 0-800."}) - - # Validasi Nomor Kartu ATM (15-16 digit angka) - if not (100000000000000 <= self.nomor_kartu_atm <= 9999999999999999): - raise ValidationError({'nomor_kartu_atm': "Nomor kartu ATM harus terdiri dari 15-16 digit angka."}) - - # Validasi Kode Mata Uang (harus 3 huruf kapital) - if not re.fullmatch(r'^[A-Z]{3}$', self.kode_mata_uang): - raise ValidationError({'kode_mata_uang': "Kode mata uang harus 3 huruf sesuai ISO 4217."}) - - def __str__(self): - return f"{self.nomor_rekening} - {self.kode_mata_uang} ({self.status_pekerjaan})" diff --git a/finance/templates/finance/create.html b/finance/templates/finance/create.html deleted file mode 100644 index 2bd4acfcea11c8b19a1475e785f5a5ab53da17ce..0000000000000000000000000000000000000000 --- a/finance/templates/finance/create.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} - Create Financial -{% endblock %} - -{% block content %} - <div class="w-full px-5 md:px-10"> - <div class="w-full flex items-center gap-10"> - <a href="/finance">{% include 'components/button.html' with variant='btn-primary' text='< Back' %}</a> - <h1 class="text-xl md:text-3xl font-bold">Create Financial</h1> - </div> - <form method="post" class="space-y-4 w-96"> - {% csrf_token %} - {% for field in form %} - {% if field.name == 'status_pekerjaan' %} - <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700">STATUS PEKERJAAN</label> - {% endif %} - {{ field }} - {% if field.errors %} - <p class="text-red-500 text-xs mt-1">{{ field.errors.0 }}</p> - {% endif %} - {% endfor %} - <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Submit</button> - </form> - </div> -{% endblock %} diff --git a/finance/templates/finance/edit.html b/finance/templates/finance/edit.html deleted file mode 100644 index 34cebd16524f76cb698e93d3f983ec03dcb9aa4a..0000000000000000000000000000000000000000 --- a/finance/templates/finance/edit.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} - Edit Financial -{% endblock %} - -{% block content %} -<div class="w-full px-5 md:px-10"> - <div class="w-full flex items-center gap-10"> - <a href="/finance"> - {% include 'components/button.html' with variant='btn-primary' text='< Back' %} - </a> - <h1 class="text-xl md:text-3xl font-bold">Edit Financial</h1> - </div> - - <form method="post" class="space-y-4 w-96"> - {% csrf_token %} - {% for field in form %} - <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700"> - {{ field.label }} - </label> - {{ field }} - {% if field.errors %} - <p class="text-red-500 text-xs mt-1">{{ field.errors.0 }}</p> - {% endif %} - {% endfor %} - <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Update</button> - </form> -</div> -{% endblock %} diff --git a/finance/templates/finance/index.html b/finance/templates/finance/index.html deleted file mode 100644 index 25e42d42f7447407e648ec23603cfaf029cbaf19..0000000000000000000000000000000000000000 --- a/finance/templates/finance/index.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} - Daftar Financials -{% endblock %} - -{% block content %} - <section class="w-full px-5 md:px-10"> - <div class="w-full flex items-center justify-between"> - <h1 class="font-bold text-3xl">Daftar Financial</h1> - <a href="/finance/create"> - {% include 'components/button.html' with variant='btn-primary' text='+ Create' %} - </a> - </div> - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-6"> - {% for finance in finance_list %} - <div class="border p-6 rounded-lg shadow-lg bg-white"> - <h2 class="font-bold text-2xl mb-2">{{ finance.nomor_rekening }}</h2> - <table class="text-gray-700 w-full text-sm"> - <tr> - <td class="font-normal">Skor Kredit:</td> - <td>{{ finance.skor_kredit }}</td> - </tr> - <tr> - <td class="font-normal">Nomor Kartu ATM:</td> - <td>{{ finance.nomor_kartu_atm }}</td> - </tr> - <tr> - <td class="font-normal">Status Pekerjaan:</td> - <td>{{ finance.status_pekerjaan }}</td> - </tr> - <tr> - <td class="font-normal">Kode Mata Uang:</td> - <td>{{ finance.kode_mata_uang }}</td> - </tr> - </table> - <div class="flex justify-end mt-4"> - <a href="/finance/{{ finance.id }}/edit" class="mr-2"> - {% include 'components/button.html' with variant='btn-secondary' text='Edit' %} - </a> - <form action="/finance/{{ finance.id }}/delete" method="post" onsubmit="return confirm('Are you sure you want to delete this item?');"> - {% csrf_token %} - {% include 'components/button.html' with variant='btn-danger' text='Delete' %} - </form> - </div> - </div> - {% endfor %} - </div> - </section> -{% endblock %} diff --git a/finance/tests.py b/finance/tests.py deleted file mode 100644 index 7ce503c2dd97ba78597f6ff6e4393132753573f6..0000000000000000000000000000000000000000 --- a/finance/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/finance/urls.py b/finance/urls.py deleted file mode 100644 index 633d33932945c02dabd53aa0f9ef09347f7e831a..0000000000000000000000000000000000000000 --- a/finance/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path -from .views import show_all_finance, show_create, show_edit, delete_finance - -urlpatterns = [ - path('', show_all_finance, name="finance-show-all"), - path('create', show_create, name="finance-create"), - path('<int:pk>/edit', show_edit, name='finance-edit'), - path('<int:pk>/delete', delete_finance, name='finance-delete') -] diff --git a/finance/views.py b/finance/views.py deleted file mode 100644 index 86348a4b649df2123974ccda2ee9f2833776180d..0000000000000000000000000000000000000000 --- a/finance/views.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.shortcuts import render, get_object_or_404 -from django.http import HttpRequest -from .forms import KeuanganForm -from django.shortcuts import redirect -from .models import Keuangan - -# Create your views here. -def show_all_finance(request: HttpRequest): - finance_list = Keuangan.objects.all() - return render(request, 'finance/index.html', {'finance_list': finance_list}) - -def show_create(request: HttpRequest): - if request.method == "POST": - form = KeuanganForm(request.POST) - if form.is_valid(): - form.save() - return redirect('finance-show-all') # Redirect to a success page - else: - form = KeuanganForm() - - return render(request, 'finance/create.html', {'form': form}) - -def show_edit(request, pk): - finance = get_object_or_404(Keuangan, pk=pk) # Fetch the specific record - - if request.method == "POST": - form = KeuanganForm(request.POST, instance=finance) - if form.is_valid(): - form.save() - return redirect('finance-show-all') # Redirect to list after update - else: - form = KeuanganForm(instance=finance) # Populate form with existing data - - return render(request, 'finance/edit.html', {'form': form, 'finance': finance}) - - -def delete_finance(request, pk): - finance = get_object_or_404(Keuangan, pk=pk) - finance.delete() - return redirect('finance-show-all') \ No newline at end of file diff --git a/finance/widgets.py b/finance/widgets.py deleted file mode 100644 index e460c0a4413df76c99193ec3a1bf0c55c1e55257..0000000000000000000000000000000000000000 --- a/finance/widgets.py +++ /dev/null @@ -1,27 +0,0 @@ -from django import forms -from django.template.loader import render_to_string - -class TailwindInput(forms.Widget): - template_name = "components/input.html" - - def __init__(self, input_type="text", attrs=None): - self.input_type = input_type - super().__init__(attrs) - - def get_context(self, name, value, attrs): - attrs = attrs or {} - context = { - "class": attrs.get("class", "mb-4"), - "id": attrs.get("id", f"id_{name}"), - "label": attrs.get("label", name.replace("_", " ").upper()), - "type": self.input_type, - "name": name, - "value": value or "", - "placeholder": attrs.get("placeholder", ""), - "step": attrs.get("step", ""), - } - return context - - def render(self, name, value, attrs=None, renderer=None): - context = self.get_context(name, value, attrs) - return render_to_string(self.template_name, context) diff --git a/main/settings.py b/main/settings.py index 69b20726cb2a3c24a6143165817a82ef6ceab9e7..baace995428d699591762357009a16be5dd134c4 100644 --- a/main/settings.py +++ b/main/settings.py @@ -16,7 +16,6 @@ import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -43,8 +42,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'finance', - 'education', + 'user' ] MIDDLEWARE = [ @@ -142,4 +140,6 @@ STATICFILES_DIRS = [ ] -STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') \ No newline at end of file +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') + +AUTH_USER_MODEL = 'user.ExtendedUser' \ No newline at end of file diff --git a/main/urls.py b/main/urls.py index c3e770cab592fb6f4cc40279d968e8009a0c012b..8894dec36a9a317a2e258cb522064e0a35cfbaa9 100644 --- a/main/urls.py +++ b/main/urls.py @@ -21,6 +21,5 @@ from .views import show_home urlpatterns = [ path('admin/', admin.site.urls), path("", show_home, name="home"), - path('education/', include('education.urls')), - path('finance/', include('finance.urls')), + path('user/', include('user.urls')), ] diff --git a/main/views.py b/main/views.py index 61ecf032dd532b2cb3a501d79ddd9c19f080754c..a0b4e30d57681e418ab1034db02e0b5012fa8017 100644 --- a/main/views.py +++ b/main/views.py @@ -2,4 +2,6 @@ from django.http import HttpResponse, HttpRequest from django.shortcuts import render def show_home(request: HttpRequest) -> HttpResponse: - return render(request, 'index.html') \ No newline at end of file + return render(request, 'index.html', context={ + "isLoggedIn": request.user.is_authenticated, + }) \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index de17676e1df112db9140d619fcad2be621e89672..05a0c58436010ae579663265fbe3bb2271de8088 100644 --- a/templates/base.html +++ b/templates/base.html @@ -22,14 +22,13 @@ <body> {% include 'components/navbar.html' %} - <div class="container mx-auto pt-32 min-h-screen"> + <div class="container mx-auto pt-32 min-h-screen flex flex-col justify-between"> {% block content %} - <!-- Child templates will insert content here --> {% endblock %} + <footer class="bg-gray-100 text-center py-3 mt-4"> + <p>© 2025 Andrew Devito Aryo - 2306152494</p> + </footer> </div> - <footer class="bg-gray-100 text-center py-3 mt-4"> - <p>© 2025 Andrew Devito Aryo - 2306152494</p> - </footer> </body> </html> diff --git a/templates/components/button.html b/templates/components/button.html index c21a0dc00b1d3a85c0e46d90381336c818f69c3a..b6b53d618c36a2c7692171e29ee0f973d647e335 100644 --- a/templates/components/button.html +++ b/templates/components/button.html @@ -39,7 +39,9 @@ } </style> -<button class="btn {% if variant %}{{ variant }}{% else %}btn-primary{% endif %}"> +<button + type="{{type}}" + class="btn {% if variant %}{{ variant }}{% else %}btn-primary{% endif %} w-full"> {% if text %} {{ text }} {% else %} diff --git a/templates/components/input.html b/templates/components/input.html index 8ad7dd0f6f1b2e198f5bb02c34d65c2fef0978c3..82e77eb721737d7405ee3d7a5f7567788e7c1eb9 100644 --- a/templates/components/input.html +++ b/templates/components/input.html @@ -1,4 +1,4 @@ <div class="{{ class }}"> <label for="{{ id }}" class="block text-sm font-medium text-gray-700">{{ label }}</label> - <input type="{{ type }}" id="{{ id }}" name="{{ name }}" value="{{ value }}" placeholder="{{ placeholder }}" step="0.01" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"> + <input type="{{ type }}" id="{{ id }}" name="{{ name }}" value="{{ value }}" placeholder="{{ placeholder }}" step="0.01" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" {% if required %}required{% endif %}> </div> diff --git a/templates/components/navbar.html b/templates/components/navbar.html index 1df92ecd208f9deabe742f211705aa569c972f2c..759f19c4abf9827449a3fe16269595591efca076 100644 --- a/templates/components/navbar.html +++ b/templates/components/navbar.html @@ -1,33 +1,5 @@ -<nav class="bg-gray-100 px-5 md:px-10 fixed w-full top-0 py-5 "> - <div class="container flex items-center justify-between"> - <a class="text-xl font-bold" href="#">El Pekape</a> - <div class="lg:flex lg:items-center lg:w-auto hidden transition-all duration-300 ease-in-out" id="navbarNav"> - <ul class="lg:flex lg:justify-between text-sm"> - <li class="nav-item"> - <a class="block mt-4 lg:inline-block lg:mt-0 text-gray-700 hover:text-gray-900 mr-4" href="/">Home</a> - </li> - <li class="nav-item"> - <a class="block mt-4 lg:inline-block lg:mt-0 text-gray-700 hover:text-gray-900 mr-4" href="/education">Education</a> - </li> - <li class="nav-item"> - <a class="block mt-4 lg:inline-block lg:mt-0 text-gray-700 hover:text-gray-900 mr-4" href="/finance">Finance</a> - </li> - </ul> - </div> - <button class="block lg:hidden px-3 py-2 border rounded text-gray-700 border-gray-700" id="navbar-toggler"> - <svg class="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> - <title>Menu</title> - <path d="M0 3h20v2H0zM0 9h20v2H0zM0 15h20v2H0z"/> - </svg> - </button> - - </div> -</nav> - -<script> - document.getElementById('navbar-toggler').addEventListener('click', function() { - var nav = document.getElementById('navbarNav'); - nav.classList.toggle('hidden'); - nav.classList.toggle('block'); - }); -</script> \ No newline at end of file +<nav class="bg-gray-100 px-5 md:px-10 fixed w-full top-0 py-5"> + <div class="container flex items-center justify-between"> + <a class="text-xl font-bold" href="#">El Pekape</a> + </div> +</nav> \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 131febb18a197e1630e7f0ea6a0a983fa6a40134..150f3bac8ec28ee35087f5eb11b45d5fe2a88126 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,21 +2,21 @@ {% load static %} {% block title %} - Home + Home {% endblock %} {% block content %} - <div class="h-screen flex items-center justify-center flex-col gap-4"> - <img src="{% static 'images/logo.png' %}" alt="Logo" /> - <h1 class="font-bold text-2xl">Welcome to the Home Page</h1> - <p>This is a simple home page for PKPL HW 06.</p> - <div class=""> - <a href="/education"> - {% include 'components/button.html' with variant='btn-primary' text='Education' %} - </a> - <a href="/finance"> - {% include 'components/button.html' with variant='btn-primary' text='Financial' %} - </a> - </div> + <div class="h-full flex items-center justify-center flex-col gap-4"> + <img src="{% static 'images/logo.png' %}" alt="Logo" /> + <h1 class="font-bold text-2xl">Welcome to the Home Page</h1> + <p>This is a simple home page for PKPL Week 06.</p> + <div class="flex gap-2 items-center"> + {% if isLoggedIn %} + <a href="{% url 'logout' %}">{% include 'components/button.html' with variant='btn-primary' text='Logout' %}</a> + {% else %} + <a href="/user/login">{% include 'components/button.html' with variant='btn-primary' text='Login' %}</a> + <a href="/user/register">{% include 'components/button.html' with variant='btn-primary' text='Register' %}</a> + {% endif %} </div> + </div> {% endblock %} diff --git a/education/__init__.py b/user/__init__.py similarity index 100% rename from education/__init__.py rename to user/__init__.py diff --git a/user/admin.py b/user/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..c0dfc75bacac515ff8929be71f9c59bc20fd3c95 --- /dev/null +++ b/user/admin.py @@ -0,0 +1,30 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import ExtendedUser + +class ExtendedUserAdmin(UserAdmin): + # Define the fields to display in the admin list view + list_display = ("username", "email", "fullname", "kategori_produk", "is_staff", "is_active") + search_fields = ("username", "email", "fullname", "nomor_hp") + list_filter = ("is_staff", "is_active", "kategori_produk") + + # Define the fieldsets for updating users in the admin panel + fieldsets = ( + (None, {"fields": ("username", "email", "password")}), + ("Personal Info", {"fields": ("fullname", "tanggal_lahir", "nomor_hp", "url_blog", "deskripsi")}), + ("Permissions", {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}), + ("Additional Info", {"fields": ("id_penjual", "kategori_produk")}), + ) + + # Define the fieldsets for creating a new user + add_fieldsets = ( + (None, { + "classes": ("wide",), + "fields": ("username", "email", "password1", "password2", "fullname", "kategori_produk"), + }), + ) + + ordering = ("username",) + +# Register the custom user model +admin.site.register(ExtendedUser, ExtendedUserAdmin) diff --git a/finance/apps.py b/user/apps.py similarity index 63% rename from finance/apps.py rename to user/apps.py index 53023d8d4b860147463f5cfe2541c8f35ddbc344..36cce4c8e4895e7c9d84a21bae83a0731e8f2058 100644 --- a/finance/apps.py +++ b/user/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class FinanceConfig(AppConfig): +class UserConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'finance' + name = 'user' diff --git a/user/forms.py b/user/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..f4471b2ed3d6536a9948cdc6c753c1adb998b91e --- /dev/null +++ b/user/forms.py @@ -0,0 +1,49 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm +from .models import ExtendedUser +from django.contrib.auth.forms import AuthenticationForm +from django.forms.widgets import PasswordInput, TextInput + + +class ExtendedUserForm(UserCreationForm): + class Meta: + model = ExtendedUser + fields = [ + "username", + "email", + "password1", + "password2", + "fullname", + "tanggal_lahir", + "nomor_hp", + "url_blog", + "deskripsi", + "id_penjual", + "kategori_produk", + ] + + labels = { + "username": "Username", + "email": "Email", + "password1": "Password", + "password2": "Confirm Password", + "fullname": "Full Name", + "tanggal_lahir": "Date of Birth", + "nomor_hp": "Phone Number", + "url_blog": "Blog URL", + "deskripsi": "Description", + "id_penjual": "Seller ID", + "kategori_produk": "Product Category", + } + + tanggal_lahir = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'})) + +class ExtendedUserLoginForm(AuthenticationForm): + username = forms.CharField( + widget=TextInput(attrs={"class": "w-full px-3 py-2 border rounded-md", "placeholder": "Username"}), + label="Username" + ) + password = forms.CharField( + widget=PasswordInput(attrs={"class": "w-full px-3 py-2 border rounded-md", "placeholder": "Password"}), + label="Password" + ) diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..9eed135f078397aad54061e08e4f4a2b94bb74c8 --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 5.1.7 on 2025-03-17 06:16 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='ExtendedUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('fullname', models.CharField(max_length=100)), + ('tanggal_lahir', models.DateField()), + ('nomor_hp', models.CharField(max_length=15)), + ('url_blog', models.CharField(max_length=255)), + ('deskripsi', models.TextField()), + ('id_penjual', models.IntegerField()), + ('kategori_produk', models.CharField(choices=[('Elektronik', 'Elektronik'), ('Fashion', 'Fashion'), ('Furnitur', 'Furnitur'), ('Obat', 'Obat'), ('Kecantikan', 'Kecantikan')], max_length=50)), + ('groups', models.ManyToManyField(blank=True, related_name='extendeduser_groups', to='auth.group')), + ('user_permissions', models.ManyToManyField(blank=True, related_name='extendeduser_permissions', to='auth.permission')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/education/migrations/__init__.py b/user/migrations/__init__.py similarity index 100% rename from education/migrations/__init__.py rename to user/migrations/__init__.py diff --git a/user/models.py b/user/models.py new file mode 100644 index 0000000000000000000000000000000000000000..345c1b189db885f196e0e3d2596b237af08139d3 --- /dev/null +++ b/user/models.py @@ -0,0 +1,88 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.core.validators import RegexValidator, MinLengthValidator, MaxLengthValidator, EmailValidator, URLValidator +from django.utils import timezone +from django.core.exceptions import ValidationError + +class ExtendedUser(AbstractUser): + enum_kategori = { + 'Elektronik': 'Elektronik', + 'Fashion': 'Fashion', + 'Furnitur': 'Furnitur', + 'Obat': 'Obat', + 'Kecantikan': 'Kecantikan' + } + + fullname = models.CharField( + max_length=255, + validators=[ + RegexValidator( + regex=r'^[a-zA-Z0-9._-]+$', + message='Nama hanya boleh berisi huruf, angka, dan karakter (., _, -).' + ) + ] + ) + password = models.CharField( + max_length=255, + validators=[ + MinLengthValidator(8), + RegexValidator( + regex=r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$', + message='Password harus berisi huruf, angka, dan karakter spesial dengan panjang minimal 8.' + ) + ] + ) + tanggal_lahir = models.DateField( + validators=[ + RegexValidator( + regex=r'^\d{4}-\d{2}-\d{2}$', + message='Tanggal lahir harus dalam format YYYY-MM-DD.' + ) + ] + ) + nomor_hp = models.CharField( + max_length=15, + validators=[ + RegexValidator( + regex=r'^62\d{6,13}$', + message='Nomor HP harus dalam format (kode negara - nomor telepon) dengan panjang minimal 8 dan maksimal 15. Contoh: 62123456, bukan +62123456 dan 62-12345.' + ) + ] + ) + email = models.CharField( + max_length=255, + validators=[EmailValidator(message='Email harus sesuai dengan format email.')] + ) + url_blog = models.CharField( + max_length=255, + validators=[URLValidator(message='URL Blog harus sesuai dengan format URL.')] + ) + deskripsi = models.TextField( + validators=[ + MinLengthValidator(5), + MaxLengthValidator(1000) + ] + ) + id_penjual = models.IntegerField() + kategori_produk = models.CharField( + max_length=50, + choices=[(tag, tag) for tag in enum_kategori.keys()] + ) + + groups = models.ManyToManyField( + "auth.Group", + related_name="extendeduser_groups", # Custom related_name + blank=True + ) + user_permissions = models.ManyToManyField( + "auth.Permission", + related_name="extendeduser_permissions", # Custom related_name + blank=True + ) + + def clean(self): + super().clean() + if self.tanggal_lahir: + age = (timezone.now().date() - self.tanggal_lahir).days / 365.25 + if age < 12: + raise ValidationError('Usia pengguna minimal 12 tahun.') \ No newline at end of file diff --git a/user/templates/login.html b/user/templates/login.html new file mode 100644 index 0000000000000000000000000000000000000000..81fa9c3714916a9010503f60b30bf13453c564c1 --- /dev/null +++ b/user/templates/login.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} +{% block title %} + Login Page +{% endblock %} + +{% block content %} + <div class="p-10"> + <form method="post" class="space-y-4"> + {% csrf_token %} + {% include 'components/input.html' with id='username' name='username' label='Username' required='true' %} + {% include 'components/input.html' with id='password' name='password' label='Password' required='true' type='password' %} + + {% if messages %} + {% for message in messages %} + <p class="text-xs text-red-700">* {{ message }}</p> + {% endfor %} + {% endif %} + + {% include 'components/button.html' with text='Login' type='submit' %} + </form> + </div> +{% endblock %} diff --git a/user/templates/register.html b/user/templates/register.html new file mode 100644 index 0000000000000000000000000000000000000000..275f2de0f462dbba599882c33e6c750a75053ddf --- /dev/null +++ b/user/templates/register.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block title %} + Register Page +{% endblock %} + +{% block content %} + <style> + input, + textarea, + select { + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; + width: 100%; + margin-top: 5px; + } + </style> + <div class="max-w-md mx-auto mt-10 bg-white p-8 rounded-lg shadow-md"> + <h2 class="text-2xl font-bold mb-6 text-center text-gray-800">Register</h2> + <form method="post" class="space-y-4"> + {% csrf_token %} + + {% for field in form %} + <div> + <label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700">{{ field.label }}</label> + {{ field }} + + {% if field.errors %} + <p class="text-sm text-red-500 mt-1">{{ field.errors.0 }}</p> + {% endif %} + </div> + {% endfor %} + + {% include 'components/button.html' with type='submit' text='Register' %} + </form> + </div> +{% endblock %} diff --git a/education/tests.py b/user/tests.py similarity index 100% rename from education/tests.py rename to user/tests.py diff --git a/user/urls.py b/user/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..5264dc3a778451fb6d860c15309a4f85205f76d5 --- /dev/null +++ b/user/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .views import login_view, register_view, logout_view + +urlpatterns = [ + path("login/", login_view, name="login"), + path("register/", register_view, name="register"), + path("logout/", logout_view, name="logout"), +] diff --git a/user/views.py b/user/views.py new file mode 100644 index 0000000000000000000000000000000000000000..fc9118dbdd2de9fc1449c0e69b28b627ed04d2fa --- /dev/null +++ b/user/views.py @@ -0,0 +1,51 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import authenticate, login, logout +from django.contrib import messages +from .forms import ExtendedUserLoginForm + +from .forms import ExtendedUserForm +from django.contrib import messages + +def login_view(request): + if request.user.is_authenticated: + return redirect("home") # Change "home" to your desired redirect URL + + if request.method == "POST": + form = ExtendedUserLoginForm(request, data=request.POST) + + print(form.errors) + + if form.is_valid(): + username = form.cleaned_data["username"] + password = form.cleaned_data["password"] + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + return redirect("home") # Redirect to home/dashboard + else: + messages.error(request, "Invalid username or password") + else: + messages.error(request, "Invalid form submission") + + else: + form = ExtendedUserLoginForm() + + return render(request, "login.html", {"form": form}) + +def register_view(request): + if request.method == "POST": + form = ExtendedUserForm(request.POST) + if form.is_valid() : + form.save() + messages.success(request, "Account created successfully. Please log in.") + return redirect("login") # Redirect to login page + else: + messages.error(request, "Please correct the errors below.") + else: + form = ExtendedUserForm() + + return render(request, "register.html", {"form": form}) + +def logout_view(request): + logout(request) + return redirect("home") \ No newline at end of file diff --git a/education/widgets.py b/user/widgets.py similarity index 100% rename from education/widgets.py rename to user/widgets.py