first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:20:45 +03:00
commit d50f14bcb1
681 changed files with 65020 additions and 0 deletions

0
portfolio/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

114
portfolio/admin.py Normal file
View File

@@ -0,0 +1,114 @@
from django.contrib import admin
from django.utils.safestring import mark_safe
from portfolio.models import Portfolio, Category
# Register your models here.
class CategoryAdmin(admin.ModelAdmin):
list_display = ('title', 'parent_category', 'category_image', 'is_active', 'order', 'slug', 'created_at')
list_filter = ('is_active', 'created_at', 'parent')
search_fields = ('title', 'slug', 'keywords', 'description')
list_editable = ('is_active', 'order')
readonly_fields = ('created_at', 'updated_at')
prepopulated_fields = {'slug': ('title',)}
fieldsets = (
('Genel Bilgiler', {
'fields': ('title', 'parent', 'slug')
}),
('SEO & Açıklama', {
'fields': ('keywords', 'description')
}),
('Görsel', {
'fields': ('image',)
}),
('Ayarlar', {
'fields': ('order', 'is_active')
}),
('Tarihler', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
class Meta:
model = Category
def parent_category(self, obj):
if obj.parent:
return obj.parent.title
return "Ana Kategori"
parent_category.short_description = 'Üst Kategori'
def category_image(self, obj):
if obj.image:
return mark_safe(
'<img src="{}" width="50" height="50" style="object-fit: cover; border-radius: 5px;" title="{}" />'.format(
obj.image.url, obj.title))
return mark_safe('<span style="color: #999;">Resim Yok</span>')
category_image.short_description = 'Görsel'
admin.site.register(Category, CategoryAdmin)
class PortfolioAdmin(admin.ModelAdmin):
list_display = ('get_portfolio_title', 'portfolio_image', 'portfolio_categories', 'is_active', 'created_at')
list_filter = ('is_active', 'created_at', 'categories')
search_fields = ('url',)
list_editable = ('is_active',)
readonly_fields = ('created_at', 'updated_at')
filter_horizontal = ('categories',)
fieldsets = (
('Portfolio Bilgileri', {
'fields': ('url', 'categories')
}),
('Görsel', {
'fields': ('image',)
}),
('Ayarlar', {
'fields': ('is_active',)
}),
('Tarihler', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
class Meta:
model = Portfolio
def get_portfolio_title(self, obj):
if obj.url:
return obj.url[:50] + '...' if len(obj.url) > 50 else obj.url
return f"Portfolio #{obj.id}"
get_portfolio_title.short_description = 'Portfolio'
def portfolio_image(self, obj):
if obj.image:
return mark_safe(
'<a href="{}" target="_blank"><img src="{}" width="80" height="60" style="object-fit: cover; border-radius: 5px; cursor: pointer;" title="Büyük görseli görmek için tıklayın" /></a>'.format(
obj.image.url, obj.image.url))
return mark_safe('<span style="color: #999;">Resim Yok</span>')
portfolio_image.short_description = 'Görsel'
def portfolio_categories(self, obj):
categories = obj.categories.all()
if categories:
html = '<ul style="margin: 0; padding-left: 20px;">'
for category in categories:
html += f'<li>{category.title}</li>'
html += '</ul>'
return mark_safe(html)
return mark_safe('<span style="color: #999;">Kategori Yok</span>')
portfolio_categories.short_description = 'Kategoriler'
admin.site.register(Portfolio, PortfolioAdmin)

8
portfolio/apps.py Normal file
View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class PortfolioConfig(AppConfig):
name = 'portfolio'
def ready(self):
import portfolio.signals

View File

@@ -0,0 +1,63 @@
import random
import io
from django.core.management.base import BaseCommand
from django.core.files.base import ContentFile
from faker import Faker
from PIL import Image, ImageDraw
from portfolio.models import Portfolio, Category
class Command(BaseCommand):
help = 'Creates fake portfolio items with images using existing categories'
def generate_image(self, width, height, format='PNG'):
"""Generates a random image."""
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
image = Image.new('RGB', (width, height), color)
draw = ImageDraw.Draw(image)
# Draw some random shapes
for _ in range(10):
shape_color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
x1 = random.randint(0, width)
y1 = random.randint(0, height)
x2 = random.randint(0, width)
y2 = random.randint(0, height)
# Ensure coordinates are in the correct order (x0, y0, x1, y1)
draw.rectangle(
[min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)],
fill=shape_color,
outline=None
)
img_io = io.BytesIO()
image.save(img_io, format=format)
return ContentFile(img_io.getvalue(), name=f'fake_image.{format.lower()}')
def handle(self, *args, **options):
fake = Faker()
# Fetch existing categories
categories = list(Category.objects.all())
if not categories:
self.stdout.write(self.style.WARNING('No categories found. Please create some categories first.'))
return
self.stdout.write(f'Found {len(categories)} categories. Creating portfolios...')
for i in range(20):
portfolio = Portfolio(
url=fake.url(),
is_active=True,
)
# Portfolio image: 640x481 (saving as PNG, model might convert to avif)
image_content = self.generate_image(640, 481, 'PNG')
portfolio.image.save(f'port_{i}.png', image_content, save=False)
portfolio.save()
# Assign random existing categories
# Ensure we don't try to sample more categories than exist
num_categories = min(len(categories), random.randint(1, 3))
portfolio.categories.set(random.sample(categories, k=num_categories))
self.stdout.write(self.style.SUCCESS('Successfully created fake portfolio items with images using existing categories.'))

View File

@@ -0,0 +1,60 @@
# Generated by Django 6.0 on 2026-01-14 10:41
import autoslug.fields
import core.utils
import django.db.models.deletion
import imagekit.models.fields
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')),
('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='ı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)),
('image', imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/category'), verbose_name='Resim 630 x 653 Olmali ve Transparan PNG Olmali')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='portfolio.category', verbose_name='Üst Kategorisi')),
],
options={
'verbose_name': 'Portfolio Kategori',
'verbose_name_plural': 'Portfolio Kategorilerileri',
'db_table': 'p_categories',
'ordering': ['order'],
'unique_together': {('slug', 'parent')},
},
),
migrations.CreateModel(
name='Portfolio',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField(blank=True, null=True, verbose_name='Portfolio Url')),
('image', imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/portfolio'))),
('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='portfolio_categories', to='portfolio.category', verbose_name='Post Kategorisi')),
('parent', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='portfolio.portfolio', verbose_name='Konular')),
],
options={
'verbose_name': 'Portfolio',
'verbose_name_plural': 'Portfolio',
'db_table': 'portfolios',
'ordering': ['created_at'],
},
),
]

