initial commit
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-19 20:53:08 +03:30
commit 88b793ed9f
169 changed files with 16763 additions and 0 deletions

159
apps/blog/admin.py Normal file
View File

@@ -0,0 +1,159 @@
from django import forms
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from simplemde.widgets import SimpleMDEEditor
from apps.blog.models import Category, Tag, Post, Comment, Like
from apps.blog.resources import PostResource, CategoryResource
from core.admin import SoftDeleteListFilter, BaseModelAdmin
@admin.register(Category)
class CategoryAdmin(BaseModelAdmin, ImportExportModelAdmin):
resource_class = CategoryResource
list_display = ('name', 'slug', 'created_at', 'is_deleted')
list_filter = ('created_at', 'is_deleted', SoftDeleteListFilter)
search_fields = ('name', 'description')
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ('created_at', 'updated_at', 'deleted_at')
fieldsets = (
('Content', {
'fields': ('name', 'slug', 'description')
}),
('Metadata', {
'fields': ('created_at', 'updated_at')
}),
('Soft Delete', {
'fields': ('is_deleted', 'deleted_at'),
'classes': ('collapse',)
})
)
actions = BaseModelAdmin.actions + ['restore_categories']
def restore_categories(self, request, queryset):
for category in queryset:
category.restore()
self.message_user(request, f"Restored {queryset.count()} categories.")
restore_categories.short_description = "Restore selected categories"
@admin.register(Tag)
class TagAdmin(BaseModelAdmin, ImportExportModelAdmin):
list_display = ('name', 'slug', 'created_at', 'is_deleted')
list_filter = ('created_at', 'is_deleted', SoftDeleteListFilter)
search_fields = ('name',)
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ('created_at', 'updated_at', 'deleted_at')
fieldsets = (
('Content', {
'fields': ('name', 'slug')
}),
('Metadata', {
'fields': ('created_at', 'updated_at')
}),
('Soft Delete', {
'fields': ('is_deleted', 'deleted_at'),
'classes': ('collapse',)
})
)
class PostAdminForm(forms.ModelForm):
content = forms.CharField(widget=SimpleMDEEditor())
excerpt = forms.CharField(widget=SimpleMDEEditor())
class Meta:
model = Post
fields = '__all__'
@admin.register(Post)
class PostAdmin(BaseModelAdmin, ImportExportModelAdmin):
form = PostAdminForm
resource_class = PostResource
list_display = ('title', 'author', 'status', 'category', 'is_featured', 'published_at', 'created_at')
list_filter = ('status', 'is_featured', 'category', 'tags', 'created_at', 'published_at', SoftDeleteListFilter)
search_fields = ('title', 'content', 'author__username')
prepopulated_fields = {'slug': ('title',)}
filter_horizontal = ('tags',)
date_hierarchy = 'published_at'
fieldsets = (
('Content', {
'fields': ('title', 'slug', 'content', 'excerpt', 'featured_image')
}),
('Metadata', {
'fields': ('author', 'category', 'tags', 'status', 'is_featured', 'published_at')
}),
('Soft Delete', {
'fields': ('is_deleted', 'deleted_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ('deleted_at',)
actions = BaseModelAdmin.actions + ['make_published', 'make_draft', 'make_featured', 'restore_posts']
def make_published(self, request, queryset):
queryset.update(status='published')
self.message_user(request, f"Published {queryset.count()} posts.")
make_published.short_description = "Mark selected posts as published"
def make_draft(self, request, queryset):
queryset.update(status='draft')
self.message_user(request, f"Marked {queryset.count()} posts as draft.")
make_draft.short_description = "Mark selected posts as draft"
def make_featured(self, request, queryset):
queryset.update(is_featured=True)
self.message_user(request, f"Featured {queryset.count()} posts.")
make_featured.short_description = "Mark selected posts as featured"
def restore_posts(self, request, queryset):
for post in queryset:
post.restore()
self.message_user(request, f"Restored {queryset.count()} posts.")
restore_posts.short_description = "Restore selected posts"
@admin.register(Comment)
class CommentAdmin(BaseModelAdmin):
list_display = ('author', 'post', 'content_preview', 'is_approved', 'created_at')
list_filter = ('is_approved', 'created_at', 'post', SoftDeleteListFilter)
search_fields = ('content', 'author__username', 'author__last_name', 'author__first_name', 'post__title')
readonly_fields = ('content_preview', 'created_at', 'updated_at', 'deleted_at')
fieldsets = (
('Content', {
'fields': ('post', 'author', 'content')
}),
('Metadata', {
'fields': ('is_approved', 'created_at', 'updated_at')
}),
('Soft Delete', {
'fields': ('is_deleted', 'deleted_at'),
'classes': ('collapse',)
})
)
actions = BaseModelAdmin.actions + ['approve_comments', 'disapprove_comments']
def content_preview(self, obj):
return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
content_preview.short_description = 'Content Preview'
def approve_comments(self, request, queryset):
queryset.update(is_approved=True)
self.message_user(request, f"Approved {queryset.count()} comments.")
approve_comments.short_description = "Approve selected comments"
def disapprove_comments(self, request, queryset):
queryset.update(is_approved=False)
self.message_user(request, f"Disapproved {queryset.count()} comments.")
disapprove_comments.short_description = "Disapprove selected comments"
@admin.register(Like)
class LikeAdmin(BaseModelAdmin):
list_display = ('user', 'post', 'created_at')
list_filter = ('created_at', 'post')
search_fields = ('user__username', 'post__title')

View File

87
apps/blog/api/schemas.py Normal file
View File

@@ -0,0 +1,87 @@
"""Blog API schemas."""
from ninja import Schema, ModelSchema
from typing import Optional, List
from datetime import datetime
from apps.blog.models import Category, Tag, Comment
class CategorySchema(ModelSchema):
class Config:
model = Category
model_fields = ['id', 'name', 'slug', 'description']
class TagSchema(ModelSchema):
class Config:
model = Tag
model_fields = ['id', 'name', 'slug']
class AuthorSchema(Schema):
id: int
username: str
first_name: str
last_name: str
profile_picture: Optional[str] = None
@staticmethod
def resolve_profile_picture(obj, context):
request = context['request']
if obj.profile_picture and hasattr(obj.profile_picture, 'url'):
return request.build_absolute_uri(obj.profile_picture.url)
return None
class PostListSchema(Schema):
id: int
title: str
slug: str
excerpt: str
author: AuthorSchema
featured_image: Optional[str] = None
status: str
published_at: Optional[datetime] = None
category: Optional[CategorySchema] = None
tags: List[TagSchema]
is_featured: bool
created_at: datetime
reading_time: int
class PostDetailSchema(PostListSchema):
content: str
content_html: str
class PostCreateSchema(Schema):
title: str
content: str
excerpt: Optional[str] = None
category_id: Optional[int] = None
tag_ids: Optional[List[int]] = []
status: str = "draft"
is_featured: bool = False
class CommentSchema(ModelSchema):
author: AuthorSchema
replies: List['CommentSchema'] = []
post_id: int
post_title: str
post_slug: str
class Config:
model = Comment
model_fields = ['id', 'content', 'created_at', 'is_approved']
@staticmethod
def resolve_post_id(obj):
return obj.post_id
@staticmethod
def resolve_post_title(obj):
return obj.post.title
@staticmethod
def resolve_post_slug(obj):
return obj.post.slug
class CommentCreateSchema(Schema):
content: str
parent_id: Optional[int] = None

304
apps/blog/api/views.py Normal file
View File

@@ -0,0 +1,304 @@
from django.shortcuts import get_object_or_404
from django.db.models import Q, Prefetch
from ninja import Router, Query
from typing import List, Optional
from apps.users.models import User
from apps.blog.api.schemas import (
CategorySchema,
CommentCreateSchema,
CommentSchema,
PostCreateSchema,
PostDetailSchema,
PostListSchema,
TagSchema,
)
from apps.blog.models import Post, Category, Tag, Comment, Like
from core.api.schemas import ErrorSchema, MessageSchema
from core.authentication import jwt_auth
blog_router = Router()
# Post endpoints
@blog_router.get("/posts", response=List[PostListSchema])
def list_posts(
request,
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=50),
category: Optional[str] = None,
tag: Optional[str] = None,
search: Optional[str] = None,
featured: Optional[bool] = None,
author: Optional[str] = None
):
"""List published posts with filtering and pagination"""
queryset = Post.objects.filter(status=Post.StatusChoices.PUBLISHED).select_related(
'author', 'category'
).prefetch_related('tags')
# Apply filters
if category:
queryset = queryset.filter(category__slug=category)
if tag:
queryset = queryset.filter(tags__slug=tag)
if search:
queryset = queryset.filter(
Q(title__icontains=search) |
Q(content__icontains=search) |
Q(excerpt__icontains=search)
)
if featured is not None:
queryset = queryset.filter(is_featured=featured)
if author:
queryset = queryset.filter(author__username=author)
# Pagination
offset = (page - 1) * limit
posts = queryset[offset:offset + limit]
return posts
@blog_router.get("/posts/{slug}", response=PostDetailSchema)
def get_post(request, slug: str):
"""Get single post by slug"""
post = get_object_or_404(
Post.objects.select_related('author', 'category').prefetch_related('tags'),
slug=slug,
status=Post.StatusChoices.PUBLISHED
)
return post
@blog_router.post("/posts", response={201: PostDetailSchema, 400: ErrorSchema}, auth=jwt_auth)
def create_post(request, data: PostCreateSchema):
"""Create a new post (committee members only)"""
user = request.auth
if not (user.is_superuser or user.is_staff):
return 400, {"error": "Only committee members can create posts"}
try:
post = Post.objects.create(
title=data.title,
content=data.content,
excerpt=data.excerpt,
author=user,
category_id=data.category_id,
status=data.status,
is_featured=data.is_featured
)
if data.tag_ids:
post.tags.set(data.tag_ids)
return 201, post
except Exception as e:
return 400, {"error": "Failed to create post", "details": str(e)}
@blog_router.put("/posts/{slug}", response={200: PostDetailSchema, 400: ErrorSchema}, auth=jwt_auth)
def update_post(request, slug: str, data: PostCreateSchema):
"""Update a post (author or committee only)"""
user = request.auth
post = get_object_or_404(Post, slug=slug)
if not (post.author == user or user.is_superuser or user.is_staff):
return 400, {"error": "You can only edit your own posts"}
try:
for field, value in data.dict(exclude_unset=True).items():
if field == 'tag_ids':
if value:
post.tags.set(value)
elif field == 'category_id':
post.category_id = value
else:
setattr(post, field, value)
post.save()
return 200, post
except Exception as e:
return 400, {"error": "Failed to update post", "details": str(e)}
@blog_router.delete("/posts/{slug}", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth)
def delete_post(request, slug: str):
"""Soft delete a post owned by the requester or committee."""
user = request.auth
post = get_object_or_404(Post, slug=slug)
if not (post.author == user or user.is_superuser or user.is_staff):
return 400, {"error": "You can only delete your own posts"}
post.delete()
return 200, {"message": "Post deleted successfully"}
@blog_router.get("/deleted/posts", response=List[PostListSchema], auth=jwt_auth)
def list_deleted_posts(request):
"""List all soft-deleted posts (Admin/Committee only)"""
if not (request.auth.is_staff or request.auth.is_superuser):
return 403, {"error": "Permission denied"}
return Post.deleted_objects.all().select_related('author', 'category').prefetch_related('tags')
@blog_router.post("deleted/posts/{post_id}/restore", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth)
def restore_post(request, post_id: int):
"""Restore a soft-deleted post (Admin/Committee only)"""
if not (request.auth.is_staff or request.auth.is_superuser):
return 403, {"error": "Permission denied"}
try:
post = Post.deleted_objects.get(id=post_id)
post.restore()
return 200, {"message": f"Post '{post.title}' restored successfully."}
except Post.DoesNotExist:
return 400, {"error": "Post not found or not soft-deleted."}
# Comment endpoints
@blog_router.get("/posts/{slug}/comments", response=List[CommentSchema])
def list_comments(request, slug: str):
"""List approved comments for a post"""
post = get_object_or_404(Post, slug=slug, status=Post.StatusChoices.PUBLISHED)
comments = Comment.objects.filter(
post=post,
is_approved=True,
parent=None
).select_related('author').prefetch_related(
Prefetch(
'replies',
queryset=Comment.objects.filter(is_approved=True).select_related('author')
)
)
return comments
@blog_router.post("/posts/{slug}/comments", response={201: CommentSchema, 400: ErrorSchema}, auth=jwt_auth)
def create_comment(request, slug: str, data: CommentCreateSchema):
"""Create a comment on a post"""
post = get_object_or_404(Post, slug=slug, status=Post.StatusChoices.PUBLISHED)
user = request.auth
try:
comment = Comment.objects.create(
post=post,
author=user,
content=data.content,
parent_id=data.parent_id
)
return 201, comment
except Exception as e:
return 400, {"error": "Failed to create comment", "details": str(e)}
@blog_router.get("/deleted/comments", response=List[CommentSchema], auth=jwt_auth)
def list_deleted_comments(request):
"""List all soft-deleted comments (Admin/Committee only)"""
if not (request.auth.is_staff or request.auth.is_superuser):
return 403, {"error": "Permission denied"}
return Comment.deleted_objects.all().select_related('author', 'post')
@blog_router.post("/deleted/comments/{comment_id}/restore", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth)
def restore_comment(request, comment_id: int):
"""Restore a soft-deleted comment (Admin/Committee only)"""
if not (request.auth.is_staff or request.auth.is_superuser):
return 403, {"error": "Permission denied"}
try:
comment = Comment.deleted_objects.get(id=comment_id)
comment.restore()
return 200, {"message": f"Comment by {comment.author.username} restored successfully."}
except Comment.DoesNotExist:
return 400, {"error": "Comment not found or not soft-deleted."}
# Like endpoints
@blog_router.post("/posts/{slug}/like", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth)
def toggle_like(request, slug: str):
"""Toggle like on a post"""
post = get_object_or_404(Post, slug=slug, status=Post.StatusChoices.PUBLISHED)
user = request.auth
like, created = Like.objects.get_or_create(post=post, user=user)
if not created:
like.delete()
return 200, {"message": "Post unliked"}
return 200, {"message": "Post liked"}
@blog_router.get("/posts/{slug}/likes", response={200: MessageSchema})
def get_likes_count(request, slug: str):
"""Get likes count for a post"""
post = get_object_or_404(Post, slug=slug, status=Post.StatusChoices.PUBLISHED)
count = post.likes.count()
return {"message": f"{count}"}
# Category endpoints
@blog_router.get("/categories", response=List[CategorySchema])
def list_categories(request):
"""List all categories"""
return Category.objects.all()
@blog_router.get("/categories/{slug}", response=CategorySchema)
def get_category(request, slug: str):
"""Get single category by slug"""
return get_object_or_404(Category, slug=slug)
@blog_router.get("/deleted/categories", response=List[CategorySchema], auth=jwt_auth)
def list_deleted_categories(request):
"""List all soft-deleted categories (Admin/Committee only)"""
if not (request.auth.is_staff or request.auth.is_superuser):
return 403, {"error": "Permission denied"}
return Category.deleted_objects.all()
@blog_router.post("/deleted/categories/{category_id}/restore", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth)
def restore_category(request, category_id: int):
"""Restore a soft-deleted category (Admin/Committee only)"""
if not (request.auth.is_staff or request.auth.is_superuser):
return 403, {"error": "Permission denied"}
try:
category = Category.deleted_objects.get(id=category_id)
category.restore()
return 200, {"message": f"Category '{category.name}' restored successfully."}
except Category.DoesNotExist:
return 400, {"error": "Category not found or not soft-deleted."}
# Tag endpoints
@blog_router.get("/tags", response=List[TagSchema])
def list_tags(request):
"""List all tags"""
return Tag.objects.all()
@blog_router.get("/tags/{slug}", response=TagSchema)
def get_tag(request, slug: str):
"""Get single tag by slug"""
return get_object_or_404(Tag, slug=slug)
@blog_router.get("/deleted/tags", response=List[TagSchema], auth=jwt_auth)
def list_deleted_tags(request):
"""List all soft-deleted tags (Admin/Committee only)"""
if not (request.auth.is_staff or request.auth.is_superuser):
return 403, {"error": "Permission denied"}
return Tag.all_objects.all()
@blog_router.post("/deleted/tags/{tag_id}/restore", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth)
def restore_tag(request, tag_id: int):
"""Restore a soft-deleted tag (Admin/Committee only)"""
if not (request.auth.is_staff or request.auth.is_superuser):
return 403, {"error": "Permission denied"}
try:
tag = Tag.deleted_objects.get(id=tag_id)
tag.restore()
return 200, {"message": f"Tag '{tag.name}' restored successfully."}
except Tag.DoesNotExist:
return 400, {"error": "Tag not found or not soft-deleted."}

6
apps/blog/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.blog"

View File

@@ -0,0 +1,672 @@
[
{
"model": "blog.category",
"pk": 1,
"fields": {
"name": "هوش مصنوعی",
"slug": "artificial-intelligence",
"description": "مقالات مربوط به هوش مصنوعی و یادگیری ماشین",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 2,
"fields": {
"name": "برنامه‌نویسی وب",
"slug": "web-programming",
"description": "آموزش‌ها و مقالات مربوط به توسعه وب",
"created_at": "2024-01-02T10:00:00Z",
"updated_at": "2024-01-02T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 3,
"fields": {
"name": "امنیت سایبری",
"slug": "cybersecurity",
"description": "مطالب مربوط به امنیت اطلاعات و سایبری",
"created_at": "2024-01-03T10:00:00Z",
"updated_at": "2024-01-03T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 4,
"fields": {
"name": "علم داده",
"slug": "data-science",
"description": "مقالات مربوط به تحلیل داده و علم داده",
"created_at": "2024-01-04T10:00:00Z",
"updated_at": "2024-01-04T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 5,
"fields": {
"name": "اپلیکیشن موبایل",
"slug": "mobile-app",
"description": "توسعه اپلیکیشن‌های موبایل",
"created_at": "2024-01-05T10:00:00Z",
"updated_at": "2024-01-05T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 6,
"fields": {
"name": "شبکه کامپیوتری",
"slug": "computer-networks",
"description": "مطالب مربوط به شبکه‌های کامپیوتری",
"created_at": "2024-01-06T10:00:00Z",
"updated_at": "2024-01-06T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 7,
"fields": {
"name": "بازی‌سازی",
"slug": "game-development",
"description": "آموزش و مقالات مربوط به توسعه بازی",
"created_at": "2024-01-07T10:00:00Z",
"updated_at": "2024-01-07T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 8,
"fields": {
"name": "طراحی UI/UX",
"slug": "ui-ux-design",
"description": "طراحی رابط کاربری و تجربه کاربری",
"created_at": "2024-01-08T10:00:00Z",
"updated_at": "2024-01-08T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 9,
"fields": {
"name": "اخبار انجمن",
"slug": "association-news",
"description": "اخبار و اطلاعیه‌های انجمن علمی",
"created_at": "2024-01-09T10:00:00Z",
"updated_at": "2024-01-09T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.category",
"pk": 10,
"fields": {
"name": "مسابقات برنامه‌نویسی",
"slug": "programming-contests",
"description": "اطلاعات مربوط به مسابقات برنامه‌نویسی",
"created_at": "2024-01-10T10:00:00Z",
"updated_at": "2024-01-10T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 1,
"fields": {
"name": "پایتون",
"slug": "python",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 2,
"fields": {
"name": "جاوااسکریپت",
"slug": "javascript",
"created_at": "2024-01-02T10:00:00Z",
"updated_at": "2024-01-02T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 3,
"fields": {
"name": "ری‌اکت",
"slug": "react",
"created_at": "2024-01-03T10:00:00Z",
"updated_at": "2024-01-03T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 4,
"fields": {
"name": "جنگو",
"slug": "django",
"created_at": "2024-01-04T10:00:00Z",
"updated_at": "2024-01-04T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 5,
"fields": {
"name": "یادگیری عمیق",
"slug": "deep-learning",
"created_at": "2024-01-05T10:00:00Z",
"updated_at": "2024-01-05T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 6,
"fields": {
"name": "تنسورفلو",
"slug": "tensorflow",
"created_at": "2024-01-06T10:00:00Z",
"updated_at": "2024-01-06T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 7,
"fields": {
"name": "کیبرنتیز",
"slug": "kubernetes",
"created_at": "2024-01-07T10:00:00Z",
"updated_at": "2024-01-07T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 8,
"fields": {
"name": "داکر",
"slug": "docker",
"created_at": "2024-01-08T10:00:00Z",
"updated_at": "2024-01-08T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 9,
"fields": {
"name": "گیت",
"slug": "git",
"created_at": "2024-01-09T10:00:00Z",
"updated_at": "2024-01-09T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 10,
"fields": {
"name": "لینوکس",
"slug": "linux",
"created_at": "2024-01-10T10:00:00Z",
"updated_at": "2024-01-10T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 11,
"fields": {
"name": "الگوریتم",
"slug": "algorithm",
"created_at": "2024-01-11T10:00:00Z",
"updated_at": "2024-01-11T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.tag",
"pk": 12,
"fields": {
"name": "ساختمان داده",
"slug": "data-structure",
"created_at": "2024-01-12T10:00:00Z",
"updated_at": "2024-01-12T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 1,
"fields": {
"title": "مقدمه‌ای بر یادگیری ماشین با پایتون",
"slug": "introduction-to-machine-learning-with-python",
"content": "# مقدمه‌ای بر یادگیری ماشین با پایتون\n\nیادگیری ماشین یکی از مهم‌ترین شاخه‌های هوش مصنوعی است که امروزه کاربردهای فراوانی در صنایع مختلف دارد.\n\n## کتابخانه‌های مهم\n\n- **Scikit-learn**: برای الگوریتم‌های کلاسیک\n- **TensorFlow**: برای یادگیری عمیق\n- **Pandas**: برای پردازش داده\n- **NumPy**: برای محاسبات عددی\n\n## مثال ساده\n\n```python\nfrom sklearn.linear_model import LinearRegression\nimport numpy as np\n\n# داده‌های نمونه\nX = np.array([[1], [2], [3], [4]])\ny = np.array([2, 4, 6, 8])\n\n# ایجاد مدل\nmodel = LinearRegression()\nmodel.fit(X, y)\n\n# پیش‌بینی\nprint(model.predict([[5]]))\n```\n\nاین مثال ساده نشان می‌دهد که چگونه می‌توان با استفاده از کتابخانه Scikit-learn یک مدل رگرسیون خطی ایجاد کرد.",
"excerpt": "آموزش مقدماتی یادگیری ماشین با استفاده از زبان پایتون و کتابخانه‌های محبوب",
"author": 1,
"status": "published",
"published_at": "2024-01-15T10:00:00Z",
"category": 1,
"is_featured": true,
"created_at": "2024-01-15T09:00:00Z",
"updated_at": "2024-01-15T09:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 2,
"fields": {
"title": "ساخت API با Django REST Framework",
"slug": "building-api-with-django-rest-framework",
"content": "# ساخت API با Django REST Framework\n\nDjango REST Framework یکی از قدرتمندترین ابزارها برای ساخت API در پایتون است.\n\n## نصب و راه‌اندازی\n\n```bash\npip install djangorestframework\n```\n\n## ایجاد Serializer\n\n```python\nfrom rest_framework import serializers\nfrom .models import Post\n\nclass PostSerializer(serializers.ModelSerializer):\n class Meta:\n model = Post\n fields = '__all__'\n```\n\n## ایجاد ViewSet\n\n```python\nfrom rest_framework import viewsets\nfrom .models import Post\nfrom .serializers import PostSerializer\n\nclass PostViewSet(viewsets.ModelViewSet):\n queryset = Post.objects.all()\n serializer_class = PostSerializer\n```\n\nبا این روش می‌توانید به راحتی API های قدرتمند و قابل اعتماد بسازید.",
"excerpt": "آموزش گام به گام ساخت API با استفاده از Django REST Framework",
"author": 2,
"status": "published",
"published_at": "2024-01-20T14:30:00Z",
"category": 2,
"is_featured": false,
"created_at": "2024-01-20T13:30:00Z",
"updated_at": "2024-01-20T13:30:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 3,
"fields": {
"title": "امنیت در اپلیکیشن‌های وب",
"slug": "web-application-security",
"content": "# امنیت در اپلیکیشن‌های وب\n\nامنیت یکی از مهم‌ترین جنبه‌های توسعه اپلیکیشن‌های وب است.\n\n## تهدیدات رایج\n\n- **SQL Injection**: تزریق کد SQL مخرب\n- **XSS**: اجرای اسکریپت مخرب در مرورگر\n- **CSRF**: درخواست جعلی بین سایتی\n- **Authentication Bypass**: دور زدن احراز هویت\n\n## راه‌های محافظت\n\n```python\n# استفاده از ORM برای جلوگیری از SQL Injection\nUser.objects.filter(username=username)\n\n# Escape کردن خروجی HTML\nfrom django.utils.html import escape\nsafe_content = escape(user_input)\n\n# استفاده از CSRF Token\n{% csrf_token %}\n```\n\nهمیشه امنیت را در اولویت قرار دهید.",
"excerpt": "بررسی تهدیدات امنیتی رایج در اپلیکیشن‌های وب و راه‌های مقابله با آن‌ها",
"author": 3,
"status": "published",
"published_at": "2024-01-25T16:00:00Z",
"category": 3,
"is_featured": true,
"created_at": "2024-01-25T15:00:00Z",
"updated_at": "2024-01-25T15:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 4,
"fields": {
"title": "تحلیل داده با Pandas",
"slug": "data-analysis-with-pandas",
"content": "# تحلیل داده با Pandas\n\nPandas یکی از قدرتمندترین کتابخانه‌های پایتون برای تحلیل داده است.\n\n## خواندن داده\n\n```python\nimport pandas as pd\n\n# خواندن از CSV\ndf = pd.read_csv('data.csv')\n\n# خواندن از Excel\ndf = pd.read_excel('data.xlsx')\n\n# خواندن از JSON\ndf = pd.read_json('data.json')\n```\n\n## عملیات پایه\n\n```python\n# نمایش اطلاعات کلی\nprint(df.info())\nprint(df.describe())\n\n# فیلتر کردن\nfiltered_df = df[df['age'] > 25]\n\n# گروه‌بندی\ngrouped = df.groupby('category').mean()\n```\n\nPandas ابزاری قدرتمند برای تحلیل داده است.",
"excerpt": "آموزش کار با کتابخانه Pandas برای تحلیل و پردازش داده در پایتون",
"author": 4,
"status": "published",
"published_at": "2024-02-01T11:00:00Z",
"category": 4,
"is_featured": false,
"created_at": "2024-02-01T10:00:00Z",
"updated_at": "2024-02-01T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 5,
"fields": {
"title": "توسعه اپلیکیشن موبایل با React Native",
"slug": "mobile-app-development-with-react-native",
"content": "# توسعه اپلیکیشن موبایل با React Native\n\nReact Native امکان توسعه اپلیکیشن‌های موبایل کراس پلتفرم را فراهم می‌کند.\n\n## مزایا\n\n- **کراس پلتفرم**: یک کد برای iOS و Android\n- **Performance**: عملکرد نزدیک به Native\n- **Hot Reload**: تغییرات فوری\n- **Community**: جامعه بزرگ و فعال\n\n## شروع پروژه\n\n```bash\nnpx react-native init MyApp\ncd MyApp\nnpx react-native run-android\n```\n\n## کامپوننت ساده\n\n```jsx\nimport React from 'react';\nimport { View, Text, StyleSheet } from 'react-native';\n\nconst App = () => {\n return (\n <View style={styles.container}>\n <Text style={styles.title}>سلام دنیا!</Text>\n </View>\n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n justifyContent: 'center',\n alignItems: 'center',\n },\n title: {\n fontSize: 24,\n fontWeight: 'bold',\n },\n});\n\nexport default App;\n```",
"excerpt": "راهنمای شروع توسعه اپلیکیشن موبایل با React Native",
"author": 5,
"status": "published",
"published_at": "2024-02-05T13:30:00Z",
"category": 5,
"is_featured": false,
"created_at": "2024-02-05T12:30:00Z",
"updated_at": "2024-02-05T12:30:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 6,
"fields": {
"title": "مبانی شبکه‌های کامپیوتری",
"slug": "computer-networks-fundamentals",
"content": "# مبانی شبکه‌های کامپیوتری\n\nشبکههای کامپیوتری پایه و اساس ارتباطات مدرن هستند.\n\n## مدل OSI\n\n1. **Physical Layer**: لایه فیزیکی\n2. **Data Link Layer**: لایه پیوند داده\n3. **Network Layer**: لایه شبکه\n4. **Transport Layer**: لایه انتقال\n5. **Session Layer**: لایه جلسه\n6. **Presentation Layer**: لایه ارائه\n7. **Application Layer**: لایه کاربرد\n\n## پروتکل‌های مهم\n\n- **TCP/IP**: پروتکل اصلی اینترنت\n- **HTTP/HTTPS**: انتقال صفحات وب\n- **FTP**: انتقال فایل\n- **SMTP**: ارسال ایمیل\n- **DNS**: تبدیل نام دامنه\n\n## مثال ساده با Python\n\n```python\nimport socket\n\n# ایجاد سوکت\ns = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n\n# اتصال به سرور\ns.connect(('google.com', 80))\n\n# ارسال درخواست HTTP\nrequest = \"GET / HTTP/1.1\\r\\nHost: google.com\\r\\n\\r\\n\"\ns.send(request.encode())\n\n# دریافت پاسخ\nresponse = s.recv(1024)\nprint(response.decode())\n\ns.close()\n```",
"excerpt": "آشنایی با مفاهیم پایه شبکه‌های کامپیوتری و پروتکل‌های مهم",
"author": 6,
"status": "published",
"published_at": "2024-02-10T15:00:00Z",
"category": 6,
"is_featured": false,
"created_at": "2024-02-10T14:00:00Z",
"updated_at": "2024-02-10T14:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 7,
"fields": {
"title": "ساخت بازی با Unity",
"slug": "game-development-with-unity",
"content": "# ساخت بازی با Unity\n\nUnity یکی از محبوب‌ترین موتورهای بازی‌سازی است.\n\n## ویژگی‌های Unity\n\n- **کراس پلتفرم**: انتشار در پلتفرم‌های مختلف\n- **Visual Scripting**: برنامه‌نویسی بصری\n- **Asset Store**: فروشگاه منابع\n- **Community**: جامعه بزرگ\n\n## اسکریپت ساده C#\n\n```csharp\nusing UnityEngine;\n\npublic class PlayerController : MonoBehaviour\n{\n public float speed = 5.0f;\n \n void Update()\n {\n float horizontal = Input.GetAxis(\"Horizontal\");\n float vertical = Input.GetAxis(\"Vertical\");\n \n Vector3 movement = new Vector3(horizontal, 0, vertical);\n transform.Translate(movement * speed * Time.deltaTime);\n }\n}\n```\n\n## مراحل ساخت بازی\n\n1. **طراحی**: ایده و مفهوم بازی\n2. **Prototyping**: نمونه اولیه\n3. **Development**: توسعه اصلی\n4. **Testing**: تست و رفع باگ\n5. **Publishing**: انتشار بازی\n\nUnity ابزاری قدرتمند برای ساخت بازی است.",
"excerpt": "راهنمای شروع بازی‌سازی با موتور Unity",
"author": 7,
"status": "published",
"published_at": "2024-02-15T12:00:00Z",
"category": 7,
"is_featured": true,
"created_at": "2024-02-15T11:00:00Z",
"updated_at": "2024-02-15T11:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 8,
"fields": {
"title": "اصول طراحی UI/UX",
"slug": "ui-ux-design-principles",
"content": "# اصول طراحی UI/UX\n\nطراحی رابط کاربری و تجربه کاربری نقش مهمی در موفقیت محصولات دیجیتال دارد.\n\n## اصول UI\n\n- **Consistency**: یکنواختی در طراحی\n- **Hierarchy**: سلسله مراتب بصری\n- **Contrast**: تضاد مناسب\n- **Alignment**: تراز بندی صحیح\n- **Proximity**: قرارگیری عناصر مرتبط\n\n## اصول UX\n\n- **Usability**: قابلیت استفاده\n- **Accessibility**: دسترسی‌پذیری\n- **User-Centered**: محوریت کاربر\n- **Feedback**: بازخورد مناسب\n- **Error Prevention**: جلوگیری از خطا\n\n## ابزارهای طراحی\n\n- **Figma**: طراحی رابط کاربری\n- **Adobe XD**: پروتوتایپ سازی\n- **Sketch**: طراحی برای Mac\n- **InVision**: همکاری تیمی\n\n## فرآیند طراحی\n\n1. **Research**: تحقیق و بررسی\n2. **Wireframing**: طراحی اسکلت\n3. **Prototyping**: نمونه‌سازی\n4. **Testing**: تست با کاربران\n5. **Iteration**: بهبود مداوم",
"excerpt": "آشنایی با اصول و مبانی طراحی رابط کاربری و تجربه کاربری",
"author": 8,
"status": "published",
"published_at": "2024-02-20T14:30:00Z",
"category": 8,
"is_featured": false,
"created_at": "2024-02-20T13:30:00Z",
"updated_at": "2024-02-20T13:30:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 9,
"fields": {
"title": "اطلاعیه برگزاری مسابقه برنامه‌نویسی",
"slug": "programming-contest-announcement",
"content": "# اطلاعیه برگزاری مسابقه برنامه‌نویسی\n\nانجمن علمی مهندسی کامپیوتر دانشگاه برگزاری مسابقه برنامه‌نویسی بهاری را اعلام می‌کند.\n\n## جزئیات مسابقه\n\n- **تاریخ**: ۲۲ مارس ۲۰۲۴\n- **زمان**: ۹ صبح تا ۱۲ ظهر\n- **مکان**: آزمایشگاه کامپیوتر شماره ۱\n- **مدت زمان**: ۳ ساعت\n- **تعداد مسائل**: ۸ مسئله\n\n## جوایز\n\n- **نفر اول**: ۵ میلیون تومان\n- **نفر دوم**: ۳ میلیون تومان\n- **نفر سوم**: ۲ میلیون تومان\n\n## قوانین\n\n- مسابقه به صورت انفرادی برگزار می‌شود\n- زبان‌های مجاز: C++, Java, Python\n- استفاده از اینترنت ممنوع است\n- ثبت نام تا ۲۰ مارس ادامه دارد\n\n## ثبت نام\n\nبرای ثبت نام به دفتر انجمن مراجعه کنید یا از طریق وب‌سایت اقدام نمایید.\n\nمنتظر حضور گرم شما هستیم!",
"excerpt": "اطلاعیه برگزاری مسابقه برنامه‌نویسی بهاری انجمن علمی",
"author": 1,
"status": "published",
"published_at": "2024-02-25T10:00:00Z",
"category": 9,
"is_featured": true,
"created_at": "2024-02-25T09:00:00Z",
"updated_at": "2024-02-25T09:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.post",
"pk": 10,
"fields": {
"title": "نتایج مسابقه ACM ICPC منطقه‌ای",
"slug": "acm-icpc-regional-results",
"content": "# نتایج مسابقه ACM ICPC منطقه‌ای\n\nتیمهای دانشگاه ما در مسابقه ACM ICPC منطقه‌ای عملکرد درخشانی داشتند.\n\n## نتایج تیم‌ها\n\n### تیم Alpha\n- **اعضا**: علی احمدی، سارا محمدی، رضا کریمی\n- **رتبه**: ۵ منطقه‌ای\n- **مسائل حل شده**: ۷ از ۱۲\n\n### تیم Beta\n- **اعضا**: مریم حسینی، حسن زارع، زهرا صفری\n- **رتبه**: ۱۲ منطقه‌ای\n- **مسائل حل شده**: ۵ از ۱۲\n\n### تیم Gamma\n- **اعضا**: محمد رحمانی، فاطمه مرادی، امیر قربانی\n- **رتبه**: ۱۸ منطقه‌ای\n- **مسائل حل شده**: ۴ از ۱۲\n\n## تبریک و تشکر\n\nاز تمامی شرکت‌کنندگان تشکر می‌کنیم و امیدواریم سال آینده نتایج بهتری کسب کنیم.\n\n## آماده‌سازی برای سال آینده\n\nبرای آماده‌سازی تیم‌های سال آینده، کارگاه‌های تمرینی برگزار خواهد شد.",
"excerpt": "گزارش عملکرد تیم‌های دانشگاه در مسابقه ACM ICPC منطقه‌ای",
"author": 2,
"status": "published",
"published_at": "2024-03-01T16:00:00Z",
"category": 10,
"is_featured": false,
"created_at": "2024-03-01T15:00:00Z",
"updated_at": "2024-03-01T15:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 1,
"fields": {
"post": 1,
"author": 3,
"content": "مقاله بسیار مفیدی بود. ممنون از نویسنده",
"is_approved": true,
"created_at": "2024-01-16T10:00:00Z",
"updated_at": "2024-01-16T10:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 2,
"fields": {
"post": 1,
"author": 4,
"content": "آیا می‌توانید مثال‌های بیشتری ارائه دهید؟",
"is_approved": true,
"created_at": "2024-01-17T11:00:00Z",
"updated_at": "2024-01-17T11:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 3,
"fields": {
"post": 2,
"author": 5,
"content": "Django REST Framework واقعاً قدرتمند است",
"is_approved": true,
"created_at": "2024-01-21T09:00:00Z",
"updated_at": "2024-01-21T09:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 4,
"fields": {
"post": 3,
"author": 6,
"content": "امنیت واقعاً مهم است. مقاله خوبی بود",
"is_approved": true,
"created_at": "2024-01-26T12:00:00Z",
"updated_at": "2024-01-26T12:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 5,
"fields": {
"post": 4,
"author": 7,
"content": "Pandas برای تحلیل داده عالی است",
"is_approved": true,
"created_at": "2024-02-02T14:00:00Z",
"updated_at": "2024-02-02T14:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 6,
"fields": {
"post": 5,
"author": 8,
"content": "React Native گزینه خوبی برای موبایل است",
"is_approved": true,
"created_at": "2024-02-06T15:00:00Z",
"updated_at": "2024-02-06T15:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 7,
"fields": {
"post": 6,
"author": 9,
"content": "شبکه پایه همه چیز است",
"is_approved": true,
"created_at": "2024-02-11T16:00:00Z",
"updated_at": "2024-02-11T16:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 8,
"fields": {
"post": 7,
"author": 10,
"content": "Unity برای شروع بازی‌سازی عالی است",
"is_approved": true,
"created_at": "2024-02-16T13:00:00Z",
"updated_at": "2024-02-16T13:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 9,
"fields": {
"post": 8,
"author": 11,
"content": "طراحی UI/UX خیلی مهم است",
"is_approved": true,
"created_at": "2024-02-21T17:00:00Z",
"updated_at": "2024-02-21T17:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.comment",
"pk": 10,
"fields": {
"post": 9,
"author": 12,
"content": "حتماً در مسابقه شرکت می‌کنم",
"is_approved": true,
"created_at": "2024-02-26T11:00:00Z",
"updated_at": "2024-02-26T11:00:00Z",
"is_deleted": false
}
},
{
"model": "blog.like",
"pk": 1,
"fields": {
"post": 1,
"user": 3,
"created_at": "2024-01-16T10:30:00Z"
}
},
{
"model": "blog.like",
"pk": 2,
"fields": {
"post": 1,
"user": 4,
"created_at": "2024-01-17T11:30:00Z"
}
},
{
"model": "blog.like",
"pk": 3,
"fields": {
"post": 1,
"user": 5,
"created_at": "2024-01-18T12:00:00Z"
}
},
{
"model": "blog.like",
"pk": 4,
"fields": {
"post": 2,
"user": 6,
"created_at": "2024-01-21T09:30:00Z"
}
},
{
"model": "blog.like",
"pk": 5,
"fields": {
"post": 2,
"user": 7,
"created_at": "2024-01-22T10:00:00Z"
}
},
{
"model": "blog.like",
"pk": 6,
"fields": {
"post": 3,
"user": 8,
"created_at": "2024-01-26T12:30:00Z"
}
},
{
"model": "blog.like",
"pk": 7,
"fields": {
"post": 3,
"user": 9,
"created_at": "2024-01-27T13:00:00Z"
}
},
{
"model": "blog.like",
"pk": 8,
"fields": {
"post": 4,
"user": 10,
"created_at": "2024-02-02T14:30:00Z"
}
},
{
"model": "blog.like",
"pk": 9,
"fields": {
"post": 5,
"user": 11,
"created_at": "2024-02-06T15:30:00Z"
}
},
{
"model": "blog.like",
"pk": 10,
"fields": {
"post": 6,
"user": 12,
"created_at": "2024-02-11T16:30:00Z"
}
},
{
"model": "blog.like",
"pk": 11,
"fields": {
"post": 7,
"user": 1,
"created_at": "2024-02-16T13:30:00Z"
}
},
{
"model": "blog.like",
"pk": 12,
"fields": {
"post": 8,
"user": 2,
"created_at": "2024-02-21T17:30:00Z"
}
}
]

View File

@@ -0,0 +1,89 @@
# 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='Category',
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)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(blank=True, max_length=100, unique=True)),
('description', models.TextField(blank=True)),
],
options={
'verbose_name_plural': 'Categories',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Comment',
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)),
('content', models.TextField()),
('is_approved', models.BooleanField(default=True)),
],
options={
'ordering': ['created_at'],
},
),
migrations.CreateModel(
name='Like',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Post',
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)),
('slug', models.SlugField(blank=True, max_length=200, unique=True)),
('content', models.TextField(help_text='Content in Markdown format')),
('excerpt', models.TextField(blank=True, max_length=300)),
('featured_image', models.ImageField(blank=True, null=True, upload_to='blog/featured/')),
('status', models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10)),
('published_at', models.DateTimeField(blank=True, null=True)),
('is_featured', models.BooleanField(default=False)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Tag',
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)),
('name', models.CharField(max_length=50, unique=True)),
('slug', models.SlugField(blank=True, unique=True)),
],
options={
'ordering': ['name'],
},
),
]

View File

@@ -0,0 +1,78 @@
# 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 = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='comment',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='blog.comment'),
),
migrations.AddField(
model_name='like',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='post',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='post',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='posts', to='blog.category'),
),
migrations.AddField(
model_name='like',
name='post',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='blog.post'),
),
migrations.AddField(
model_name='comment',
name='post',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='blog.post'),
),
migrations.AddField(
model_name='post',
name='tags',
field=models.ManyToManyField(blank=True, related_name='posts', to='blog.tag'),
),
migrations.AddIndex(
model_name='like',
index=models.Index(fields=['post'], name='blog_like_post_id_c95f0b_idx'),
),
migrations.AlterUniqueTogether(
name='like',
unique_together={('post', 'user')},
),
migrations.AddIndex(
model_name='comment',
index=models.Index(fields=['post', 'is_approved'], name='blog_commen_post_id_7710b1_idx'),
),
migrations.AddIndex(
model_name='post',
index=models.Index(fields=['status', 'published_at'], name='blog_post_status_5b2843_idx'),
),
migrations.AddIndex(
model_name='post',
index=models.Index(fields=['is_featured'], name='blog_post_is_feat_837e2e_idx'),
),
]

View File

137
apps/blog/models.py Normal file
View File

@@ -0,0 +1,137 @@
from django.db import models
from django.conf import settings
from django.utils.text import slugify
from django.utils import timezone
import markdown
from core.models import BaseModel
class Category(BaseModel):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=100, unique=True, blank=True)
description = models.TextField(blank=True)
class Meta:
verbose_name_plural = "Categories"
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Tag(BaseModel):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True, blank=True)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Post(BaseModel):
class StatusChoices(models.TextChoices):
DRAFT = 'draft', 'Draft'
PUBLISHED = 'published', 'Published'
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True, blank=True)
content = models.TextField(help_text="Content in Markdown format")
excerpt = models.TextField(max_length=300, blank=True)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='posts')
featured_image = models.ImageField(upload_to='blog/featured/', null=True, blank=True)
status = models.CharField(max_length=10, choices=StatusChoices.choices, default=StatusChoices.DRAFT)
published_at = models.DateTimeField(null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, related_name='posts')
tags = models.ManyToManyField(Tag, blank=True, related_name='posts')
is_featured = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', 'published_at']),
models.Index(fields=['is_featured']),
]
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
# Auto-generate excerpt if not provided
if not self.excerpt and self.content:
# Convert markdown to plain text for excerpt
plain_text = markdown.markdown(self.content, extensions=['markdown.extensions.extra'])
# Remove HTML tags and truncate
import re
plain_text = re.sub('<[^<]+?>', '', plain_text)
self.excerpt = plain_text[:297] + '...' if len(plain_text) > 300 else plain_text
if self.status == Post.StatusChoices.PUBLISHED and not self.published_at:
self.published_at = timezone.now()
super().save(*args, **kwargs)
@property
def content_html(self):
"""Convert markdown content to HTML"""
return markdown.markdown(
self.content,
extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
]
)
@property
def reading_time(self):
"""Estimate reading time in minutes assuming 200 words per minute."""
word_count = len(self.content.split())
return max(1, word_count // 200)
class Comment(BaseModel):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='comments')
content = models.TextField()
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies')
is_approved = models.BooleanField(default=True)
class Meta:
ordering = ['created_at']
indexes = [
models.Index(fields=['post', 'is_approved']),
]
def __str__(self):
return f'Comment by {self.author.username} on {self.post.title}'
@property
def is_reply(self):
return self.parent is not None
class Like(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='likes')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='likes')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['post', 'user']
indexes = [
models.Index(fields=['post']),
]
def __str__(self):
return f'{self.user.username} likes {self.post.title}'

32
apps/blog/resources.py Normal file
View File

@@ -0,0 +1,32 @@
from import_export import resources, fields
from import_export.widgets import ForeignKeyWidget, ManyToManyWidget
from apps.users.models import User
from apps.blog.models import Post, Category, Tag
class CategoryResource(resources.ModelResource):
class Meta:
model = Category
fields = ('id', 'name', 'slug', 'description', 'created_at')
class PostResource(resources.ModelResource):
author = fields.Field(
column_name='author',
attribute='author',
widget=ForeignKeyWidget(User, 'username')
)
category = fields.Field(
column_name='category',
attribute='category',
widget=ForeignKeyWidget(Category, 'name')
)
tags = fields.Field(
column_name='tags',
attribute='tags',
widget=ManyToManyWidget(Tag, field='name', separator='|')
)
class Meta:
model = Post
fields = ('id', 'title', 'slug', 'content', 'excerpt', 'author',
'category', 'tags', 'status', 'is_featured', 'published_at', 'created_at')