diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..cf8b83e 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..ecb1721 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..a3a8d0a 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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' diff --git a/config/urls.py b/config/urls.py index bcfc074..6c5d25d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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) \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..ebd4fa1 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..e088272 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/translation.cpython-311.pyc b/core/__pycache__/translation.cpython-311.pyc new file mode 100644 index 0000000..8c38538 Binary files /dev/null and b/core/__pycache__/translation.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..175a82e 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..6c24bb9 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..9156386 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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) \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..d115aea --- /dev/null +++ b/core/migrations/0001_initial.py @@ -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'), + ), + ] diff --git a/core/migrations/0002_remove_vendor_logo_remove_vendor_slug_and_more.py b/core/migrations/0002_remove_vendor_logo_remove_vendor_slug_and_more.py new file mode 100644 index 0000000..b53c3d1 --- /dev/null +++ b/core/migrations/0002_remove_vendor_logo_remove_vendor_slug_and_more.py @@ -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), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..b5a036d Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_remove_vendor_logo_remove_vendor_slug_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_remove_vendor_logo_remove_vendor_slug_and_more.cpython-311.pyc new file mode 100644 index 0000000..b0308f9 Binary files /dev/null and b/core/migrations/__pycache__/0002_remove_vendor_logo_remove_vendor_slug_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..2db87a3 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,102 @@ from django.db import models +from django.contrib.auth.models import User +from django.utils.text import slugify +from django.utils import timezone -# Create your models here. +class Category(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField(unique=True, blank=True) + image = models.ImageField(upload_to='categories/', blank=True, null=True) + description = models.TextField(blank=True) + + class Meta: + verbose_name_plural = 'Categories' + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + +class Vendor(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + business_name = models.CharField(max_length=255) + description = models.TextField(blank=True) + address = models.CharField(max_length=255) + phone = models.CharField(max_length=20) + is_verified = models.BooleanField(default=False) + created_at = models.DateTimeField(default=timezone.now) + + def __str__(self): + return self.business_name + +class Product(models.Model): + category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE) + vendor = models.ForeignKey(Vendor, related_name='products', on_delete=models.CASCADE) + name = models.CharField(max_length=255) + slug = models.SlugField(unique=True, blank=True) + image = models.ImageField(upload_to='products/', blank=True, null=True) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + stock = models.IntegerField(default=0) + is_available = models.BooleanField(default=True) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + +class Profile(models.Model): + ROLE_CHOICES = ( + ('customer', 'Customer'), + ('seller', 'Seller'), + ('admin', 'Admin'), + ) + user = models.OneToOneField(User, on_delete=models.CASCADE) + role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='customer') + phone = models.CharField(max_length=20, blank=True) + address = models.TextField(blank=True) + + def __str__(self): + return f"{self.user.username} Profile" + +class Order(models.Model): + STATUS_CHOICES = ( + ('Pending', 'Pending'), + ('Processing', 'Processing'), + ('Shipped', 'Shipped'), + ('Delivered', 'Delivered'), + ('Cancelled', 'Cancelled'), + ) + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + full_name = models.CharField(max_length=255) + email = models.EmailField(blank=True) + phone = models.CharField(max_length=20) + address = models.TextField() + total_price = models.DecimalField(max_digits=10, decimal_places=2) + payment_method = models.CharField(max_length=50) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='Pending') + created_at = models.DateTimeField(default=timezone.now) + + def __str__(self): + return f"Order {self.id} - {self.full_name}" + +class OrderItem(models.Model): + order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + price = models.DecimalField(max_digits=10, decimal_places=2) + quantity = models.PositiveIntegerField(default=1) + + def __str__(self): + return f"{self.quantity} x {self.product.name}" + + @property + def total_price(self): + return self.price * self.quantity \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..9c7a16f 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,169 @@ +{% load i18n static %} - + - {% block title %}Knowledge Base{% endblock %} + + {% block title %}{% trans "Ethio-Marketplace" %}{% endblock %} + {% if project_description %} {% endif %} + {% if project_image_url %} {% endif %} - {% load static %} + + + + + + + + {% block head %}{% endblock %} - {% block content %}{% endblock %} + + + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+ {% block content %}{% endblock %} +
+ + + + + + {% block scripts %}{% endblock %} - + \ No newline at end of file diff --git a/core/templates/core/cart_detail.html b/core/templates/core/cart_detail.html new file mode 100644 index 0000000..a24a543 --- /dev/null +++ b/core/templates/core/cart_detail.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{% trans "Shopping Cart" %}{% endblock %} + +{% block content %} +
+

