first commit
This commit is contained in:
0
product/__init__.py
Normal file
0
product/__init__.py
Normal file
136
product/admin.py
Normal file
136
product/admin.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import format_html
|
||||
|
||||
from product.models import Product, Category, Images, Tags, ProductTree
|
||||
|
||||
|
||||
# Register your models here.
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'urun_resim', 'is_active', 'urun_kategorileri', 'brim', 'price', 'slug', 'urun_galeri')
|
||||
list_filter = ('is_active', 'categories')
|
||||
search_fields = ('title', 'is_active', 'slug', 'content')
|
||||
list_editable = ('is_active', 'brim', 'price', 'slug',)
|
||||
readonly_fields = ('thumb', 'image_preview', 'thumb_preview')
|
||||
filter_horizontal = ('categories', 'tags', 'gallery')
|
||||
|
||||
fieldsets = (
|
||||
('Temel Bilgiler', {
|
||||
'fields': ('title', 'content', 'categories', 'tags')
|
||||
}),
|
||||
('Fiyat ve Birim', {
|
||||
'fields': ('brim', 'price')
|
||||
}),
|
||||
('SEO ve Medya', {
|
||||
'fields': ('slug', 'keywords', 'video')
|
||||
}),
|
||||
('Görseller', {
|
||||
'fields': ('images', 'image_preview', 'thumb_preview', 'gallery'),
|
||||
'description': 'Thumb otomatik oluşturulur, images yüklediğinizde.'
|
||||
}),
|
||||
('Durum', {
|
||||
'fields': ('is_active', 'is_front')
|
||||
}),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
|
||||
def formatted_hit_count(self, obj):
|
||||
return obj.current_hit_count if obj.current_hit_count > 0 else '-'
|
||||
|
||||
formatted_hit_count.admin_order_field = 'hit_count'
|
||||
formatted_hit_count.short_description = 'Hits'
|
||||
|
||||
def blog_tags(self, obj):
|
||||
tags = '<ul>'
|
||||
for tag in obj.tags.all():
|
||||
tags += '<li>' + tag.tag + '</li>'
|
||||
tags += '</ul>'
|
||||
return mark_safe(tags)
|
||||
|
||||
def urun_kategorileri(self, obj):
|
||||
html = '<ul>'
|
||||
for category in obj.categories.all():
|
||||
html += '<li>' + category.title + '</li>'
|
||||
html += '</ul>'
|
||||
return mark_safe(html)
|
||||
|
||||
def urun_resim(self, obj):
|
||||
if obj.images:
|
||||
return mark_safe('<a href="/admin/product/product/{}/change/#id_images" onclick="window.location.href=\'/admin/product/product/{}/change/#id_images\'; return false;"><img src="{}" width="50" height="50" style="object-fit: cover; cursor: pointer;" title="Resmi değiştirmek için tıklayın" /></a>'.format(obj.id, obj.id, obj.images.url))
|
||||
return mark_safe('<a href="/admin/product/product/{}/change/#id_images" onclick="window.location.href=\'/admin/product/product/{}/change/#id_images\'; return false;">Resim Yok</a>'.format(obj.id, obj.id))
|
||||
|
||||
urun_resim.short_description = 'Ürün Resmi'
|
||||
|
||||
def image_preview(self, obj):
|
||||
if obj.images:
|
||||
return format_html('<img src="{0}" width="260" height="260" style="object-fit: cover; border: 1px solid #ddd;" />', obj.images.url)
|
||||
return "Resim Yok"
|
||||
image_preview.short_description = 'Ana Resim Önizleme'
|
||||
|
||||
def thumb_preview(self, obj):
|
||||
if obj.thumb:
|
||||
return format_html('<img src="{0}" width="150" height="150" style="object-fit: cover; border: 1px solid #ddd;" />', obj.thumb.url)
|
||||
return "Thumb Yok (Kaydet ve otomatik oluşur)"
|
||||
thumb_preview.short_description = 'Thumb Önizleme'
|
||||
|
||||
def urun_galeri(self, obj):
|
||||
html = '<ul>'
|
||||
for gal in obj.gallery.all():
|
||||
html += '<li>' + gal.title + '</li>'
|
||||
html += '</ul>'
|
||||
return mark_safe(html)
|
||||
|
||||
|
||||
admin.site.register(Product, ProductAdmin)
|
||||
|
||||
|
||||
class ProductTreeAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'is_active', 'price')
|
||||
list_filter = ('is_active',)
|
||||
search_fields = ('title', 'is_active', 'content')
|
||||
list_editable = ('is_active', 'price',)
|
||||
|
||||
class Meta:
|
||||
model = ProductTree
|
||||
|
||||
admin.site.register(ProductTree, ProductTreeAdmin)
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'parent', 'is_active', 'created_at', 'order', 'slug')
|
||||
list_filter = ('title', 'is_active', 'created_at',)
|
||||
search_fields = ('title', 'is_active', 'slug')
|
||||
list_editable = ('is_active', 'order', 'slug')
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
|
||||
|
||||
admin.site.register(Category, CategoryAdmin)
|
||||
|
||||
|
||||
class ImagesAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'images', 'created_at',)
|
||||
list_filter = ('title',)
|
||||
search_fields = ('title', 'images')
|
||||
list_editable = ('images',)
|
||||
|
||||
class Meta:
|
||||
model = Images
|
||||
|
||||
|
||||
admin.site.register(Images, ImagesAdmin)
|
||||
|
||||
|
||||
class TagsAdmin(admin.ModelAdmin):
|
||||
list_display = ('tag', 'created_at',)
|
||||
list_filter = ('tag',)
|
||||
search_fields = ('tag',)
|
||||
|
||||
class Meta:
|
||||
model = Tags
|
||||
|
||||
|
||||
admin.site.register(Tags, TagsAdmin)
|
||||
9
product/apps.py
Normal file
9
product/apps.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProductConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'product'
|
||||
|
||||
def ready(self):
|
||||
import product.signals
|
||||
0
product/management/__init__.py
Normal file
0
product/management/__init__.py
Normal file
0
product/management/commands/__init__.py
Normal file
0
product/management/commands/__init__.py
Normal file
131
product/management/commands/generate_fake_data.py
Normal file
131
product/management/commands/generate_fake_data.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import random
|
||||
import string
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files.base import ContentFile
|
||||
from PIL import Image, ImageDraw
|
||||
from product.models import Product, Category, Tags, Images
|
||||
|
||||
# Try to import Faker, use fallback if not available
|
||||
try:
|
||||
from faker import Faker
|
||||
fake = Faker(['tr_TR'])
|
||||
HAS_FAKER = True
|
||||
except ImportError:
|
||||
HAS_FAKER = False
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Generates fake data for products, categories, and tags.'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write("Generating fake data...")
|
||||
|
||||
if not HAS_FAKER:
|
||||
self.stdout.write(self.style.WARNING("Faker library not found. Using simple random data generator. Install with 'pip install faker' for better data."))
|
||||
|
||||
# 1. Generate Categories
|
||||
categories = []
|
||||
for i in range(10):
|
||||
title = fake.word().capitalize() if HAS_FAKER else f"Kategori {i+1}"
|
||||
keywords = ','.join(fake.words(nb=5)) if HAS_FAKER else "test, kategori, urun"
|
||||
description = fake.sentence() if HAS_FAKER else f"Bu kategori {i+1} için açıklamadır."
|
||||
|
||||
cat = Category.objects.create(
|
||||
title=title,
|
||||
keywords=keywords,
|
||||
description=description
|
||||
)
|
||||
categories.append(cat)
|
||||
self.stdout.write(self.style.SUCCESS(f"{len(categories)} categories created."))
|
||||
|
||||
# 2. Generate Tags
|
||||
tags = []
|
||||
for i in range(20):
|
||||
tag_name = fake.word() if HAS_FAKER else f"Etiket {i+1}"
|
||||
tag = Tags.objects.create(tag=tag_name)
|
||||
tags.append(tag)
|
||||
self.stdout.write(self.style.SUCCESS(f"{len(tags)} tags created."))
|
||||
|
||||
# 3. Generate Gallery Images
|
||||
gallery_images = []
|
||||
for i in range(15):
|
||||
img_name = f"gallery_{i}.jpg" # Using jpg for better compatibility
|
||||
img = self._generate_random_image(img_name)
|
||||
title = fake.word() if HAS_FAKER else f"Resim {i+1}"
|
||||
|
||||
image_instance = Images.objects.create(title=title)
|
||||
image_instance.images.save(img_name, img)
|
||||
gallery_images.append(image_instance)
|
||||
self.stdout.write(self.style.SUCCESS(f"{len(gallery_images)} gallery images created."))
|
||||
|
||||
# 4. Generate Products
|
||||
products = []
|
||||
for i in range(50):
|
||||
product_title = fake.company() if HAS_FAKER else f"Ürün {i+1} - {self._random_string(5)}"
|
||||
content = fake.paragraph(nb_sentences=10) if HAS_FAKER else f"Bu ürün {i+1} için detaylı açıklamadır. " * 5
|
||||
keywords = ','.join(fake.words(nb=5)) if HAS_FAKER else "urun, satis, online"
|
||||
video_code = fake.password(length=11, special_chars=False, upper_case=True, lower_case=True, digits=True) if HAS_FAKER else self._random_string(11)
|
||||
|
||||
product = Product.objects.create(
|
||||
title=product_title,
|
||||
content=content,
|
||||
keywords=keywords,
|
||||
brim=random.choice(['Adet', 'Kg', 'Porsiyon', 'Dilim']),
|
||||
price=round(random.uniform(10.0, 500.0), 2),
|
||||
video=f"https://www.youtube.com/watch?v={video_code}"
|
||||
)
|
||||
|
||||
# Assign categories, tags, and gallery
|
||||
product.categories.set(random.sample(categories, k=random.randint(1, 3)))
|
||||
product.tags.set(random.sample(tags, k=random.randint(1, 5)))
|
||||
product.gallery.set(random.sample(gallery_images, k=random.randint(1, 4)))
|
||||
|
||||
# Generate and assign main product image
|
||||
img_name = f"product_{i}.jpg"
|
||||
main_image = self._generate_random_image(img_name)
|
||||
product.images.save(img_name, main_image)
|
||||
|
||||
products.append(product)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"{len(products)} products created."))
|
||||
self.stdout.write(self.style.SUCCESS("Fake data generation complete!"))
|
||||
|
||||
def _generate_random_image(self, name):
|
||||
"""Generates a random image file in memory."""
|
||||
width, height = 400, 400
|
||||
img = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Random background color
|
||||
bg_color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
|
||||
draw.rectangle([0, 0, width, height], fill=bg_color)
|
||||
|
||||
# Draw some random shapes
|
||||
for _ in range(random.randint(3, 7)):
|
||||
shape_type = random.choice(['ellipse', 'rectangle'])
|
||||
|
||||
# Generate coordinates and sort them to ensure x0 <= x1 and y0 <= y1
|
||||
x1 = random.randint(0, width)
|
||||
x2 = random.randint(0, width)
|
||||
y1 = random.randint(0, height)
|
||||
y2 = random.randint(0, height)
|
||||
|
||||
xy = [
|
||||
(min(x1, x2), min(y1, y2)),
|
||||
(max(x1, x2), max(y1, y2))
|
||||
]
|
||||
|
||||
fill_color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
|
||||
|
||||
if shape_type == 'ellipse':
|
||||
draw.ellipse(xy, fill=fill_color)
|
||||
else:
|
||||
draw.rectangle(xy, fill=fill_color)
|
||||
|
||||
# Save image to a byte buffer
|
||||
from io import BytesIO
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format='JPEG') # Using JPEG as it is standard in PIL
|
||||
return ContentFile(buffer.getvalue(), name=name)
|
||||
|
||||
def _random_string(self, length):
|
||||
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
|
||||
38
product/management/commands/generate_product_thumbs.py
Normal file
38
product/management/commands/generate_product_thumbs.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from product.models import Product
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Tüm ürünler için eksik thumb dosyalarını oluşturur'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
products = Product.objects.filter(images__isnull=False)
|
||||
total = products.count()
|
||||
created = 0
|
||||
skipped = 0
|
||||
|
||||
self.stdout.write(f'\n{total} ürün kontrol ediliyor...\n')
|
||||
|
||||
for product in products:
|
||||
if not product.thumb:
|
||||
try:
|
||||
product.save() # save() metodu thumb'ı otomatik oluşturacak
|
||||
created += 1
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'✓ Thumb oluşturuldu: {product.title}')
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'✗ Hata ({product.title}): {str(e)}')
|
||||
)
|
||||
else:
|
||||
skipped += 1
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'- Atlandı (zaten var): {product.title}')
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'\n✓ Tamamlandı! {created} thumb oluşturuldu, {skipped} atlandı.\n'
|
||||
)
|
||||
)
|
||||
123
product/migrations/0001_initial.py
Normal file
123
product/migrations/0001_initial.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-03 04:02
|
||||
|
||||
import autoslug.fields
|
||||
import django.db.models.deletion
|
||||
import imagekit.models.fields
|
||||
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='Images',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=254, verbose_name='Resim Başlığı')),
|
||||
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı ?')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
|
||||
('images', imagekit.models.fields.ProcessedImageField(upload_to='uploads/product/%Y')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ürün Resmi',
|
||||
'verbose_name_plural': 'Ürün Resimleri',
|
||||
'db_table': 'images',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=254, verbose_name='Kategori')),
|
||||
('keywords', models.CharField(max_length=254, verbose_name='Seo Kelimeleri Aralarına Virgül Koyunuz')),
|
||||
('description', models.CharField(max_length=254, verbose_name='Açıklama')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
|
||||
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı')),
|
||||
('order', models.IntegerField(db_index=True, default=1, verbose_name='Görüntülenme Sırası')),
|
||||
('slug', autoslug.fields.AutoSlugField(blank=True, editable=True, max_length=250, populate_from='title', unique=True)),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='product.category', verbose_name='Üst Kategorisi')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ürün Kategori',
|
||||
'verbose_name_plural': 'Ürün Kategorilerileri',
|
||||
'db_table': 'categories',
|
||||
'ordering': ['order'],
|
||||
'unique_together': {('slug', 'parent')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tags',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tag', models.CharField(max_length=254, verbose_name='Ürün Tagları')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
|
||||
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı')),
|
||||
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='tag', unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ürün Tagı',
|
||||
'verbose_name_plural': 'Ürün Tagları',
|
||||
'db_table': 'tags',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('slug',)},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=254, verbose_name='Ürün Başlığı')),
|
||||
('content', models.TextField(blank=True, null=True, verbose_name='Ürün İçeriği')),
|
||||
('keywords', models.CharField(max_length=254, verbose_name='Seo Kelimeleri Aralarına Virgül Koyunuz')),
|
||||
('price', models.FloatField(verbose_name='Fiyatı')),
|
||||
('video', models.CharField(blank=True, default='none', max_length=254, null=True, verbose_name='Video')),
|
||||
('slug', autoslug.fields.AutoSlugField(editable=False, max_length=250, populate_from='title', unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
|
||||
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı ?')),
|
||||
('categories', models.ManyToManyField(related_name='categories', to='product.category', verbose_name='Ürün Kategorisi')),
|
||||
('images', models.ManyToManyField(related_name='img', to='product.images', verbose_name='Ürün Resimleri')),
|
||||
('tags', models.ManyToManyField(related_name='tags', to='product.tags', verbose_name='Ürün Tagları')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ürün',
|
||||
'verbose_name_plural': 'Ürünler',
|
||||
'db_table': 'products',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('slug',)},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=254, verbose_name='Yorum Başlığı')),
|
||||
('body', models.TextField(verbose_name='Yorum')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
|
||||
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı')),
|
||||
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='product.comment')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cuser', to=settings.AUTH_USER_MODEL)),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_product', to='product.product')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ürüm Yorum',
|
||||
'verbose_name_plural': 'Ürün Yorumları',
|
||||
'db_table': 'comments',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('slug', 'parent')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-03 04:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='kd_price',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Kg Fiyatı'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='price',
|
||||
field=models.FloatField(verbose_name='Top Fiyatı'),
|
||||
),
|
||||
]
|
||||
18
product/migrations/0003_rename_kd_price_product_kg_price.py
Normal file
18
product/migrations/0003_rename_kd_price_product_kg_price.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-03 04:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0002_product_kd_price_alter_product_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='product',
|
||||
old_name='kd_price',
|
||||
new_name='kg_price',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-03 13:21
|
||||
|
||||
import imagekit.models.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0003_rename_kd_price_product_kg_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='images',
|
||||
options={'ordering': ['-created_at'], 'verbose_name': 'Galeri Resmi', 'verbose_name_plural': 'Galeri Resimleri'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='images',
|
||||
name='images',
|
||||
field=imagekit.models.fields.ProcessedImageField(upload_to='uploads/galeri/%Y'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='images',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='price',
|
||||
field=models.FloatField(verbose_name='Birim Fiyatı'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='images',
|
||||
field=imagekit.models.fields.ProcessedImageField(default=1, upload_to='uploads/product/%Y'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-12 01:46
|
||||
|
||||
import imagekit.models.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0004_alter_images_options_alter_images_images_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='special',
|
||||
field=models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=False, verbose_name='Özel Ürünmü ?'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='special_images',
|
||||
field=imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to='uploads/product/special/%Y', verbose_name='Eğer bu Ürün Özel Ürünise Resim yükleyin !!'),
|
||||
),
|
||||
]
|
||||
18
product/migrations/0006_product_thumbnail.py
Normal file
18
product/migrations/0006_product_thumbnail.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-12 04:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0005_product_special_product_special_images'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='thumbnail',
|
||||
field=models.ImageField(blank=True, editable=False, null=True, upload_to='uploads/product/thumbs/%Y'),
|
||||
),
|
||||
]
|
||||
20
product/migrations/0007_category_images.py
Normal file
20
product/migrations/0007_category_images.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-12 18:43
|
||||
|
||||
import imagekit.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0006_product_thumbnail'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='images',
|
||||
field=imagekit.models.fields.ProcessedImageField(default=1, upload_to='uploads/category/%Y'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
17
product/migrations/0008_remove_product_special_images.py
Normal file
17
product/migrations/0008_remove_product_special_images.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-13 01:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0007_category_images'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='special_images',
|
||||
),
|
||||
]
|
||||
17
product/migrations/0009_remove_product_special.py
Normal file
17
product/migrations/0009_remove_product_special.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-13 01:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0008_remove_product_special_images'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='special',
|
||||
),
|
||||
]
|
||||
18
product/migrations/0010_alter_product_price.py
Normal file
18
product/migrations/0010_alter_product_price.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-13 03:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0009_remove_product_special'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='price',
|
||||
field=models.FloatField(default='50', verbose_name='Birim Fiyatı'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-13 12:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0010_alter_product_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='kg_price',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='brim',
|
||||
field=models.CharField(default=1, max_length=10, verbose_name='Birim'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
18
product/migrations/0012_alter_product_brim.py
Normal file
18
product/migrations/0012_alter_product_brim.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-13 12:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0011_remove_product_kg_price_product_brim'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='brim',
|
||||
field=models.CharField(choices=[('Top', 'Top'), ('Kg', 'Kg'), ('Adet', 'Adet'), ('Litre', 'Litre')], max_length=10, verbose_name='Birim'),
|
||||
),
|
||||
]
|
||||
18
product/migrations/0013_alter_product_brim.py
Normal file
18
product/migrations/0013_alter_product_brim.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-13 12:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0012_alter_product_brim'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='brim',
|
||||
field=models.CharField(choices=[('Top', 'Top'), ('Kg', 'Kg'), ('Adet', 'Adet'), ('Dilim', 'Dilim'), ('Litre', 'Litre')], max_length=10, verbose_name='Birim'),
|
||||
),
|
||||
]
|
||||
19
product/migrations/0014_alter_product_slug.py
Normal file
19
product/migrations/0014_alter_product_slug.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-13 13:02
|
||||
|
||||
import autoslug.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0013_alter_product_brim'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='slug',
|
||||
field=autoslug.fields.AutoSlugField(blank=True, editable=True, max_length=250, populate_from='title', unique=True),
|
||||
),
|
||||
]
|
||||
18
product/migrations/0015_alter_product_brim.py
Normal file
18
product/migrations/0015_alter_product_brim.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-14 14:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0014_alter_product_slug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='brim',
|
||||
field=models.CharField(choices=[('Top', 'Top'), ('Kg', 'Kg'), ('Adet', 'Adet'), ('Fincan', 'Fincan'), ('Bardak', 'Bardak'), ('Dilim', 'Dilim'), ('Litre', 'Litre')], max_length=10, verbose_name='Birim'),
|
||||
),
|
||||
]
|
||||
18
product/migrations/0016_product_gallery.py
Normal file
18
product/migrations/0016_product_gallery.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-14 15:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0015_alter_product_brim'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='gallery',
|
||||
field=models.ManyToManyField(related_name='gallery', to='product.images', verbose_name='Galeri'),
|
||||
),
|
||||
]
|
||||
18
product/migrations/0017_alter_product_brim.py
Normal file
18
product/migrations/0017_alter_product_brim.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-15 01:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0016_product_gallery'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='brim',
|
||||
field=models.CharField(choices=[('Top', 'Top'), ('Kg', 'Kg'), ('Adet', 'Adet'), ('Porsiyon', 'Porsiyon'), ('Fincan', 'Fincan'), ('Bardak', 'Bardak'), ('Dilim', 'Dilim'), ('Litre', 'Litre')], max_length=10, verbose_name='Birim'),
|
||||
),
|
||||
]
|
||||
18
product/migrations/0018_product_is_front.py
Normal file
18
product/migrations/0018_product_is_front.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-15 01:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0017_alter_product_brim'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='is_front',
|
||||
field=models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Önde Görünsünmü ?'),
|
||||
),
|
||||
]
|
||||
20
product/migrations/0019_product_thumb.py
Normal file
20
product/migrations/0019_product_thumb.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-15 14:32
|
||||
|
||||
import imagekit.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0018_product_is_front'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='thumb',
|
||||
field=imagekit.models.fields.ProcessedImageField(default=1, upload_to='uploads/thumb/%Y'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-15 14:35
|
||||
|
||||
import imagekit.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0019_product_thumb'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='thumbnail',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='thumb',
|
||||
field=imagekit.models.fields.ProcessedImageField(editable=False, upload_to='uploads/thumb/%Y'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 6.0 on 2026-01-18 22:26
|
||||
|
||||
import core.utils
|
||||
import imagekit.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0020_remove_product_thumbnail_alter_product_thumb'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='thumb',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='images',
|
||||
field=imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/category')),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='images',
|
||||
name='images',
|
||||
field=imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/images')),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='images',
|
||||
field=imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/post')),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 6.0 on 2026-01-18 22:37
|
||||
|
||||
import core.utils
|
||||
import imagekit.models.fields
|
||||
import tinymce.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0021_remove_product_thumb_alter_category_images_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='content',
|
||||
field=tinymce.models.HTMLField(blank=True, null=True, verbose_name='Ürün İçeriği'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='images',
|
||||
field=imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/products')),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 6.0 on 2026-01-19 19:04
|
||||
|
||||
import core.utils
|
||||
import imagekit.models.fields
|
||||
import tinymce.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0022_alter_product_content_alter_product_images'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='brim',
|
||||
field=models.CharField(choices=[('Kg', 'Kg'), ('Adet', 'Adet')], max_length=10, verbose_name='Birim'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='categories',
|
||||
field=models.ManyToManyField(related_name='products', to='product.category', verbose_name='Ürün Kategorisi'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductTree',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=254, verbose_name='Text')),
|
||||
('button', models.CharField(max_length=254, verbose_name='Button Text')),
|
||||
('content', tinymce.models.HTMLField(blank=True, null=True, verbose_name='Ürün İçeriği')),
|
||||
('price', models.FloatField(default='50', verbose_name='Birim Fiyatı')),
|
||||
('images', imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/products'))),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
|
||||
('is_active', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Yayındamı ?')),
|
||||
('is_front', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Önde Görünsünmü ?')),
|
||||
('categories', models.ManyToManyField(related_name='product_trees', to='product.category', verbose_name='Ürün Kategorisi')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ürün',
|
||||
'verbose_name_plural': 'Ürünler',
|
||||
'db_table': 'products_tree',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 6.0 on 2026-01-19 19:08
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0023_alter_product_brim_alter_product_categories_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='producttree',
|
||||
options={'ordering': ['-created_at'], 'verbose_name': 'Ürün Tree', 'verbose_name_plural': 'Ürünler Tree'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='producttree',
|
||||
name='categories',
|
||||
),
|
||||
]
|
||||
18
product/migrations/0025_alter_producttree_content.py
Normal file
18
product/migrations/0025_alter_producttree_content.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2026-01-19 19:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0024_alter_producttree_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='producttree',
|
||||
name='content',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Ürün İçeriği'),
|
||||
),
|
||||
]
|
||||
18
product/migrations/0026_producttree_categories.py
Normal file
18
product/migrations/0026_producttree_categories.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2026-01-20 18:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0025_alter_producttree_content'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='producttree',
|
||||
name='categories',
|
||||
field=models.ManyToManyField(related_name='product_trees', to='product.category', verbose_name='Ürün Kategorisi'),
|
||||
),
|
||||
]
|
||||
20
product/migrations/0027_product_thumb.py
Normal file
20
product/migrations/0027_product_thumb.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 6.0 on 2026-01-21 01:07
|
||||
|
||||
import core.utils
|
||||
import imagekit.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0026_producttree_categories'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='thumb',
|
||||
field=imagekit.models.fields.ProcessedImageField(blank=True, editable=False, null=True, upload_to=core.utils.UniquePathAndRename('uploads/products/thumb')),
|
||||
),
|
||||
]
|
||||
0
product/migrations/__init__.py
Normal file
0
product/migrations/__init__.py
Normal file
275
product/models.py
Normal file
275
product/models.py
Normal file
@@ -0,0 +1,275 @@
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from imagekit.models import ProcessedImageField
|
||||
from autoslug import AutoSlugField
|
||||
from tinymce.models import HTMLField
|
||||
|
||||
from core.utils import image_optimizer
|
||||
from reviews.models import RateableMixin # RateableMixin import edildi
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class Category(models.Model):
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
title = models.CharField(max_length=254, verbose_name="Kategori")
|
||||
keywords = models.CharField(max_length=254, verbose_name="Seo Kelimeleri Aralarına Virgül Koyunuz")
|
||||
description = models.CharField(max_length=254, verbose_name="Açıklama")
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
|
||||
is_active = models.BooleanField(default=True, verbose_name='Yayındamı', choices=aktif)
|
||||
order = models.IntegerField(verbose_name='Görüntülenme Sırası', default=1, db_index=True)
|
||||
slug = AutoSlugField(populate_from='title', null=False, unique=True, editable=True, db_index=True, max_length=250,
|
||||
blank=True)
|
||||
parent = models.ForeignKey('self', related_name='child', on_delete=models.CASCADE, blank=True, null=True,
|
||||
verbose_name='Üst Kategorisi')
|
||||
images = ProcessedImageField(**image_optimizer('uploads/category', 400, 400, 90, 'avif'), null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["order"]
|
||||
db_table = 'categories'
|
||||
verbose_name_plural = "Ürün Kategorilerileri"
|
||||
verbose_name = "Ürün Kategori"
|
||||
unique_together = ('slug', 'parent',)
|
||||
|
||||
def get_slug(self):
|
||||
slug = self.title.replace('ı', "i").replace('İ', 'i')
|
||||
number = 1
|
||||
while Category.objects.filter(slug=slug).exists():
|
||||
slug = '{}-{}'.format(slug, number)
|
||||
number += 1
|
||||
return slug
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = self.get_slug()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
full_path = [self.title]
|
||||
k = self.parent
|
||||
while k is not None:
|
||||
full_path.append(k.title)
|
||||
k = k.parent
|
||||
return ' -> '.join(full_path[::-1])
|
||||
|
||||
|
||||
class Tags(models.Model):
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
tag = models.CharField(max_length=254, verbose_name="Ürün Tagları")
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
|
||||
is_active = models.BooleanField(default=True, verbose_name='Yayındamı', choices=aktif)
|
||||
slug = AutoSlugField(populate_from='tag', null=False, unique=True, editable=False, db_index=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
db_table = 'tags'
|
||||
verbose_name_plural = "Ürün Tagları"
|
||||
verbose_name = "Ürün Tagı"
|
||||
unique_together = ('slug',)
|
||||
|
||||
def get_slug(self):
|
||||
slug = self.tag.replace('ı', "i").replace('İ', 'i')
|
||||
number = 1
|
||||
while Tags.objects.filter(slug=slug).exists():
|
||||
slug = '{}-{}'.format(slug, number)
|
||||
number += 1
|
||||
return slug
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = self.get_slug()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
class Images(models.Model):
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
title = models.CharField(max_length=254, verbose_name="Resim Başlığı")
|
||||
is_active = models.BooleanField(default=True, verbose_name='Yayındamı ?', choices=aktif)
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
|
||||
images = ProcessedImageField(**image_optimizer('uploads/images', 1500, 1500, 90, 'avif'), null=True, blank=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
db_table = 'images'
|
||||
verbose_name_plural = "Galeri Resimleri"
|
||||
verbose_name = "Galeri Resmi"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
|
||||
class Product(RateableMixin, models.Model): # RateableMixin eklendi
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
birim = (
|
||||
('Kg', 'Kg'),
|
||||
('Adet', 'Adet'),
|
||||
)
|
||||
title = models.CharField(max_length=254, verbose_name="Ürün Başlığı")
|
||||
content = HTMLField(blank=True, null=True, verbose_name='Ürün İçeriği')
|
||||
categories = models.ManyToManyField(Category, verbose_name="Ürün Kategorisi", related_name='products')
|
||||
keywords = models.CharField(max_length=254, verbose_name="Seo Kelimeleri Aralarına Virgül Koyunuz")
|
||||
brim = models.CharField(max_length=10, verbose_name="Birim",choices=birim)
|
||||
price = models.FloatField(verbose_name='Birim Fiyatı',default='50')
|
||||
tags = models.ManyToManyField(Tags, verbose_name="Ürün Tagları", related_name='tags')
|
||||
gallery = models.ManyToManyField(Images, verbose_name="Galeri", related_name='gallery')
|
||||
images = ProcessedImageField(**image_optimizer('uploads/products', 260, 260, 90, 'avif'), null=True, blank=True)
|
||||
thumb = ProcessedImageField(**image_optimizer('uploads/products/thumb', 150, 150, 85, 'avif'), null=True, blank=True, editable=False)
|
||||
video = models.CharField(verbose_name="Video", null=True, blank=True, max_length=254, default='none')
|
||||
slug = AutoSlugField(populate_from='title', null=False, unique=True, editable=True, db_index=True, max_length=250,
|
||||
blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
|
||||
is_active = models.BooleanField(default=True, verbose_name='Yayındamı ?', choices=aktif)
|
||||
is_front = models.BooleanField(default=True, verbose_name='Önde Görünsünmü ?', choices=aktif)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
db_table = 'products'
|
||||
verbose_name_plural = "Ürünler"
|
||||
verbose_name = "Ürün"
|
||||
unique_together = ('slug',)
|
||||
|
||||
def get_slug(self):
|
||||
slug = self.title.replace('ı', "i").replace('İ', 'i')
|
||||
number = 1
|
||||
while Product.objects.filter(slug=slug).exists():
|
||||
slug = '{}-{}'.format(slug, number)
|
||||
number += 1
|
||||
return slug
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = self.get_slug()
|
||||
|
||||
if self.images:
|
||||
# Eğer yeni bir kayıt ise veya resim değişmişse veya thumb yoksa
|
||||
update_thumb = False
|
||||
if not self.pk:
|
||||
update_thumb = True
|
||||
elif not self.thumb:
|
||||
# Thumb yoksa oluştur
|
||||
update_thumb = True
|
||||
else:
|
||||
try:
|
||||
old_instance = self.__class__.objects.get(pk=self.pk)
|
||||
if self.images != old_instance.images:
|
||||
update_thumb = True
|
||||
except self.__class__.DoesNotExist:
|
||||
pass
|
||||
|
||||
if update_thumb:
|
||||
try:
|
||||
if hasattr(self.images, 'closed') and self.images.closed:
|
||||
self.images.open()
|
||||
|
||||
if hasattr(self.images, 'seek'):
|
||||
self.images.seek(0)
|
||||
|
||||
content = self.images.read()
|
||||
filename = os.path.basename(self.images.name)
|
||||
|
||||
# Doğrudan alana ata, super().save() işleyecek
|
||||
self.thumb = ContentFile(content, name=filename)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if hasattr(self.images, 'seek'):
|
||||
self.images.seek(0)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"Ürünler: {self.title}"
|
||||
|
||||
class ProductTree(models.Model):
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
birim = (
|
||||
('Kg', 'Kg'),
|
||||
('Adet', 'Adet'),
|
||||
)
|
||||
title = models.CharField(max_length=254, verbose_name="Text")
|
||||
button = models.CharField(max_length=254, verbose_name="Button Text")
|
||||
content = models.TextField(blank=True, null=True, verbose_name='Ürün İçeriği')
|
||||
categories = models.ManyToManyField(Category, verbose_name="Ürün Kategorisi", related_name='product_trees')
|
||||
price = models.FloatField(verbose_name='Birim Fiyatı',default='50')
|
||||
images = ProcessedImageField(**image_optimizer('uploads/products', 740, 1100, 90, 'avif'), null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
|
||||
is_active = models.BooleanField(default=True, verbose_name='Yayındamı ?', choices=aktif)
|
||||
is_front = models.BooleanField(default=True, verbose_name='Önde Görünsünmü ?', choices=aktif)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
db_table = 'products_tree'
|
||||
verbose_name_plural = "Ürünler Tree"
|
||||
verbose_name = "Ürün Tree"
|
||||
|
||||
def __str__(self):
|
||||
return f"Ürünler Tree: {self.title}"
|
||||
|
||||
class Comment(models.Model):
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='cuser')
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='_product')
|
||||
title = models.CharField(max_length=254, verbose_name="Yorum Başlığı")
|
||||
body = models.TextField(verbose_name='Yorum')
|
||||
created_at = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Oluşturulma Tarihi")
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False, verbose_name="Güncelleme Tarihi")
|
||||
is_active = models.BooleanField(default=True, verbose_name='Yayındamı', choices=aktif)
|
||||
slug = AutoSlugField(populate_from='title', null=False, unique=True, editable=False, db_index=True)
|
||||
parent = models.ForeignKey('self', related_name='child', on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
db_table = 'comments'
|
||||
verbose_name_plural = "Ürün Yorumları"
|
||||
verbose_name = "Ürüm Yorum"
|
||||
unique_together = ('slug', 'parent',)
|
||||
|
||||
def get_slug(self):
|
||||
slug = self.title.replace('ı', "i").replace('İ', 'i')
|
||||
number = 1
|
||||
while Comment.objects.filter(slug=slug).exists():
|
||||
slug = '{}-{}'.format(slug, number)
|
||||
number += 1
|
||||
return slug
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = self.get_slug()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
full_path = [self.title]
|
||||
k = self.parent
|
||||
while k is not None:
|
||||
full_path.append(k.title)
|
||||
k = k.parent
|
||||
return ' -> '.join(full_path[::-1])
|
||||
100
product/serializers.py
Normal file
100
product/serializers.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from rest_framework import serializers
|
||||
from product.models import Category, Product, Images, Tags, ProductTree
|
||||
|
||||
|
||||
class CateSerializer(serializers.ModelSerializer):
|
||||
images = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ['title', 'parent', 'is_active', 'created_at', 'order', 'slug', 'images', 'keywords', 'description']
|
||||
|
||||
def get_images(self, obj):
|
||||
if obj.images:
|
||||
return obj.images.url
|
||||
return None
|
||||
|
||||
|
||||
class GalSerializer(serializers.ModelSerializer):
|
||||
images = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Images
|
||||
fields = ['title', 'images']
|
||||
|
||||
def get_images(self, obj):
|
||||
if obj.images:
|
||||
return obj.images.url
|
||||
return None
|
||||
|
||||
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Tags
|
||||
fields = ['tag', 'slug']
|
||||
|
||||
|
||||
class ProductTreeSerializer(serializers.ModelSerializer):
|
||||
images = serializers.SerializerMethodField()
|
||||
categories = CateSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = ProductTree
|
||||
fields = ['id', 'title', 'button', 'content', 'categories', 'price', 'images', 'created_at', 'updated_at', 'is_active', 'is_front']
|
||||
|
||||
def get_images(self, obj):
|
||||
if obj.images:
|
||||
return obj.images.url
|
||||
return None
|
||||
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
categories = CateSerializer(read_only=True, many=True)
|
||||
gallery = GalSerializer(read_only=True, many=True)
|
||||
tags = TagSerializer(read_only=True, many=True)
|
||||
images = serializers.SerializerMethodField()
|
||||
thumb = serializers.SerializerMethodField()
|
||||
|
||||
# RateableMixin'den gelen property'leri buraya ekliyoruz
|
||||
average_rating = serializers.FloatField(read_only=True)
|
||||
rating_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['id', 'title', 'content', 'categories', 'keywords', 'brim', 'tags', 'gallery', 'images', 'thumb', 'video',
|
||||
'slug', 'created_at', 'updated_at', 'is_active', 'is_front', 'price', 'average_rating', 'rating_count']
|
||||
|
||||
def get_images(self, obj):
|
||||
if obj.images:
|
||||
return obj.images.url
|
||||
return None
|
||||
|
||||
def get_thumb(self, obj):
|
||||
try:
|
||||
if obj.thumb:
|
||||
return obj.thumb.url
|
||||
except Exception:
|
||||
# Thumbnail oluşturulamadıysa veya bir hata olursa ana resmi dene
|
||||
if obj.images:
|
||||
return obj.images.url
|
||||
return None
|
||||
|
||||
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
categories = ProductSerializer(read_only=True, many=True)
|
||||
child = serializers.SerializerMethodField()
|
||||
images = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ['title', 'parent', 'is_active', 'created_at', 'order', 'slug', 'images', 'keywords', 'description',
|
||||
'categories', 'child']
|
||||
|
||||
def get_images(self, obj):
|
||||
if obj.images:
|
||||
return obj.images.url
|
||||
return None
|
||||
|
||||
def get_child(self, obj):
|
||||
serializer = self.__class__(obj.child.all(), many=True, context=self.context)
|
||||
return serializer.data
|
||||
35
product/signals.py
Normal file
35
product/signals.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.core.cache import cache
|
||||
from .models import Product, Category, ProductTree
|
||||
|
||||
@receiver([post_save, post_delete], sender=Product)
|
||||
def clear_product_cache(sender, instance, **kwargs):
|
||||
"""
|
||||
Product modeli güncellendiğinde, silindiğinde veya yeni bir ürün eklendiğinde
|
||||
ilgili cache'leri temizler.
|
||||
"""
|
||||
cache.delete('product:products:list')
|
||||
cache.delete('product:products:featured')
|
||||
cache.delete(f'product:product:{instance.slug}')
|
||||
|
||||
@receiver([post_save, post_delete], sender=Category)
|
||||
def clear_category_cache(sender, instance, **kwargs):
|
||||
"""
|
||||
Category modeli güncellendiğinde, silindiğinde veya yeni bir kategori eklendiğinde
|
||||
ilgili cache'leri temizler.
|
||||
"""
|
||||
cache.delete('product:categories:list')
|
||||
cache.delete(f'product:category:{instance.slug}')
|
||||
# Eğer alt kategoriler varsa, üst kategorinin de cache'ini temizlemek gerekebilir.
|
||||
if instance.parent:
|
||||
cache.delete(f'product:category:{instance.parent.slug}')
|
||||
|
||||
|
||||
@receiver([post_save, post_delete], sender=ProductTree)
|
||||
def clear_product_tree_cache(sender, instance, **kwargs):
|
||||
"""
|
||||
ProductTree modeli güncellendiğinde, silindiğinde veya yeni bir nesne eklendiğinde
|
||||
ilgili cache'i temizler.
|
||||
"""
|
||||
cache.delete('product:product_trees:list')
|
||||
3
product/tests.py
Normal file
3
product/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
13
product/urls.py
Normal file
13
product/urls.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.urls import path
|
||||
|
||||
from product.views import CategoryListView, CategoryRetrieveAPIView, ProductListAPIView, ProductRetrieveAPIView, \
|
||||
ProductListTreeAPIView, FeaturedProductListAPIView
|
||||
|
||||
urlpatterns = [ # Success/Error pages
|
||||
path('categories/', CategoryListView.as_view(), name='categories.list'),
|
||||
path('categories/<slug:slug>/', CategoryRetrieveAPIView.as_view(), name='categories.details'),
|
||||
path('products-tree/', ProductListTreeAPIView.as_view(), name='products.tree'),
|
||||
path('products-featured/', FeaturedProductListAPIView.as_view(), name='products.featured'),
|
||||
path('products/', ProductListAPIView.as_view(), name='products.list'),
|
||||
path('products/<slug:slug>/', ProductRetrieveAPIView.as_view(), name='products.details'),
|
||||
]
|
||||
137
product/views.py
Normal file
137
product/views.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from django.core.cache import cache
|
||||
from rest_framework import generics
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
from core.Permission import ReadOnly
|
||||
from product.models import Category, Product, ProductTree
|
||||
from product.serializers import CategorySerializer, ProductSerializer, ProductTreeSerializer
|
||||
|
||||
CACHE_TTL = 60 * 5 # 5 dakika
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 12
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class CategoryListView(generics.ListAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
serializer_class = CategorySerializer
|
||||
queryset = Category.objects.order_by('order').filter(is_active=True, parent__isnull=True).all()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
cache_key = 'product:categories:list'
|
||||
cached_data = cache.get(cache_key)
|
||||
if cached_data:
|
||||
return Response(cached_data)
|
||||
|
||||
queryset = self.get_queryset()
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
cache.set(cache_key, serializer.data, timeout=CACHE_TTL)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class CategoryRetrieveAPIView(generics.RetrieveAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
queryset = Category.objects.order_by('order').filter(is_active=True).all()
|
||||
serializer_class = CategorySerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
slug = self.kwargs.get('slug')
|
||||
cache_key = f'product:category:{slug}'
|
||||
cached_data = cache.get(cache_key)
|
||||
if cached_data:
|
||||
return Response(cached_data)
|
||||
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
cache.set(cache_key, serializer.data, timeout=CACHE_TTL)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ProductListAPIView(generics.ListAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
serializer_class = ProductSerializer
|
||||
queryset = Product.objects.filter(is_active=True).all()
|
||||
pagination_class = StandardResultsSetPagination
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Sayfa numarasını al
|
||||
page = request.query_params.get('page', 1)
|
||||
cache_key = f'product:products:list:page:{page}'
|
||||
|
||||
cached_data = cache.get(cache_key)
|
||||
if cached_data:
|
||||
return Response(cached_data)
|
||||
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# Sayfalama işlemini uygula
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
# Sayfalanmış yanıtı al (count, next, previous, results yapısı)
|
||||
response = self.get_paginated_response(serializer.data)
|
||||
# Yanıtın verisini (data) cache'e kaydet
|
||||
cache.set(cache_key, response.data, timeout=CACHE_TTL)
|
||||
return response
|
||||
|
||||
# Sayfalama yoksa normal döndür
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class FeaturedProductListAPIView(generics.ListAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
serializer_class = ProductSerializer
|
||||
queryset = Product.objects.filter(is_active=True, is_front=True).all()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
cache_key = 'product:products:featured'
|
||||
cached_data = cache.get(cache_key)
|
||||
if cached_data:
|
||||
return Response(cached_data)
|
||||
|
||||
queryset = self.get_queryset()
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
cache.set(cache_key, serializer.data, timeout=CACHE_TTL)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ProductListTreeAPIView(generics.ListAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
serializer_class = ProductTreeSerializer
|
||||
queryset = ProductTree.objects.filter(is_active=True).all()[:3]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
cache_key = 'product:product_trees:list'
|
||||
cached_data = cache.get(cache_key)
|
||||
if cached_data:
|
||||
return Response(cached_data)
|
||||
|
||||
queryset = self.get_queryset()
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
cache.set(cache_key, serializer.data, timeout=CACHE_TTL)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ProductRetrieveAPIView(generics.RetrieveAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
queryset = Product.objects.filter(is_active=True).all()
|
||||
serializer_class = ProductSerializer
|
||||
lookup_field = 'slug'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
slug = self.kwargs.get('slug')
|
||||
cache_key = f'product:product:{slug}'
|
||||
cached_data = cache.get(cache_key)
|
||||
if cached_data:
|
||||
return Response(cached_data)
|
||||
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
cache.set(cache_key, serializer.data, timeout=CACHE_TTL)
|
||||
return Response(serializer.data)
|
||||
Reference in New Issue
Block a user