diff --git a/assets/pasted-20260220-193237-23a07b2b.png b/assets/pasted-20260220-193237-23a07b2b.png new file mode 100644 index 0000000..869309f Binary files /dev/null and b/assets/pasted-20260220-193237-23a07b2b.png differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 881731c..30014fd 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 42d995d..d36b34c 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..fef83d9 100644 --- a/config/settings.py +++ b/config/settings.py @@ -155,6 +155,10 @@ STATICFILES_DIRS = [ BASE_DIR / 'node_modules', ] +# Media files (User-uploaded files) +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + # Email EMAIL_BACKEND = os.getenv( "EMAIL_BACKEND", @@ -179,4 +183,4 @@ if EMAIL_USE_SSL: # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file diff --git a/config/urls.py b/config/urls.py index bcfc074..cccd18f 100644 --- a/config/urls.py +++ b/config/urls.py @@ -27,3 +27,4 @@ urlpatterns = [ if settings.DEBUG: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 2964e11..94d4d0e 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 18a063c..50f7e00 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/pexels.cpython-311.pyc b/core/__pycache__/pexels.cpython-311.pyc new file mode 100644 index 0000000..6244dee Binary files /dev/null and b/core/__pycache__/pexels.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ebb8c6e..8b31aba 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 8d204fa..0214b21 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..28679e5 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,67 @@ from django.contrib import admin +from .models import Category, Vendor, Product -# Register your models here. +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + prepopulated_fields = {'slug': ('name',)} + +@admin.register(Vendor) +class VendorAdmin(admin.ModelAdmin): + list_display = ('name', 'whatsapp_number', 'created_at') + prepopulated_fields = {'slug': ('name',)} + search_fields = ('name', 'whatsapp_number') + + def get_queryset(self, request): + qs = super().get_queryset(request) + if request.user.is_superuser: + return qs + return qs.filter(user=request.user) + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ('name', 'vendor', 'category', 'price', 'stock', 'is_active', 'created_at') + list_filter = ('vendor', 'category', 'is_active') + prepopulated_fields = {'slug': ('name',)} + search_fields = ('name', 'vendor__name', 'category__name') + + def get_queryset(self, request): + qs = super().get_queryset(request) + if request.user.is_superuser: + return qs + # Filter products by the vendor linked to the user + try: + vendor = request.user.vendor_profile + return qs.filter(vendor=vendor) + except: + return qs.none() + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + if not request.user.is_superuser: + # If a vendor is logged in, hide the 'vendor' field from the form + if 'vendor' in form.base_fields: + form.base_fields['vendor'].required = False + # Remove it from the displayed fields if it's there + # However, for simplicity, we'll just handle it in save_model + return form + + def save_model(self, request, obj, form, change): + if not request.user.is_superuser: + # Automatically set the vendor if it's not set and the user is a vendor + try: + obj.vendor = request.user.vendor_profile + except: + pass + super().save_model(request, obj, form, change) + + def get_fieldsets(self, request, obj=None): + fieldsets = super().get_fieldsets(request, obj) + if not request.user.is_superuser: + # Exclude vendor field from the form for vendors + for fieldset in fieldsets: + if 'vendor' in fieldset[1]['fields']: + # Create a new tuple without 'vendor' + new_fields = tuple(f for f in fieldset[1]['fields'] if f != 'vendor') + fieldset[1]['fields'] = new_fields + return fieldsets diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5941dd6 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..4c04dcc Binary files /dev/null and b/core/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/create_vendor_accounts.cpython-311.pyc b/core/management/commands/__pycache__/create_vendor_accounts.cpython-311.pyc new file mode 100644 index 0000000..c2cc51c Binary files /dev/null and b/core/management/commands/__pycache__/create_vendor_accounts.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/seed_data.cpython-311.pyc b/core/management/commands/__pycache__/seed_data.cpython-311.pyc new file mode 100644 index 0000000..7a895f9 Binary files /dev/null and b/core/management/commands/__pycache__/seed_data.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/update_images.cpython-311.pyc b/core/management/commands/__pycache__/update_images.cpython-311.pyc new file mode 100644 index 0000000..ff9f868 Binary files /dev/null and b/core/management/commands/__pycache__/update_images.cpython-311.pyc differ diff --git a/core/management/commands/create_vendor_accounts.py b/core/management/commands/create_vendor_accounts.py new file mode 100644 index 0000000..203a5bc --- /dev/null +++ b/core/management/commands/create_vendor_accounts.py @@ -0,0 +1,50 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User, Permission +from django.contrib.contenttypes.models import ContentType +from core.models import Vendor, Product, Category + +class Command(BaseCommand): + help = 'Create user accounts for existing vendors and grant permissions' + + def handle(self, *args, **kwargs): + vendors = Vendor.objects.all() + password = 'vendorpass' + + # Get permissions + models = [Vendor, Product, Category] + permissions = [] + for model in models: + content_type = ContentType.objects.get_for_model(model) + permissions.extend(Permission.objects.filter(content_type=content_type)) + + for vendor in vendors: + if not vendor.user: + username = vendor.slug.replace('-', '_') + + # Ensure unique username + base_username = username + counter = 1 + while User.objects.filter(username=username).exists(): + username = f"{base_username}{counter}" + counter += 1 + + user = User.objects.create_user( + username=username, + email='', + password=password, + is_staff=True + ) + + vendor.user = user + vendor.save() + self.stdout.write(self.style.SUCCESS(f'Created user "{username}" for vendor "{vendor.name}"')) + else: + user = vendor.user + user.is_staff = True + user.save() + + # Grant permissions + user.user_permissions.add(*permissions) + self.stdout.write(self.style.SUCCESS(f'Granted permissions to user "{user.username}"')) + + self.stdout.write(self.style.SUCCESS('Successfully processed vendor accounts. Default password: vendorpass')) \ No newline at end of file diff --git a/core/management/commands/seed_data.py b/core/management/commands/seed_data.py new file mode 100644 index 0000000..2047306 --- /dev/null +++ b/core/management/commands/seed_data.py @@ -0,0 +1,124 @@ +from django.core.management.base import BaseCommand +from core.models import Category, Vendor, Product +from django.utils.text import slugify + +class Command(BaseCommand): + help = 'Seeds initial data for Pasar UMKM Pemagarsari' + + def handle(self, *args, **options): + # Create Categories + categories_data = [ + {'name': 'Makanan', 'icon': 'fa-utensils'}, + {'name': 'Minuman', 'icon': 'fa-coffee'}, + {'name': 'Kerajinan', 'icon': 'fa-palette'}, + {'name': 'Pakaian', 'icon': 'fa-tshirt'}, + {'name': 'Jasa', 'icon': 'fa-tools'}, + {'name': 'Pertanian', 'icon': 'fa-leaf'}, + ] + + cats = {} + for cat in categories_data: + obj, created = Category.objects.get_or_create( + name=cat['name'], + defaults={'icon': cat['icon']} + ) + cats[cat['name']] = obj + if created: + self.stdout.write(self.style.SUCCESS(f'Created category: {cat["name"]}')) + + # Create Vendors + vendors_data = [ + { + 'name': 'Dapur Bu Siti', + 'whatsapp_number': '628123456789', + 'description': 'Spesialis kue basah dan masakan tradisional khas Pemagarsari.', + 'address': 'RT 01 RW 02, Desa Pemagarsari' + }, + { + 'name': 'Kerajinan Bambu Jaya', + 'whatsapp_number': '628987654321', + 'description': 'Produk anyaman bambu berkualitas tinggi untuk dekorasi rumah.', + 'address': 'RT 05 RW 01, Desa Pemagarsari' + }, + { + 'name': 'Kopi Desa Kita', + 'whatsapp_number': '628555444333', + 'description': 'Kopi asli hasil perkebunan Desa Pemagarsari yang diproses secara tradisional.', + 'address': 'RT 03 RW 03, Desa Pemagarsari' + } + ] + + vens = {} + for ven in vendors_data: + obj, created = Vendor.objects.get_or_create( + name=ven['name'], + defaults={ + 'whatsapp_number': ven['whatsapp_number'], + 'description': ven['description'], + 'address': ven['address'] + } + ) + vens[ven['name']] = obj + if created: + self.stdout.write(self.style.SUCCESS(f'Created vendor: {ven["name"]}')) + + # Create Products + products_data = [ + { + 'name': 'Kue Klepon Lumer', + 'vendor': vens['Dapur Bu Siti'], + 'category': cats['Makanan'], + 'price': 15000, + 'description': 'Klepon dengan gula merah cair yang melimpah dan taburan kelapa gurih. Isi 10 pcs.' + }, + { + 'name': 'Nasi Liwet Komplit', + 'vendor': vens['Dapur Bu Siti'], + 'category': cats['Makanan'], + 'price': 25000, + 'description': 'Nasi liwet dengan lauk ayam goreng, tahu tempe, sambal, dan lalapan.' + }, + { + 'name': 'Tas Anyaman Bambu', + 'vendor': vens['Kerajinan Bambu Jaya'], + 'category': cats['Kerajinan'], + 'price': 85000, + 'description': 'Tas jinjing elegan hasil anyaman tangan pengrajin lokal. Kuat dan ramah lingkungan.' + }, + { + 'name': 'Wadah Serbaguna', + 'vendor': vens['Kerajinan Bambu Jaya'], + 'category': cats['Kerajinan'], + 'price': 45000, + 'description': 'Satu set wadah serbaguna untuk keperluan dapur atau dekorasi.' + }, + { + 'name': 'Kopi Robusta Pemagarsari (250g)', + 'vendor': vens['Kopi Desa Kita'], + 'category': cats['Minuman'], + 'price': 35000, + 'description': 'Bubuk kopi robusta pilihan dengan aroma yang kuat dan rasa yang mantap.' + }, + { + 'name': 'Gula Semut Aren', + 'vendor': vens['Kopi Desa Kita'], + 'category': cats['Minuman'], + 'price': 20000, + 'description': 'Gula aren kualitas premium, sangat cocok untuk pemanis kopi atau minuman lainnya.' + } + ] + + for prod in products_data: + obj, created = Product.objects.get_or_create( + name=prod['name'], + vendor=prod['vendor'], + defaults={ + 'category': prod['category'], + 'price': prod['price'], + 'description': prod['description'] + } + ) + if created: + self.stdout.write(self.style.SUCCESS(f'Created product: {prod["name"]}')) + + self.stdout.write(self.style.SUCCESS('Successfully seeded initial data.')) diff --git a/core/management/commands/update_images.py b/core/management/commands/update_images.py new file mode 100644 index 0000000..6c54de3 --- /dev/null +++ b/core/management/commands/update_images.py @@ -0,0 +1,36 @@ +from django.core.management.base import BaseCommand +from core.models import Vendor, Product +from core.pexels import fetch_first +import os + +class Command(BaseCommand): + help = 'Fetches sample images from Pexels for existing products and vendors' + + def handle(self, *args, **options): + self.stdout.write("Fetching images for vendors...") + for vendor in Vendor.objects.all(): + if not vendor.logo: + self.stdout.write(f"Fetching logo for vendor: {vendor.name}") + result = fetch_first(f"shop logo {vendor.name}", orientation="square") + if result: + vendor.logo = result['local_path'] + vendor.save() + self.stdout.write(self.style.SUCCESS(f"Updated logo for {vendor.name}")) + else: + self.stdout.write(self.style.WARNING(f"Could not fetch logo for {vendor.name}")) + + self.stdout.write("Fetching images for products...") + for product in Product.objects.all(): + if not product.image: + self.stdout.write(f"Fetching image for product: {product.name}") + # Use a specific query based on the product name and category + query = f"{product.category.name} {product.name}" + result = fetch_first(query, orientation="landscape") + if result: + product.image = result['local_path'] + product.save() + self.stdout.write(self.style.SUCCESS(f"Updated image for {product.name}")) + else: + self.stdout.write(self.style.WARNING(f"Could not fetch image for {product.name}")) + + self.stdout.write(self.style.SUCCESS('Successfully updated all missing images.')) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..ed3d47a --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2.7 on 2026-02-20 18:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(blank=True, unique=True)), + ('icon', models.CharField(blank=True, help_text='FontAwesome icon class', max_length=50)), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( + name='Vendor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('slug', models.SlugField(blank=True, unique=True)), + ('description', models.TextField(blank=True)), + ('whatsapp_number', models.CharField(help_text='Format: 628123456789', max_length=20)), + ('address', models.TextField(blank=True)), + ('logo', models.ImageField(blank=True, null=True, upload_to='vendors/')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('slug', models.SlugField(blank=True, unique=True)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=12)), + ('image', models.ImageField(blank=True, null=True, upload_to='products/')), + ('stock', models.PositiveIntegerField(default=1)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.category')), + ('vendor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.vendor')), + ], + ), + ] diff --git a/core/migrations/0002_vendor_user.py b/core/migrations/0002_vendor_user.py new file mode 100644 index 0000000..a6635ca --- /dev/null +++ b/core/migrations/0002_vendor_user.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2026-02-20 19:03 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='vendor', + name='user', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vendor_profile', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..517b6c5 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_vendor_user.cpython-311.pyc b/core/migrations/__pycache__/0002_vendor_user.cpython-311.pyc new file mode 100644 index 0000000..0f2d807 Binary files /dev/null and b/core/migrations/__pycache__/0002_vendor_user.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..9c7984d 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,64 @@ from django.db import models +from django.urls import reverse +from django.utils.text import slugify +from django.contrib.auth.models import User -# Create your models here. +class Category(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True, blank=True) + icon = models.CharField(max_length=50, blank=True, help_text="FontAwesome icon class") + + class Meta: + verbose_name_plural = "Categories" + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def __str__(self): + return self.name + +class Vendor(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='vendor_profile', null=True, blank=True) + name = models.CharField(max_length=200) + slug = models.SlugField(unique=True, blank=True) + description = models.TextField(blank=True) + whatsapp_number = models.CharField(max_length=20, help_text="Format: 628123456789") + address = models.TextField(blank=True) + logo = models.ImageField(upload_to='vendors/', blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def __str__(self): + return self.name + +class Product(models.Model): + vendor = models.ForeignKey(Vendor, on_delete=models.CASCADE, related_name='products') + category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='products') + name = models.CharField(max_length=255) + slug = models.SlugField(unique=True, blank=True) + description = models.TextField() + price = models.DecimalField(max_digits=12, decimal_places=2) + image = models.ImageField(upload_to='products/', blank=True, null=True) + stock = models.PositiveIntegerField(default=1) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def __str__(self): + return self.name + + def get_whatsapp_url(self): + message = f"Halo {self.vendor.name}, saya tertarik membeli {self.name} seharga Rp {self.price:,.0f}. Apakah stok masih ada?" + import urllib.parse + encoded_message = urllib.parse.quote(message) + return f"https://wa.me/{self.vendor.whatsapp_number}?text={encoded_message}" \ No newline at end of file diff --git a/core/pexels.py b/core/pexels.py new file mode 100644 index 0000000..588b95c --- /dev/null +++ b/core/pexels.py @@ -0,0 +1,44 @@ +import os +from pathlib import Path +import httpx + +API_KEY = os.getenv("PEXELS_KEY", "Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18") +CACHE_DIR = Path("media/pexels") + +def _client(): + return httpx.Client( + base_url="https://api.pexels.com/v1/", + headers={"Authorization": API_KEY}, + timeout=15, + ) + +def fetch_first(query: str, orientation: str = "portrait") -> dict | None: + if not API_KEY: + return None + try: + with _client() as client: + resp = client.get( + "search", + params={"query": query, "orientation": orientation, "per_page": 1, "page": 1}, + ) + resp.raise_for_status() + data = resp.json() + photo = (data.get("photos") or [None])[0] + if not photo: + return None + src = photo["src"].get("large2x") or photo["src"].get("large") or photo["src"].get("original") + CACHE_DIR.mkdir(parents=True, exist_ok=True) + target = CACHE_DIR / f"{photo['id']}.jpg" + if src: + img = httpx.get(src, timeout=15) + img.raise_for_status() + target.write_bytes(img.content) + return { + "id": photo["id"], + "local_path": f"pexels/{photo['id']}.jpg", + "photographer": photo.get("photographer"), + "photographer_url": photo.get("photographer_url"), + } + except Exception as e: + print(f"Error fetching from Pexels: {e}") + return None diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..196857c 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,103 @@ +{% load static %} - - +
- -