{% trans "Your Shopping Cart" %}

+ + {% if cart_items %} +
+
+
+ + + + + + + + + + + + {% for item in cart_items %} + + + + + + + + {% endfor %} + +
{% trans "Product" %}{% trans "Price" %}{% trans "Quantity" %}{% trans "Subtotal" %}
+
+ {% if item.product.image %} + {{ item.product.name }} + {% endif %} +
+
{{ item.product.name }}
+ {{ item.product.vendor.business_name }} +
+
+
{{ item.product.price }} ETB{{ item.quantity }}{{ item.subtotal }} ETB + + {% trans "Remove" %} + +
+
+
+
+
+

{% trans "Order Summary" %}

+
+ {% trans "Subtotal" %} + {{ total }} ETB +
+
+ {% trans "Shipping" %} + {% trans "Free" %} +
+
+
+ {% trans "Total" %} + {{ total }} ETB +
+ + {% trans "Proceed to Checkout" %} + +
+
+
+ {% else %} +
+

{% trans "Your cart is empty." %}

+ {% trans "Start Shopping" %} +
+ {% endif %} +
+{% endblock %} diff --git a/core/templates/core/category_products.html b/core/templates/core/category_products.html new file mode 100644 index 0000000..e5bf750 --- /dev/null +++ b/core/templates/core/category_products.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{{ category.name }}{% endblock %} + +{% block content %} +
+
+

{{ category.name }}

+ {% if category.description %} +

{{ category.description }}

+ {% endif %} +
+ +
+ {% for product in products %} +
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ {% trans "No Image" %} +
+ {% endif %} + +
+
+ {% empty %} +
+

{% trans "No products available in this category yet." %}

+
+ {% endfor %} +
+
+{% endblock %} diff --git a/core/templates/core/checkout.html b/core/templates/core/checkout.html new file mode 100644 index 0000000..eb6fe49 --- /dev/null +++ b/core/templates/core/checkout.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{% trans "Checkout" %}{% endblock %} + +{% block content %} +
+

{% trans "Checkout" %}

+ +
+ {% csrf_token %} +
+
+

{% trans "Shipping Address" %}

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +

{% trans "Payment Method" %}

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

+ {% trans "Your cart" %} +

+
    +
  • + {% trans "Total (ETB)" %} + {{ total }} ETB +
  • +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..50f3099 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,98 @@ {% extends "base.html" %} +{% load i18n static %} -{% block title %}{{ project_name }}{% endblock %} +{% block title %}{% trans "Ethio-Marketplace | Home" %}{% endblock %} + +{% block content %} + +
+
+

{% trans "Welcome to Ethio-Marketplace" %}

+

{% trans "Discover amazing products from local vendors across Ethiopia." %}

+ +
+
+ + +
+
+
+

{% trans "Shop by Category" %}

+ {% trans "View All" %} +
+
+ {% for category in categories %} + + {% empty %} +

{% trans "No categories found." %}

+ {% endfor %} +
+
+
+ + +
+
+
+

{% trans "Featured Products" %}

+ {% trans "View All" %} +
+
+ {% for product in featured_products %} +
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ {% trans "No Image" %} +
+ {% endif %} +
+

{{ product.category.name }}

+
+ + {{ product.name }} + +
+

{{ product.price }} ETB

+ + {% trans "Add to Cart" %} + +
+
+
+ {% empty %} +
+

{% trans "No featured products available." %}

