initial commit
This commit is contained in:
159
apps/blog/admin.py
Normal file
159
apps/blog/admin.py
Normal 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')
|
||||
0
apps/blog/api/__init__.py
Normal file
0
apps/blog/api/__init__.py
Normal file
87
apps/blog/api/schemas.py
Normal file
87
apps/blog/api/schemas.py
Normal 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
304
apps/blog/api/views.py
Normal 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
6
apps/blog/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.blog"
|
||||
672
apps/blog/fixtures/blog.json
Normal file
672
apps/blog/fixtures/blog.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
89
apps/blog/migrations/0001_initial.py
Normal file
89
apps/blog/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
78
apps/blog/migrations/0002_initial.py
Normal file
78
apps/blog/migrations/0002_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
0
apps/blog/migrations/__init__.py
Normal file
0
apps/blog/migrations/__init__.py
Normal file
137
apps/blog/models.py
Normal file
137
apps/blog/models.py
Normal 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
32
apps/blog/resources.py
Normal 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')
|
||||
Reference in New Issue
Block a user