pull/14/head
zhangyu 3 months ago
parent 029004f302
commit 77b4835a47

@ -0,0 +1,135 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, get_object_or_404
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from .models import Article, Category, Tag
from .forms import ArticleForm
class ArticleCreateView(LoginRequiredMixin, CreateView):
"""创建文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_create.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def form_valid(self, form):
# 先不保存表单,先处理分类
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
# 直接设置分类对象,而不是名称
form.instance.category = category
# 先保存文章实例,以便可以添加多对多关系
# 但在保存前先从表单中移除category字段避免表单尝试保存它
category_temp = form.cleaned_data.pop('category')
tags_temp = form.cleaned_data.pop('tags')
response = super().form_valid(form)
# 处理标签
if tags_temp:
tag_names = [tag.strip() for tag in tags_temp.split(',') if tag.strip()]
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
"""更新文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_edit.html'
pk_url_kwarg = 'article_id'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def get_initial(self):
initial = super().get_initial()
# 设置分类初始值
if self.object.category:
initial['category'] = self.object.category.name
# 设置标签初始值
if self.object.tags.exists():
tag_names = [tag.name for tag in self.object.tags.all()]
initial['tags'] = ', '.join(tag_names)
return initial
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
# 只有文章作者或管理员可以编辑
if obj.author != request.user and not request.user.is_superuser:
return redirect('blog:detailbyid',
article_id=obj.id,
year=obj.creation_time.year,
month=obj.creation_time.month,
day=obj.creation_time.day)
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
# 先不保存表单,先处理分类
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
# 直接设置分类对象,而不是名称
form.instance.category = category
# 先保存文章实例,以便可以添加多对多关系
# 但在保存前先从表单中移除category字段避免表单尝试保存它
category_temp = form.cleaned_data.pop('category')
tags_temp = form.cleaned_data.pop('tags')
response = super().form_valid(form)
# 处理标签
if tags_temp:
tag_names = [tag.strip() for tag in tags_temp.split(',') if tag.strip()]
form.instance.tags.clear() # 清除现有标签
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
else:
form.instance.tags.clear() # 如果没有标签,清除所有标签
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})

@ -1,7 +1,9 @@
import logging
from django import forms
from django.utils.translation import gettext_lazy as _
from haystack.forms import SearchForm
from .models import Article, Category, Tag
logger = logging.getLogger(__name__)
@ -17,3 +19,62 @@ class BlogSearchForm(SearchForm):
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas
class ArticleForm(forms.ModelForm):
"""文章表单"""
title = forms.CharField(label='标题', max_length=200, required=True)
body = forms.CharField(label='内容', widget=forms.Textarea(attrs={'id': 'id_body_md'}), required=True)
status = forms.ChoiceField(
label='状态',
choices=[('p', '发布'), ('d', '草稿')],
initial='p',
widget=forms.Select,
required=True
)
comment_status = forms.ChoiceField(
label='评论状态',
choices=[('o', '开启'), ('c', '关闭')],
initial='o',
widget=forms.Select,
required=True
)
type = forms.ChoiceField(
label='类型',
choices=[('a', '文章'), ('p', '页面')],
initial='a',
widget=forms.Select,
required=True
)
show_toc = forms.BooleanField(label='显示目录', required=False)
category = forms.CharField(
label='分类',
required=True,
widget=forms.TextInput(attrs={
'class': 'form-control',
'list': 'category-list',
'placeholder': '选择或输入分类名称'
})
)
tags = forms.CharField(
label='标签',
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'list': 'tag-list',
'placeholder': '选择或输入标签,多个标签用逗号分隔'
})
)
class Meta:
model = Article
fields = ['title', 'body', 'status', 'comment_status', 'type', 'show_toc']
def __init__(self, *args, **kwargs):
super(ArticleForm, self).__init__(*args, **kwargs)
self.fields['title'].widget.attrs.update({'class': 'form-control'})
self.fields['body'].widget.attrs.update({'class': 'form-control mdeditor', 'rows': 20})
self.fields['status'].widget.attrs.update({'class': 'form-control'})
self.fields['comment_status'].widget.attrs.update({'class': 'form-control'})
self.fields['type'].widget.attrs.update({'class': 'form-control'})
self.fields['show_toc'].widget.attrs.update({'class': 'form-check-input'})

@ -0,0 +1,15 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import ArticleImage
class ImageUploadForm(forms.ModelForm):
"""图片上传表单"""
class Meta:
model = ArticleImage
fields = ['image', 'description']
widgets = {
'image': forms.FileInput(attrs={'class': 'form-control', 'accept': 'image/*'}),
'description': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('可选,添加图片描述')})
}

