From 515819bc3d0a00c908e2c908f69c0c4878062178 Mon Sep 17 00:00:00 2001 From: starrysky yuanye <1928258918@qq.com> Date: Thu, 20 Nov 2025 16:50:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/blog/article_views.py | 135 ++++++++++++++++++ src/blog/forms.py | 61 ++++++++ .../management/commands/create_categories.py | 41 ++++++ .../management/commands/list_categories.py | 13 ++ src/blog/urls.py | 13 ++ src/blog/views.py | 79 ++++++++++ src/locale/zh_Hans/LC_MESSAGES/django.po | 92 ++++++++++++ src/templates/blog/article_create.html | 116 +++++++++++++++ src/templates/blog/article_edit.html | 118 +++++++++++++++ src/templates/blog/my_articles.html | 66 +++++++++ .../blog/tags/article_meta_info.html | 2 + src/templates/blog/tags/sidebar.html | 2 + src/templates/share_layout/nav.html | 1 + 13 files changed, 739 insertions(+) create mode 100644 src/blog/article_views.py create mode 100644 src/blog/management/commands/create_categories.py create mode 100644 src/blog/management/commands/list_categories.py create mode 100644 src/templates/blog/article_create.html create mode 100644 src/templates/blog/article_edit.html create mode 100644 src/templates/blog/my_articles.html diff --git a/src/blog/article_views.py b/src/blog/article_views.py new file mode 100644 index 0000000..426b4d0 --- /dev/null +++ b/src/blog/article_views.py @@ -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 + }) diff --git a/src/blog/forms.py b/src/blog/forms.py index 715be76..ec3e539 100644 --- a/src/blog/forms.py +++ b/src/blog/forms.py @@ -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, 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', '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'}) diff --git a/src/blog/management/commands/create_categories.py b/src/blog/management/commands/create_categories.py new file mode 100644 index 0000000..4b7bdb1 --- /dev/null +++ b/src/blog/management/commands/create_categories.py @@ -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('分类创建完成')) diff --git a/src/blog/management/commands/list_categories.py b/src/blog/management/commands/list_categories.py new file mode 100644 index 0000000..3ce3752 --- /dev/null +++ b/src/blog/management/commands/list_categories.py @@ -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()} 个分类') diff --git a/src/blog/urls.py b/src/blog/urls.py index adf2703..19e93c8 100644 --- a/src/blog/urls.py +++ b/src/blog/urls.py @@ -2,6 +2,7 @@ from django.urls import path from django.views.decorators.cache import cache_page from . import views +from . import article_views app_name = "blog" urlpatterns = [ @@ -17,6 +18,18 @@ urlpatterns = [ r'article////.html', views.ArticleDetailView.as_view(), name='detailbyid'), + path( + r'article/create/', + article_views.ArticleCreateView.as_view(), + name='article_create'), + path( + r'article/edit//', + article_views.ArticleUpdateView.as_view(), + name='article_edit'), + path( + r'my-articles/', + views.my_articles, + name='my_articles'), path( r'category/.html', views.CategoryDetailView.as_view(), diff --git a/src/blog/views.py b/src/blog/views.py index 773bb75..cab73f6 100644 --- a/src/blog/views.py +++ b/src/blog/views.py @@ -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}) diff --git a/src/locale/zh_Hans/LC_MESSAGES/django.po b/src/locale/zh_Hans/LC_MESSAGES/django.po index 200b7e6..1400c9e 100644 --- a/src/locale/zh_Hans/LC_MESSAGES/django.po +++ b/src/locale/zh_Hans/LC_MESSAGES/django.po @@ -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 "创建您的第一篇文章" diff --git a/src/templates/blog/article_create.html b/src/templates/blog/article_create.html new file mode 100644 index 0000000..07f6200 --- /dev/null +++ b/src/templates/blog/article_create.html @@ -0,0 +1,116 @@ +{% extends "share_layout/base.html" %} +{% load static %} +{% load i18n %} + +{% block title %}{% trans "Create Article" %}{% endblock %} + +{% block content %} +
+
+
+

{% trans "Create Article" %}

