diff --git a/add_reviews.py b/add_reviews.py new file mode 100644 index 0000000..df70f01 --- /dev/null +++ b/add_reviews.py @@ -0,0 +1,35 @@ +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from core.models import Product, ProductReview +from django.contrib.auth.models import User + +def add_reviews(): + products = Product.objects.all() + if not products.exists(): + print("No products found.") + return + + reviews_data = [ + {"product_idx": 0, "name": "Chala Jimma", "rating": 5, "comment": "Excellent quality! Highly recommended for anyone in Jimma."}, + {"product_idx": 0, "name": "Aster K.", "rating": 4, "comment": "Very good service and the product is as described."}, + {"product_idx": 1 if products.count() > 1 else 0, "name": "Dawit H.", "rating": 5, "comment": "Fast delivery to Kochi and great price."}, + {"product_idx": 2 if products.count() > 2 else 0, "name": "Mulu B.", "rating": 3, "comment": "Decent product, but took a bit longer to arrive."}, + ] + + for data in reviews_data: + p = products[data["product_idx"]] + ProductReview.objects.create( + product=p, + full_name=data["name"], + rating=data["rating"], + comment=data["comment"] + ) + + print(f"Created {len(reviews_data)} sample reviews.") + +if __name__ == "__main__": + add_reviews() diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index cf8b83e..89a4b70 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 ecb1721..7b44cfb 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 a3a8d0a..1dc833c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -176,3 +176,6 @@ if EMAIL_USE_SSL: EMAIL_USE_TLS = False DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_REDIRECT_URL = 'index' +LOGOUT_REDIRECT_URL = 'index' \ No newline at end of file diff --git a/config/urls.py b/config/urls.py index 6c5d25d..c364e8d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -10,9 +10,10 @@ urlpatterns = [ urlpatterns += i18n_patterns( path('admin/', admin.site.py_urls if hasattr(admin.site, 'py_urls') else admin.site.urls), + path('accounts/', include('django.contrib.auth.urls')), # Added auth urls path('', include('core.urls')), ) if settings.DEBUG: 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 + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index ebd4fa1..ce57130 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..d50b43c 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..507cff2 Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e088272..535f8f0 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 index 8c38538..2eb1aa4 100644 Binary files a/core/__pycache__/translation.cpython-311.pyc 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 175a82e..a5304bc 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 6c24bb9..3d2851c 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 9156386..4c9a64d 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from modeltranslation.admin import TranslationAdmin -from .models import Profile, Category, Vendor, Product, Order, OrderItem +from .models import Profile, Category, Vendor, Product, Order, OrderItem, Article @admin.register(Category) class CategoryAdmin(TranslationAdmin): @@ -30,4 +30,11 @@ class OrderAdmin(admin.ModelAdmin): list_filter = ('status', 'payment_method', 'created_at') inlines = [OrderItemInline] -admin.site.register(Profile) \ No newline at end of file +@admin.register(Article) +class ArticleAdmin(TranslationAdmin): + list_display = ('title', 'author', 'is_published', 'created_at') + list_filter = ('is_published', 'created_at', 'author') + search_fields = ('title', 'content') + prepopulated_fields = {'slug': ('title',)} + +admin.site.register(Profile) diff --git a/core/context_processors.py b/core/context_processors.py index 0bf87c3..4e95d51 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -1,5 +1,6 @@ import os import time +from .models import Category def project_context(request): """ @@ -8,6 +9,6 @@ def project_context(request): return { "project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - # Used for cache-busting static assets "deployment_timestamp": int(time.time()), - } + "categories_all": Category.objects.all(), + } \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..c551a35 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,37 @@ +from django import forms +from django.contrib.auth.models import User +from django.contrib.auth.forms import UserCreationForm +from .models import Product, ProductImage, Profile + +class ProductForm(forms.ModelForm): + class Meta: + model = Product + fields = ['category', 'name', 'description', 'price', 'stock', 'image', 'is_available'] + widgets = { + 'category': forms.Select(attrs={'class': 'form-select'}), + 'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Product Name'}), + 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': 'Detailed description of your product...'}), + 'price': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '0.00'}), + 'stock': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '0'}), + 'image': forms.ClearableFileInput(attrs={'class': 'form-control'}), + 'is_available': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + } + +class SignUpForm(UserCreationForm): + email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'Email Address'})) + first_name = forms.CharField(max_length=30, required=True, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'First Name'})) + last_name = forms.CharField(max_length=30, required=True, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Last Name'})) + + class Meta(UserCreationForm.Meta): + model = User + fields = UserCreationForm.Meta.fields + ('email', 'first_name', 'last_name') + + def save(self, commit=True): + user = super().save(commit=False) + user.email = self.cleaned_data["email"] + user.first_name = self.cleaned_data["first_name"] + user.last_name = self.cleaned_data["last_name"] + if commit: + user.save() + Profile.objects.get_or_create(user=user) + return user diff --git a/core/migrations/0003_order_delivery_time_slot_order_kebele_productimage_and_more.py b/core/migrations/0003_order_delivery_time_slot_order_kebele_productimage_and_more.py new file mode 100644 index 0000000..dc8b280 --- /dev/null +++ b/core/migrations/0003_order_delivery_time_slot_order_kebele_productimage_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.7 on 2026-02-05 18:03 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_remove_vendor_logo_remove_vendor_slug_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='delivery_time_slot', + field=models.CharField(choices=[('Morning', 'Morning (8:00 AM - 12:00 PM)'), ('Afternoon', 'Afternoon (12:00 PM - 5:00 PM)'), ('Evening', 'Evening (5:00 PM - 8:00 PM)')], default='Morning', max_length=50), + ), + migrations.AddField( + model_name='order', + name='kebele', + field=models.CharField(blank=True, choices=[('Bosa Addis', 'Bosa Addis'), ('Bosa Kitto', 'Bosa Kitto'), ('Ginjo', 'Ginjo'), ('Ginjo Guduru', 'Ginjo Guduru'), ('Hermata', 'Hermata'), ('Hermata Merkato', 'Hermata Merkato'), ('Jiren', 'Jiren'), ('Kofe', 'Kofe'), ('Mendera Kochi', 'Mendera Kochi'), ('Seto Semero', 'Seto Semero')], max_length=100), + ), + migrations.CreateModel( + name='ProductImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to='products/gallery/')), + ('alt_text', models.CharField(blank=True, max_length=255)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='core.product')), + ], + ), + migrations.CreateModel( + name='ProductReview', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=255)), + ('rating', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)])), + ('comment', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='core.product')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0004_article.py b/core/migrations/0004_article.py new file mode 100644 index 0000000..95f32b7 --- /dev/null +++ b/core/migrations/0004_article.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.7 on 2026-02-05 18:50 + +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', '0003_order_delivery_time_slot_order_kebele_productimage_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('slug', models.SlugField(blank=True, unique=True)), + ('content', models.TextField()), + ('image', models.ImageField(blank=True, null=True, upload_to='articles/')), + ('is_published', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0005_article_content_am_article_content_en_and_more.py b/core/migrations/0005_article_content_am_article_content_en_and_more.py new file mode 100644 index 0000000..f501e0a --- /dev/null +++ b/core/migrations/0005_article_content_am_article_content_en_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.7 on 2026-02-05 18:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_article'), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='content_am', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='article', + name='content_en', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='article', + name='content_om', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='article', + name='title_am', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='article', + name='title_en', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='article', + name='title_om', + field=models.CharField(max_length=255, null=True), + ), + ] diff --git a/core/migrations/0006_vendor_kebele.py b/core/migrations/0006_vendor_kebele.py new file mode 100644 index 0000000..b5c6d52 --- /dev/null +++ b/core/migrations/0006_vendor_kebele.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-05 19:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_article_content_am_article_content_en_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='vendor', + name='kebele', + field=models.CharField(blank=True, choices=[('Bosa Addis', 'Bosa Addis'), ('Bosa Kitto', 'Bosa Kitto'), ('Ginjo', 'Ginjo'), ('Ginjo Guduru', 'Ginjo Guduru'), ('Hermata', 'Hermata'), ('Hermata Merkato', 'Hermata Merkato'), ('Jiren', 'Jiren'), ('Kofe', 'Kofe'), ('Mendera Kochi', 'Mendera Kochi'), ('Seto Semero', 'Seto Semero')], max_length=100), + ), + ] diff --git a/core/migrations/__pycache__/0003_order_delivery_time_slot_order_kebele_productimage_and_more.cpython-311.pyc b/core/migrations/__pycache__/0003_order_delivery_time_slot_order_kebele_productimage_and_more.cpython-311.pyc new file mode 100644 index 0000000..49b1919 Binary files /dev/null and b/core/migrations/__pycache__/0003_order_delivery_time_slot_order_kebele_productimage_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0004_article.cpython-311.pyc b/core/migrations/__pycache__/0004_article.cpython-311.pyc new file mode 100644 index 0000000..cfe09c8 Binary files /dev/null and b/core/migrations/__pycache__/0004_article.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0005_article_content_am_article_content_en_and_more.cpython-311.pyc b/core/migrations/__pycache__/0005_article_content_am_article_content_en_and_more.cpython-311.pyc new file mode 100644 index 0000000..51b25eb Binary files /dev/null and b/core/migrations/__pycache__/0005_article_content_am_article_content_en_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0006_vendor_kebele.cpython-311.pyc b/core/migrations/__pycache__/0006_vendor_kebele.cpython-311.pyc new file mode 100644 index 0000000..059cbad Binary files /dev/null and b/core/migrations/__pycache__/0006_vendor_kebele.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 2db87a3..4b085a9 100644 --- a/core/models.py +++ b/core/models.py @@ -2,6 +2,8 @@ from django.db import models from django.contrib.auth.models import User from django.utils.text import slugify from django.utils import timezone +from django.core.validators import MinValueValidator, MaxValueValidator +from django.db.models import Avg class Category(models.Model): name = models.CharField(max_length=100) @@ -21,10 +23,24 @@ class Category(models.Model): super().save(*args, **kwargs) class Vendor(models.Model): + JIMMA_KEBELES = ( + ('Bosa Addis', 'Bosa Addis'), + ('Bosa Kitto', 'Bosa Kitto'), + ('Ginjo', 'Ginjo'), + ('Ginjo Guduru', 'Ginjo Guduru'), + ('Hermata', 'Hermata'), + ('Hermata Merkato', 'Hermata Merkato'), + ('Jiren', 'Jiren'), + ('Kofe', 'Kofe'), + ('Mendera Kochi', 'Mendera Kochi'), + ('Seto Semero', 'Seto Semero'), + ) + 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) + kebele = models.CharField(max_length=100, choices=JIMMA_KEBELES, blank=True) phone = models.CharField(max_length=20) is_verified = models.BooleanField(default=False) created_at = models.DateTimeField(default=timezone.now) @@ -37,7 +53,7 @@ class Product(models.Model): 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) + image = models.ImageField(upload_to='products/', blank=True, null=True) # Main image description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.IntegerField(default=0) @@ -53,6 +69,34 @@ class Product(models.Model): self.slug = slugify(self.name) super().save(*args, **kwargs) + @property + def average_rating(self): + avg = self.reviews.aggregate(Avg('rating'))['rating__avg'] + return round(avg, 1) if avg else 0 + + @property + def review_count(self): + return self.reviews.count() + +class ProductImage(models.Model): + product = models.ForeignKey(Product, related_name='images', on_delete=models.CASCADE) + image = models.ImageField(upload_to='products/gallery/') + alt_text = models.CharField(max_length=255, blank=True) + + def __str__(self): + return f"Image for {self.product.name}" + +class ProductReview(models.Model): + product = models.ForeignKey(Product, related_name='reviews', on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) # Optional user + full_name = models.CharField(max_length=255) # For guests + rating = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)]) + comment = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Review for {self.product.name} by {self.full_name}" + class Profile(models.Model): ROLE_CHOICES = ( ('customer', 'Customer'), @@ -75,11 +119,35 @@ class Order(models.Model): ('Delivered', 'Delivered'), ('Cancelled', 'Cancelled'), ) + + JIMMA_KEBELES = ( + ('Bosa Addis', 'Bosa Addis'), + ('Bosa Kitto', 'Bosa Kitto'), + ('Ginjo', 'Ginjo'), + ('Ginjo Guduru', 'Ginjo Guduru'), + ('Hermata', 'Hermata'), + ('Hermata Merkato', 'Hermata Merkato'), + ('Jiren', 'Jiren'), + ('Kofe', 'Kofe'), + ('Mendera Kochi', 'Mendera Kochi'), + ('Seto Semero', 'Seto Semero'), + # Add more as needed + ) + + TIME_SLOTS = ( + ('Morning', 'Morning (8:00 AM - 12:00 PM)'), + ('Afternoon', 'Afternoon (12:00 PM - 5:00 PM)'), + ('Evening', 'Evening (5:00 PM - 8:00 PM)'), + ) + 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() + kebele = models.CharField(max_length=100, choices=JIMMA_KEBELES, blank=True) + delivery_time_slot = models.CharField(max_length=50, choices=TIME_SLOTS, default='Morning') + 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') @@ -99,4 +167,22 @@ class OrderItem(models.Model): @property def total_price(self): - return self.price * self.quantity \ No newline at end of file + return self.price * self.quantity + +class Article(models.Model): + title = models.CharField(max_length=255) + slug = models.SlugField(unique=True, blank=True) + content = models.TextField() + image = models.ImageField(upload_to='articles/', blank=True, null=True) + author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='articles') + is_published = models.BooleanField(default=False) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.title) + super().save(*args, **kwargs) diff --git a/core/templates/base.html b/core/templates/base.html index 9c7a16f..2e2f842 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,169 +1,222 @@ {% load i18n static %} -
- - -