init
This commit is contained in:
159
backend/blog/admin.py
Normal file
159
backend/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 blog.models import Category, Tag, Post, Comment, Like
|
||||
from blog.resources import PostResource, CategoryResource
|
||||
from utils.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')
|
||||
5
backend/blog/apps.py
Normal file
5
backend/blog/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'blog'
|
||||
672
backend/blog/fixtures/blog.json
Normal file
672
backend/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
backend/blog/migrations/0001_initial.py
Normal file
89
backend/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
backend/blog/migrations/0002_initial.py
Normal file
78
backend/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
backend/blog/migrations/__init__.py
Normal file
0
backend/blog/migrations/__init__.py
Normal file
137
backend/blog/models.py
Normal file
137
backend/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 utils.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
backend/blog/resources.py
Normal file
32
backend/blog/resources.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from import_export import resources, fields
|
||||
from import_export.widgets import ForeignKeyWidget, ManyToManyWidget
|
||||
|
||||
from users.models import User
|
||||
from 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