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.
|
Django settings for config project.
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 5.2.7.
|
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
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
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
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
load_dotenv(BASE_DIR.parent / ".env")
|
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")
|
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
|
||||||
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
||||||
|
|
||||||
@ -37,18 +58,17 @@ CSRF_TRUSTED_ORIGINS = [
|
|||||||
for host in 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
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
SESSION_COOKIE_SAMESITE = "None"
|
SESSION_COOKIE_SAMESITE = "None"
|
||||||
CSRF_COOKIE_SAMESITE = "None"
|
CSRF_COOKIE_SAMESITE = "None"
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
X_FRAME_OPTIONS = 'ALLOWALL'
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'modeltranslation', # Must be before admin
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
@ -61,16 +81,13 @@ INSTALLED_APPS = [
|
|||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'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'
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
@ -83,7 +100,7 @@ TEMPLATES = [
|
|||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'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',
|
'core.context_processors.project_context',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -92,10 +109,6 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'config.wsgi.application'
|
WSGI_APPLICATION = 'config.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
'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 = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
||||||
'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.MinimumLengthValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
LANGUAGE_CODE = 'en'
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
TIME_ZONE = 'Africa/Addis_Ababa'
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
LANGUAGES = [
|
||||||
|
('en', _('English')),
|
||||||
|
('am', _('Amharic')),
|
||||||
|
('om', _('Afaan Oromoo')),
|
||||||
|
]
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
MODELTRANSLATION_DEFAULT_LANGUAGE = 'en'
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
|
||||||
|
LOCALE_PATHS = [
|
||||||
|
BASE_DIR / 'locale',
|
||||||
|
]
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / 'static',
|
BASE_DIR / 'static',
|
||||||
BASE_DIR / 'assets',
|
BASE_DIR / 'assets',
|
||||||
BASE_DIR / 'node_modules',
|
BASE_DIR / 'node_modules',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAIL_BACKEND = os.getenv(
|
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
"EMAIL_BACKEND",
|
|
||||||
"django.core.mail.backends.smtp.EmailBackend"
|
|
||||||
)
|
|
||||||
EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1")
|
EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1")
|
||||||
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
|
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
|
||||||
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
|
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_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true"
|
||||||
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").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")
|
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com")
|
||||||
CONTACT_EMAIL_TO = [
|
CONTACT_EMAIL_TO = [item.strip() for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",") if item.strip()]
|
||||||
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:
|
if EMAIL_USE_SSL:
|
||||||
EMAIL_USE_TLS = False
|
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'
|
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.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import path, include
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from django.conf.urls.i18n import i18n_patterns
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path('i18n/', include('django.conf.urls.i18n')),
|
||||||
path("", include("core.urls")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_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 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.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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{ LANGUAGE_CODE }}">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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 %}
|
{% if project_description %}
|
||||||
<meta name="description" content="{{ project_description }}">
|
<meta name="description" content="{{ project_description }}">
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
<meta property="og:description" content="{{ project_description }}">
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<meta property="twitter:description" content="{{ project_description }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if project_image_url %}
|
{% if project_image_url %}
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
<meta property="og:image" content="{{ project_image_url }}">
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||||
{% endif %}
|
{% 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 }}">
|
<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 %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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>
|
</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" %}
|
{% 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>
|
<style>
|
||||||
:root {
|
.transition-hover:hover {
|
||||||
--bg-color-start: #6a11cb;
|
transform: translateY(-5px);
|
||||||
--bg-color-end: #2575fc;
|
transition: transform 0.3s ease;
|
||||||
--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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% 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 django.urls import path
|
||||||
|
from . import views
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
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
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
import platform
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from .models import Category, Product, Vendor, Order, OrderItem, Profile
|
||||||
from django import get_version as django_version
|
from django.contrib import messages
|
||||||
from django.shortcuts import render
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with loader and environment details."""
|
categories = Category.objects.all()[:6]
|
||||||
host_name = request.get_host().lower()
|
featured_products = Product.objects.filter(is_available=True)[:8]
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
return render(request, 'core/index.html', {
|
||||||
now = timezone.now()
|
'categories': categories,
|
||||||
|
'featured_products': featured_products,
|
||||||
|
})
|
||||||
|
|
||||||
context = {
|
def product_list(request):
|
||||||
"project_name": "New Style",
|
query = request.GET.get('q')
|
||||||
"agent_brand": agent_brand,
|
products = Product.objects.filter(is_available=True)
|
||||||
"django_version": django_version(),
|
if query:
|
||||||
"python_version": platform.python_version(),
|
products = products.filter(
|
||||||
"current_time": now,
|
Q(name__icontains=query) |
|
||||||
"host_name": host_name,
|
Q(description__icontains=query)
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
)
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
return render(request, 'core/product_list.html', {'products': products})
|
||||||
}
|
|
||||||
return render(request, "core/index.html", context)
|
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
|
Django==5.2.7
|
||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
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