Compare commits

...

5 Commits

Binary file not shown.

@ -0,0 +1 @@
DJANGO_SECRET_KEY='f)jnngb81u_)*5!e%zclyr=zoh^61268qhm!kebtl8_c-pp_d^'

@ -90,6 +90,7 @@ python manage.py migrate
# 创建一个超级管理员账户
python manage.py createsuperuser
--zyd202020 zbzydzfy123456
```
### 5. 运行项目

@ -58,3 +58,21 @@ class BlogUserAdmin(UserAdmin):
list_display_links = ('id', 'username')
ordering = ('-id',)
search_fields = ('username', 'nickname', 'email')
# 修复字段集配置
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'nickname')}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
(_('Additional info'), {'fields': ('source', 'creation_time', 'last_modify_time')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'email', 'password1', 'password2'),
}),
)

@ -56,7 +56,7 @@ class AccountTest(TestCase):
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
self.assertEquals(
self.assertEqual(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
@ -66,7 +66,7 @@ class AccountTest(TestCase):
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
self.assertEquals(
self.assertEqual(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
@ -205,3 +205,31 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
def test_favorites_page(self):
user = BlogUser.objects.create_superuser(
email="fav@user.com",
username="favuser",
password="favpass")
category = Category()
category.name = "favcategory"
category.save()
article = Article()
article.title = "favorite title"
article.body = "favorite body"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
# 收藏
article.favorite_users.add(user)
# 登录并访问收藏页
self.client.login(username='favuser', password='favpass')
resp = self.client.get(reverse('account:favorites'))
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, "favorite title")

@ -25,4 +25,5 @@ urlpatterns = [re_path(r'^login/$',
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
path('favorites/', views.FavoriteArticlesView.as_view(), name='favorites'),
]

@ -20,6 +20,8 @@ from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
@ -202,3 +204,20 @@ class ForgetPasswordEmailCode(View):
utils.set_code(to_email, code)
return HttpResponse("ok")
class FavoriteArticlesView(LoginRequiredMixin, ListView):
template_name = 'blog/article_index.html'
context_object_name = 'article_list'
paginate_by = settings.PAGINATE_BY if hasattr(settings, 'PAGINATE_BY') else 10
def get_queryset(self):
return self.request.user.favorite_articles.filter(status='p').order_by('-pub_time')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['page_type'] = '我的收藏'
ctx['tag_name'] = None
ctx['linktype'] = 'i'
ctx['sort'] = 'latest'
return ctx

@ -9,8 +9,16 @@ class Command(BaseCommand):
help = 'create test datas'
def handle(self, *args, **options):
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# 先尝试通过用户名查找用户
try:
user = get_user_model().objects.get(username='测试用户')
except get_user_model().DoesNotExist:
# 如果用户不存在,则创建新用户
user = get_user_model().objects.create(
email='test@test.com',
username='测试用户',
password=make_password('test!q@w#eTYU')
)
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
@ -19,21 +27,26 @@ class Command(BaseCommand):
name='子类目', parent_category=pcategory)[0]
category.save()
basetag = Tag()
basetag.name = "标签"
basetag.save()
# 使用 get_or_create 处理基础标签
basetag, created = Tag.objects.get_or_create(name="标签")
for i in range(1, 20):
article = Article.objects.get_or_create(
article, article_created = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
article.tags.add(tag)
article.tags.add(basetag)
article.save()
author=user
)
# 使用 get_or_create 处理每个标签
tag, tag_created = Tag.objects.get_or_create(name="标签" + str(i))
# 只有在文章是新创建的时候才添加标签
if article_created:
article.tags.add(tag)
article.tags.add(basetag)
article.save()
from djangoblog.utils import cache
cache.clear()

@ -0,0 +1,33 @@
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0006_alter_blogsettings_options'),
]
operations = [
migrations.AddField(
model_name='article',
name='like_count',
field=models.PositiveIntegerField(default=0, verbose_name='likes'),
),
migrations.AddField(
model_name='article',
name='favorite_count',
field=models.PositiveIntegerField(default=0, verbose_name='favorites'),
),
migrations.AddField(
model_name='article',
name='like_users',
field=models.ManyToManyField(blank=True, related_name='liked_articles', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='article',
name='favorite_users',
field=models.ManyToManyField(blank=True, related_name='favorite_articles', to=settings.AUTH_USER_MODEL),
),
]

@ -88,6 +88,18 @@ class Article(BaseModel):
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
like_count = models.PositiveIntegerField('likes', default=0)
favorite_count = models.PositiveIntegerField('favorites', default=0)
like_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='liked_articles',
blank=True
)
favorite_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='favorite_articles',
blank=True
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
@ -139,6 +151,26 @@ class Article(BaseModel):
self.views += 1
self.save(update_fields=['views'])
def like_once_by_user(self, user):
if not user.is_authenticated:
return False
if self.like_users.filter(pk=user.pk).exists():
return False
self.like_users.add(user)
self.like_count += 1
self.save(update_fields=['like_count'])
return True
def favorite_once_by_user(self, user):
if not user.is_authenticated:
return False
if self.favorite_users.filter(pk=user.pk).exists():
return False
self.favorite_users.add(user)
self.favorite_count += 1
self.save(update_fields=['favorite_count'])
return True
def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)

@ -273,7 +273,7 @@ def load_article_metas(article, user):
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
def load_pagination_info(page_obj, page_type, tag_name, sort=None):
previous_url = ''
next_url = ''
if page_type == '':
@ -334,6 +334,12 @@ def load_pagination_info(page_obj, page_type, tag_name):
'page': previous_number,
'category_name': category.slug})
if sort:
if next_url:
next_url = f"{next_url}?sort={sort}"
if previous_url:
previous_url = f"{previous_url}?sort={sort}"
return {
'previous_url': previous_url,
'next_url': next_url,
@ -341,8 +347,8 @@ def load_pagination_info(page_obj, page_type, tag_name):
}
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
@register.inclusion_tag('blog/tags/article_info.html', takes_context=True)
def load_article_detail(context, article, isindex, user):
"""
加载文章详情
:param article:
@ -352,11 +358,25 @@ def load_article_detail(article, isindex, user):
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
request = context.get('request')
liked_ids = request.session.get('liked_articles', []) if request else []
favorited_ids = request.session.get('favorited_articles', []) if request else []
has_liked = False
has_favorited = False
if request and hasattr(request, 'user') and request.user.is_authenticated:
has_liked = article.like_users.filter(pk=request.user.pk).exists()
has_favorited = article.favorite_users.filter(pk=request.user.pk).exists()
else:
has_liked = article.id in liked_ids
has_favorited = article.id in favorited_ids
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
'has_liked': has_liked,
'has_favorited': has_favorited,
}

@ -230,3 +230,57 @@ class ArticleTest(TestCase):
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
def test_like_and_favorite(self):
user = BlogUser.objects.get_or_create(
email="user@test.com",
username="user1")[0]
user.set_password("pwd12345")
user.save()
category = Category()
category.name = "likecat"
category.save()
article = Article()
article.title = "likefavtitle"
article.body = "body"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
like_url = reverse('blog:like_article', kwargs={'article_id': article.id})
fav_url = reverse('blog:favorite_article', kwargs={'article_id': article.id})
rsp = self.client.post(like_url)
self.assertEqual(rsp.status_code, 200)
article.refresh_from_db()
self.assertEqual(article.like_count, 1)
rsp = self.client.post(like_url)
article.refresh_from_db()
self.assertEqual(article.like_count, 1)
rsp = self.client.post(fav_url)
self.assertEqual(rsp.status_code, 200)
article.refresh_from_db()
self.assertEqual(article.favorite_count, 1)
rsp = self.client.post(fav_url)
article.refresh_from_db()
self.assertEqual(article.favorite_count, 1)
self.client.login(username='user1', password='pwd12345')
rsp = self.client.post(like_url)
article.refresh_from_db()
self.assertEqual(article.like_count, 2)
rsp = self.client.post(like_url)
article.refresh_from_db()
self.assertEqual(article.like_count, 2)
rsp = self.client.post(fav_url)
article.refresh_from_db()
self.assertEqual(article.favorite_count, 2)
rsp = self.client.post(fav_url)
article.refresh_from_db()
self.assertEqual(article.favorite_count, 2)

@ -41,6 +41,14 @@ urlpatterns = [
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
path(
r'article/<int:article_id>/like/',
views.like_article,
name='like_article'),
path(
r'article/<int:article_id>/favorite/',
views.favorite_article,
name='favorite_article'),
path(
'archives.html',
cache_page(

@ -5,6 +5,7 @@ import uuid
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.http import JsonResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
@ -59,6 +60,20 @@ class ArticleListView(ListView):
"""
raise NotImplementedError()
def get_sort(self):
sort = self.request.GET.get('sort', 'latest')
return sort if sort in ('latest', 'hot') else 'latest'
def order_queryset(self, qs):
sort = self.get_sort()
if sort == 'hot':
from django.utils import timezone
from datetime import timedelta
week_ago = timezone.now() - timedelta(days=7)
qs = qs.filter(pub_time__gte=week_ago)
return qs.order_by('-article_order', '-views', '-pub_time')
return qs.order_by('-article_order', '-pub_time')
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
@ -86,6 +101,7 @@ class ArticleListView(ListView):
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
kwargs['sort'] = self.get_sort()
return super(ArticleListView, self).get_context_data(**kwargs)
@ -97,11 +113,11 @@ class IndexView(ArticleListView):
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
qs = Article.objects.filter(type='a', status='p')
return self.order_queryset(qs)
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
cache_key = 'index_{page}_sort_{sort}'.format(page=self.page_number, sort=self.get_sort())
return cache_key
@ -159,6 +175,14 @@ class ArticleDetailView(DetailView):
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
liked_ids = self.request.session.get('liked_articles', [])
favorited_ids = self.request.session.get('favorited_articles', [])
context['has_liked'] = (
self.request.user.is_authenticated and article.like_users.filter(pk=self.request.user.pk).exists()
) or (article.id in liked_ids)
context['has_favorited'] = (
self.request.user.is_authenticated and article.favorite_users.filter(pk=self.request.user.pk).exists()
) or (article.id in favorited_ids)
return context
@ -176,17 +200,17 @@ class CategoryDetailView(ArticleListView):
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
qs = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
return self.order_queryset(qs)
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
cache_key = 'category_list_{categoryname}_{page}_sort_{sort}'.format(
categoryname=categoryname, page=self.page_number, sort=self.get_sort())
return cache_key
def get_context_data(self, **kwargs):
@ -210,15 +234,15 @@ class AuthorDetailView(ArticleListView):
def get_queryset_cache_key(self):
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
cache_key = 'author_{author_name}_{page}_sort_{sort}'.format(
author_name=author_name, page=self.page_number, sort=self.get_sort())
return cache_key
def get_queryset_data(self):
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
qs = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
return self.order_queryset(qs)
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
@ -238,17 +262,17 @@ class TagDetailView(ArticleListView):
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
qs = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
return self.order_queryset(qs)
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
cache_key = 'tag_{tag_name}_{page}_sort_{sort}'.format(
tag_name=tag_name, page=self.page_number, sort=self.get_sort())
return cache_key
def get_context_data(self, **kwargs):
@ -355,6 +379,50 @@ def page_not_found_view(
status=404)
@csrf_exempt
def like_article(request, article_id):
if request.method != 'POST':
return HttpResponseBadRequest('only post')
article = get_object_or_404(Article, pk=article_id)
if request.user.is_authenticated:
if article.like_users.filter(pk=request.user.pk).exists():
return JsonResponse({'ok': False, 'message': '已点赞', 'like_count': article.like_count})
article.like_users.add(request.user)
article.like_count += 1
article.save(update_fields=['like_count'])
return JsonResponse({'ok': True, 'message': '点赞成功', 'like_count': article.like_count})
liked = request.session.get('liked_articles', [])
if article.id in liked:
return JsonResponse({'ok': False, 'message': '已点赞', 'like_count': article.like_count})
liked.append(article.id)
request.session['liked_articles'] = liked
article.like_count += 1
article.save(update_fields=['like_count'])
return JsonResponse({'ok': True, 'message': '点赞成功', 'like_count': article.like_count})
@csrf_exempt
def favorite_article(request, article_id):
if request.method != 'POST':
return HttpResponseBadRequest('only post')
article = get_object_or_404(Article, pk=article_id)
if request.user.is_authenticated:
if article.favorite_users.filter(pk=request.user.pk).exists():
return JsonResponse({'ok': False, 'message': '已收藏', 'favorite_count': article.favorite_count})
article.favorite_users.add(request.user)
article.favorite_count += 1
article.save(update_fields=['favorite_count'])
return JsonResponse({'ok': True, 'message': '收藏成功', 'favorite_count': article.favorite_count})
favorited = request.session.get('favorited_articles', [])
if article.id in favorited:
return JsonResponse({'ok': False, 'message': '已收藏', 'favorite_count': article.favorite_count})
favorited.append(article.id)
request.session['favorited_articles'] = favorited
article.favorite_count += 1
article.save(update_fields=['favorite_count'])
return JsonResponse({'ok': True, 'message': '收藏成功', 'favorite_count': article.favorite_count})
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,

@ -3,6 +3,24 @@ from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
class ApprovalStatusFilter(admin.SimpleListFilter):
title = _('approval status')
parameter_name = 'approval'
def lookups(self, request, model_admin):
return (
('pending', _('Pending')),
('approved', _('Approved')),
)
def queryset(self, request, queryset):
value = self.value()
if value == 'pending':
return queryset.filter(is_enable=False)
if value == 'approved':
return queryset.filter(is_enable=True)
return queryset
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
@ -11,9 +29,17 @@ def disable_commentstatus(modeladmin, request, queryset):
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
def approve_comments(modeladmin, request, queryset):
queryset.update(is_enable=True)
def reject_comments(modeladmin, request, queryset):
queryset.update(is_enable=False)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
approve_comments.short_description = _('批量同意评论')
reject_comments.short_description = _('批量拒绝评论')
class CommentAdmin(admin.ModelAdmin):
@ -26,11 +52,11 @@ class CommentAdmin(admin.ModelAdmin):
'is_enable',
'creation_time')
list_display_links = ('id', 'body', 'is_enable')
list_filter = ('is_enable',)
list_filter = (ApprovalStatusFilter, 'is_enable')
exclude = ('creation_time', 'last_modify_time')
actions = [disable_commentstatus, enable_commentstatus]
actions = [approve_comments, reject_comments, enable_commentstatus, disable_commentstatus]
raw_id_fields = ('author', 'article')
search_fields = ('body',)
search_fields = ('body', 'author__username', 'article__title')
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._meta.model_name)

@ -74,7 +74,57 @@ class CommentsTest(TransactionTestCase):
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
parent_comment_id = article.comment_list()[0].id
def test_sensitive_words_block(self):
self.client.login(username='liangliangyy1', password='liangliangyy1')
category = Category()
category.name = "categorysens"
category.save()
from blog.models import Article
article = Article()
article.title = "sensitivetitle"
article.body = "content"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
comment_url = reverse('comments:postcomment', kwargs={'article_id': article.id})
rsp = self.client.post(comment_url, {'body': '这段话包含敏感词:政治'})
self.assertEqual(rsp.status_code, 200)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
def test_nested_reply_and_review(self):
self.client.login(username='liangliangyy1', password='liangliangyy1')
category = Category()
category.name = "categorynested"
category.save()
from blog.models import Article
article = Article()
article.title = "nestedtitle"
article.body = "content"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
comment_url = reverse('comments:postcomment', kwargs={'article_id': article.id})
rsp = self.client.post(comment_url, {'body': '父评论'})
self.assertEqual(rsp.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
self.update_article_comment_status(article)
parent = article.comment_set.first()
parent_comment_id = parent.pk
rsp = self.client.post(comment_url, {'body': '子回复', 'parent_comment_id': parent.pk})
self.assertEqual(rsp.status_code, 302)
self.update_article_comment_status(article)
comments = list(article.comment_list())
self.assertTrue(any(c.parent_comment_id == parent.pk for c in comments))
response = self.client.post(comment_url,
{
'body': '''
@ -99,7 +149,7 @@ class CommentsTest(TransactionTestCase):
self.assertEqual(len(article.comment_list()), 3)
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1)
self.assertEqual(len(tree), 2)
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
s = get_max_articleid_commentid()

@ -36,3 +36,18 @@ def send_comment_email(comment):
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e)
SENSITIVE_WORDS = {
'政治', '暴力', '辱骂', '黄赌毒', 'fuck', 'shit'
}
def contains_sensitive(text):
if not text:
return False
s = str(text).lower()
for w in SENSITIVE_WORDS:
if w.lower() in s:
return True
return False

@ -10,6 +10,7 @@ from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm
from .models import Comment
from .utils import contains_sensitive
class CommentPostView(FormView):
@ -44,6 +45,9 @@ class CommentPostView(FormView):
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
if contains_sensitive(form.cleaned_data.get('body', '')):
form.add_error('body', '包含敏感词,评论未提交')
return self.form_invalid(form)
comment = form.save(False)
comment.article = article
from djangoblog.utils import get_blog_setting

@ -111,7 +111,7 @@ DATABASES = {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWOR') or '123456',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
@ -290,11 +290,6 @@ LOGGING = {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
}
@ -361,20 +356,7 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
# 安全头部配置 - 防XSS和其他攻击
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# 内容安全策略 (CSP) - 防XSS攻击
CSP_DEFAULT_SRC = ["'self'"]
CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "cdn.mathjax.org", "*.googleapis.com"]
CSP_STYLE_SRC = ["'self'", "'unsafe-inline'", "*.googleapis.com", "*.gstatic.com"]
CSP_IMG_SRC = ["'self'", "data:", "*.lylinux.net", "*.gravatar.com", "*.githubusercontent.com"]
CSP_FONT_SRC = ["'self'", "*.googleapis.com", "*.gstatic.com"]
CSP_CONNECT_SRC = ["'self'"]
CSP_FRAME_SRC = ["'none'"]
CSP_OBJECT_SRC = ["'none'"]
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

@ -22,9 +22,8 @@ from django.urls import re_path
from haystack.views import search_view_factory
from django.http import JsonResponse
import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from blog.views import EsSearchView
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap

@ -0,0 +1,151 @@
# 开源软件的维护报告(组 8
组号8
小组成员:赵一迪,徐教峰,宋尧君,高升,刘帅
## 3. 开源软件的维护
### 3.1 维护的内容
对开源软件进行了如表 3 所示的软件维护,包括增加新的功能、优化系统以及修复缺陷。
表 3. 开源软件维护的要素
| 序号 | 维护类别 | 名称 | 描述 | 负责组员 |
|------|------------------|--------------------------|----------------------------------------------------------------------|---------|
| 1 | 新增功能/优化系统 | 列表页排序与分页优化 | 顶部“最新/最热(近一周)”切换,分页保留排序参数,列表与缓存协同 | 赵一迪 |
| 2 | 新增功能/优化系统 | 最近一周“最热”逻辑 | 近 7 天过滤 + 热度排序,模板文案显示“最热(近一周)” | 徐教峰 |
| 3 | 新增功能/优化系统 | 点赞与收藏 | 模型新增点赞/收藏字段、多对多关系,幂等方法与详情页状态合并 | 宋尧君 |
| 4 | 新增功能/优化系统 | 我的收藏页 | 新增 `/favorites/` 路由与列表视图,侧边栏入口,分页展示 | 高升 |
| 5 | 新增功能/优化系统 | 评论系统增强 | 嵌套回复(楼中楼)、审核开关、敏感词拦截,前台仅显示已审核评论 | 刘帅 |
| 6 | 新增功能/优化系统 | 后台评论管理增强 | 审批状态筛选器、批量同意/拒绝动作、搜索增强(正文/作者/文章标题) | 赵一迪 |
| 7 | 修复缺陷 | 迁移顺序导致字段缺失问题 | 调整迁移依赖与顺序,修复 `Unknown column 'blog_article.like_count'` | 团队协作 |
参考代码锚点:
- 排序与分页:`templates/blog/article_index.html:29-41``templates/blog/tags/article_pagination.html`
- 点赞与收藏:`blog/models.py:89-106, 154-172``blog/views.py:176-186`
- 我的收藏:`accounts/urls.py:28``accounts/views.py:201-212``templates/blog/tags/sidebar.html:121`
- 评论增强:`comments/models.py:23-28``comments/utils.py:29-40``comments/views.py:49-53``blog/models.py:174-184`
- 后台评论:`comments/admin.py:8-22, 33-42, 49-53`
### 3.2 维护所产生的设计
为实现上述维护,对以下包与类的设计进行了调整与新增。下图为维护后关键子系统的设计类图(以不同颜色标识修改/新增):
#### 图*. 经维护后“评论子系统”的设计类图
```mermaid
classDiagram
class Article {
+id
+title
+comment_list()
}
class Comment {
+id
+body
+is_enable: bool
+parent_comment: Comment
}
class CommentPostView {
+form_valid()
}
class ApprovalStatusFilter {
+queryset()
}
class CommentAdmin {
+actions
+list_filter
}
class BlogSettings {
+comment_need_review: bool
}
class Utils {
+contains_sensitive(text)
}
Article <.. Comment :
CommentPostView --> Article : 设定目标文章
CommentPostView --> Comment : 创建评论
CommentPostView --> BlogSettings : 审核开关
CommentPostView --> Utils : 敏感词拦截
CommentAdmin ..> ApprovalStatusFilter : 使用筛选器
CommentAdmin <.. Comment :
```
- 修改类(绿色):`CommentPostView`(新增审核与敏感词判定)、`CommentAdmin`(新增筛选与批量动作)
- 新增类(蓝色):`ApprovalStatusFilter`、`Utils.contains_sensitive`
- 既有类(灰色):`Article`、`Comment`、`BlogSettings`
#### 图*. 经维护后“收藏子系统”的设计类图
```mermaid
classDiagram
class BlogUser {
+id
+username
}
class Article {
+id
+title
+favorite_users: M2M<BlogUser>
+favorite_count: int
+favorite_once_by_user(user)
}
class FavoriteArticlesView {
+get_queryset()
+get_context_data()
}
class AccountsUrls {
+/favorites -> FavoriteArticlesView
}
BlogUser "0..*" -- "0..*" Article : favorite_users 多对多
FavoriteArticlesView --> BlogUser : request.user
FavoriteArticlesView --> Article : queryset按发布时间倒序
AccountsUrls --> FavoriteArticlesView : 路由绑定
```
- 修改类(绿色):`Article`(新增收藏字段/方法)
- 新增类(蓝色):`FavoriteArticlesView`、`AccountsUrls` 路由项
- 既有类(灰色):`BlogUser`
### 3.3 维护代码数量及质量情况
表 4. 开源软件维护的代码分析(按文件估算)
| 序号 | 维护类别 | 名称 | 受影响的代码行数(±) | 负责组员 |
|------|--------------------|----------------|-----------------------|---------|
| 1 | 新增功能/优化系统 | 评论敏感词与审核 | ~+19`comments/utils.py:15``comments/views.py:4` | 刘帅 |
| 2 | 新增功能/优化系统 | 评论测试补充 | ~+50`comments/tests.py:50` | 团队 |
| 3 | 新增功能/优化系统 | 后台审核增强 | ~+29`comments/admin.py:29` | 赵一迪 |
| 4 | 新增功能/优化系统 | 我的收藏页 | ~+19视图+1路由+1模板入口 | 高升 |
| 5 | 修复缺陷 | 测试用例修复 | ~+2/-2`accounts/tests.py` | 团队 |
合计:~120 行左右;所有改动遵循既有代码风格与安全规范,不引入敏感信息;通过现有测试套件验证(`python manage.py test accounts comments -v 2`),确保功能正确与回归稳定。
质量说明:
- 代码规范:遵循项目既有命名与模块划分,避免引入新依赖;复用模板与通用工具。
- 可测试性:为关键功能补充了集成/场景测试;用例独立、断言明确。
- 缓存一致性:评论审核通过触发信号清理详情与侧边栏相关缓存(`djangoblog/blog_signals.py:85-121`)。
### 3.4 维护后的软件原型
- 列表页:顶部“最新/最热(近一周)”排序切换;分页导航保留排序参数;文章卡片统一渲染。
- 详情页:点赞与收藏按钮,防重复计数;评论区提交、敏感词拦截、待审不可见;审核通过后展示;子回复形成缩进楼层。
- 我的收藏:登录用户“我的收藏”列表页,分页显示收藏文章;侧边栏入口直达。
- 后台评论:按“审批状态”筛选列表;支持“批量同意/批量拒绝”;可按正文/作者用户名/文章标题搜索。
与原有差异:用户探索更便捷(排序)、互动更顺畅(点赞/收藏)、治理更高效(审核与批量)、用户资产可视化(我的收藏)。
## 4. 实践收获与体会
- 收获 1用例驱动的阅读和维护能快速锁定主干与关键类方法显著提升改造效率。
- 收获 2在不新增依赖的前提下复用模板与工具能降低回归风险提高一致性。
- 收获 3审核与缓存的联动是评论场景的关键信号清理与列表过滤确保数据一致。
- 问题 1早期迁移顺序导致字段缺失后续建立迁移依赖检查清单避免同类问题。
- 问题 2敏感词列表应可配置并支持后台维护后续计划将其从代码常量迁移至配置项。
---
附:若需要导出为 Word/PDF可依据本 Markdown 文档转换,并在图示处插入渲染后的 Mermaid 导出图片。

@ -26,11 +26,18 @@
</header><!-- .archive-header -->
{% endif %}
<div class="sort-controls" style="margin-bottom:10px;">
<span>排序:</span>
<a href="{{ request.path }}?sort=latest" {% if sort == 'latest' %}style="font-weight:bold;"{% endif %}>最新</a>
|
<a href="{{ request.path }}?sort=hot" {% if sort == 'hot' %}style="font-weight:bold;"{% endif %}>最热(近一周)</a>
</div>
{% for article in article_list %}
{% load_article_detail article True user %}
{% endfor %}
{% if is_paginated %}
{% load_pagination_info page_obj page_type tag_name %}
{% load_pagination_info page_obj page_type tag_name sort %}
{% endif %}
</div><!-- #content -->

@ -71,6 +71,50 @@
{% load_article_metas article user %}
{% if not isindex %}
<div class="article-actions" style="margin-top:10px;">
<button id="like-btn" data-id="{{ article.pk }}" data-url-like="{% url 'blog:like_article' article.pk %}" {% if has_liked %}disabled{% endif %}>
👍 点赞 (<span id="like-count">{{ article.like_count }}</span>)
</button>
<button id="fav-btn" data-id="{{ article.pk }}" data-url-fav="{% url 'blog:favorite_article' article.pk %}" {% if has_favorited %}disabled{% endif %}>
⭐ 收藏 (<span id="fav-count">{{ article.favorite_count }}</span>)
</button>
<span id="action-msg" style="margin-left:10px;"></span>
</div>
<script>
(function(){
function post(url){
return fetch(url, {method: 'POST'}).then(function(r){return r.json()});
}
var likeBtn = document.getElementById('like-btn');
var favBtn = document.getElementById('fav-btn');
var likeCount = document.getElementById('like-count');
var favCount = document.getElementById('fav-count');
var msg = document.getElementById('action-msg');
if(likeBtn){
likeBtn.addEventListener('click', function(){
var url = likeBtn.getAttribute('data-url-like');
post(url).then(function(d){
if(d.like_count!==undefined){ likeCount.textContent = d.like_count; }
likeBtn.disabled = d.ok === true || d.message === '已点赞';
msg.textContent = d.message || '';
});
});
}
if(favBtn){
favBtn.addEventListener('click', function(){
var url = favBtn.getAttribute('data-url-fav');
post(url).then(function(d){
if(d.favorite_count!==undefined){ favCount.textContent = d.favorite_count; }
favBtn.disabled = d.ok === true || d.message === '已收藏';
msg.textContent = d.message || '';
});
});
}
})();
</script>
{% endif %}
</article><!-- #post -->
<!-- 文章底部插件 -->

@ -119,6 +119,7 @@
<ul>
<li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li>
{% if user.is_authenticated %}
<li><a href="{% url 'account:favorites' %}">我的收藏</a></li>
<li><a href="{% url "account:logout" %}" rel="nofollow">{% trans 'logout' %}</a>
</li>

Loading…
Cancel
Save