Autosave: 20260205-172908

This commit is contained in:
Flatlogic Bot 2026-02-05 17:29:10 +00:00
parent 62fa70915a
commit 81ab8ae93e
30 changed files with 1430 additions and 247 deletions

View File

@ -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'

View File

@ -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.

View File

@ -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)

View 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'),
),
]

View File

@ -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),
),
]

View File

@ -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

View File

@ -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">&copy; 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>

View 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 %}

View 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 %}

View 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 %}

View File

@ -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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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
View 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')

View File

@ -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'),
] ]

View File

@ -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
View 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()

View File

@ -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