first commit
This commit is contained in:
0
blog/__init__.py
Normal file
0
blog/__init__.py
Normal file
197
blog/admin.py
Normal file
197
blog/admin.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from blog.models import PCategory, Post, PComment, PCategoryView
|
||||
|
||||
|
||||
class ChildInline(admin.TabularInline):
|
||||
"""Alt kategorileri inline olarak göster"""
|
||||
model = PCategory
|
||||
extra = 0
|
||||
fk_name = 'parent'
|
||||
fields = ('title', 'is_active', 'order', 'slug')
|
||||
readonly_fields = ('slug',)
|
||||
|
||||
|
||||
@admin.register(PCategory)
|
||||
class PCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'parent', 'is_active', 'order', 'created_at', 'post_count', 'view_count', 'image_preview')
|
||||
list_filter = ('is_active', 'created_at', 'parent')
|
||||
search_fields = ('title', 'keywords', 'description', 'slug')
|
||||
list_editable = ('order', 'is_active')
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
readonly_fields = ('created_at', 'updated_at', 'image_preview', 'view_count')
|
||||
|
||||
fieldsets = (
|
||||
('Temel Bilgiler', {
|
||||
'fields': ('title', 'parent', 'order', 'is_active')
|
||||
}),
|
||||
('SEO Bilgileri', {
|
||||
'fields': ('slug', 'keywords', 'description')
|
||||
}),
|
||||
('Görsel', {
|
||||
'fields': ('image', 'image_preview')
|
||||
}),
|
||||
('Tarihler', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
inlines = [ChildInline]
|
||||
|
||||
def image_preview(self, obj):
|
||||
if obj.image:
|
||||
return format_html('<img src="{}" width="100" height="100" style="object-fit: cover;" />', obj.image.url)
|
||||
return "Resim Yok"
|
||||
image_preview.short_description = 'Resim Önizleme'
|
||||
|
||||
def post_count(self, obj):
|
||||
return obj.post_categories.count()
|
||||
post_count.short_description = 'Post Sayısı'
|
||||
|
||||
def view_count(self, obj):
|
||||
return obj.category_views.count()
|
||||
view_count.short_description = 'Görüntülenme'
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super().get_queryset(request)
|
||||
queryset = queryset.prefetch_related('post_categories', 'category_views')
|
||||
return queryset
|
||||
|
||||
|
||||
class PostCategoriesInline(admin.TabularInline):
|
||||
"""Post kategorilerini inline olarak göster"""
|
||||
model = Post.categories.through
|
||||
extra = 1
|
||||
verbose_name = 'Kategori'
|
||||
verbose_name_plural = 'Kategoriler'
|
||||
|
||||
|
||||
@admin.register(Post)
|
||||
class PostAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'user', 'is_active', 'is_front', 'created_at', 'comment_count', 'image_preview')
|
||||
list_filter = ('is_active', 'is_front', 'created_at', 'updated_at', 'categories')
|
||||
search_fields = ('title', 'content', 'keywords', 'slug')
|
||||
list_editable = ('is_active', 'is_front')
|
||||
readonly_fields = ('created_at', 'updated_at', 'slug', 'image_preview', 'thumb_preview', 'thumb', 'parent')
|
||||
filter_horizontal = ('categories',)
|
||||
|
||||
fieldsets = (
|
||||
('Temel Bilgiler', {
|
||||
'fields': ('title', 'user', 'content', 'categories')
|
||||
}),
|
||||
('SEO ve Medya', {
|
||||
'fields': ('slug', 'keywords', 'video')
|
||||
}),
|
||||
('Görseller', {
|
||||
'fields': ('image', 'image_preview', 'thumb_preview'),
|
||||
'description': 'Thumb otomatik oluşturulur, image yüklediğinizde.'
|
||||
}),
|
||||
('Durum', {
|
||||
'fields': ('is_active', 'is_front', 'parent')
|
||||
}),
|
||||
('Tarihler', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def image_preview(self, obj):
|
||||
if obj.image:
|
||||
return format_html('<img src="{0}" width="150" height="90" style="object-fit: cover; border: 1px solid #ddd;" />', obj.image.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="100" height="60" 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 comment_count(self, obj):
|
||||
return obj._post.filter(is_active=True).count()
|
||||
comment_count.short_description = 'Yorum Sayısı'
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super().get_queryset(request)
|
||||
queryset = queryset.prefetch_related('categories', '_post').select_related('user')
|
||||
return queryset
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Post kaydedilirken user otomatik atanabilir"""
|
||||
if not obj.user:
|
||||
obj.user = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
class ChildCommentInline(admin.TabularInline):
|
||||
"""Alt yorumları inline olarak göster"""
|
||||
model = PComment
|
||||
extra = 0
|
||||
fk_name = 'parent'
|
||||
fields = ('user', 'title', 'body', 'is_active')
|
||||
readonly_fields = ('user',)
|
||||
|
||||
|
||||
@admin.register(PComment)
|
||||
class PCommentAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'user', 'post', 'parent', 'is_active', 'created_at', 'child_count')
|
||||
list_filter = ('is_active', 'created_at', 'post')
|
||||
search_fields = ('title', 'body', 'user__username', 'user__email', 'post__title')
|
||||
list_editable = ('is_active',)
|
||||
readonly_fields = ('created_at', 'updated_at', 'slug', 'user', 'post')
|
||||
|
||||
fieldsets = (
|
||||
('Yorum Bilgileri', {
|
||||
'fields': ('user', 'post', 'parent')
|
||||
}),
|
||||
('İçerik', {
|
||||
'fields': ('title', 'body', 'slug')
|
||||
}),
|
||||
('Durum', {
|
||||
'fields': ('is_active',)
|
||||
}),
|
||||
('Tarihler', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
inlines = [ChildCommentInline]
|
||||
|
||||
def child_count(self, obj):
|
||||
return obj.child.count()
|
||||
child_count.short_description = 'Alt Yorum Sayısı'
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super().get_queryset(request)
|
||||
queryset = queryset.select_related('user', 'post', 'parent').prefetch_related('child')
|
||||
return queryset
|
||||
|
||||
|
||||
@admin.register(PCategoryView)
|
||||
class PCategoryViewAdmin(admin.ModelAdmin):
|
||||
list_display = ('category', 'ip_address', 'created_at', 'short_user_agent')
|
||||
list_filter = ('created_at', 'category')
|
||||
search_fields = ('ip_address', 'user_agent', 'category__title')
|
||||
readonly_fields = ('category', 'ip_address', 'user_agent', 'created_at')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
def short_user_agent(self, obj):
|
||||
if obj.user_agent:
|
||||
return obj.user_agent[:50] + '...' if len(obj.user_agent) > 50 else obj.user_agent
|
||||
return '-'
|
||||
short_user_agent.short_description = 'User Agent'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Kategori ziyaretleri manuel eklenemez"""
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Kategori ziyaretleri düzenlenemez"""
|
||||
return False
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super().get_queryset(request)
|
||||
queryset = queryset.select_related('category')
|
||||
return queryset
|
||||
9
blog/apps.py
Normal file
9
blog/apps.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
name = 'blog'
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
|
||||
def ready(self):
|
||||
import blog.signals
|
||||
0
blog/management/__init__.py
Normal file
0
blog/management/__init__.py
Normal file
0
blog/management/commands/__init__.py
Normal file
0
blog/management/commands/__init__.py
Normal file
85
blog/management/commands/create_fake_posts.py
Normal file
85
blog/management/commands/create_fake_posts.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import requests
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files.base import ContentFile
|
||||
from blog.models import Post, Category, Tags
|
||||
|
||||
try:
|
||||
from faker import Faker
|
||||
fake = Faker()
|
||||
HAS_FAKER = True
|
||||
except ImportError:
|
||||
HAS_FAKER = False
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Creates 300 fake posts with random images'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
User = get_user_model()
|
||||
# Prefer an existing staff user, otherwise any existing user, otherwise create a fallback user
|
||||
user = User.objects.filter(is_staff=True).first() or User.objects.first()
|
||||
if not user:
|
||||
self.stdout.write('Hiç kullanıcı bulunamadı, `fakeuser` oluşturuluyor...')
|
||||
user = User.objects.create(username='fakeuser', email='fake@example.com')
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
categories = list(Category.objects.all())
|
||||
if not categories:
|
||||
self.stdout.write(self.style.ERROR('Lütfen önce en az bir kategori oluşturun!'))
|
||||
return
|
||||
|
||||
tags = list(Tags.objects.all())
|
||||
if not tags:
|
||||
self.stdout.write('Tag bulunamadı, oluşturuluyor...')
|
||||
for i in range(5):
|
||||
tag_name = self.get_random_string(8) if not HAS_FAKER else fake.word()
|
||||
Tags.objects.create(tag=tag_name)
|
||||
tags = list(Tags.objects.all())
|
||||
|
||||
self.stdout.write('300 adet fake post oluşturuluyor...')
|
||||
|
||||
for i in range(300):
|
||||
if HAS_FAKER:
|
||||
title = fake.sentence(nb_words=6).replace('.', '')
|
||||
content = '\n\n'.join(fake.paragraphs(nb=5))
|
||||
keywords = ", ".join(fake.words(nb=5))
|
||||
else:
|
||||
title = self.get_random_string(30)
|
||||
content = self.get_random_string(500)
|
||||
keywords = self.get_random_string(20)
|
||||
|
||||
post = Post(
|
||||
user=user,
|
||||
title=title,
|
||||
content=content,
|
||||
keywords=keywords,
|
||||
video='none',
|
||||
is_active=True,
|
||||
is_front=True
|
||||
)
|
||||
post.save()
|
||||
|
||||
# ManyToMany ilişkileri
|
||||
post.categories.add(random.choice(categories))
|
||||
post.tags.add(random.choice(tags))
|
||||
|
||||
# Resim ekle
|
||||
try:
|
||||
# Picsum'dan rastgele resim (800x600)
|
||||
img_url = f"https://picsum.photos/seed/{random.randint(1, 10000)}/800/600"
|
||||
response = requests.get(img_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
file_name = f"fake_post_{i}_{random.randint(1000,9999)}.jpg"
|
||||
post.image.save(file_name, ContentFile(response.content), save=True)
|
||||
self.stdout.write(f'Post {i+1}/300 oluşturuldu: {title} (Resimli)')
|
||||
else:
|
||||
self.stdout.write(f'Post {i+1}/300 oluşturuldu: {title} (Resimsiz - İndirme hatası)')
|
||||
except Exception as e:
|
||||
self.stdout.write(f'Post {i+1}/300 oluşturuldu: {title} (Resimsiz - Hata: {str(e)})')
|
||||
|
||||
def get_random_string(self, length):
|
||||
letters = string.ascii_letters + string.digits + ' '
|
||||
return ''.join(random.choice(letters) for i in range(length))
|
||||
38
blog/management/commands/generate_blog_thumbs.py
Normal file
38
blog/management/commands/generate_blog_thumbs.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from blog.models import Post
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Tüm blog postları için eksik thumb dosyalarını oluşturur'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
posts = Post.objects.filter(image__isnull=False)
|
||||
total = posts.count()
|
||||
created = 0
|
||||
skipped = 0
|
||||
|
||||
self.stdout.write(f'\n{total} post kontrol ediliyor...\n')
|
||||
|
||||
for post in posts:
|
||||
if not post.thumb:
|
||||
try:
|
||||
post.save() # save() metodu thumb'ı otomatik oluşturacak
|
||||
created += 1
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'✓ Thumb oluşturuldu: {post.title}')
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'✗ Hata ({post.title}): {str(e)}')
|
||||
)
|
||||
else:
|
||||
skipped += 1
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'- Atlandı (zaten var): {post.title}')
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'\n✓ Tamamlandı! {created} thumb oluşturuldu, {skipped} atlandı.\n'
|
||||
)
|
||||
)
|
||||
109
blog/migrations/0001_initial.py
Normal file
109
blog/migrations/0001_initial.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Generated by Django 6.0 on 2026-01-21 00:59
|
||||
|
||||
import autoslug.fields
|
||||
import core.utils
|
||||
import django.db.models.deletion
|
||||
import imagekit.models.fields
|
||||
import tinymce.models
|
||||
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='PCategory',
|
||||
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)),
|
||||
('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='blog.pcategory', verbose_name='Üst Kategorisi')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Post Kategori',
|
||||
'verbose_name_plural': 'Post Kategorilerileri',
|
||||
'db_table': 'p_categories',
|
||||
'ordering': ['order'],
|
||||
'unique_together': {('slug', 'parent')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Post',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=254, verbose_name='Post Başlığı')),
|
||||
('content', tinymce.models.HTMLField(blank=True, null=True, verbose_name='Post İçeriği')),
|
||||
('keywords', models.CharField(max_length=254, verbose_name='Seo Kelimeleri Aralarına Virgül Koyunuz')),
|
||||
('image', imagekit.models.fields.ProcessedImageField(blank=True, null=True, upload_to=core.utils.UniquePathAndRename('uploads/post'))),
|
||||
('thumb', imagekit.models.fields.ProcessedImageField(blank=True, editable=False, null=True, upload_to=core.utils.UniquePathAndRename('uploads/post/thumb'))),
|
||||
('video', models.CharField(blank=True, default='none', max_length=254, null=True, verbose_name='Video')),
|
||||
('slug', autoslug.fields.AutoSlugField(blank=True, editable=True, 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ı ?')),
|
||||
('is_front', models.BooleanField(choices=[(True, 'Evet'), (False, 'Hayır')], default=True, verbose_name='Önde Görünsünmü ?')),
|
||||
('categories', models.ManyToManyField(related_name='post_categories', to='blog.pcategory', verbose_name='Post Kategorisi')),
|
||||
('parent', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='blog.post', verbose_name='Konular')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Post',
|
||||
'verbose_name_plural': 'Posts',
|
||||
'db_table': 'posts',
|
||||
'ordering': ['created_at'],
|
||||
'unique_together': {('slug',)},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PCategoryView',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ip_address', models.GenericIPAddressField(verbose_name='IP Adresi')),
|
||||
('user_agent', models.TextField(blank=True, null=True, verbose_name='User Agent')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Ziyaret Tarihi')),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='category_views', to='blog.pcategory')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Kategori Ziyareti',
|
||||
'verbose_name_plural': 'Kategori Ziyaretleri',
|
||||
'db_table': 'p_category_views',
|
||||
'indexes': [models.Index(fields=['category', 'ip_address', 'created_at'], name='p_category__categor_8efae6_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PComment',
|
||||
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='blog.pcomment')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_cuser', to=settings.AUTH_USER_MODEL)),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_post', to='blog.post')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Post Yorum',
|
||||
'verbose_name_plural': 'Post Yorumları',
|
||||
'db_table': 'p_comments',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('slug', 'parent')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
blog/migrations/__init__.py
Normal file
0
blog/migrations/__init__.py
Normal file
216
blog/models.py
Normal file
216
blog/models.py
Normal file
@@ -0,0 +1,216 @@
|
||||
import os
|
||||
from autoslug import AutoSlugField
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from imagekit.models import ProcessedImageField
|
||||
from tinymce.models import HTMLField
|
||||
|
||||
from core.utils import image_optimizer
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class PCategory(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')
|
||||
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 = "Post Kategorilerileri"
|
||||
verbose_name = "Post Kategori"
|
||||
unique_together = ('slug', 'parent',)
|
||||
|
||||
def get_slug(self):
|
||||
slug = self.title.replace('ı', "i").replace('İ', 'i')
|
||||
number = 1
|
||||
while PCategory.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 Post(models.Model):
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
|
||||
title = models.CharField(max_length=254, verbose_name="Post Başlığı")
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='posts', null=True, blank=True)
|
||||
content = HTMLField(blank=True, null=True, verbose_name='Post İçeriği')
|
||||
categories = models.ManyToManyField(PCategory, verbose_name="Post Kategorisi", related_name='post_categories')
|
||||
keywords = models.CharField(max_length=254, verbose_name="Seo Kelimeleri Aralarına Virgül Koyunuz")
|
||||
image = ProcessedImageField(**image_optimizer('uploads/post', 840, 500, 85, 'avif'), null=True, blank=True)
|
||||
thumb = ProcessedImageField(**image_optimizer('uploads/post/thumb', 348, 160, 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)
|
||||
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 = 'posts'
|
||||
verbose_name_plural = "Posts"
|
||||
verbose_name = "Post"
|
||||
unique_together = ('slug',)
|
||||
|
||||
def get_slug(self):
|
||||
slug = self.title.replace('ı', "i").replace('İ', 'i')
|
||||
number = 1
|
||||
while Post.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.image:
|
||||
# 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.image != old_instance.image:
|
||||
update_thumb = True
|
||||
except self.__class__.DoesNotExist:
|
||||
pass
|
||||
|
||||
if update_thumb:
|
||||
try:
|
||||
if hasattr(self.image, 'closed') and self.image.closed:
|
||||
self.image.open()
|
||||
|
||||
if hasattr(self.image, 'seek'):
|
||||
self.image.seek(0)
|
||||
|
||||
content = self.image.read()
|
||||
filename = os.path.basename(self.image.name)
|
||||
|
||||
# Doğrudan alana ata, super().save() işleyecek
|
||||
self.thumb = ContentFile(content, name=filename)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if hasattr(self.image, 'seek'):
|
||||
self.image.seek(0)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"Postlar: {self.title}"
|
||||
|
||||
|
||||
class PCategoryView(models.Model):
|
||||
"""Kategori ziyaretlerini takip etmek için model"""
|
||||
category = models.ForeignKey(PCategory, on_delete=models.CASCADE, related_name='category_views')
|
||||
ip_address = models.GenericIPAddressField(verbose_name='IP Adresi')
|
||||
user_agent = models.TextField(blank=True, null=True, verbose_name='User Agent')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Ziyaret Tarihi')
|
||||
|
||||
class Meta:
|
||||
db_table = 'p_category_views'
|
||||
verbose_name = 'Kategori Ziyareti'
|
||||
verbose_name_plural = 'Kategori Ziyaretleri'
|
||||
# unique_together kısıtlamasını kaldırdık - artık günlük bazda kontrol edeceğiz
|
||||
indexes = [
|
||||
models.Index(fields=['category', 'ip_address', 'created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.category.title} - {self.ip_address} - {self.created_at.strftime('%Y-%m-%d %H:%M')}"
|
||||
|
||||
|
||||
class PComment(models.Model):
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='_cuser')
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='_post')
|
||||
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 = 'p_comments'
|
||||
verbose_name_plural = "Post Yorumları"
|
||||
verbose_name = "Post 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()
|
||||
|
||||
if self.parent:
|
||||
self.post = self.parent.post
|
||||
if self.parent.parent:
|
||||
self.parent = self.parent.parent
|
||||
|
||||
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])
|
||||
153
blog/serializers.py
Normal file
153
blog/serializers.py
Normal file
@@ -0,0 +1,153 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from blog.models import PCategory, Post, PComment
|
||||
from blog.tasks import send_comment_notification_email
|
||||
|
||||
|
||||
class CateSerializer(serializers.ModelSerializer):
|
||||
parent = serializers.StringRelatedField() # ID yerine __str__ metodundaki değeri döndürür
|
||||
|
||||
class Meta:
|
||||
model = PCategory
|
||||
fields = ['title', 'parent', 'is_active', 'created_at', 'order', 'slug', 'image', 'keywords', 'description']
|
||||
|
||||
|
||||
|
||||
|
||||
class CommentSerializer(serializers.ModelSerializer):
|
||||
child = serializers.SerializerMethodField()
|
||||
user = serializers.StringRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PComment
|
||||
fields = ['id', 'user', 'post', 'title', 'body', 'created_at', 'slug', 'parent', 'child']
|
||||
read_only_fields = ['slug', 'created_at', 'user']
|
||||
|
||||
def get_child(self, obj):
|
||||
# Sadece aktif alt yorumları getir
|
||||
children = obj.child.filter(is_active=True).order_by('created_at')
|
||||
return CommentSerializer(children, many=True).data
|
||||
|
||||
def create(self, validated_data):
|
||||
# Kullanıcıyı request'ten al
|
||||
request = self.context.get('request')
|
||||
if request and hasattr(request, 'user'):
|
||||
validated_data['user'] = request.user
|
||||
|
||||
instance = super().create(validated_data)
|
||||
|
||||
# Celery task'ini tetikle
|
||||
# Kullanıcı email'i varsa al, yoksa username kullan
|
||||
user_email = instance.user.email if instance.user.email else instance.user.username
|
||||
|
||||
send_comment_notification_email.delay(
|
||||
comment_title=instance.title,
|
||||
comment_body=instance.body,
|
||||
post_title=instance.post.title,
|
||||
user_email=user_email
|
||||
)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class PostSerializer(serializers.ModelSerializer):
|
||||
categories = CateSerializer(read_only=True, many=True)
|
||||
comments = serializers.SerializerMethodField()
|
||||
image = serializers.SerializerMethodField()
|
||||
thumb = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = ['id','title', 'content', 'categories', 'keywords', 'image', 'thumb', 'video',
|
||||
'slug', 'created_at', 'updated_at', 'is_active', 'is_front', 'comments']
|
||||
# fields = '__all__'
|
||||
|
||||
def get_image(self, obj):
|
||||
if obj.image:
|
||||
# Sadece path kısmını döndür (media/ ile başlayan kısım)
|
||||
url = obj.image.url
|
||||
# URL'de domain varsa çıkar, yoksa olduğu gibi döndür
|
||||
if 'http://' in url or 'https://' in url:
|
||||
# URL'den sadece path kısmını al
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
return parsed.path
|
||||
return url
|
||||
return None
|
||||
|
||||
def get_thumb(self, obj):
|
||||
if obj.thumb:
|
||||
# Sadece path kısmını döndür (media/ ile başlayan kısım)
|
||||
url = obj.thumb.url
|
||||
# URL'de domain varsa çıkar, yoksa olduğu gibi döndür
|
||||
if 'http://' in url or 'https://' in url:
|
||||
# URL'den sadece path kısmını al
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
return parsed.path
|
||||
return url
|
||||
return None
|
||||
|
||||
def get_comments(self, obj):
|
||||
# Sadece ana yorumları (parent=None) ve aktif olanları getir
|
||||
comments = obj._post.filter(parent__isnull=True, is_active=True).order_by('-created_at')
|
||||
return CommentSerializer(comments, many=True).data
|
||||
|
||||
|
||||
class PostSYalinerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = ['slug', ]
|
||||
# fields = '__all__'
|
||||
|
||||
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
|
||||
posts = PostSYalinerializer(source='c_categories', read_only=True, many=True)
|
||||
child = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = PCategory
|
||||
fields = ['title', 'parent', 'is_active', 'created_at', 'order', 'slug', 'image', 'keywords', 'description',
|
||||
'posts', 'child']
|
||||
|
||||
def get_child(self, obj):
|
||||
serializer = self.__class__(obj.child.all(), many=True, context=self.context)
|
||||
return serializer.data
|
||||
|
||||
|
||||
class CategoryPostSerializer(serializers.ModelSerializer):
|
||||
|
||||
posts = serializers.SerializerMethodField()
|
||||
child = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = PCategory
|
||||
fields = ['title', 'parent', 'is_active', 'created_at', 'order', 'slug', 'image', 'keywords', 'description',
|
||||
'posts', 'child']
|
||||
|
||||
def get_posts(self, obj):
|
||||
# Pagination context'ini al
|
||||
paginator = self.context.get('paginator')
|
||||
request = self.context.get('request')
|
||||
|
||||
posts = obj.c_categories.all()
|
||||
|
||||
if paginator and request:
|
||||
# Pagination uygula
|
||||
paginated_posts = paginator.paginate_queryset(posts, request)
|
||||
serializer = PostSerializer(paginated_posts, many=True, context=self.context)
|
||||
return {
|
||||
'results': serializer.data,
|
||||
'count': posts.count(),
|
||||
'next': paginator.get_next_link(),
|
||||
'previous': paginator.get_previous_link(),
|
||||
}
|
||||
else:
|
||||
# Pagination yoksa normal döndür
|
||||
serializer = PostSerializer(posts, many=True, context=self.context)
|
||||
return serializer.data
|
||||
|
||||
def get_child(self, obj):
|
||||
serializer = self.__class__(obj.child.all(), many=True, context=self.context)
|
||||
return serializer.data
|
||||
52
blog/signals.py
Normal file
52
blog/signals.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.core.files.base import ContentFile
|
||||
from .models import Post
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Post)
|
||||
def update_post_thumb(sender, instance, **kwargs):
|
||||
"""
|
||||
Post kaydedilmeden önce, image alanı doluysa thumb'ı da güncelle
|
||||
"""
|
||||
if instance.image:
|
||||
# Yeni kayıt veya image güncellenmiş mi kontrol et
|
||||
should_update_thumb = False
|
||||
|
||||
if instance.pk:
|
||||
try:
|
||||
old_instance = Post.objects.get(pk=instance.pk)
|
||||
# Image değişmişse thumb'ı da güncelle
|
||||
if str(old_instance.image) != str(instance.image):
|
||||
should_update_thumb = True
|
||||
except Post.DoesNotExist:
|
||||
# Kayıt bulunamadı, yeni kayıt gibi davran
|
||||
should_update_thumb = True
|
||||
else:
|
||||
# Yeni kayıt (pk yok)
|
||||
should_update_thumb = True
|
||||
|
||||
if should_update_thumb and hasattr(instance.image, 'file'):
|
||||
# Image dosyasını thumb alanına kopyala
|
||||
try:
|
||||
# Image dosyasının içeriğini oku
|
||||
instance.image.file.seek(0)
|
||||
image_content = instance.image.file.read()
|
||||
instance.image.file.seek(0)
|
||||
|
||||
# Dosya adını al
|
||||
image_name = instance.image.name.split('/')[-1]
|
||||
|
||||
# Thumb alanına kaydet
|
||||
instance.thumb.save(
|
||||
image_name,
|
||||
ContentFile(image_content),
|
||||
save=False
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Thumb oluşturma hatası: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
|
||||
40
blog/tasks.py
Normal file
40
blog/tasks.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from celery import shared_task
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
|
||||
@shared_task
|
||||
def send_comment_notification_email(comment_title, comment_body, post_title, user_email):
|
||||
"""
|
||||
Yeni bir yorum yapıldığında admin'e e-posta gönderir.
|
||||
"""
|
||||
subject = f'Yeni Yorum: {post_title}'
|
||||
message = f"""
|
||||
Merhaba Admin,
|
||||
|
||||
"{post_title}" başlıklı yazıya yeni bir yorum yapıldı.
|
||||
|
||||
Yorum Yapan: {user_email}
|
||||
Başlık: {comment_title}
|
||||
Yorum: {comment_body}
|
||||
|
||||
Kontrol etmek için admin paneline giriş yapabilirsiniz.
|
||||
"""
|
||||
|
||||
# Admin e-posta adresini settings'den veya doğrudan buraya yazabilirsiniz
|
||||
# Örnek olarak settings.DEFAULT_FROM_EMAIL kullanıldı, admin listesi de kullanılabilir
|
||||
admin_email = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# Eğer settings.ADMINS tanımlıysa oradaki ilk kişiye de atılabilir
|
||||
if hasattr(settings, 'ADMINS') and settings.ADMINS:
|
||||
recipient_list = [email for name, email in settings.ADMINS]
|
||||
else:
|
||||
# Fallback olarak bir email
|
||||
recipient_list = ['admin@example.com']
|
||||
|
||||
send_mail(
|
||||
subject,
|
||||
message,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list,
|
||||
fail_silently=False,
|
||||
)
|
||||
3
blog/tests.py
Normal file
3
blog/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
11
blog/urls.py
Normal file
11
blog/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from blog.views import CategoryList, CategoryDetail, PostDetail, PostList, CommentCreate
|
||||
|
||||
urlpatterns = [
|
||||
path('categories/', CategoryList.as_view(), name='categories.list'),
|
||||
path('categories/<slug:slug>/', CategoryDetail.as_view(), name='categories.details'),
|
||||
path('post/', PostList.as_view(), name='post.list'),
|
||||
path('post/<slug:slug>/', PostDetail.as_view(), name='post.details'),
|
||||
path('comment/create/', CommentCreate.as_view(), name='comment.create'),
|
||||
]
|
||||
53
blog/views.py
Normal file
53
blog/views.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView, CreateAPIView
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from blog.models import Post, PCategory, PComment
|
||||
from blog.serializers import PostSerializer, CategorySerializer, CategoryPostSerializer, CommentSerializer
|
||||
from core.Permission import ReadOnly
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 10
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
# Create your views here.
|
||||
class CategoryList(ListAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
queryset = PCategory.objects.order_by('order').filter(is_active=True, parent__isnull=True).all()
|
||||
serializer_class = CategorySerializer
|
||||
|
||||
|
||||
class CategoryDetail(RetrieveAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
queryset = PCategory.objects.order_by('order').filter(is_active=True).all()
|
||||
serializer_class = CategoryPostSerializer
|
||||
lookup_field = 'slug' # Slug ile arama yapılacak
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context['paginator'] = StandardResultsSetPagination()
|
||||
return context
|
||||
|
||||
|
||||
# Create your views here.
|
||||
class PostList(ListAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
queryset = Post.objects.all()
|
||||
serializer_class = PostSerializer
|
||||
pagination_class = StandardResultsSetPagination
|
||||
|
||||
|
||||
class PostDetail(RetrieveAPIView):
|
||||
permission_classes = [ReadOnly]
|
||||
queryset = Post.objects.all()
|
||||
serializer_class = PostSerializer
|
||||
lookup_field = 'slug' # Slug ile arama yapılacak
|
||||
|
||||
|
||||
class CommentCreate(CreateAPIView):
|
||||
queryset = PComment.objects.all()
|
||||
serializer_class = CommentSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
Reference in New Issue
Block a user