+
+ {% endfor %} +
+
+
-{% block head %} - - - {% endblock %} - -{% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… -
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file diff --git a/core/templates/core/order_success.html b/core/templates/core/order_success.html new file mode 100644 index 0000000..905cd0a --- /dev/null +++ b/core/templates/core/order_success.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{% trans "Order Successful" %}{% endblock %} + +{% block content %} +
+
+ +
+

{% trans "Thank you for your order!" %}

+

{% trans "Your order ID is" %} #{{ order.id }}. {% trans "We have received your request and will contact you shortly for delivery." %}

+ +
+
+
+
{% trans "Order Details" %}
+

{% trans "Name" %}: {{ order.full_name }}

+

{% trans "Phone" %}: {{ order.phone }}

+

{% trans "Address" %}: {{ order.address }}

+

{% trans "Total" %}: {{ order.total_price }} ETB

+

{% trans "Payment" %}: {{ order.get_payment_method_display }}

+
+
+
+ +
+ {% trans "Return to Shop" %} +
+
+{% endblock %} diff --git a/core/templates/core/product_detail.html b/core/templates/core/product_detail.html new file mode 100644 index 0000000..8e99307 --- /dev/null +++ b/core/templates/core/product_detail.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{{ product.name }}{% endblock %} + +{% block content %} +
+ + +
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ {% trans "No Image Available" %} +
+ {% endif %} +
+
+

{{ product.name }}

+

{{ product.category.name }}

+

{{ product.price }} ETB

+ +
+
{% trans "Description" %}
+

{{ product.description }}

+
+ +
+
{% trans "Sold by" %}: {{ product.vendor.business_name }}
+

{% trans "Located in" %}: {{ product.vendor.address }}

+
+ + +
+
+
+{% endblock %} diff --git a/core/templates/core/product_list.html b/core/templates/core/product_list.html new file mode 100644 index 0000000..2b5ce48 --- /dev/null +++ b/core/templates/core/product_list.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{% trans "All Products" %}{% endblock %} + +{% block content %} +
+
+
+

{% trans "Our Products" %}

+
+
+
+ + +
+
+
+ +
+ {% for product in products %} +
+
+ {% if product.image %} + {{ product.name }} + {% else %} +
+ {% trans "No Image" %} +
+ {% endif %} +
+

{{ product.category.name }}

+
+ + {{ product.name }} + +
+

{{ product.price }} ETB

+ + {% trans "Add to Cart" %} + +
+
+
+ {% empty %} +
+

{% trans "No products found matching your search." %}

+
+ {% endfor %} +
+
+{% endblock %} diff --git a/core/templates/core/vendor_dashboard.html b/core/templates/core/vendor_dashboard.html new file mode 100644 index 0000000..07c94cc --- /dev/null +++ b/core/templates/core/vendor_dashboard.html @@ -0,0 +1,128 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{% trans "Vendor Dashboard" %}{% endblock %} + +{% block content %} +
+
+
+

{{ vendor.business_name }}

+

{% trans "Manage your products and orders" %}

+
+ + {% trans "Add New Product" %} + +
+ +
+ +
+
+
{% trans "Total Products" %}
+

{{ products.count }}

+
+
+ +
+
+
{% trans "Total Orders" %}
+

{{ orders.count }}

+
+
+ +
+
+
{% trans "Status" %}
+ {% if vendor.is_verified %} + {% trans "Verified" %} + {% else %} + {% trans "Pending Verification" %} + {% endif %} +
+
+
+ +
+ + +
+
+
+ + + + + + + + + + + + {% for product in products %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Product" %}{% trans "Category" %}{% trans "Price" %}{% trans "Status" %}{% trans "Actions" %}
{{ product.name }}{{ product.category.name }}{{ product.price }} ETB + {% if product.is_available %} + {% trans "Available" %} + {% else %} + {% trans "Out of Stock" %} + {% endif %} + + {% trans "Edit" %} +
{% trans "No products found." %}
+
+
+
+
+ + + + + + + + + + + + {% for item in orders %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Order ID" %}{% trans "Product" %}{% trans "Customer" %}{% trans "Total" %}{% trans "Status" %}
#{{ item.order.id }}{{ item.product.name }}{{ item.order.full_name }}{{ item.total_price }} ETB{{ item.order.status }}
{% trans "No orders found." %}
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/vendor_register.html b/core/templates/core/vendor_register.html new file mode 100644 index 0000000..44ab179 --- /dev/null +++ b/core/templates/core/vendor_register.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{% trans "Become a Seller" %}{% endblock %} + +{% block content %} +
+
+
+
+

{% trans "Register as a Vendor" %}

+

{% trans "Join our community and start selling your products to thousands of customers." %}

+ +
+ {% csrf_token %} +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+
+
+{% endblock %} diff --git a/core/translation.py b/core/translation.py new file mode 100644 index 0000000..f1455d0 --- /dev/null +++ b/core/translation.py @@ -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') diff --git a/core/urls.py b/core/urls.py index 6299e3d..0cfd2ca 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,16 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), + path('', views.home, name='index'), + path('products/', views.product_list, name='product_list'), + path('product//', views.product_detail, name='product_detail'), + path('category//', views.category_products, name='category_products'), + path('cart/', views.cart_detail, name='cart_detail'), + path('cart/add//', views.cart_add, name='cart_add'), + path('cart/remove//', views.cart_remove, name='cart_remove'), + path('checkout/', views.checkout, name='checkout'), + path('order/success//', views.order_success, name='order_success'), + path('vendor/register/', views.vendor_register, name='vendor_register'), + path('vendor/dashboard/', views.vendor_dashboard, name='vendor_dashboard'), ] diff --git a/core/views.py b/core/views.py index c9aed12..9485a1e 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,143 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone - +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.decorators import login_required +from .models import Category, Product, Vendor, Order, OrderItem, Profile +from django.contrib import messages +from django.db.models import Q def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + categories = Category.objects.all()[:6] + featured_products = Product.objects.filter(is_available=True)[:8] + return render(request, 'core/index.html', { + 'categories': categories, + 'featured_products': featured_products, + }) - context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - } - return render(request, "core/index.html", context) +def product_list(request): + query = request.GET.get('q') + products = Product.objects.filter(is_available=True) + if query: + products = products.filter( + Q(name__icontains=query) | + Q(description__icontains=query) + ) + return render(request, 'core/product_list.html', {'products': products}) + +def product_detail(request, slug): + product = get_object_or_404(Product, slug=slug, is_available=True) + return render(request, 'core/product_detail.html', {'product': product}) + +def category_products(request, slug): + category = get_object_or_404(Category, slug=slug) + products = category.products.filter(is_available=True) + return render(request, 'core/category_products.html', {'category': category, 'products': products}) + +# Basic Cart System using Session +def get_cart(request): + cart = request.session.get('cart', {}) + return cart + +def cart_add(request, product_id): + cart = get_cart(request) + product_id_str = str(product_id) + if product_id_str in cart: + cart[product_id_str] += 1 + else: + cart[product_id_str] = 1 + request.session['cart'] = cart + messages.success(request, "Product added to cart") + return redirect('cart_detail') + +def cart_remove(request, product_id): + cart = get_cart(request) + product_id_str = str(product_id) + if product_id_str in cart: + del cart[product_id_str] + request.session['cart'] = cart + return redirect('cart_detail') + +def cart_detail(request): + cart = get_cart(request) + cart_items = [] + total = 0 + for product_id, quantity in cart.items(): + product = get_object_or_404(Product, id=product_id) + subtotal = product.price * quantity + total += subtotal + cart_items.append({'product': product, 'quantity': quantity, 'subtotal': subtotal}) + return render(request, 'core/cart_detail.html', {'cart_items': cart_items, 'total': total}) + +def checkout(request): + cart = get_cart(request) + if not cart: + return redirect('product_list') + + if request.method == 'POST': + full_name = request.POST.get('full_name') + email = request.POST.get('email') + phone = request.POST.get('phone') + address = request.POST.get('address') + payment_method = request.POST.get('payment_method') + + total = 0 + order_items = [] + for product_id, quantity in cart.items(): + product = get_object_or_404(Product, id=product_id) + total += product.price * quantity + order_items.append((product, quantity, product.price)) + + order = Order.objects.create( + user=request.user if request.user.is_authenticated else None, + full_name=full_name, + email=email, + phone=phone, + address=address, + total_price=total, + payment_method=payment_method + ) + + for product, quantity, price in order_items: + OrderItem.objects.create(order=order, product=product, quantity=quantity, price=price) + + request.session['cart'] = {} + return redirect('order_success', order_id=order.id) + + return render(request, 'core/checkout.html') + +def order_success(request, order_id): + order = get_object_or_404(Order, id=order_id) + return render(request, 'core/order_success.html', {'order': order}) + +@login_required +def vendor_register(request): + if hasattr(request.user, 'vendor'): + return redirect('vendor_dashboard') + + if request.method == 'POST': + business_name = request.POST.get('business_name') + description = request.POST.get('description') + address = request.POST.get('address') + phone = request.POST.get('phone') + + Vendor.objects.create( + user=request.user, + business_name=business_name, + description=description, + address=address, + phone=phone + ) + # Update user role + profile, created = Profile.objects.get_or_create(user=request.user) + profile.role = 'seller' + profile.save() + + messages.success(request, "Vendor registration successful. Wait for admin verification.") + return redirect('vendor_dashboard') + + return render(request, 'core/vendor_register.html') + +@login_required +def vendor_dashboard(request): + vendor = get_object_or_404(Vendor, user=request.user) + products = vendor.products.all() + orders = OrderItem.objects.filter(product__vendor=vendor).select_related('order') + return render(request, 'core/vendor_dashboard.html', {'vendor': vendor, 'products': products, 'orders': orders}) \ No newline at end of file diff --git a/populate_db.py b/populate_db.py new file mode 100644 index 0000000..654653d --- /dev/null +++ b/populate_db.py @@ -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() diff --git a/requirements.txt b/requirements.txt index e22994c..e836360 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file