@ -0,0 +1,118 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.generic.edit import CreateView, DeleteView
from django.urls import reverse_lazy
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
import json
from .models import Article, ArticleImage
from .forms_image import ImageUploadForm
class ImageUploadView(LoginRequiredMixin, CreateView):
"""上传图片视图"""
model = ArticleImage
form_class = ImageUploadForm
template_name = 'blog/image_upload.html'
def form_valid(self, form):
article_id = self.kwargs.get('article_id')
article = get_object_or_404(Article, id=article_id)
# 检查用户是否有权限上传图片
if article.author != self.request.user and not self.request.user.is_superuser:
return JsonResponse({'status': 'error', 'message': '没有权限上传图片'}, status=403)
form.instance.article = article
self.object = form.save()
# 返回JSON响应包含图片URL和ID
return JsonResponse({
'status': 'success',
'image_id': self.object.id,
'image_url': self.object.image.url,
'description': self.object.description
})
def form_invalid(self, form):
return JsonResponse({'status': 'error', 'errors': form.errors}, status=400)
@login_required
@require_POST
@csrf_exempt
def markdown_image_upload(request):
"""为Markdown编辑器提供的图片上传接口"""
if request.method == 'POST' and request.FILES.get('image'):
image_file = request.FILES['image']
try:
# 创建一个默认文章对象(如果需要的话)
# 或者使用一个特殊的标记值来表示这是一个临时图片
from blog.models import Article
default_article = Article.objects.filter(author=request.user).first()
if not default_article:
# 如果用户没有任何文章,创建一个临时的草稿文章
default_article = Article(
title="临时图片存储",
body="",
status="d", # 草稿状态
author=request.user
)
default_article.save()
# 创建图片对象,关联到默认文章
temp_image = ArticleImage(
image=image_file,
article=default_article,
description="Markdown编辑器上传"
)
# 保存图片
temp_image.save()
# 返回Markdown编辑器需要的格式
return JsonResponse({
'success': 1,
'message': '上传成功',
'url': temp_image.image.url
})
except Exception as e:
# 记录错误并返回失败信息
import logging
logger = logging.getLogger(__name__)
logger.error(f"图片上传失败: {str(e)}")
return JsonResponse({
'success': 0,
'message': f'上传失败: {str(e)}'
})
return JsonResponse({
'success': 0,
'message': '上传失败:未提供图片文件'
})
class ImageDeleteView(LoginRequiredMixin, DeleteView):
"""删除图片视图"""
model = ArticleImage
pk_url_kwarg = 'image_id'
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
# 检查用户是否有权限删除图片
if self.object.article.author != request.user and not request.user.is_superuser:
return JsonResponse({'status': 'error', 'message': '没有权限删除图片'}, status=403)
# 删除图片文件
self.object.image.delete()
self.object.delete()
return JsonResponse({'status': 'success', 'message': '图片已删除'})

@ -0,0 +1,41 @@
from django.core.management.base import BaseCommand
from blog.models import Category
class Command(BaseCommand):
help = '创建文章分类'
def handle(self, *args, **options):
categories = [
'技术',
'生活',
'旅行',
'美食',
'摄影',
'读书',
'电影',
'音乐',
'编程',
'设计',
'健康',
'教育',
'职场',
'财经',
'历史',
'文化',
'体育',
'游戏',
'科技'
]
for name in categories:
category, created = Category.objects.get_or_create(
name=name,
defaults={'parent_category': None}
)
if created:
self.stdout.write(self.style.SUCCESS(f'创建分类: {name}'))
else:
self.stdout.write(self.style.WARNING(f'分类已存在: {name}'))
self.stdout.write(self.style.SUCCESS('分类创建完成'))

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from blog.models import Category
class Command(BaseCommand):
help = '列出所有文章分类'
def handle(self, *args, **options):
categories = Category.objects.all()
self.stdout.write('当前数据库中的分类:')
for category in categories:
self.stdout.write(f'- {category.name} (ID: {category.id})')
self.stdout.write(f'总计: {categories.count()} 个分类')

@ -0,0 +1,29 @@
# Generated by Django 4.2.14 on 2025-11-20 20:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
]
operations = [
migrations.CreateModel(
name='ArticleImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='article_images/%Y/%m/%d/', verbose_name='图片')),
('description', models.CharField(blank=True, max_length=255, verbose_name='图片描述')),
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='blog.article')),
],
options={
'verbose_name': '文章图片',
'verbose_name_plural': '文章图片',
'ordering': ['-created_time'],
},
),
]

