diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc
index 881731c..80c1dd3 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 42d995d..4b360a6 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..6ed069d 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -180,3 +180,7 @@ if EMAIL_USE_SSL:
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+# Media files (Uploads)
+MEDIA_URL = 'media/'
+MEDIA_ROOT = BASE_DIR / 'media'
\ No newline at end of file
diff --git a/config/urls.py b/config/urls.py
index bcfc074..4d15824 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -1,19 +1,3 @@
-"""
-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.conf import settings
@@ -25,5 +9,5 @@ urlpatterns = [
]
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 2964e11..7847a78 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 18a063c..4fac4a6 100644
Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ
diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index ebb8c6e..2678d58 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 8d204fa..420bd20 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..a8ffa32 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -1,3 +1,53 @@
from django.contrib import admin
+from .models import Category, Book, Order
+from django.utils.html import format_html
-# Register your models here.
+@admin.register(Category)
+class CategoryAdmin(admin.ModelAdmin):
+ list_display = ('name', 'slug', 'book_count', 'display_image')
+ prepopulated_fields = {'slug': ('name',)}
+
+ def book_count(self, obj):
+ return obj.books.count()
+ book_count.short_description = 'Books'
+
+ def display_image(self, obj):
+ if obj.image:
+ return format_html('', obj.image.url)
+ return "-"
+ display_image.short_description = 'Image'
+
+@admin.register(Book)
+class BookAdmin(admin.ModelAdmin):
+ list_display = ('title', 'category', 'price', 'is_featured', 'display_cover', 'created_at')
+ list_filter = ('category', 'is_featured', 'created_at')
+ list_editable = ('is_featured',)
+ search_fields = ('title', 'description')
+ prepopulated_fields = {'slug': ('title',)}
+
+ def display_cover(self, obj):
+ if obj.cover_image:
+ return format_html('
', obj.cover_image.url)
+ return "-"
+ display_cover.short_description = 'Cover'
+
+@admin.register(Order)
+class OrderAdmin(admin.ModelAdmin):
+ list_display = ('book', 'user_email', 'transaction_id', 'status', 'display_screenshot', 'created_at')
+ list_filter = ('status', 'created_at')
+ search_fields = ('user_email', 'transaction_id')
+ actions = ['approve_orders', 'reject_orders']
+
+ def approve_orders(self, request, queryset):
+ queryset.update(status='approved')
+ approve_orders.short_description = "✅ Mark as Approved"
+
+ def reject_orders(self, request, queryset):
+ queryset.update(status='rejected')
+ reject_orders.short_description = "❌ Mark as Rejected"
+
+ def display_screenshot(self, obj):
+ if obj.payment_screenshot:
+ return format_html('
', obj.payment_screenshot.url, obj.payment_screenshot.url)
+ return "No Proof"
+ display_screenshot.short_description = 'Payment Proof'
\ 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..57dd77c
--- /dev/null
+++ b/core/migrations/0001_initial.py
@@ -0,0 +1,52 @@
+# Generated by Django 5.2.7 on 2026-02-19 18:13
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ 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)),
+ ('slug', models.SlugField(blank=True, unique=True)),
+ ],
+ options={
+ 'verbose_name_plural': 'Categories',
+ },
+ ),
+ migrations.CreateModel(
+ name='Book',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=255)),
+ ('description', models.TextField()),
+ ('price', models.DecimalField(decimal_places=2, max_digits=10)),
+ ('pdf_file', models.FileField(upload_to='books/pdfs/')),
+ ('cover_image', models.ImageField(blank=True, null=True, upload_to='books/covers/')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('slug', models.SlugField(blank=True, unique=True)),
+ ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='books', to='core.category')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Order',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('user_email', models.EmailField(max_length=254)),
+ ('transaction_id', models.CharField(help_text='Enter your EVC/Zaad/Sahal transaction ID or reference', max_length=100)),
+ ('payment_screenshot', models.ImageField(blank=True, null=True, upload_to='payments/')),
+ ('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=20)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.book')),
+ ],
+ ),
+ ]
diff --git a/core/migrations/0002_book_is_featured_category_image.py b/core/migrations/0002_book_is_featured_category_image.py
new file mode 100644
index 0000000..cf077db
--- /dev/null
+++ b/core/migrations/0002_book_is_featured_category_image.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.7 on 2026-02-19 18:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='book',
+ name='is_featured',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='category',
+ name='image',
+ field=models.ImageField(blank=True, null=True, upload_to='categories/'),
+ ),
+ ]
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..c470a15
Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/core/migrations/__pycache__/0002_book_is_featured_category_image.cpython-311.pyc b/core/migrations/__pycache__/0002_book_is_featured_category_image.cpython-311.pyc
new file mode 100644
index 0000000..49b4dbf
Binary files /dev/null and b/core/migrations/__pycache__/0002_book_is_featured_category_image.cpython-311.pyc differ
diff --git a/core/models.py b/core/models.py
index 71a8362..9fb9ac2 100644
--- a/core/models.py
+++ b/core/models.py
@@ -1,3 +1,53 @@
from django.db import models
+from django.utils.text import slugify
-# 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)
+
+ def save(self, *args, **kwargs):
+ if not self.slug:
+ self.slug = slugify(self.name)
+ super().save(*args, **kwargs)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ verbose_name_plural = "Categories"
+
+class Book(models.Model):
+ title = models.CharField(max_length=255)
+ description = models.TextField()
+ price = models.DecimalField(max_digits=10, decimal_places=2)
+ pdf_file = models.FileField(upload_to='books/pdfs/')
+ cover_image = models.ImageField(upload_to='books/covers/', blank=True, null=True)
+ category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='books')
+ is_featured = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ slug = models.SlugField(unique=True, blank=True)
+
+ def save(self, *args, **kwargs):
+ if not self.slug:
+ self.slug = slugify(self.title)
+ super().save(*args, **kwargs)
+
+ def __str__(self):
+ return self.title
+
+class Order(models.Model):
+ STATUS_CHOICES = (
+ ('pending', 'Pending'),
+ ('approved', 'Approved'),
+ ('rejected', 'Rejected'),
+ )
+ book = models.ForeignKey(Book, on_delete=models.CASCADE)
+ user_email = models.EmailField()
+ transaction_id = models.CharField(max_length=100, help_text="Enter your EVC/Zaad/Sahal transaction ID or reference")
+ payment_screenshot = models.ImageField(upload_to='payments/', blank=True, null=True)
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ def __str__(self):
+ return f"Order for {self.book.title} by {self.user_email}"
\ No newline at end of file
diff --git a/core/templates/base.html b/core/templates/base.html
index 1e7e5fb..f5c447c 100644
--- a/core/templates/base.html
+++ b/core/templates/base.html
@@ -1,25 +1,140 @@
+{% load static %}
-