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.
Generated by 'django-admin startproject' using Django 5.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
import os
from dotenv import load_dotenv
from django.utils.translation import gettext_lazy as _
import django.conf.locale
import django.utils.translation
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR.parent / ".env")
# Oromo and Amharic are not in Django's default LANG_INFO
EXTRA_LANG_INFO = {
'om': {
'bidi': False,
'code': 'om',
'name': 'Oromo',
'name_local': 'Afaan Oromoo',
},
'am': {
'bidi': False,
'code': 'am',
'name': 'Amharic',
'name_local': 'አማርኛ',
},
}
# Add custom languages to LANG_INFO
for code, info in EXTRA_LANG_INFO.items():
if code not in django.conf.locale.LANG_INFO:
django.conf.locale.LANG_INFO[code] = info
# Also update the reference in django.utils.translation if it exists
if hasattr(django.utils.translation, 'LANG_INFO') and code not in django.utils.translation.LANG_INFO:
django.utils.translation.LANG_INFO[code] = info
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
@ -37,18 +58,17 @@ CSRF_TRUSTED_ORIGINS = [
for host in CSRF_TRUSTED_ORIGINS
]
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "None"
CSRF_COOKIE_SAMESITE = "None"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
X_FRAME_OPTIONS = 'ALLOWALL'
# Application definition
INSTALLED_APPS = [
'modeltranslation', # Must be before admin
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -61,16 +81,13 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
X_FRAME_OPTIONS = 'ALLOWALL'
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
@ -83,7 +100,7 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
# IMPORTANT: do not remove injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
'django.template.context_processors.i18n',
'core.context_processors.project_context',
],
},
@ -92,10 +109,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
@ -110,56 +123,46 @@ DATABASES = {
},
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en'
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Africa/Addis_Ababa'
USE_I18N = True
USE_TZ = True
LANGUAGES = [
('en', _('English')),
('am', _('Amharic')),
('om', _('Afaan Oromoo')),
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
MODELTRANSLATION_DEFAULT_LANGUAGE = 'en'
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
STATIC_URL = 'static/'
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
BASE_DIR / 'assets',
BASE_DIR / 'node_modules',
]
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Email
EMAIL_BACKEND = os.getenv(
"EMAIL_BACKEND",
"django.core.mail.backends.smtp.EmailBackend"
)
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1")
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
@ -167,16 +170,9 @@ EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true"
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true"
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com")
CONTACT_EMAIL_TO = [
item.strip()
for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",")
if item.strip()
]
CONTACT_EMAIL_TO = [item.strip() for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",") if item.strip()]
# When both TLS and SSL flags are enabled, prefer SSL explicitly
if EMAIL_USE_SSL:
EMAIL_USE_TLS = False
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

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.urls import include, path
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("core.urls")),
path('i18n/', include('django.conf.urls.i18n')),
]
urlpatterns += i18n_patterns(
path('admin/', admin.site.py_urls if hasattr(admin.site, 'py_urls') else admin.site.urls),
path('', include('core.urls')),
)
if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Binary file not shown.

View File

