Autosave: 20260205-172908
This commit is contained in:
parent
62fa70915a
commit
81ab8ae93e
Binary file not shown.
Binary file not shown.
@ -2,21 +2,42 @@
|
||||
Django settings for config project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.7.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django.conf.locale
|
||||
import django.utils.translation
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
load_dotenv(BASE_DIR.parent / ".env")
|
||||
|
||||
# Oromo and Amharic are not in Django's default LANG_INFO
|
||||
EXTRA_LANG_INFO = {
|
||||
'om': {
|
||||
'bidi': False,
|
||||
'code': 'om',
|
||||
'name': 'Oromo',
|
||||
'name_local': 'Afaan Oromoo',
|
||||
},
|
||||
'am': {
|
||||
'bidi': False,
|
||||
'code': 'am',
|
||||
'name': 'Amharic',
|
||||
'name_local': 'አማርኛ',
|
||||
},
|
||||
}
|
||||
|
||||
# Add custom languages to LANG_INFO
|
||||
for code, info in EXTRA_LANG_INFO.items():
|
||||
if code not in django.conf.locale.LANG_INFO:
|
||||
django.conf.locale.LANG_INFO[code] = info
|
||||
# Also update the reference in django.utils.translation if it exists
|
||||
if hasattr(django.utils.translation, 'LANG_INFO') and code not in django.utils.translation.LANG_INFO:
|
||||
django.utils.translation.LANG_INFO[code] = info
|
||||
|
||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
|
||||
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
||||
|
||||
@ -37,18 +58,17 @@ CSRF_TRUSTED_ORIGINS = [
|
||||
for host in CSRF_TRUSTED_ORIGINS
|
||||
]
|
||||
|
||||
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SAMESITE = "None"
|
||||
CSRF_COOKIE_SAMESITE = "None"
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
X_FRAME_OPTIONS = 'ALLOWALL'
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'modeltranslation', # Must be before admin
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
@ -61,16 +81,13 @@ INSTALLED_APPS = [
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
|
||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
X_FRAME_OPTIONS = 'ALLOWALL'
|
||||
|
||||
ROOT_URLCONF = 'config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
@ -83,7 +100,7 @@ TEMPLATES = [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
||||
'django.template.context_processors.i18n',
|
||||
'core.context_processors.project_context',
|
||||
],
|
||||
},
|
||||
@ -92,10 +109,6 @@ TEMPLATES = [
|
||||
|
||||
WSGI_APPLICATION = 'config.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
@ -110,56 +123,46 @@ DATABASES = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
LANGUAGE_CODE = 'en'
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = 'Africa/Addis_Ababa'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = [
|
||||
('en', _('English')),
|
||||
('am', _('Amharic')),
|
||||
('om', _('Afaan Oromoo')),
|
||||
]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
MODELTRANSLATION_DEFAULT_LANGUAGE = 'en'
|
||||
|
||||
LOCALE_PATHS = [
|
||||
BASE_DIR / 'locale',
|
||||
]
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'static',
|
||||
BASE_DIR / 'assets',
|
||||
BASE_DIR / 'node_modules',
|
||||
]
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND = os.getenv(
|
||||
"EMAIL_BACKEND",
|
||||
"django.core.mail.backends.smtp.EmailBackend"
|
||||
)
|
||||
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1")
|
||||
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
|
||||
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
|
||||
@ -167,16 +170,9 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
|
||||
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true"
|
||||
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true"
|
||||
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com")
|
||||
CONTACT_EMAIL_TO = [
|
||||
item.strip()
|
||||
for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",")
|
||||
if item.strip()
|
||||
]
|
||||
CONTACT_EMAIL_TO = [item.strip() for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",") if item.strip()]
|
||||
|
||||
# When both TLS and SSL flags are enabled, prefer SSL explicitly
|
||||
if EMAIL_USE_SSL:
|
||||
EMAIL_USE_TLS = False
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
@ -1,29 +1,18 @@
|
||||
"""
|
||||
URL configuration for config project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("core.urls")),
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
]
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
path('admin/', admin.site.py_urls if hasattr(admin.site, 'py_urls') else admin.site.urls),
|
||||
path('', include('core.urls')),
|
||||
)
|
||||
|
||||
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)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/translation.cpython-311.pyc
Normal file
BIN
core/__pycache__/translation.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,33 @@
|
||||
from django.contrib import admin
|
||||
from modeltranslation.admin import TranslationAdmin
|
||||
from .models import Profile, Category, Vendor, Product, Order, OrderItem
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(Category)
|
||||
class CategoryAdmin(TranslationAdmin):
|
||||
list_display = ('name', 'slug')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
|
||||
@admin.register(Vendor)
|
||||
class VendorAdmin(TranslationAdmin):
|
||||
list_display = ('business_name', 'user', 'is_verified')
|
||||
list_filter = ('is_verified',)
|
||||
search_fields = ('business_name', 'user__username')
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(TranslationAdmin):
|
||||
list_display = ('name', 'vendor', 'category', 'price', 'stock', 'is_available')
|
||||
list_filter = ('is_available', 'category', 'vendor')
|
||||
search_fields = ('name', 'description')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
|
||||
class OrderItemInline(admin.TabularInline):
|
||||
model = OrderItem
|
||||
extra = 0
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'full_name', 'total_price', 'status', 'payment_method', 'created_at')
|
||||
list_filter = ('status', 'payment_method', 'created_at')
|
||||
inlines = [OrderItemInline]
|
||||
|
||||
admin.site.register(Profile)
|
||||
118
core/migrations/0001_initial.py
Normal file
118
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,118 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-04 18:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
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)),
|
||||
('name_om', models.CharField(max_length=100, null=True)),
|
||||
('name_am', models.CharField(max_length=100, null=True)),
|
||||
('name_en', models.CharField(max_length=100, null=True)),
|
||||
('slug', models.SlugField(blank=True, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('description_om', models.TextField(blank=True, null=True)),
|
||||
('description_am', models.TextField(blank=True, null=True)),
|
||||
('description_en', models.TextField(blank=True, null=True)),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='categories/')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Categories',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('full_name', models.CharField(max_length=255)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('phone', models.CharField(max_length=20)),
|
||||
('address', models.TextField()),
|
||||
('total_price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('shipped', 'Shipped'), ('delivered', 'Delivered'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
|
||||
('payment_method', models.CharField(choices=[('cod', 'Cash on Delivery'), ('telebirr', 'Telebirr'), ('cbe_birr', 'CBE Birr'), ('bank_transfer', 'Bank Transfer')], default='cod', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
('name_om', models.CharField(max_length=255, null=True)),
|
||||
('name_am', models.CharField(max_length=255, null=True)),
|
||||
('name_en', models.CharField(max_length=255, null=True)),
|
||||
('slug', models.SlugField(blank=True, unique=True)),
|
||||
('description', models.TextField()),
|
||||
('description_om', models.TextField(null=True)),
|
||||
('description_am', models.TextField(null=True)),
|
||||
('description_en', models.TextField(null=True)),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('stock', models.IntegerField(default=0)),
|
||||
('image', models.ImageField(upload_to='products/')),
|
||||
('is_available', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.category')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrderItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('quantity', models.PositiveIntegerField(default=1)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.order')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('buyer', 'Buyer'), ('seller', 'Seller'), ('admin', 'Admin')], default='buyer', max_length=10)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('address', models.TextField(blank=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Vendor',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('business_name', models.CharField(max_length=200)),
|
||||
('business_name_om', models.CharField(max_length=200, null=True)),
|
||||
('business_name_am', models.CharField(max_length=200, null=True)),
|
||||
('business_name_en', models.CharField(max_length=200, null=True)),
|
||||
('slug', models.SlugField(blank=True, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('description_om', models.TextField(blank=True, null=True)),
|
||||
('description_am', models.TextField(blank=True, null=True)),
|
||||
('description_en', models.TextField(blank=True, null=True)),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='vendors/')),
|
||||
('address', models.TextField()),
|
||||
('phone', models.CharField(max_length=20)),
|
||||
('is_verified', models.BooleanField(default=False)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='vendor',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.vendor'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,95 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-04 18:49
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
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.RemoveField(
|
||||
model_name='vendor',
|
||||
name='logo',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='vendor',
|
||||
name='slug',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vendor',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, max_length=254),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='payment_method',
|
||||
field=models.CharField(max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('Pending', 'Pending'), ('Processing', 'Processing'), ('Shipped', 'Shipped'), ('Delivered', 'Delivered'), ('Cancelled', 'Cancelled')], default='Pending', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='products/'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('customer', 'Customer'), ('seller', 'Seller'), ('admin', 'Admin')], default='customer', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vendor',
|
||||
name='address',
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vendor',
|
||||
name='business_name',
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vendor',
|
||||
name='business_name_am',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vendor',
|
||||
name='business_name_en',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vendor',
|
||||
name='business_name_om',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
101
core/models.py
101
core/models.py
@ -1,3 +1,102 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.text import slugify
|
||||
from django.utils import timezone
|
||||
|
||||
# Create your models here.
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
image = models.ImageField(upload_to='categories/', blank=True, null=True)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = 'Categories'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Vendor(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
business_name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
address = models.CharField(max_length=255)
|
||||
phone = models.CharField(max_length=20)
|
||||
is_verified = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
|
||||
def __str__(self):
|
||||
return self.business_name
|
||||
|
||||
class Product(models.Model):
|
||||
category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE)
|
||||
vendor = models.ForeignKey(Vendor, related_name='products', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
image = models.ImageField(upload_to='products/', blank=True, null=True)
|
||||
description = models.TextField()
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
stock = models.IntegerField(default=0)
|
||||
is_available = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Profile(models.Model):
|
||||
ROLE_CHOICES = (
|
||||
('customer', 'Customer'),
|
||||
('seller', 'Seller'),
|
||||
('admin', 'Admin'),
|
||||
)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='customer')
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
address = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} Profile"
|
||||
|
||||
class Order(models.Model):
|
||||
STATUS_CHOICES = (
|
||||
('Pending', 'Pending'),
|
||||
('Processing', 'Processing'),
|
||||
('Shipped', 'Shipped'),
|
||||
('Delivered', 'Delivered'),
|
||||
('Cancelled', 'Cancelled'),
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
full_name = models.CharField(max_length=255)
|
||||
email = models.EmailField(blank=True)
|
||||
phone = models.CharField(max_length=20)
|
||||
address = models.TextField()
|
||||
total_price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
payment_method = models.CharField(max_length=50)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Pending')
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
|
||||
def __str__(self):
|
||||
return f"Order {self.id} - {self.full_name}"
|
||||
|
||||
class OrderItem(models.Model):
|
||||
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.quantity} x {self.product.name}"
|
||||
|
||||
@property
|
||||
def total_price(self):
|
||||
return self.price * self.quantity
|
||||
@ -1,25 +1,169 @@
|
||||
{% load i18n static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{% trans "Ethio-Marketplace" %}{% endblock %}</title>
|
||||
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link rel="stylesheet" href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #0d6efd;
|
||||
--secondary-color: #6c757d;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
}
|
||||
.navbar {
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,.05);
|
||||
}
|
||||
.hero-section {
|
||||
padding: 5rem 0;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
}
|
||||
.card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,.08) !important;
|
||||
}
|
||||
.btn-primary {
|
||||
padding: 0.6rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-white sticky-top py-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold text-primary fs-4" href="{% url 'index' %}">
|
||||
<i class="bi bi-shop me-2"></i>{% trans "Ethio-Market" %}
|
||||
</a>
|
||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="{% url 'index' %}">{% trans "Home" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3" href="{% url 'product_list' %}">{% trans "Products" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- Language Switcher -->
|
||||
<form action="{% url 'set_language' %}" method="post" class="me-3">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<select name="language" onchange="this.form.submit()" class="form-select form-select-sm bg-light border-0">
|
||||
{% get_current_language as CURRENT_LANGUAGE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<option value="{{ language.code }}" {% if language.code == CURRENT_LANGUAGE %}selected{% endif %}>
|
||||
{{ language.name_local }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<!-- Cart -->
|
||||
<a href="{% url 'cart_detail' %}" class="btn btn-link text-dark position-relative me-3">
|
||||
<i class="bi bi-cart3 fs-5"></i>
|
||||
{% if request.session.cart %}
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
|
||||
{{ request.session.cart|length }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<div class="dropdown">
|
||||
<a class="btn btn-outline-primary btn-sm dropdown-toggle d-flex align-items-center" href="#" role="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-person-circle me-1"></i> {{ user.username }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-sm" aria-labelledby="userDropdown">
|
||||
{% if user.vendor %}
|
||||
<li><a class="dropdown-item" href="{% url 'vendor_dashboard' %}"><i class="bi bi-speedometer2 me-2"></i>{% trans "Vendor Dashboard" %}</a></li>
|
||||
{% else %}
|
||||
<li><a class="dropdown-item" href="{% url 'vendor_register' %}"><i class="bi bi-shop-window me-2"></i>{% trans "Become a Seller" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="{% url 'admin:index' %}"><i class="bi bi-gear me-2"></i>{% trans "Admin" %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form action="{% url 'admin:logout' %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="dropdown-item text-danger"><i class="bi bi-box-arrow-right me-2"></i>{% trans "Logout" %}</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{% url 'admin:index' %}" class="btn btn-primary btn-sm">{% trans "Login" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% if messages %}
|
||||
<div class="container mt-3">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="bg-white py-5 mt-5 border-top">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||
<h5 class="fw-bold text-primary mb-3">{% trans "Ethio-Market" %}</h5>
|
||||
<p class="text-muted small">{% trans "Connecting Ethiopian local vendors with customers everywhere." %}</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-center text-md-end">
|
||||
<p class="text-muted mb-0">© 2026 {% trans "Ethio-Marketplace" %}. {% trans "All rights reserved." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap 5 Bundle with Popper -->
|
||||
<script src="{% static 'bootstrap/dist/js/bootstrap.bundle.min.js' %}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
81
core/templates/core/cart_detail.html
Normal file
81
core/templates/core/cart_detail.html
Normal file
@ -0,0 +1,81 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{% trans "Shopping Cart" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<h1 class="fw-bold mb-4">{% trans "Your Shopping Cart" %}</h1>
|
||||
|
||||
{% if cart_items %}
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-8">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Product" %}</th>
|
||||
<th scope="col">{% trans "Price" %}</th>
|
||||
<th scope="col">{% trans "Quantity" %}</th>
|
||||
<th scope="col">{% trans "Subtotal" %}</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in cart_items %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
{% if item.product.image %}
|
||||
<img src="{{ item.product.image.url }}" alt="{{ item.product.name }}" class="rounded me-3" style="width: 60px; height: 60px; object-fit: cover;">
|
||||
{% endif %}
|
||||
<div>
|
||||
<h6 class="mb-0">{{ item.product.name }}</h6>
|
||||
<small class="text-muted">{{ item.product.vendor.business_name }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.product.price }} ETB</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ item.subtotal }} ETB</td>
|
||||
<td>
|
||||
<a href="{% url 'cart_remove' item.product.id %}" class="text-danger">
|
||||
<i class="bi bi-trash"></i> {% trans "Remove" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm p-4">
|
||||
<h4 class="fw-bold mb-4">{% trans "Order Summary" %}</h4>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span>{% trans "Subtotal" %}</span>
|
||||
<span>{{ total }} ETB</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<span>{% trans "Shipping" %}</span>
|
||||
<span class="text-success">{% trans "Free" %}</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<span class="fw-bold">{% trans "Total" %}</span>
|
||||
<span class="fw-bold text-primary fs-4">{{ total }} ETB</span>
|
||||
</div>
|
||||
<a href="{% url 'checkout' %}" class="btn btn-primary btn-lg w-100">
|
||||
{% trans "Proceed to Checkout" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<p class="lead text-muted mb-4">{% trans "Your cart is empty." %}</p>
|
||||
<a href="{% url 'product_list' %}" class="btn btn-primary">{% trans "Start Shopping" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
46
core/templates/core/category_products.html
Normal file
46
core/templates/core/category_products.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{{ category.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="mb-4">
|
||||
<h1 class="fw-bold">{{ category.name }}</h1>
|
||||
{% if category.description %}
|
||||
<p class="lead text-muted">{{ category.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{% for product in products %}
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
{% if product.image %}
|
||||
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name }}" style="height: 200px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 200px;">
|
||||
<span class="text-white">{% trans "No Image" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title h6 mb-2">
|
||||
<a href="{% url 'product_detail' product.slug %}" class="text-decoration-none text-dark">
|
||||
{{ product.name }}
|
||||
</a>
|
||||
</h5>
|
||||
<p class="fw-bold text-primary mb-3">{{ product.price }} ETB</p>
|
||||
<a href="{% url 'cart_add' product.id %}" class="btn btn-outline-primary btn-sm w-100">
|
||||
{% trans "Add to Cart" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<p class="text-muted">{% trans "No products available in this category yet." %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
73
core/templates/core/checkout.html
Normal file
73
core/templates/core/checkout.html
Normal file
@ -0,0 +1,73 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{% trans "Checkout" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<h1 class="fw-bold mb-4">{% trans "Checkout" %}</h1>
|
||||
|
||||
<form action="{% url 'checkout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row g-5">
|
||||
<div class="col-md-7 col-lg-8">
|
||||
<h4 class="mb-3">{% trans "Shipping Address" %}</h4>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label for="full_name" class="form-label">{% trans "Full Name" %}</label>
|
||||
<input type="text" class="form-control" id="full_name" name="full_name" required>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="email" class="form-label">{% trans "Email" %} <span class="text-muted">({% trans "Optional" %})</span></label>
|
||||
<input type="email" class="form-control" id="email" name="email" placeholder="you@example.com">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="phone" class="form-label">{% trans "Phone Number" %}</label>
|
||||
<input type="text" class="form-control" id="phone" name="phone" placeholder="+251..." required>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="address" class="form-label">{% trans "Address" %}</label>
|
||||
<textarea class="form-control" id="address" name="address" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h4 class="mb-3">{% trans "Payment Method" %}</h4>
|
||||
|
||||
<div class="my-3">
|
||||
<div class="form-check">
|
||||
<input id="cod" name="payment_method" type="radio" class="form-check-input" value="COD" checked required>
|
||||
<label class="form-check-label" for="cod">{% trans "Cash on Delivery" %}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="telebirr" name="payment_method" type="radio" class="form-check-input" value="Telebirr" required>
|
||||
<label class="form-check-label" for="telebirr">Telebirr</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input id="cbe" name="payment_method" type="radio" class="form-check-input" value="CBE" required>
|
||||
<label class="form-check-label" for="cbe">CBE Birr</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="w-100 btn btn-primary btn-lg" type="submit">{% trans "Place Order" %}</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5 col-lg-4 order-md-last">
|
||||
<h4 class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-primary">{% trans "Your cart" %}</span>
|
||||
</h4>
|
||||
<ul class="list-group mb-3">
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<span>{% trans "Total (ETB)" %}</span>
|
||||
<strong>{{ total }} ETB</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,145 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
{% block title %}{% trans "Ethio-Marketplace | Home" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section text-center">
|
||||
<div class="container">
|
||||
<h1 class="display-4 fw-bold mb-3">{% trans "Welcome to Ethio-Marketplace" %}</h1>
|
||||
<p class="lead mb-4">{% trans "Discover amazing products from local vendors across Ethiopia." %}</p>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a href="{% url 'product_list' %}" class="btn btn-primary btn-lg px-4 me-md-2">{% trans "Shop Now" %}</a>
|
||||
<a href="{% url 'vendor_register' %}" class="btn btn-outline-secondary btn-lg px-4">{% trans "Become a Seller" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Categories Section -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">{% trans "Shop by Category" %}</h2>
|
||||
<a href="#" class="text-decoration-none">{% trans "View All" %}</a>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
{% for category in categories %}
|
||||
<div class="col-6 col-md-4 col-lg-2">
|
||||
<a href="{% url 'category_products' category.slug %}" class="text-decoration-none text-dark">
|
||||
<div class="card h-100 text-center border-0 shadow-sm transition-hover">
|
||||
<div class="card-body">
|
||||
{% if category.image %}
|
||||
<img src="{{ category.image.url }}" alt="{{ category.name }}" class="img-fluid mb-2" style="height: 60px; object-fit: contain;">
|
||||
{% else %}
|
||||
<div class="bg-light rounded-circle mx-auto mb-2 d-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
|
||||
<i class="bi bi-tag fs-3 text-primary"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h6 class="card-title mb-0">{{ category.name }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-center text-muted">{% trans "No categories found." %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Products Section -->
|
||||
<section class="py-5 bg-light">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">{% trans "Featured Products" %}</h2>
|
||||
<a href="{% url 'product_list' %}" class="text-decoration-none">{% trans "View All" %}</a>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
{% for product in featured_products %}
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
{% if product.image %}
|
||||
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name }}" style="height: 200px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 200px;">
|
||||
<span class="text-white">{% trans "No Image" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">{{ product.category.name }}</p>
|
||||
<h5 class="card-title h6 mb-2">
|
||||
<a href="{% url 'product_detail' product.slug %}" class="text-decoration-none text-dark">
|
||||
{{ product.name }}
|
||||
</a>
|
||||
</h5>
|
||||
<p class="fw-bold text-primary mb-3">{{ product.price }} ETB</p>
|
||||
<a href="{% url 'cart_add' product.id %}" class="btn btn-outline-primary btn-sm w-100">
|
||||
{% trans "Add to Cart" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<p class="text-muted">{% trans "No featured products available." %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
.transition-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
31
core/templates/core/order_success.html
Normal file
31
core/templates/core/order_success.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{% trans "Order Successful" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5 text-center">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-check-circle-fill text-success" style="font-size: 5rem;"></i>
|
||||
</div>
|
||||
<h1 class="fw-bold mb-3">{% trans "Thank you for your order!" %}</h1>
|
||||
<p class="lead mb-4">{% trans "Your order ID is" %} #{{ order.id }}. {% trans "We have received your request and will contact you shortly for delivery." %}</p>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm p-4 text-start">
|
||||
<h5 class="fw-bold mb-3">{% trans "Order Details" %}</h5>
|
||||
<p class="mb-1"><strong>{% trans "Name" %}:</strong> {{ order.full_name }}</p>
|
||||
<p class="mb-1"><strong>{% trans "Phone" %}:</strong> {{ order.phone }}</p>
|
||||
<p class="mb-1"><strong>{% trans "Address" %}:</strong> {{ order.address }}</p>
|
||||
<p class="mb-1"><strong>{% trans "Total" %}:</strong> {{ order.total_price }} ETB</p>
|
||||
<p class="mb-0"><strong>{% trans "Payment" %}:</strong> {{ order.get_payment_method_display }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<a href="{% url 'index' %}" class="btn btn-primary px-4">{% trans "Return to Shop" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
49
core/templates/core/product_detail.html
Normal file
49
core/templates/core/product_detail.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{{ product.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'index' %}">{% trans "Home" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'category_products' product.category.slug %}">{{ product.category.name }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ product.name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row g-5">
|
||||
<div class="col-md-6">
|
||||
{% if product.image %}
|
||||
<img src="{{ product.image.url }}" class="img-fluid rounded shadow-sm" alt="{{ product.name }}">
|
||||
{% else %}
|
||||
<div class="bg-light d-flex align-items-center justify-content-center rounded" style="min-height: 400px;">
|
||||
<span class="text-muted">{% trans "No Image Available" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h1 class="fw-bold mb-3">{{ product.name }}</h1>
|
||||
<p class="text-muted mb-4">{{ product.category.name }}</p>
|
||||
<h2 class="text-primary fw-bold mb-4">{{ product.price }} ETB</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<h5 class="fw-bold">{% trans "Description" %}</h5>
|
||||
<p class="text-secondary">{{ product.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 p-3 bg-light rounded">
|
||||
<h6 class="fw-bold mb-1">{% trans "Sold by" %}: {{ product.vendor.business_name }}</h6>
|
||||
<p class="small text-muted mb-0">{% trans "Located in" %}: {{ product.vendor.address }}</p>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'cart_add' product.id %}" class="btn btn-primary btn-lg">
|
||||
{% trans "Add to Cart" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
52
core/templates/core/product_list.html
Normal file
52
core/templates/core/product_list.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{% trans "All Products" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h1 class="fw-bold">{% trans "Our Products" %}</h1>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<form action="{% url 'product_list' %}" method="get" class="d-flex">
|
||||
<input type="text" name="q" class="form-control me-2" placeholder="{% trans 'Search products...' %}" value="{{ request.GET.q }}">
|
||||
<button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{% for product in products %}
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
{% if product.image %}
|
||||
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name }}" style="height: 200px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 200px;">
|
||||
<span class="text-white">{% trans "No Image" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">{{ product.category.name }}</p>
|
||||
<h5 class="card-title h6 mb-2">
|
||||
<a href="{% url 'product_detail' product.slug %}" class="text-decoration-none text-dark">
|
||||
{{ product.name }}
|
||||
</a>
|
||||
</h5>
|
||||
<p class="fw-bold text-primary mb-3">{{ product.price }} ETB</p>
|
||||
<a href="{% url 'cart_add' product.id %}" class="btn btn-outline-primary btn-sm w-100">
|
||||
{% trans "Add to Cart" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<p class="text-muted">{% trans "No products found matching your search." %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
128
core/templates/core/vendor_dashboard.html
Normal file
128
core/templates/core/vendor_dashboard.html
Normal file
@ -0,0 +1,128 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{% trans "Vendor Dashboard" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5">
|
||||
<div>
|
||||
<h1 class="fw-bold mb-1">{{ vendor.business_name }}</h1>
|
||||
<p class="text-muted mb-0">{% trans "Manage your products and orders" %}</p>
|
||||
</div>
|
||||
<a href="{% url 'admin:core_product_add' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg"></i> {% trans "Add New Product" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Products Summary -->
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm p-4 text-center">
|
||||
<h6 class="text-muted text-uppercase small fw-bold mb-2">{% trans "Total Products" %}</h6>
|
||||
<h2 class="fw-bold mb-0">{{ products.count }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Orders Summary -->
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm p-4 text-center">
|
||||
<h6 class="text-muted text-uppercase small fw-bold mb-2">{% trans "Total Orders" %}</h6>
|
||||
<h2 class="fw-bold mb-0">{{ orders.count }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm p-4 text-center">
|
||||
<h6 class="text-muted text-uppercase small fw-bold mb-2">{% trans "Status" %}</h6>
|
||||
{% if vendor.is_verified %}
|
||||
<span class="badge bg-success py-2 px-3">{% trans "Verified" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark py-2 px-3">{% trans "Pending Verification" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<ul class="nav nav-tabs mb-4" id="dashboardTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="products-tab" data-bs-toggle="tab" data-bs-target="#products" type="button" role="tab">{% trans "Your Products" %}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="orders-tab" data-bs-toggle="tab" data-bs-target="#orders" type="button" role="tab">{% trans "Recent Orders" %}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="dashboardTabsContent">
|
||||
<div class="tab-pane fade show active" id="products" role="tabpanel">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Product" %}</th>
|
||||
<th>{% trans "Category" %}</th>
|
||||
<th>{% trans "Price" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for product in products %}
|
||||
<tr>
|
||||
<td>{{ product.name }}</td>
|
||||
<td>{{ product.category.name }}</td>
|
||||
<td>{{ product.price }} ETB</td>
|
||||
<td>
|
||||
{% if product.is_available %}
|
||||
<span class="badge bg-success">{% trans "Available" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Out of Stock" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'admin:core_product_change' product.id %}" class="btn btn-sm btn-outline-secondary">{% trans "Edit" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4">{% trans "No products found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="orders" role="tabpanel">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order ID" %}</th>
|
||||
<th>{% trans "Product" %}</th>
|
||||
<th>{% trans "Customer" %}</th>
|
||||
<th>{% trans "Total" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in orders %}
|
||||
<tr>
|
||||
<td>#{{ item.order.id }}</td>
|
||||
<td>{{ item.product.name }}</td>
|
||||
<td>{{ item.order.full_name }}</td>
|
||||
<td>{{ item.total_price }} ETB</td>
|
||||
<td>{{ item.order.status }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4">{% trans "No orders found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
44
core/templates/core/vendor_register.html
Normal file
44
core/templates/core/vendor_register.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{% trans "Become a Seller" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card border-0 shadow-sm p-4 p-md-5">
|
||||
<h1 class="fw-bold text-center mb-4">{% trans "Register as a Vendor" %}</h1>
|
||||
<p class="text-center text-muted mb-5">{% trans "Join our community and start selling your products to thousands of customers." %}</p>
|
||||
|
||||
<form action="{% url 'vendor_register' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="business_name" class="form-label">{% trans "Business Name" %}</label>
|
||||
<input type="text" class="form-control" id="business_name" name="business_name" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">{% trans "Business Phone" %}</label>
|
||||
<input type="text" class="form-control" id="phone" name="phone" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="address" class="form-label">{% trans "Business Address" %}</label>
|
||||
<input type="text" class="form-control" id="address" name="address" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="description" class="form-label">{% trans "Business Description" %}</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">{% trans "Submit Application" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
14
core/translation.py
Normal file
14
core/translation.py
Normal file
@ -0,0 +1,14 @@
|
||||
from modeltranslation.translator import register, TranslationOptions
|
||||
from .models import Category, Product, Vendor
|
||||
|
||||
@register(Category)
|
||||
class CategoryTranslationOptions(TranslationOptions):
|
||||
fields = ('name', 'description')
|
||||
|
||||
@register(Product)
|
||||
class ProductTranslationOptions(TranslationOptions):
|
||||
fields = ('name', 'description')
|
||||
|
||||
@register(Vendor)
|
||||
class VendorTranslationOptions(TranslationOptions):
|
||||
fields = ('business_name', 'description')
|
||||
15
core/urls.py
15
core/urls.py
@ -1,7 +1,16 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path('', views.home, name='index'),
|
||||
path('products/', views.product_list, name='product_list'),
|
||||
path('product/<slug:slug>/', views.product_detail, name='product_detail'),
|
||||
path('category/<slug:slug>/', views.category_products, name='category_products'),
|
||||
path('cart/', views.cart_detail, name='cart_detail'),
|
||||
path('cart/add/<int:product_id>/', views.cart_add, name='cart_add'),
|
||||
path('cart/remove/<int:product_id>/', views.cart_remove, name='cart_remove'),
|
||||
path('checkout/', views.checkout, name='checkout'),
|
||||
path('order/success/<int:order_id>/', views.order_success, name='order_success'),
|
||||
path('vendor/register/', views.vendor_register, name='vendor_register'),
|
||||
path('vendor/dashboard/', views.vendor_dashboard, name='vendor_dashboard'),
|
||||
]
|
||||
|
||||
162
core/views.py
162
core/views.py
@ -1,25 +1,143 @@
|
||||
import os
|
||||
import platform
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .models import Category, Product, Vendor, Order, OrderItem, Profile
|
||||
from django.contrib import messages
|
||||
from django.db.models import Q
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
categories = Category.objects.all()[:6]
|
||||
featured_products = Product.objects.filter(is_available=True)[:8]
|
||||
return render(request, 'core/index.html', {
|
||||
'categories': categories,
|
||||
'featured_products': featured_products,
|
||||
})
|
||||
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
def product_list(request):
|
||||
query = request.GET.get('q')
|
||||
products = Product.objects.filter(is_available=True)
|
||||
if query:
|
||||
products = products.filter(
|
||||
Q(name__icontains=query) |
|
||||
Q(description__icontains=query)
|
||||
)
|
||||
return render(request, 'core/product_list.html', {'products': products})
|
||||
|
||||
def product_detail(request, slug):
|
||||
product = get_object_or_404(Product, slug=slug, is_available=True)
|
||||
return render(request, 'core/product_detail.html', {'product': product})
|
||||
|
||||
def category_products(request, slug):
|
||||
category = get_object_or_404(Category, slug=slug)
|
||||
products = category.products.filter(is_available=True)
|
||||
return render(request, 'core/category_products.html', {'category': category, 'products': products})
|
||||
|
||||
# Basic Cart System using Session
|
||||
def get_cart(request):
|
||||
cart = request.session.get('cart', {})
|
||||
return cart
|
||||
|
||||
def cart_add(request, product_id):
|
||||
cart = get_cart(request)
|
||||
product_id_str = str(product_id)
|
||||
if product_id_str in cart:
|
||||
cart[product_id_str] += 1
|
||||
else:
|
||||
cart[product_id_str] = 1
|
||||
request.session['cart'] = cart
|
||||
messages.success(request, "Product added to cart")
|
||||
return redirect('cart_detail')
|
||||
|
||||
def cart_remove(request, product_id):
|
||||
cart = get_cart(request)
|
||||
product_id_str = str(product_id)
|
||||
if product_id_str in cart:
|
||||
del cart[product_id_str]
|
||||
request.session['cart'] = cart
|
||||
return redirect('cart_detail')
|
||||
|
||||
def cart_detail(request):
|
||||
cart = get_cart(request)
|
||||
cart_items = []
|
||||
total = 0
|
||||
for product_id, quantity in cart.items():
|
||||
product = get_object_or_404(Product, id=product_id)
|
||||
subtotal = product.price * quantity
|
||||
total += subtotal
|
||||
cart_items.append({'product': product, 'quantity': quantity, 'subtotal': subtotal})
|
||||
return render(request, 'core/cart_detail.html', {'cart_items': cart_items, 'total': total})
|
||||
|
||||
def checkout(request):
|
||||
cart = get_cart(request)
|
||||
if not cart:
|
||||
return redirect('product_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
full_name = request.POST.get('full_name')
|
||||
email = request.POST.get('email')
|
||||
phone = request.POST.get('phone')
|
||||
address = request.POST.get('address')
|
||||
payment_method = request.POST.get('payment_method')
|
||||
|
||||
total = 0
|
||||
order_items = []
|
||||
for product_id, quantity in cart.items():
|
||||
product = get_object_or_404(Product, id=product_id)
|
||||
total += product.price * quantity
|
||||
order_items.append((product, quantity, product.price))
|
||||
|
||||
order = Order.objects.create(
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
full_name=full_name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
address=address,
|
||||
total_price=total,
|
||||
payment_method=payment_method
|
||||
)
|
||||
|
||||
for product, quantity, price in order_items:
|
||||
OrderItem.objects.create(order=order, product=product, quantity=quantity, price=price)
|
||||
|
||||
request.session['cart'] = {}
|
||||
return redirect('order_success', order_id=order.id)
|
||||
|
||||
return render(request, 'core/checkout.html')
|
||||
|
||||
def order_success(request, order_id):
|
||||
order = get_object_or_404(Order, id=order_id)
|
||||
return render(request, 'core/order_success.html', {'order': order})
|
||||
|
||||
@login_required
|
||||
def vendor_register(request):
|
||||
if hasattr(request.user, 'vendor'):
|
||||
return redirect('vendor_dashboard')
|
||||
|
||||
if request.method == 'POST':
|
||||
business_name = request.POST.get('business_name')
|
||||
description = request.POST.get('description')
|
||||
address = request.POST.get('address')
|
||||
phone = request.POST.get('phone')
|
||||
|
||||
Vendor.objects.create(
|
||||
user=request.user,
|
||||
business_name=business_name,
|
||||
description=description,
|
||||
address=address,
|
||||
phone=phone
|
||||
)
|
||||
# Update user role
|
||||
profile, created = Profile.objects.get_or_create(user=request.user)
|
||||
profile.role = 'seller'
|
||||
profile.save()
|
||||
|
||||
messages.success(request, "Vendor registration successful. Wait for admin verification.")
|
||||
return redirect('vendor_dashboard')
|
||||
|
||||
return render(request, 'core/vendor_register.html')
|
||||
|
||||
@login_required
|
||||
def vendor_dashboard(request):
|
||||
vendor = get_object_or_404(Vendor, user=request.user)
|
||||
products = vendor.products.all()
|
||||
orders = OrderItem.objects.filter(product__vendor=vendor).select_related('order')
|
||||
return render(request, 'core/vendor_dashboard.html', {'vendor': vendor, 'products': products, 'orders': orders})
|
||||
111
populate_db.py
Normal file
111
populate_db.py
Normal file
@ -0,0 +1,111 @@
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||
django.setup()
|
||||
|
||||
from core.models import Category, Product, Vendor, Profile
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.text import slugify
|
||||
|
||||
def populate():
|
||||
# Create Superuser if not exists
|
||||
if not User.objects.filter(username='admin').exists():
|
||||
User.objects.create_superuser('admin', 'admin@example.com', 'adminpass')
|
||||
print("Superuser created: admin / adminpass")
|
||||
|
||||
# Categories
|
||||
categories_data = [
|
||||
{'name': 'Electronics', 'name_om': 'Meeshaalee Elektirooniksii', 'name_am': 'የኤሌክትሮኒክስ ዕቃዎች'},
|
||||
{'name': 'Clothing', 'name_om': 'Uffata', 'name_am': 'ልብስ'},
|
||||
{'name': 'Home & Garden', 'name_om': 'Mana fi Muka', 'name_am': 'ቤት እና የአትክልት ቦታ'},
|
||||
{'name': 'Food & Groceries', 'name_om': 'Nyaataa fi Meeshaalee Nyaataa', 'name_am': 'ምግብ እና ግሮሰሪ'},
|
||||
{'name': 'Handmade Crafts', 'name_om': 'Hojii Harkaa', 'name_am': 'በእጅ የተሰሩ ስራዎች'},
|
||||
{'name': 'Books', 'name_om': 'Kitaabota', 'name_am': 'መጽሐፍት'},
|
||||
]
|
||||
|
||||
categories = []
|
||||
for cat in categories_data:
|
||||
category, created = Category.objects.get_or_create(
|
||||
name=cat['name'],
|
||||
defaults={
|
||||
'name_om': cat['name_om'],
|
||||
'name_am': cat['name_am'],
|
||||
'slug': slugify(cat['name'])
|
||||
}
|
||||
)
|
||||
categories.append(category)
|
||||
if created:
|
||||
print(f"Category created: {cat['name']}")
|
||||
|
||||
# Create a Vendor
|
||||
vendor_user, created = User.objects.get_or_create(username='vendor1', email='vendor1@example.com')
|
||||
if created:
|
||||
vendor_user.set_password('vendorpass')
|
||||
vendor_user.save()
|
||||
Profile.objects.get_or_create(user=vendor_user, role='seller')
|
||||
print("Vendor user created: vendor1")
|
||||
|
||||
vendor, created = Vendor.objects.get_or_create(
|
||||
user=vendor_user,
|
||||
defaults={
|
||||
'business_name': 'Ethio Tech Solutions',
|
||||
'business_name_om': 'Furmaata Teeknoojii Itiyoophiyaa',
|
||||
'description': 'Leading provider of tech gadgets in Addis.',
|
||||
'address': 'Bole, Addis Ababa',
|
||||
'phone': '+251911000000'
|
||||
}
|
||||
)
|
||||
if created:
|
||||
print("Vendor created: Ethio Tech Solutions")
|
||||
|
||||
# Products
|
||||
products_data = [
|
||||
{
|
||||
'name': 'Smartphone X1',
|
||||
'name_om': 'Bilbila Ammayya X1',
|
||||
'category': categories[0],
|
||||
'price': 25000,
|
||||
'description': 'High performance smartphone.'
|
||||
},
|
||||
{
|
||||
'name': 'Traditional Coffee Pot (Jebena)',
|
||||
'name_om': 'Jabanaa',
|
||||
'category': categories[4],
|
||||
'price': 500,
|
||||
'description': 'Handmade traditional clay pot.'
|
||||
},
|
||||
{
|
||||
'name': 'Cotton Scarf',
|
||||
'name_om': 'Shaashii',
|
||||
'category': categories[1],
|
||||
'price': 1200,
|
||||
'description': 'Pure Ethiopian cotton.'
|
||||
},
|
||||
{
|
||||
'name': 'Organic Honey',
|
||||
'name_om': 'Damma',
|
||||
'category': categories[3],
|
||||
'price': 800,
|
||||
'description': 'Pure honey from Gojam.'
|
||||
},
|
||||
]
|
||||
|
||||
for prod in products_data:
|
||||
product, created = Product.objects.get_or_create(
|
||||
name=prod['name'],
|
||||
defaults={
|
||||
'name_om': prod['name_om'],
|
||||
'category': prod['category'],
|
||||
'vendor': vendor,
|
||||
'price': prod['price'],
|
||||
'description': prod['description'],
|
||||
'slug': slugify(prod['name']),
|
||||
'is_available': True
|
||||
}
|
||||
)
|
||||
if created:
|
||||
print(f"Product created: {prod['name']}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
populate()
|
||||
@ -1,3 +1,6 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
Pillow==11.1.0
|
||||
django-modeltranslation==0.19.11
|
||||
httpx==0.28.1
|
||||
Loading…
x
Reference in New Issue
Block a user