+
+ {% csrf_token %} +
+ + {{ form.title }} + {% if form.title.errors %} +
+ {{ form.title.errors }} +
+ {% endif %} +
+ +
+ + {{ form.body }} + {% if form.body.errors %} +
+ {{ form.body.errors }} +
+ {% endif %} +
+ +
+
+ + {{ form.status }} + {% if form.status.errors %} +
+ {{ form.status.errors }} +
+ {% endif %} +
+ +
+ + {{ form.comment_status }} + {% if form.comment_status.errors %} +
+ {{ form.comment_status.errors }} +
+ {% endif %} +
+
+ +
+
+ + {{ form.type }} + {% if form.type.errors %} +
+ {{ form.type.errors }} +
+ {% endif %} +
+ +
+ + {{ form.category }} + + {% for category in all_categories %} + + {% if form.category.errors %} +
+ {{ form.category.errors }} +
+ {% endif %} +
+
+ +
+ + {{ form.tags }} + + {% for tag in all_tags %} + + {% if form.tags.errors %} +
+ {{ form.tags.errors }} +
+ {% endif %} +
+ +
+ {{ form.show_toc }} + + {% if form.show_toc.errors %} +
+ {{ form.show_toc.errors }} +
+ {% endif %} +
+ +
+ + {% trans "取消" %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/blog/article_edit.html b/src/templates/blog/article_edit.html new file mode 100644 index 0000000..8e69357 --- /dev/null +++ b/src/templates/blog/article_edit.html @@ -0,0 +1,118 @@ +{% extends "share_layout/base.html" %} +{% load static %} +{% load i18n %} + +{% block title %}{% trans "Edit Article" %}{% endblock %} + +{% block content %} +
+
+
+

{% trans "Edit Article" %}: {{ article.title }}

+
+ {% csrf_token %} +
+ + {{ form.title }} + {% if form.title.errors %} +
+ {{ form.title.errors }} +
+ {% endif %} +
+ +
+ + {{ form.body }} + {% if form.body.errors %} +
+ {{ form.body.errors }} +
+ {% endif %} +
+ +
+
+ + {{ form.status }} + {% if form.status.errors %} +
+ {{ form.status.errors }} +
+ {% endif %} +
+ +
+ + {{ form.comment_status }} + {% if form.comment_status.errors %} +
+ {{ form.comment_status.errors }} +
+ {% endif %} +
+
+ +
+
+ + {{ form.type }} + {% if form.type.errors %} +
+ {{ form.type.errors }} +
+ {% endif %} +
+ +
+ + {{ form.category }} + + {% for category in all_categories %} + +
选择现有分类或创建新分类
+ {% if form.category.errors %} +
+ {{ form.category.errors }} +
+ {% endif %} +
+
+ +
+ + {{ form.tags }} + + {% for tag in all_tags %} + +
选择现有标签或创建新标签,多个标签用逗号分隔
+ {% if form.tags.errors %} +
+ {{ form.tags.errors }} +
+ {% endif %} +
+ +
+ {{ form.show_toc }} + + {% if form.show_toc.errors %} +
+ {{ form.show_toc.errors }} +
+ {% endif %} +
+ +
+ + {% trans "取消" %} +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/blog/my_articles.html b/src/templates/blog/my_articles.html new file mode 100644 index 0000000..e2053f1 --- /dev/null +++ b/src/templates/blog/my_articles.html @@ -0,0 +1,66 @@ +{% extends "share_layout/base.html" %} +{% load static %} +{% load i18n %} + +{% block title %}{% trans "My Articles" %}{% endblock %} + +{% block content %} +
+
+
+
+

{% trans "My Articles" %}

+ + {% trans "发布新文章" %} + +
+ + {% if articles %} +
+ + + + + + + + + + + + + {% for article in articles %} + + + + + + + + + {% endfor %} + +
{% trans "标题" %}{% trans "状态" %}{% trans "分类" %}{% trans "创建时间" %}{% trans "浏览量" %}{% trans "操作" %}
+ {{ article.title }} + + {% if article.status == 'p' %} + {% trans "已发布" %} + {% else %} + {% trans "草稿" %} + {% endif %} + {{ article.category.name }}{{ article.creation_time|date:"Y-m-d H:i" }}{{ article.views }} + + {% trans "编辑" %} + +
+
+ {% else %} +
+ {% trans "您还没有创建任何文章。" %} + {% trans "创建您的第一篇文章" %} +
+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/blog/tags/article_meta_info.html b/src/templates/blog/tags/article_meta_info.html index ec8a0f9..6a0c12f 100644 --- a/src/templates/blog/tags/article_meta_info.html +++ b/src/templates/blog/tags/article_meta_info.html @@ -50,6 +50,8 @@ {% if user.is_superuser %} {% trans 'edit' %} + {% elif user.is_authenticated and user == article.author %} + {% trans 'edit' %} {% endif %} diff --git a/src/templates/blog/tags/sidebar.html b/src/templates/blog/tags/sidebar.html index ecb6d20..aa10fd5 100644 --- a/src/templates/blog/tags/sidebar.html +++ b/src/templates/blog/tags/sidebar.html @@ -119,6 +119,8 @@ \ No newline at end of file