@ -1,3 +1,33 @@
from django.contrib import admin
from modeltranslation.admin import TranslationAdmin
from .models import Profile, Category, Vendor, Product, Order, OrderItem
# Register your models here.
@admin.register(Category)
class CategoryAdmin(TranslationAdmin):
list_display = ('name', 'slug')
prepopulated_fields = {'slug': ('name',)}
@admin.register(Vendor)
class VendorAdmin(TranslationAdmin):
list_display = ('business_name', 'user', 'is_verified')
list_filter = ('is_verified',)
search_fields = ('business_name', 'user__username')
@admin.register(Product)
class ProductAdmin(TranslationAdmin):
list_display = ('name', 'vendor', 'category', 'price', 'stock', 'is_available')
list_filter = ('is_available', 'category', 'vendor')
search_fields = ('name', 'description')
prepopulated_fields = {'slug': ('name',)}
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'full_name', 'total_price', 'status', 'payment_method', 'created_at')
list_filter = ('status', 'payment_method', 'created_at')
inlines = [OrderItemInline]
admin.site.register(Profile)

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.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>
<html lang="en">
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% trans "Ethio-Marketplace" %}{% endblock %}</title>
{% if project_description %}
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
<meta property="twitter:description" content="{{ project_description }}">
{% endif %}
{% if project_image_url %}
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
{% load static %}
<!-- Bootstrap 5 CSS -->
<link rel="stylesheet" href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
<style>
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: #f8f9fa;
color: #333;
}
.navbar {
box-shadow: 0 2px 10px rgba(0,0,0,.05);
}
.hero-section {
padding: 5rem 0;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,.08) !important;
}
.btn-primary {
padding: 0.6rem 1.5rem;
border-radius: 8px;
}
</style>
{% block head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<nav class="navbar navbar-expand-lg navbar-light bg-white sticky-top py-3">
<div class="container">
<a class="navbar-brand fw-bold text-primary fs-4" href="{% url 'index' %}">
<i class="bi bi-shop me-2"></i>{% trans "Ethio-Market" %}
</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link px-3" href="{% url 'index' %}">{% trans "Home" %}</a>
</li>
<li class="nav-item">
<a class="nav-link px-3" href="{% url 'product_list' %}">{% trans "Products" %}</a>
</li>
</ul>
<div class="d-flex align-items-center">
<!-- Language Switcher -->
<form action="{% url 'set_language' %}" method="post" class="me-3">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<select name="language" onchange="this.form.submit()" class="form-select form-select-sm bg-light border-0">
{% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}" {% if language.code == CURRENT_LANGUAGE %}selected{% endif %}>
{{ language.name_local }}
</option>
{% endfor %}
</select>
</form>
<!-- Cart -->
<a href="{% url 'cart_detail' %}" class="btn btn-link text-dark position-relative me-3">
<i class="bi bi-cart3 fs-5"></i>
{% if request.session.cart %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
{{ request.session.cart|length }}
</span>
{% endif %}
</a>
{% if user.is_authenticated %}
<div class="dropdown">
<a class="btn btn-outline-primary btn-sm dropdown-toggle d-flex align-items-center" href="#" role="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle me-1"></i> {{ user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-sm" aria-labelledby="userDropdown">
{% if user.vendor %}
<li><a class="dropdown-item" href="{% url 'vendor_dashboard' %}"><i class="bi bi-speedometer2 me-2"></i>{% trans "Vendor Dashboard" %}</a></li>
{% else %}
<li><a class="dropdown-item" href="{% url 'vendor_register' %}"><i class="bi bi-shop-window me-2"></i>{% trans "Become a Seller" %}</a></li>
{% endif %}
<li><a class="dropdown-item" href="{% url 'admin:index' %}"><i class="bi bi-gear me-2"></i>{% trans "Admin" %}</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<form action="{% url 'admin:logout' %}" method="post" class="d-inline">
{% csrf_token %}
<button type="submit" class="dropdown-item text-danger"><i class="bi bi-box-arrow-right me-2"></i>{% trans "Logout" %}</button>
</form>
</li>
</ul>
</div>
{% else %}
<a href="{% url 'admin:index' %}" class="btn btn-primary btn-sm">{% trans "Login" %}</a>
{% endif %}
</div>
</div>
</div>
</nav>
{% if messages %}
<div class="container mt-3">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
<main>
{% block content %}{% endblock %}
</main>
<footer class="bg-white py-5 mt-5 border-top">
<div class="container">
<div class="row">
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
<h5 class="fw-bold text-primary mb-3">{% trans "Ethio-Market" %}</h5>
<p class="text-muted small">{% trans "Connecting Ethiopian local vendors with customers everywhere." %}</p>
</div>
<div class="col-md-6 text-center text-md-end">
<p class="text-muted mb-0">&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>
</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" %}
{% load i18n static %}
{% block title %}{{ project_name }}{% endblock %}
{% block title %}{% trans "Ethio-Marketplace | Home" %}{% endblock %}
{% block content %}
<!-- Hero Section -->
<section class="hero-section text-center">
<div class="container">
<h1 class="display-4 fw-bold mb-3">{% trans "Welcome to Ethio-Marketplace" %}</h1>
<p class="lead mb-4">{% trans "Discover amazing products from local vendors across Ethiopia." %}</p>
<div class="d-flex justify-content-center">
<a href="{% url 'product_list' %}" class="btn btn-primary btn-lg px-4 me-md-2">{% trans "Shop Now" %}</a>
<a href="{% url 'vendor_register' %}" class="btn btn-outline-secondary btn-lg px-4">{% trans "Become a Seller" %}</a>
</div>
</div>
</section>
<!-- Categories Section -->
<section class="py-5">
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">{% trans "Shop by Category" %}</h2>
<a href="#" class="text-decoration-none">{% trans "View All" %}</a>
</div>
<div class="row g-4">
{% for category in categories %}
<div class="col-6 col-md-4 col-lg-2">
<a href="{% url 'category_products' category.slug %}" class="text-decoration-none text-dark">
<div class="card h-100 text-center border-0 shadow-sm transition-hover">
<div class="card-body">
{% if category.image %}
<img src="{{ category.image.url }}" alt="{{ category.name }}" class="img-fluid mb-2" style="height: 60px; object-fit: contain;">
{% else %}
<div class="bg-light rounded-circle mx-auto mb-2 d-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
<i class="bi bi-tag fs-3 text-primary"></i>
</div>
{% endif %}
<h6 class="card-title mb-0">{{ category.name }}</h6>
</div>
</div>
</a>
</div>
{% empty %}
<p class="text-center text-muted">{% trans "No categories found." %}</p>
{% endfor %}
</div>
</div>
</section>
<!-- Featured Products Section -->
<section class="py-5 bg-light">
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">{% trans "Featured Products" %}</h2>
<a href="{% url 'product_list' %}" class="text-decoration-none">{% trans "View All" %}</a>
</div>
<div class="row g-4">
{% for product in featured_products %}
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 border-0 shadow-sm">
{% if product.image %}
<img src="{{ product.image.url }}" class="card-img-top" alt="{{ product.name }}" style="height: 200px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-secondary d-flex align-items-center justify-content-center" style="height: 200px;">
<span class="text-white">{% trans "No Image" %}</span>
</div>
{% endif %}
<div class="card-body">
<p class="text-muted small mb-1">{{ product.category.name }}</p>
<h5 class="card-title h6 mb-2">
<a href="{% url 'product_detail' product.slug %}" class="text-decoration-none text-dark">
{{ product.name }}
</a>
</h5>
<p class="fw-bold text-primary mb-3">{{ product.price }} ETB</p>
<a href="{% url 'cart_add' product.id %}" class="btn btn-outline-primary btn-sm w-100">
{% trans "Add to Cart" %}
</a>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<p class="text-muted">{% trans "No featured products available." %}</p>
</div>
{% endfor %}
</div>
</div>
</section>
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
.transition-hover:hover {
transform: translateY(-5px);
transition: transform 0.3s ease;
}
</style>
{% endblock %}
{% block content %}
<main>
<div class="card">
<h1>Analyzing your requirements and generating your app…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %}

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 .views import home
from . import views
urlpatterns = [
path("", home, name="home"),
path('', views.home, name='index'),
path('products/', views.product_list, name='product_list'),
path('product/<slug:slug>/', views.product_detail, name='product_detail'),
path('category/<slug:slug>/', views.category_products, name='category_products'),
path('cart/', views.cart_detail, name='cart_detail'),
path('cart/add/<int:product_id>/', views.cart_add, name='cart_add'),
path('cart/remove/<int:product_id>/', views.cart_remove, name='cart_remove'),
path('checkout/', views.checkout, name='checkout'),
path('order/success/<int:order_id>/', views.order_success, name='order_success'),
path('vendor/register/', views.vendor_register, name='vendor_register'),
path('vendor/dashboard/', views.vendor_dashboard, name='vendor_dashboard'),
]

View File

@ -1,25 +1,143 @@
import os
import platform
from django import get_version as django_version
from django.shortcuts import render
from django.utils import timezone
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from .models import Category, Product, Vendor, Order, OrderItem, Profile
from django.contrib import messages
from django.db.models import Q
def home(request):
"""Render the landing screen with loader and environment details."""
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now()
categories = Category.objects.all()[:6]
featured_products = Product.objects.filter(is_available=True)[:8]
return render(request, 'core/index.html', {
'categories': categories,
'featured_products': featured_products,
})
context = {
"project_name": "New Style",
"agent_brand": agent_brand,
"django_version": django_version(),
"python_version": platform.python_version(),
"current_time": now,
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
}
return render(request, "core/index.html", context)
def product_list(request):
query = request.GET.get('q')
products = Product.objects.filter(is_available=True)
if query:
products = products.filter(
Q(name__icontains=query) |
Q(description__icontains=query)
)
return render(request, 'core/product_list.html', {'products': products})
def product_detail(request, slug):
product = get_object_or_404(Product, slug=slug, is_available=True)
return render(request, 'core/product_detail.html', {'product': product})
def category_products(request, slug):
category = get_object_or_404(Category, slug=slug)
products = category.products.filter(is_available=True)
return render(request, 'core/category_products.html', {'category': category, 'products': products})
# Basic Cart System using Session
def get_cart(request):
cart = request.session.get('cart', {})
return cart
def cart_add(request, product_id):
cart = get_cart(request)
product_id_str = str(product_id)
if product_id_str in cart:
cart[product_id_str] += 1
else:
cart[product_id_str] = 1
request.session['cart'] = cart
messages.success(request, "Product added to cart")
return redirect('cart_detail')
def cart_remove(request, product_id):
cart = get_cart(request)
product_id_str = str(product_id)
if product_id_str in cart:
del cart[product_id_str]
request.session['cart'] = cart
return redirect('cart_detail')
def cart_detail(request):
cart = get_cart(request)
cart_items = []
total = 0
for product_id, quantity in cart.items():
product = get_object_or_404(Product, id=product_id)
subtotal = product.price * quantity
total += subtotal
cart_items.append({'product': product, 'quantity': quantity, 'subtotal': subtotal})
return render(request, 'core/cart_detail.html', {'cart_items': cart_items, 'total': total})
def checkout(request):
cart = get_cart(request)
if not cart:
return redirect('product_list')
if request.method == 'POST':
full_name = request.POST.get('full_name')
email = request.POST.get('email')
phone = request.POST.get('phone')
address = request.POST.get('address')
payment_method = request.POST.get('payment_method')
total = 0
order_items = []
for product_id, quantity in cart.items():
product = get_object_or_404(Product, id=product_id)
total += product.price * quantity
order_items.append((product, quantity, product.price))
order = Order.objects.create(
user=request.user if request.user.is_authenticated else None,
full_name=full_name,
email=email,
phone=phone,
address=address,
total_price=total,
payment_method=payment_method
)
for product, quantity, price in order_items:
OrderItem.objects.create(order=order, product=product, quantity=quantity, price=price)
request.session['cart'] = {}
return redirect('order_success', order_id=order.id)
return render(request, 'core/checkout.html')
def order_success(request, order_id):
order = get_object_or_404(Order, id=order_id)
return render(request, 'core/order_success.html', {'order': order})
@login_required
def vendor_register(request):
if hasattr(request.user, 'vendor'):
return redirect('vendor_dashboard')
if request.method == 'POST':
business_name = request.POST.get('business_name')
description = request.POST.get('description')
address = request.POST.get('address')
phone = request.POST.get('phone')
Vendor.objects.create(
user=request.user,
business_name=business_name,
description=description,
address=address,
phone=phone
)
# Update user role
profile, created = Profile.objects.get_or_create(user=request.user)
profile.role = 'seller'
profile.save()
messages.success(request, "Vendor registration successful. Wait for admin verification.")
return redirect('vendor_dashboard')
return render(request, 'core/vendor_register.html')
@login_required
def vendor_dashboard(request):
vendor = get_object_or_404(Vendor, user=request.user)
products = vendor.products.all()
orders = OrderItem.objects.filter(product__vendor=vendor).select_related('order')
return render(request, 'core/vendor_dashboard.html', {'vendor': vendor, 'products': products, 'orders': orders})

111
populate_db.py Normal file
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
mysqlclient==2.2.7
python-dotenv==1.1.1
Pillow==11.1.0
django-modeltranslation==0.19.11
httpx==0.28.1