@ -304,6 +304,22 @@ class SideBar(models.Model):
return self.name
class ArticleImage(models.Model):
"""文章图片模型"""
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='images')
image = models.ImageField(_('图片'), upload_to='article_images/%Y/%m/%d/')
description = models.CharField(_('图片描述'), max_length=255, blank=True)
created_time = models.DateTimeField(_('创建时间'), auto_now_add=True)
class Meta:
verbose_name = _('文章图片')
verbose_name_plural = verbose_name
ordering = ['-created_time']
def __str__(self):
return f"{self.article.title} - {self.image.name}"
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField(

@ -2,6 +2,8 @@ from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
from . import article_views
from . import image_views
app_name = "blog"
urlpatterns = [
@ -17,6 +19,18 @@ urlpatterns = [
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
path(
r'article/create/',
article_views.ArticleCreateView.as_view(),
name='article_create'),
path(
r'article/edit/<int:article_id>/',
article_views.ArticleUpdateView.as_view(),
name='article_edit'),
path(
r'my-articles/',
views.my_articles,
name='my_articles'),
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
@ -59,4 +73,17 @@ urlpatterns = [
r'clean',
views.clean_cache_view,
name='clean'),
# 图片相关路由
path(
'article/<int:article_id>/upload-image/',
image_views.ImageUploadView.as_view(),
name='upload_image'),
path(
'markdown-image-upload/',
image_views.markdown_image_upload,
name='markdown_image_upload'),
path(
'image/<int:image_id>/delete/',
image_views.ImageDeleteView.as_view(),
name='delete_image'),
]

@ -378,3 +378,82 @@ def permission_denied_view(
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from .forms import ArticleForm
class ArticleCreateView(LoginRequiredMixin, CreateView):
"""创建文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_create.html'
def form_valid(self, form):
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
form.instance.category = category
# 处理标签
tags_str = form.cleaned_data['tags']
if tags_str:
tag_names = [tag.strip() for tag in tags_str.split(',') if tag.strip()]
form.instance.save() # 保存文章实例,以便可以添加多对多关系
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
"""更新文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_edit.html'
pk_url_kwarg = 'article_id'
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
# 只有文章作者或管理员可以编辑
if obj.author != request.user and not request.user.is_superuser:
return redirect('blog:detailbyid',
article_id=obj.id,
year=obj.creation_time.year,
month=obj.creation_time.month,
day=obj.creation_time.day)
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})
@login_required
def my_articles(request):
"""显示当前用户的文章列表"""
articles = Article.objects.filter(author=request.user)
return render(request, 'blog/my_articles.html', {'articles': articles})

@ -665,3 +665,95 @@ msgstr "快捷登录"
#: .\templates\share_layout\nav.html:26
msgid "Article archive"
msgstr "文章归档"
#: .\templates\share_layout\nav.html:29
msgid "Create Article"
msgstr "创建文章"
#: .\templates\share_layout\nav.html:33
msgid "My Articles"
msgstr "我的文章"
#: .\templates\blog\article_create.html:5
msgid "Create Article"
msgstr "创建文章"
#: .\templates\blog\article_edit.html:5
msgid "Edit Article"
msgstr "编辑文章"
#: .\templates\blog\my_articles.html:5
msgid "My Articles"
msgstr "我的文章"
#: .\templates\share_layout\nav.html:29
msgid "发布文章"
msgstr "发布文章"
#: .\templates\share_layout\nav.html:33
msgid "我的文章"
msgstr "我的文章"
#: .\templates\blog\article_create.html:97
msgid "发布"
msgstr "发布"
#: .\templates\blog\article_create.html:98
msgid "取消"
msgstr "取消"
#: .\templates\blog\article_edit.html:97
msgid "更新"
msgstr "更新"
#: .\templates\blog\article_edit.html:98
msgid "取消"
msgstr "取消"
#: .\templates\blog\my_articles.html:13
msgid "发布新文章"
msgstr "发布新文章"
#: .\templates\blog\my_articles.html:22
msgid "标题"
msgstr "标题"
#: .\templates\blog\my_articles.html:23
msgid "状态"
msgstr "状态"
#: .\templates\blog\my_articles.html:24
msgid "分类"
msgstr "分类"
#: .\templates\blog\my_articles.html:25
msgid "创建时间"
msgstr "创建时间"
#: .\templates\blog\my_articles.html:26
msgid "浏览量"
msgstr "浏览量"
#: .\templates\blog\my_articles.html:27
msgid "操作"
msgstr "操作"
#: .\templates\blog\my_articles.html:38
msgid "已发布"
msgstr "已发布"
#: .\templates\blog\my_articles.html:40
msgid "草稿"
msgstr "草稿"
#: .\templates\blog\my_articles.html:48
msgid "编辑"
msgstr "编辑"
#: .\templates\blog\my_articles.html:58
msgid "您还没有创建任何文章。"
msgstr "您还没有创建任何文章。"
#: .\templates\blog\my_articles.html:59
msgid "创建您的第一篇文章"
msgstr "创建您的第一篇文章"

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save