first commit
This commit is contained in:
0
image/__init__.py
Normal file
0
image/__init__.py
Normal file
27
image/admin.py
Normal file
27
image/admin.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import PostImages
|
||||
|
||||
|
||||
@admin.register(PostImages)
|
||||
class PostImagesAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id',
|
||||
'title',
|
||||
'path',
|
||||
'processed_path',
|
||||
'original_filename',
|
||||
'format',
|
||||
'width',
|
||||
'height',
|
||||
'size',
|
||||
'quality',
|
||||
'slug',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'is_active',
|
||||
'is_front',
|
||||
)
|
||||
list_filter = ('created_at', 'updated_at', 'is_active', 'is_front')
|
||||
search_fields = ('slug',)
|
||||
date_hierarchy = 'created_at'
|
||||
9
image/apps.py
Normal file
9
image/apps.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ImageConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'image'
|
||||
|
||||
def ready(self):
|
||||
import image.signals
|
||||
42
image/migrations/0001_initial.py
Normal file
42
image/migrations/0001_initial.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-31 01:20
|
||||
|
||||
import autoslug.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
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='Resim Başlığı')),
|
||||
('path', models.CharField(max_length=254, verbose_name='Resim Yolu')),
|
||||
('processed_path', models.CharField(max_length=254, verbose_name='Orijinal Resim Yolu')),
|
||||
('original_filename', models.CharField(max_length=254, verbose_name='Orijinal Resim Adı')),
|
||||
('format', models.CharField(choices=[('png', 'PNG'), ('webp', 'WebP'), ('jpg', 'JPG'), ('avif', 'AVIF')], default='webp', max_length=254, verbose_name='Resim Formati')),
|
||||
('width', models.IntegerField()),
|
||||
('height', models.IntegerField()),
|
||||
('size', models.IntegerField()),
|
||||
('quality', models.IntegerField()),
|
||||
('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ü ?')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Post',
|
||||
'verbose_name_plural': 'Posts',
|
||||
'db_table': 'images',
|
||||
'ordering': ['created_at'],
|
||||
'unique_together': {('slug',)},
|
||||
},
|
||||
),
|
||||
]
|
||||
17
image/migrations/0002_rename_post_postimages.py
Normal file
17
image/migrations/0002_rename_post_postimages.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-31 01:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('image', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Post',
|
||||
new_name='PostImages',
|
||||
),
|
||||
]
|
||||
22
image/migrations/0003_postimages_user.py
Normal file
22
image/migrations/0003_postimages_user.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.9 on 2026-01-03 21:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('image', '0002_rename_post_postimages'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='postimages',
|
||||
name='user',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='image_user', to=settings.AUTH_USER_MODEL, verbose_name='Kullanıcı'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
17
image/migrations/0004_alter_postimages_options.py
Normal file
17
image/migrations/0004_alter_postimages_options.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.9 on 2026-01-09 07:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('image', '0003_postimages_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='postimages',
|
||||
options={'ordering': ['created_at'], 'verbose_name': 'Resim', 'verbose_name_plural': 'Resimler'},
|
||||
),
|
||||
]
|
||||
0
image/migrations/__init__.py
Normal file
0
image/migrations/__init__.py
Normal file
56
image/models.py
Normal file
56
image/models.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from autoslug import AutoSlugField
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
class PostImages(models.Model):
|
||||
aktif = (
|
||||
(True, 'Evet'),
|
||||
(False, 'Hayır'),
|
||||
)
|
||||
format_choose = (
|
||||
('png', 'PNG'),
|
||||
('webp', 'WebP'),
|
||||
('jpg', 'JPG'),
|
||||
('avif', 'AVIF'),
|
||||
)
|
||||
title = models.CharField(max_length=254, verbose_name="Resim Başlığı")
|
||||
user = models.ForeignKey('accounts.CustomUser', on_delete=models.CASCADE, related_name='image_user',
|
||||
verbose_name="Kullanıcı")
|
||||
path = models.CharField(max_length=254, verbose_name="Resim Yolu")
|
||||
processed_path = models.CharField(max_length=254, verbose_name="Orijinal Resim Yolu")
|
||||
original_filename = models.CharField(max_length=254, verbose_name="Orijinal Resim Adı")
|
||||
format = models.CharField(max_length=254, verbose_name="Resim Formati", choices=format_choose,default='webp')
|
||||
width = models.IntegerField()
|
||||
height = models.IntegerField()
|
||||
size = models.IntegerField()
|
||||
quality = models.IntegerField()
|
||||
|
||||
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 = 'images'
|
||||
verbose_name_plural = "Posts"
|
||||
verbose_name = "Post"
|
||||
unique_together = ('slug',)
|
||||
|
||||
def get_slug(self):
|
||||
slug = self.title.replace('ı', "i").replace('İ', 'i')
|
||||
number = 1
|
||||
while PostImages.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 f"Resimler: {self.title}"
|
||||
108
image/serializers.py
Normal file
108
image/serializers.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import PostImages
|
||||
|
||||
|
||||
class PostImagesSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PostImages
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class PostImageCreateSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for uploading and processing an image.
|
||||
"""
|
||||
image = serializers.ImageField(write_only=True, help_text="Yüklenecek resim dosyası.")
|
||||
title = serializers.CharField(max_length=254, help_text="Resim için bir başlık.")
|
||||
width = serializers.IntegerField(help_text="Resmin yeni genişliği (pixel).")
|
||||
height = serializers.IntegerField(help_text="Resmin yeni yüksekliği (pixel).")
|
||||
quality = serializers.IntegerField(default=85, min_value=1, max_value=100, help_text="JPG/WebP için kalite (1-100).")
|
||||
format = serializers.ChoiceField(
|
||||
choices=['png', 'webp', 'jpg', 'avif'],
|
||||
default='avif',
|
||||
help_text="Çıktı resim formatı."
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
image_file = validated_data.pop('image')
|
||||
title = validated_data.pop('title')
|
||||
new_width = validated_data.pop('width')
|
||||
new_height = validated_data.pop('height')
|
||||
quality = validated_data.pop('quality')
|
||||
output_format = validated_data.pop('format')
|
||||
|
||||
# Open the image with Pillow
|
||||
img = Image.open(image_file)
|
||||
|
||||
# Preserve original metadata for the model
|
||||
original_filename = image_file.name
|
||||
original_size = image_file.size
|
||||
|
||||
# Process the image: Resize and crop to fill target dimensions (Center Crop).
|
||||
# This ensures the image fills the dimensions without distortion or padding.
|
||||
|
||||
# ImageOps.fit resizes and crops the image to the requested size.
|
||||
img = ImageOps.fit(img, (new_width, new_height), method=Image.Resampling.LANCZOS, centering=(0.5, 0.5))
|
||||
|
||||
# If output format does not support transparency (like JPG), convert to RGB.
|
||||
# This also ensures compatibility for formats like JPEG that don't support P or RGBA.
|
||||
if output_format != 'png' and img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Save the processed image to an in-memory buffer
|
||||
buffer = BytesIO()
|
||||
|
||||
# Pillow expects 'JPEG' format identifier, not 'jpg'
|
||||
save_format = output_format.upper()
|
||||
if save_format == 'JPG':
|
||||
save_format = 'JPEG'
|
||||
|
||||
save_kwargs = {'format': save_format}
|
||||
if output_format in ['jpg', 'webp']:
|
||||
save_kwargs['quality'] = quality
|
||||
img.save(buffer, **save_kwargs)
|
||||
buffer.seek(0)
|
||||
|
||||
# Generate a unique filename and path
|
||||
new_filename = f"processed/{uuid.uuid4()}.{output_format}"
|
||||
file_path = os.path.join(settings.MEDIA_ROOT, new_filename)
|
||||
|
||||
# Ensure the directory exists
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
# Save the new file to the media directory
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(buffer.getvalue())
|
||||
|
||||
# Get the size of the new file
|
||||
new_size = os.path.getsize(file_path)
|
||||
|
||||
# Get user from validated_data if provided (serializer.save(user=...))
|
||||
user = validated_data.pop('user', None)
|
||||
|
||||
# Create the PostImages model instance
|
||||
instance = PostImages.objects.create(
|
||||
title=title,
|
||||
user=user,
|
||||
path=new_filename, # Store the relative path
|
||||
processed_path=original_filename, # Let's use this field for original name for now
|
||||
original_filename=original_filename,
|
||||
format=output_format,
|
||||
width=new_width, # Use the target width
|
||||
height=new_height, # Use the target height
|
||||
size=new_size,
|
||||
quality=quality,
|
||||
)
|
||||
return instance
|
||||
|
||||
def to_representation(self, instance):
|
||||
# Use the PostImagesSerializer to represent the created object
|
||||
return PostImagesSerializer(instance).data
|
||||
22
image/signals.py
Normal file
22
image/signals.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import PostImages
|
||||
|
||||
|
||||
@receiver(post_delete, sender=PostImages)
|
||||
def delete_image_file(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes the associated image file from the filesystem when a PostImages instance is deleted.
|
||||
"""
|
||||
# Check if the instance has a path and the path is not empty
|
||||
if instance.path:
|
||||
file_path = os.path.join(settings.MEDIA_ROOT, instance.path)
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except OSError as e:
|
||||
# Optionally, log the error e.g., print(f"Error deleting file {file_path}: {e}")
|
||||
pass
|
||||
3
image/tests.py
Normal file
3
image/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
8
image/urls.py
Normal file
8
image/urls.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.urls import path, include
|
||||
from .views import ImageUploadView, ImageDownloadView, ImageListView # Import ImageDownloadView
|
||||
|
||||
urlpatterns = [
|
||||
path('upload/', ImageUploadView.as_view(), name='image-upload'),
|
||||
path('list/', ImageListView.as_view(), name='image-list'),
|
||||
path('<slug:slug>/download/', ImageDownloadView.as_view(), name='image-download'), # New download URL
|
||||
]
|
||||
67
image/views.py
Normal file
67
image/views.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAdminUser, AllowAny
|
||||
from rest_framework import generics
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.http import FileResponse, Http404
|
||||
from .serializers import PostImageCreateSerializer, PostImagesSerializer
|
||||
from .models import PostImages # Make sure to import PostImages model
|
||||
|
||||
class ImageUploadView(APIView):
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
permission_classes = (IsAdminUser,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Accepts an image upload and processing parameters.
|
||||
Processes the image and saves it.
|
||||
"""
|
||||
serializer = PostImageCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
instance = serializer.save(user=request.user)
|
||||
# Return the details of the created object using the representation serializer
|
||||
return Response(serializer.to_representation(instance), status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ImageDownloadView(APIView):
|
||||
"""
|
||||
API endpoint to download a processed image by its slug.
|
||||
"""
|
||||
def get(self, request, slug, *args, **kwargs):
|
||||
try:
|
||||
image_instance = PostImages.objects.get(slug=slug)
|
||||
except PostImages.DoesNotExist:
|
||||
raise Http404("Image not found.")
|
||||
|
||||
file_path = os.path.join(settings.MEDIA_ROOT, image_instance.path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise Http404("File not found on server.")
|
||||
|
||||
# Determine content type based on format
|
||||
content_type_map = {
|
||||
'png': 'image/png',
|
||||
'webp': 'image/webp',
|
||||
'jpg': 'image/jpeg',
|
||||
'avif': 'image/avif',
|
||||
}
|
||||
content_type = content_type_map.get(image_instance.format.lower(), 'application/octet-stream')
|
||||
|
||||
# Prepare the filename for download
|
||||
filename = f"{image_instance.slug}.{image_instance.format.lower()}"
|
||||
|
||||
response = FileResponse(open(file_path, 'rb'), content_type=content_type)
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
|
||||
class ImageListView(generics.ListAPIView):
|
||||
"""List all images."""
|
||||
queryset = PostImages.objects.all().order_by('-created_at')
|
||||
serializer_class = PostImagesSerializer
|
||||
permission_classes = (AllowAny,)
|
||||
Reference in New Issue
Block a user