init
Some checks failed
CI/CD / Backend & Frontend Checks (push) Has been cancelled
CI/CD / Deploy to Production (push) Has been cancelled

This commit is contained in:
2026-05-18 11:34:07 +03:30
commit 7a8ddeabed
279 changed files with 37390 additions and 0 deletions

89
backend/gallery/admin.py Normal file
View File

@@ -0,0 +1,89 @@
from django.contrib import admin
from django.utils.html import format_html
from import_export.admin import ImportExportModelAdmin
from gallery.models import Gallery
from gallery.resources import GalleryResource
from utils.admin import SoftDeleteListFilter, BaseModelAdmin
@admin.register(Gallery)
class GalleryAdmin(BaseModelAdmin, ImportExportModelAdmin):
resource_class = GalleryResource
list_display = ('title', 'image_preview', 'uploaded_by', 'file_size_display', 'dimensions', 'is_public', 'created_at')
list_filter = ('is_public', 'created_at', SoftDeleteListFilter)
search_fields = ('title', 'description', 'alt_text')
readonly_fields = ('uploaded_by', 'file_size', 'width', 'height', 'image_preview_large', 'markdown_url')
fieldsets = (
('Image Info', {
'fields': ('title', 'description', 'image', 'alt_text', 'is_public')
}),
('Uploader', {
'fields': ('uploaded_by',),
'classes': ('collapse',)
}),
('Metadata', {
'fields': ('file_size', 'width', 'height'),
'classes': ('collapse',)
}),
('Preview & Usage', {
'fields': ('image_preview_large', 'markdown_url'),
'classes': ('collapse',)
}),
('Soft Delete', {
'fields': ('is_deleted', 'deleted_at'),
'classes': ('collapse',)
}),
)
actions = BaseModelAdmin.actions + ['make_public', 'make_private', 'restore_images']
def image_preview(self, obj):
if obj.image:
return format_html(
'<img src="{}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;" />',
obj.image.url
)
return "No Image"
image_preview.short_description = "Preview"
def image_preview_large(self, obj):
if obj.image:
return format_html(
'<img src="{}" style="max-width: 300px; max-height: 300px; object-fit: contain;" />',
obj.image.url
)
return "No Image"
image_preview_large.short_description = "Image Preview"
def file_size_display(self, obj):
return f"{obj.file_size_mb} MB" if obj.file_size else "Unknown"
file_size_display.short_description = "File Size"
def dimensions(self, obj):
if obj.width and obj.height:
return f"{obj.width} × {obj.height}"
return "Unknown"
dimensions.short_description = "Dimensions"
def make_public(self, request, queryset):
queryset.update(is_public=True)
self.message_user(request, f"Made {queryset.count()} images public.")
make_public.short_description = "Make selected images public"
def make_private(self, request, queryset):
queryset.update(is_public=False)
self.message_user(request, f"Made {queryset.count()} images private.")
make_private.short_description = "Make selected images private"
def restore_images(self, request, queryset):
for image in queryset:
image.restore()
self.message_user(request, f"Restored {queryset.count()} images.")
restore_images.short_description = "Restore selected images"
def save_model(self, request, obj, form, change):
if not obj.uploaded_by_id:
obj.uploaded_by = request.user
super().save_model(request, obj, form, change)

5
backend/gallery/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class GalleryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gallery'

View File