View File

79
portfolio/models.py Normal file
View File

@@ -0,0 +1,79 @@
from autoslug import AutoSlugField
from django.db import models
from imagekit.models import ProcessedImageField
from core.utils import image_optimizer
# 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="ı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')
image = ProcessedImageField(**image_optimizer('uploads/category', 300, 300, 85, 'PNG'),
verbose_name='Resim 630 x 653 Olmali ve Transparan PNG Olmali', blank=True, null=True)
class Meta:
ordering = ["order"]
db_table = 'p_categories'
verbose_name_plural = "Portfolio Kategorilerileri"
verbose_name = "Portfolio 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 Portfolio(models.Model):
aktif = (
(True, 'Evet'),
(False, 'Hayır'),
)
url = models.URLField(verbose_name="Portfolio Url", null=True, blank=True)
categories = models.ManyToManyField(Category, verbose_name="Post Kategorisi", related_name='portfolio_categories')
image = ProcessedImageField(**image_optimizer('uploads/portfolio', 640, 481, 85, '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)
parent = models.ForeignKey('self', related_name='child', on_delete=models.CASCADE, blank=True, null=True,
editable=False,
verbose_name='Konular')
class Meta:
ordering = ["created_at"]
db_table = 'portfolios'
verbose_name_plural = "Portfolio"
verbose_name = "Portfolio"
def __str__(self):
return f"Portfolio: {self.url if self.url else f'ID: {self.id}'}"

58
portfolio/serializers.py Normal file
View File

@@ -0,0 +1,58 @@
from rest_framework import serializers
from django.conf import settings
from portfolio.models import Category, Portfolio
class PortfolioCateSerializer(serializers.ModelSerializer):
image = serializers.SerializerMethodField()
parent = serializers.StringRelatedField()
class Meta:
model = Portfolio
fields = ['id', 'url', 'image', 'parent', 'created_at', 'updated_at', 'is_active']
def get_image(self, obj):
if obj.image:
# Sadece path döndür, domain olmadan
return obj.image.url
return None
class CategorySerializer(serializers.ModelSerializer):
parent = serializers.StringRelatedField() # ID yerine __str__ metodundaki değeri döndürür
image = serializers.SerializerMethodField()
portfolio_categories = serializers.SerializerMethodField() # ManyToMany ilişkisi üzerinden portfolio'ları getir
class Meta:
model = Category
fields = ['id', 'title', 'parent', 'is_active', 'created_at', 'updated_at', 'order', 'slug', 'image',
'keywords', 'description', 'portfolio_categories']
def get_image(self, obj):
if obj.image:
# Sadece path döndür, domain olmadan
return obj.image.url
return None
def get_portfolio_categories(self, obj):
"""Category'ye ait portfolio'ları döndürür"""
portfolios = obj.portfolio_categories.filter(is_active=True).all()
# Circular import'u önlemek için PortfolioCateSerializer kullanıyoruz
return PortfolioCateSerializer(portfolios, many=True, context=self.context).data
class PortfolioSerializer(serializers.ModelSerializer):
categories = CategorySerializer(read_only=True, many=True)
image = serializers.SerializerMethodField()
parent = serializers.StringRelatedField()
class Meta:
model = Portfolio
fields = ['id', 'url', 'categories', 'image', 'parent', 'created_at', 'updated_at', 'is_active']
def get_image(self, obj):
if obj.image:
# Sadece path döndür, domain olmadan
return obj.image.url
return None

38
portfolio/signals.py Normal file
View File

@@ -0,0 +1,38 @@
from django.core.cache import cache
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from portfolio.models import Category, Portfolio
CATEGORY_LIST_CACHE_KEY = 'portfolio:category_list'
PORTFOLIO_LIST_CACHE_KEY = 'portfolio:portfolio_list'
def clear_category_cache(slug=None):
cache.delete(CATEGORY_LIST_CACHE_KEY)
if slug:
cache.delete(f"portfolio:category_detail:{slug}")
def clear_portfolio_cache(pk=None):
cache.delete(PORTFOLIO_LIST_CACHE_KEY)
if pk:
cache.delete(f"portfolio:portfolio_detail:{pk}")
@receiver(post_save, sender=Category)
@receiver(post_delete, sender=Category)
def clear_category_cache_on_change(sender, instance, **kwargs):
clear_category_cache(slug=instance.slug)
@receiver(post_save, sender=Portfolio)
@receiver(post_delete, sender=Portfolio)
def clear_portfolio_cache_on_change(sender, instance, **kwargs):
clear_portfolio_cache(pk=instance.pk)
@receiver(m2m_changed, sender=Portfolio.categories.through)
def clear_portfolio_cache_on_category_change(sender, instance, action, **kwargs):
if action in {"post_add", "post_remove", "post_clear"}:
clear_portfolio_cache(pk=instance.pk)

3
portfolio/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
portfolio/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from portfolio.views import CategoryList, CategoryDetail, PortfolioList, PortfolioDetail
urlpatterns = [
path('categories/', CategoryList.as_view(), name='categories.list'),
path('categories/<slug:slug>/', CategoryDetail.as_view(), name='categories.details'),
path('portfolio/', PortfolioList.as_view(), name='portfolio.list'),
path('portfolio/<int:pk>/', PortfolioDetail.as_view(), name='portfolio.details'),
]

82
portfolio/views.py Normal file
View File

@@ -0,0 +1,82 @@
from django.core.cache import cache
from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.response import Response
from core.Permission import ReadOnly
from portfolio.models import Portfolio, Category
from portfolio.serializers import PortfolioSerializer, CategorySerializer
CACHE_TTL_SECONDS = 60 * 5
class CategoryList(ListAPIView):
permission_classes = [ReadOnly]
queryset = Category.objects.order_by('order').filter(is_active=True, parent__isnull=True).all()
# serializer_class = ParentSerializer
serializer_class = CategorySerializer
def list(self, request, *args, **kwargs):
cache_key = 'portfolio:category_list'
cached_data = cache.get(cache_key)
if cached_data:
return Response(cached_data)
response = super().list(request, *args, **kwargs)
cache.set(cache_key, response.data, timeout=CACHE_TTL_SECONDS)
return response
class CategoryDetail(RetrieveAPIView):
permission_classes = [ReadOnly]
queryset = Category.objects.order_by('order').filter(is_active=True).all()
serializer_class = CategorySerializer
lookup_field = 'slug' # Slug ile arama yapılacak
def retrieve(self, request, *args, **kwargs):
cache_key = f"portfolio:category_detail:{kwargs.get('slug')}"
cached_data = cache.get(cache_key)
if cached_data:
return Response(cached_data)
response = super().retrieve(request, *args, **kwargs)
cache.set(cache_key, response.data, timeout=CACHE_TTL_SECONDS)
return response
def get_serializer_context(self):
context = super().get_serializer_context()
return context
# Create your views here.
class PortfolioList(ListAPIView):
permission_classes = [ReadOnly]
queryset = Portfolio.objects.all()
serializer_class = PortfolioSerializer
def list(self, request, *args, **kwargs):
cache_key = 'portfolio:portfolio_list'
cached_data = cache.get(cache_key)
if cached_data:
return Response(cached_data)
response = super().list(request, *args, **kwargs)
cache.set(cache_key, response.data, timeout=CACHE_TTL_SECONDS)
return response
class PortfolioDetail(RetrieveAPIView):
permission_classes = [ReadOnly]
queryset = Portfolio.objects.all()
serializer_class = PortfolioSerializer
lookup_field = 'pk' # Portfolio modelinde slug olmadığı için pk kullanılıyor
def retrieve(self, request, *args, **kwargs):
cache_key = f"portfolio:portfolio_detail:{kwargs.get('pk')}"
cached_data = cache.get(cache_key)
if cached_data:
return Response(cached_data)
response = super().retrieve(request, *args, **kwargs)
cache.set(cache_key, response.data, timeout=CACHE_TTL_SECONDS)
return response