@@ -0,0 +1,218 @@
[
{
"model": "gallery.gallery",
"pk": 1,
"fields": {
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"is_deleted": false,
"title": "کارگاه یادگیری ماشین - تصویر ۱",
"description": "شرکت‌کنندگان در حال یادگیری مفاهیم یادگیری ماشین",
"image": "gallery/ml_workshop_1.jpg",
"uploaded_by": 1,
"alt_text": "دانشجویان در کارگاه یادگیری ماشین",
"file_size": 2048000,
"width": 1920,
"height": 1080,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 2,
"fields": {
"created_at": "2024-01-20T14:15:00Z",
"updated_at": "2024-01-20T14:15:00Z",
"is_deleted": false,
"title": "مسابقه برنامه‌نویسی - لحظه اعلام نتایج",
"description": "اعلام نتایج مسابقه برنامه‌نویسی و اهدای جوایز",
"image": "gallery/programming_contest_results.jpg",
"uploaded_by": 2,
"alt_text": "اهدای جوایز مسابقه برنامه‌نویسی",
"file_size": 1536000,
"width": 1600,
"height": 900,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 3,
"fields": {
"created_at": "2024-01-25T09:45:00Z",
"updated_at": "2024-01-25T09:45:00Z",
"is_deleted": false,
"title": "سمینار امنیت سایبری",
"description": "دکتر رضایی در حال ارائه مطالب امنیت سایبری",
"image": "gallery/cybersecurity_seminar.jpg",
"uploaded_by": 5,
"alt_text": "سخنرانی در سمینار امنیت سایبری",
"file_size": 1792000,
"width": 1800,
"height": 1200,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 4,
"fields": {
"created_at": "2024-02-01T16:20:00Z",
"updated_at": "2024-02-01T16:20:00Z",
"is_deleted": false,
"title": "کارگاه React.js - کدنویسی عملی",
"description": "شرکت‌کنندگان در حال کدنویسی با React.js",
"image": "gallery/react_workshop_coding.jpg",
"uploaded_by": 9,
"alt_text": "کدنویسی در کارگاه React.js",
"file_size": 2304000,
"width": 2048,
"height": 1152,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 5,
"fields": {
"created_at": "2024-02-05T11:30:00Z",
"updated_at": "2024-02-05T11:30:00Z",
"is_deleted": false,
"title": "بازدید از دیجی‌کالا - ورودی شرکت",
"description": "دانشجویان در ورودی شرکت دیجی‌کالا",
"image": "gallery/digikala_visit_entrance.jpg",
"uploaded_by": 3,
"alt_text": "بازدید از شرکت دیجی‌کالا",
"file_size": 1920000,
"width": 1920,
"height": 1280,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 6,
"fields": {
"created_at": "2024-02-10T22:45:00Z",
"updated_at": "2024-02-10T22:45:00Z",
"is_deleted": false,
"title": "هکاتون هوش مصنوعی - شب اول",
"description": "تیم‌ها در حال کار شبانه روزی در هکاتون",
"image": "gallery/ai_hackathon_night.jpg",
"uploaded_by": 6,
"alt_text": "کار شبانه در هکاتون هوش مصنوعی",
"file_size": 1664000,
"width": 1600,
"height": 1067,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 7,
"fields": {
"created_at": "2024-02-15T13:10:00Z",
"updated_at": "2024-02-15T13:10:00Z",
"is_deleted": false,
"title": "سمینار کارآفرینی - پنل بحث",
"description": "پنل بحث با کارآفرینان موفق فناوری",
"image": "gallery/entrepreneurship_panel.jpg",
"uploaded_by": 1,
"alt_text": "پنل بحث کارآفرینی فناوری",
"file_size": 2176000,
"width": 1920,
"height": 1080,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 8,
"fields": {
"created_at": "2024-02-20T15:25:00Z",
"updated_at": "2024-02-20T15:25:00Z",
"is_deleted": false,
"title": "کارگاه DevOps - آموزش Docker",
"description": "آموزش عملی Docker و کانتینرها",
"image": "gallery/devops_docker_training.jpg",
"uploaded_by": 8,
"alt_text": "آموزش Docker در کارگاه DevOps",
"file_size": 1856000,
"width": 1728,
"height": 1152,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 9,
"fields": {
"created_at": "2024-02-25T12:40:00Z",
"updated_at": "2024-02-25T12:40:00Z",
"is_deleted": false,
"title": "مسابقه طراحی UI/UX - آثار شرکت‌کنندگان",
"description": "نمایش آثار طراحی شده توسط شرکت‌کنندگان",
"image": "gallery/uiux_contest_designs.jpg",
"uploaded_by": 12,
"alt_text": "آثار مسابقه طراحی UI/UX",
"file_size": 2048000,
"width": 2048,
"height": 1365,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 10,
"fields": {
"created_at": "2024-03-01T17:55:00Z",
"updated_at": "2024-03-01T17:55:00Z",
"is_deleted": false,
"title": "نشست فارغ‌التحصیلان - عکس گروهی",
"description": "عکس یادگاری با فارغ‌التحصیلان و دانشجویان فعلی",
"image": "gallery/alumni_group_photo.jpg",
"uploaded_by": 5,
"alt_text": "عکس گروهی نشست فارغ‌التحصیلان",
"file_size": 2560000,
"width": 2560,
"height": 1440,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 11,
"fields": {
"created_at": "2024-03-05T08:20:00Z",
"updated_at": "2024-03-05T08:20:00Z",
"is_deleted": false,
"title": "آزمایشگاه کامپیوتر - محیط کار",
"description": "نمایی از آزمایشگاه کامپیوتر دانشکده",
"image": "gallery/computer_lab.jpg",
"uploaded_by": 9,
"alt_text": "آزمایشگاه کامپیوتر دانشکده",
"file_size": 1792000,
"width": 1792,
"height": 1024,
"is_public": true
}
},
{
"model": "gallery.gallery",
"pk": 12,
"fields": {
"created_at": "2024-03-10T14:35:00Z",
"updated_at": "2024-03-10T14:35:00Z",
"is_deleted": false,
"title": "کتابخانه دانشکده - بخش کتب فنی",
"description": "بخش کتب فنی و مهندسی کامپیوتر کتابخانه",
"image": "gallery/library_tech_books.jpg",
"uploaded_by": 4,
"alt_text": "کتب فنی کتابخانه دانشکده",
"file_size": 1536000,
"width": 1536,
"height": 1024,
"is_public": true
}
}
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.2.5 on 2025-10-16 12:07
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Gallery',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('title', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('image', models.ImageField(upload_to='gallery/')),
('alt_text', models.CharField(blank=True, max_length=200)),
('file_size', models.PositiveIntegerField(blank=True, null=True)),
('width', models.PositiveIntegerField(blank=True, null=True)),
('height', models.PositiveIntegerField(blank=True, null=True)),
('is_public', models.BooleanField(default=True)),
],
options={
'verbose_name_plural': 'Gallery Images',
'ordering': ['-created_at'],
},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.5 on 2025-10-16 12:07
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('gallery', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='gallery',
name='uploaded_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gallery_images', to=settings.AUTH_USER_MODEL),
),
]

View File

82
backend/gallery/models.py Normal file
View File

@@ -0,0 +1,82 @@
from django.db import models
from django.conf import settings
from PIL import Image
from utils.models import BaseModel
MAX_IMAGE_FILE_SIZE_BYTES = 2 * 1024 * 1024
class Gallery(BaseModel):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
image = models.ImageField(upload_to='gallery/')
uploaded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='gallery_images')
alt_text = models.CharField(max_length=200, blank=True)
file_size = models.PositiveIntegerField(null=True, blank=True)
width = models.PositiveIntegerField(null=True, blank=True)
height = models.PositiveIntegerField(null=True, blank=True)
is_public = models.BooleanField(default=True)
class Meta:
ordering = ['-created_at']
verbose_name_plural = "Gallery Images"
def __str__(self):
return self.title
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.image:
# Get file size
self.file_size = self.image.size
# Get image dimensions
with Image.open(self.image.path) as img:
self.width, self.height = img.size
# Compress image if it's too large
self.compress_image()
# Update fields without triggering save again
Gallery.objects.filter(pk=self.pk).update(
file_size=self.file_size,
width=self.width,
height=self.height
)
def compress_image(self):
"""Compress image if it's larger than 2MB or dimensions are too large"""
if not self.image:
return
with Image.open(self.image.path) as img:
# Convert to RGB if necessary
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
# Resize if too large
max_size = (1920, 1080)
if img.size[0] > max_size[0] or img.size[1] > max_size[1]:
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# Compress if file size is too large
quality = 85
if self.file_size and self.file_size > MAX_IMAGE_FILE_SIZE_BYTES:
quality = 70
img.save(self.image.path, "JPEG", quality=quality, optimize=True)
@property
def file_size_mb(self):
"""Return file size in MB"""
if self.file_size:
return round(self.file_size / (1024 * 1024), 2)
return 0
@property
def markdown_url(self):
"""Return URL for use in markdown"""
return f"![{self.alt_text or self.title}]({settings.BACKEND_ROOT}{self.image.url})"

View File

@@ -0,0 +1,17 @@
from import_export import resources, fields
from import_export.widgets import ForeignKeyWidget
from gallery.models import Gallery
from users.models import User
class GalleryResource(resources.ModelResource):
uploaded_by = fields.Field(
column_name='uploaded_by',
attribute='uploaded_by',
widget=ForeignKeyWidget(User, 'username')
)
class Meta:
model = Gallery
fields = ('id', 'title', 'description', 'image', 'uploaded_by',
'alt_text', 'file_size', 'width', 'height', 'is_public', 'created_at')

23
backend/gallery/tasks.py Normal file
View File

@@ -0,0 +1,23 @@
from celery import shared_task
from PIL import Image
import logging
logger = logging.getLogger(__name__)
@shared_task
def process_uploaded_image(gallery_id):
"""Process uploaded image: compress, resize, extract metadata"""
try:
from .models import Gallery
gallery_item = Gallery.objects.get(id=gallery_id)
if gallery_item.image:
# This will trigger the compression and metadata extraction
gallery_item.compress_image()
logger.info(f"Processed image: {gallery_item.title}")
return f"Processed image: {gallery_item.title}"
except Exception as exc:
logger.error(f"Failed to process image: {exc}")
raise exc