Compare commits

..

3 Commits

Author SHA1 Message Date
flj666 a9395660f5 Update code
3 months ago
flj666 6884ccbc47 update finally
3 months ago
flj666 addb57bb2a update fifth
3 months ago

@ -0,0 +1 @@
mysql

@ -4,12 +4,12 @@ WORKDIR /code/djangoblog/
RUN apt-get update && \
apt-get install default-libmysqlclient-dev gettext -y && \
rm -rf /var/lib/apt/lists/*
ADD requirements.txt requirements.txt
COPY requirements.txt requirements.txt
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn[gevent] && \
pip cache purge
ADD . .
COPY . .
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]

@ -48,7 +48,7 @@ class AccountTest(TestCase):
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
@ -96,7 +96,7 @@ class AccountTest(TestCase):
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()

@ -124,8 +124,7 @@ class LoginView(FormView):
#zxm 表单验证成功后的处理
def form_valid(self, form):
form = AuthenticationForm(data=self.request.POST, request=self.request)
# 不再重新创建form直接使用传入的form参数
if form.is_valid():
delete_sidebar_cache() #zxm 删除侧边栏缓存
logger.info(self.redirect_field_name)
@ -151,11 +150,11 @@ class LoginView(FormView):
#xy 账户结果处理函数
def account_result(request):
type = request.GET.get('type') #zxm 获取类型参数
id = request.GET.get('id') #zxm 获取用户ID
action_type = request.GET.get('type') #zxm 获取类型参数避免与内置type冲突
user_id = request.GET.get('id') #zxm 获取用户ID避免与内置id冲突
user = get_object_or_404(get_user_model(), id=id) #zxm 获取用户对象
logger.info(type)
user = get_object_or_404(get_user_model(), id=user_id) #zxm 获取用户对象
logger.info(action_type)
if user.is_active: #zxm 如果用户已激活
return HttpResponseRedirect('/') #zxm 重定向到首页
if type and type in ['register', 'validation']: #zxm 处理注册或验证类型

@ -51,10 +51,10 @@ class ArticlelAdmin(admin.ModelAdmin):
'creation_time',
'views',
'status',
'type',
'article_type',
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
list_filter = ('status', 'article_type', 'category')
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True

@ -27,7 +27,7 @@ def seo_processor(requests):
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'nav_pages': Article.objects.filter(
type='p',
article_type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,

@ -197,7 +197,7 @@ class ArticleDocumentManager():
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
type=article.article_type,
views=article.views,
article_order=article.article_order) for article in articles]

@ -0,0 +1,90 @@
# Generated by Django 5.2.4 on 2025-11-23 18:24
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='article',
name='type',
),
migrations.RemoveField(
model_name='links',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='article_type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AddField(
model_name='links',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='blogsettings',
name='beian_code',
field=models.CharField(blank=True, default='', max_length=2000, verbose_name='备案号'),
),
migrations.AlterField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', verbose_name='公共尾部'),
),
migrations.AlterField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', verbose_name='公共头部'),
),
migrations.AlterField(
model_name='blogsettings',
name='gongan_beiancode',
field=models.TextField(blank=True, default='', max_length=2000, verbose_name='公安备案号'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, verbose_name='adsense code'),
),
migrations.CreateModel(
name='Favorite',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('last_modify_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'favorite',
'verbose_name_plural': 'favorite',
'unique_together': {('user', 'article')},
},
),
migrations.CreateModel(
name='Like',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('last_modify_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'like',
'verbose_name_plural': 'like',
'unique_together': {('user', 'article')},
},
),
]

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-11-23 19:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0007_remove_article_type_remove_links_last_mod_time_and_more'),
]
operations = [
migrations.AddField(
model_name='article',
name='slug',
field=models.SlugField(blank=True, max_length=200, null=True, verbose_name='slug'),
),
]

@ -3,10 +3,12 @@
import logging
import re
from abc import abstractmethod
from django.utils.functional import cached_property
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Count
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@ -30,10 +32,17 @@ class LinkShowType(models.TextChoices):
#fkc 所有模型的基类,包含通用字段,避免重复代码
# 定义缓存时间常量,避免重复使用字面量
CACHE_10_HOURS = 60 * 60 * 10
# 字段名称常量
CREATION_TIME_LABEL = 'creation time'
MODIFY_TIME_LABEL = 'modify time'
CACHE_100_MINUTES = 60 * 100
class BaseModel(models.Model):
id = models.AutoField(primary_key=True) #fkc 主键,自动递增
creation_time = models.DateTimeField(_('creation time'), default=now) #fkc 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) #fkc 最后修改时间
creation_time = models.DateTimeField(_(CREATION_TIME_LABEL), default=now) #fkc 创建时间
last_modify_time = models.DateTimeField(_(MODIFY_TIME_LABEL), default=now) #fkc 最后修改时间
#fkc 重写保存方法自动处理slug字段用于生成友好的URL
def save(self, *args, **kwargs):
@ -86,6 +95,7 @@ class Article(BaseModel):
('p', _('Page')), # 静态页面
)
title = models.CharField(_('title'), max_length=200, unique=True) #cll 文章标题
slug = models.SlugField(_('slug'), max_length=200, blank=True, null=True) #cll URL友好的标识符
body = MDTextField(_('body')) #cll 文章正文支持Markdown格式
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) #cll 发布时间
@ -99,7 +109,7 @@ class Article(BaseModel):
max_length=1,
choices=COMMENT_STATUS,
default='o') #cll 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') #cll 内容类型
article_type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') #cll 内容类型
views = models.PositiveIntegerField(_('views'), default=0) #cll 浏览次数
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
@ -134,13 +144,13 @@ class Article(BaseModel):
#cll 获取文章的URL
def get_absolute_url(self):
if self.type == 'a':
if self.article_type == 'a':
return reverse('blog:detail', kwargs={'article_id': self.id, 'slug': self.slug})
elif self.type == 'p':
elif self.article_type == 'p':
return reverse('blog:page', kwargs={'article_id': self.id, 'slug': self.slug})
#cll 获取分类树缓存10小时
@cache_decorator(60 * 60 * 10) # 缓存10小时
@cache_decorator(CACHE_10_HOURS) # 缓存10小时
def get_category_tree(self):
category = self.category
names = [category.name]
@ -149,9 +159,18 @@ class Article(BaseModel):
names.append(category.name)
return names
#cll 保存文章,更新修改时间
#cll 保存文章,更新修改时间并自动生成slug
def save(self, *args, **kwargs):
self.last_modify_time = now()
# 自动生成slug
if not self.slug:
self.slug = slugify(self.title)
# 确保slug唯一性
original_slug = self.slug
counter = 1
while Article.objects.filter(slug=self.slug).exclude(id=self.id).exists():
self.slug = f"{original_slug}-{counter}"
counter += 1
return super().save(*args, **kwargs)
#cll 增加文章浏览次数
@ -170,12 +189,12 @@ class Article(BaseModel):
return reverse('admin:%s_%s_change' % info, args=(self.id,))
#cll 获取下一篇文章缓存100分钟
@cache_decorator(expiration=60 * 100) # 缓存100分钟
@cache_decorator(expiration=CACHE_100_MINUTES) # 缓存100分钟
def next_article(self):
return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first()
#cll 获取上一篇文章缓存100分钟
@cache_decorator(expiration=60 * 100) # 缓存100分钟
@cache_decorator(expiration=CACHE_100_MINUTES) # 缓存100分钟
def prev_article(self):
return Article.objects.filter(id__lt=self.id, status='p').order_by('-id').first()
@ -187,6 +206,14 @@ class Article(BaseModel):
return result.group(1)
else:
return None
@cached_property
def likes_count(self):
return self.like_set.count()
@cached_property
def favorites_count(self):
return self.favorite_set.count()
#xy 分类模型
@ -215,7 +242,7 @@ class Category(BaseModel):
return self.name
#xy 获取分类树缓存10小时
@cache_decorator(60 * 60 * 10) # 缓存10小时
@cache_decorator(CACHE_10_HOURS) # 缓存10小时
def get_category_tree(self):
names = [self.name]
category = self.parent_category
@ -225,7 +252,7 @@ class Category(BaseModel):
return names
#xy 获取子分类列表缓存10小时
@cache_decorator(60 * 60 * 10) # 缓存10小时
@cache_decorator(CACHE_10_HOURS) # 缓存10小时
def get_sub_categorys(self):
categories = []
all_categories = Category.objects.all()
@ -254,7 +281,7 @@ class Tag(BaseModel):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
#zhj 获取标签下的文章数量缓存10小时
@cache_decorator(60 * 60 * 10) # 缓存10小时
@cache_decorator(CACHE_10_HOURS) # 缓存10小时
def get_article_count(self):
return Article.objects.filter(tags=self).count()
@ -264,6 +291,9 @@ class Tag(BaseModel):
verbose_name_plural = verbose_name #zhj 复数形式
# 文章点赞和收藏模型将在文件末尾使用继承BaseModel的版本定义
#flj 友情链接模型
class Links(models.Model):
name = models.CharField(_('link name'), max_length=30, unique=True) #flj 链接名称
@ -276,8 +306,8 @@ class Links(models.Model):
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I) #flj 显示类型
creation_time = models.DateTimeField(_('creation time'), default=now) #flj 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) #flj 修改时间
creation_time = models.DateTimeField(_(CREATION_TIME_LABEL), default=now) #flj 创建时间
last_modify_time = models.DateTimeField(_(MODIFY_TIME_LABEL), default=now) #flj 修改时间,与基类保持一致
class Meta:
ordering = ['sequence'] #flj 按序号排序
@ -295,8 +325,8 @@ class SideBar(models.Model):
content = models.TextField(_('content')) #zxm 侧边栏内容支持HTML
sequence = models.IntegerField(_('order'), unique=True) #zxm 排序序号
is_enable = models.BooleanField(_('is enable'), default=True) #zxm 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) #zxm 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) #zxm 修改时间
creation_time = models.DateTimeField(_(CREATION_TIME_LABEL), default=now) #zxm 创建时间
last_mod_time = models.DateTimeField(_(MODIFY_TIME_LABEL), default=now) #zxm 修改时间
class Meta:
ordering = ['sequence'] #zxm 按序号排序
@ -309,6 +339,45 @@ class SideBar(models.Model):
#fkc 网站设置模型
# 点赞模型
class Like(BaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'),
on_delete=models.CASCADE) # 关联用户
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 关联文章
class Meta:
unique_together = ('user', 'article') # 确保一个用户只能对一篇文章点赞一次
verbose_name = _('like')
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.user.username} likes {self.article.title}"
# 收藏模型
class Favorite(BaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'),
on_delete=models.CASCADE) # 关联用户
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 关联文章
class Meta:
unique_together = ('user', 'article') # 确保一个用户只能对一篇文章收藏一次
verbose_name = _('favorite')
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.user.username} favorites {self.article.title}"
class BlogSettings(models.Model):
site_name = models.CharField(
_('site name'),
@ -338,19 +407,18 @@ class BlogSettings(models.Model):
show_google_adsense = models.BooleanField(_('show adsense'), default=False) #fkc 是否显示广告
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='') #fkc 广告代码
_('adsense code'), max_length=2000, blank=True, default='') #fkc 广告代码
open_site_comment = models.BooleanField(_('open site comment'), default=True) #fkc 是否开放评论
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) #fkc 评论是否需要审核
global_header = models.TextField("公共头部", null=True, blank=True, default='') #fkc 公共头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') #fkc 公共尾部HTML
global_header = models.TextField("公共头部", blank=True, default='') #fkc 公共头部HTML
global_footer = models.TextField("公共尾部", blank=True, default='') #fkc 公共尾部HTML
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='') #fkc ICP备案号
show_gongan_code = models.BooleanField(
@ -358,7 +426,6 @@ class BlogSettings(models.Model):
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='') #fkc 公安备案号

@ -0,0 +1,64 @@
{% extends "blog/base.html" %}
{% load staticfiles %}
{% load custom_markdown %}
{% load likes_tags %}
{% load static %}
{% block title %}{{ title }} - {{ site_name }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="well well-lg">
<h2 class="text-center">我的收藏</h2>
{% if article_list %}
<div class="article-list">
{% for article in article_list %}
<div class="article">
<div class="title">
<h3><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h3>
</div>
<div class="meta">
<span class="author"><a href="{% url 'blog:author' article.author.username %}">{{ article.author.username }}</a></span>
<span class="views">{{ article.views }} 阅读</span>
<span class="likes">{{ article.total_likes }} 点赞</span>
<span class="favorites">{{ article.total_favorites }} 收藏</span>
<span class="time">{{ article.created_time }}</span>
</div>
<div class="entry-content">
<p>{{ article.body | custom_markdown | truncatechars:200 }}</p>
</div>
<div class="article-footer">
<a href="{{ article.get_absolute_url }}" class="btn btn-primary btn-sm">阅读全文</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info text-center">
<p>您还没有收藏任何文章</p>
</div>
{% endif %}
{% if article_list %}
<div class="pagination">
{% if article_list.has_previous %}
<a href="?page={{ article_list.previous_page_number }}" class="page-link">上一页</a>
{% endif %}
<span class="current-page">{{ article_list.number }} / {{ article_list.paginator.num_pages }}</span>
{% if article_list.has_next %}
<a href="?page={{ article_list.next_page_number }}" class="page-link">下一页</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
{% include "blog/right_sidebar.html" %}
</div>
</div>
</div>
{% endblock %}

@ -59,7 +59,7 @@ class ArticleTest(TestCase):
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
@ -74,7 +74,7 @@ class ArticleTest(TestCase):
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)

@ -73,4 +73,18 @@ urlpatterns = [
r'clean',
views.clean_cache_view,
name='clean'), # 清除缓存接口
# 点赞和收藏功能
path(
r'article/like/',
views.like_article,
name='like_article'), # 文章点赞接口
path(
r'article/favorite/',
views.favorite_article,
name='favorite_article'), # 文章收藏接口
# 我的收藏页面
path(
r'user/favorites/',
views.user_favorites,
name='user_favorites'), # 我的收藏页面
]

@ -1,9 +1,13 @@
#flj 博客视图文件,处理博客相关的页面请求
import json
import logging
import os
import uuid
from django.conf import settings
# 模板常量定义
ERROR_PAGE_TEMPLATE = 'blog/error_page.html'
from django.core.paginator import Paginator # 用于分页
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
@ -16,7 +20,7 @@ from django.views.generic.detail import DetailView # 详情页视图基类
from django.views.generic.list import ListView # 列表页视图基类
from haystack.views import SearchView # 搜索视图
from blog.models import Article, Category, LinkShowType, Links, Tag
from blog.models import Article, Category, LinkShowType, Links, Tag, Like, Favorite
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks # 插件管理
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -89,7 +93,7 @@ class IndexView(ArticleListView):
#flj 获取首页文章数据,过滤条件为类型为文章(a)且状态为已发布(p)
def get_queryset_data(self):
# 获取所有已发布的文章
article_list = Article.objects.filter(type='a', status='p')
article_list = Article.objects.filter(article_type='a', status='p')
return article_list
#flj 生成首页的缓存键,包含页码信息
@ -112,11 +116,38 @@ class ArticleDetailView(DetailView):
#flj 获取文章详情页的上下文数据,包括评论表单、相关文章等
def get_context_data(self, **kwargs):
#flj 调用父类方法获取基础上下文数据
kwargs = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
article.viewed() # 增加浏览次数
#flj 添加评论表单
#flj 获取文章评论列表
#flj 添加相关文章
comments = article.comment_list()
comment_form = CommentForm()
#flj 添加点赞和收藏信息
is_liked = False
is_favorited = False
like_count = article.like_set.count()
favorite_count = article.favorite_set.count()
if self.request.user.is_authenticated:
is_liked = Like.objects.filter(user=self.request.user, article=article).exists()
is_favorited = Favorite.objects.filter(user=self.request.user, article=article).exists()
#flj 添加到上下文
kwargs['comments'] = comments
kwargs['comment_form'] = comment_form
kwargs['like_count'] = like_count
kwargs['is_liked'] = is_liked
kwargs['favorite_count'] = favorite_count
kwargs['is_favorited'] = is_favorited
#flj 调用插件处理文章内容
return super().get_context_data(**kwargs)
article_content = article.body_to_string()
article_content = hooks.call_hook(ARTICLE_CONTENT_HOOK_NAME, request=self.request, content=article_content)
article.body = article_content
return kwargs
#flj 分类详情视图,显示指定分类下的文章列表
@ -128,6 +159,7 @@ class CategoryDetailView(ArticleListView):
#flj 获取分类下的文章数据根据URL参数中的分类ID过滤
def get_queryset_data(self):
# 使用局部变量避免未使用参数警告
#flj 获取分类ID
#flj 过滤该分类下的已发布文章
#flj 返回查询结果
@ -159,6 +191,7 @@ class AuthorDetailView(ArticleListView):
#flj 获取作者的文章数据根据URL参数中的作者ID过滤
def get_queryset_data(self):
# 使用局部变量避免未使用参数警告
#flj 获取作者ID
#flj 过滤该作者的已发布文章
return article_list
@ -179,6 +212,7 @@ class TagDetailView(ArticleListView):
#flj 获取标签下的文章数据根据URL参数中的标签ID过滤
def get_queryset_data(self):
# 使用局部变量避免未使用参数警告
#flj 获取标签ID
#flj 过滤该标签下的已发布文章
return article_list
@ -260,22 +294,23 @@ def fileupload(request):
#flj 处理页面未找到的情况
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
template_name=ERROR_PAGE_TEMPLATE):
'''
404错误页面
'''
#flj 渲染错误页面
context = get_blog_setting()
return render(request, template_name, context, status=404)
#flj 500错误页面视图
#flj 处理服务器内部错误的情况
def server_error_view(request, template_name='blog/error_page.html'):
def server_error_view(request, template_name=ERROR_PAGE_TEMPLATE):
'''
500错误页面
'''
#flj 渲染错误页面
context = get_blog_setting()
return render(request, template_name, context, status=500)
@ -283,12 +318,12 @@ def server_error_view(request, template_name='blog/error_page.html'):
#flj 处理权限拒绝的情况
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
template_name=ERROR_PAGE_TEMPLATE):
'''
403错误页面
'''
#flj 渲染错误页面
context = get_blog_setting()
return render(request, template_name, context, status=403)
@ -302,3 +337,113 @@ def clean_cache_view(request):
#flj 清理缓存
#flj 返回成功信息
return HttpResponse(_('清理缓存成功'))
# 点赞视图
@csrf_exempt
def like_article(request):
'''
处理文章点赞
'''
if request.method == 'POST' and request.user.is_authenticated:
try:
data = json.loads(request.body)
article_id = data.get('article_id')
article = Article.objects.get(id=article_id)
like, created = Like.objects.get_or_create(user=request.user, article=article)
if not created:
# 如果已经点赞,则取消点赞
like.delete()
# 获取最新的点赞数
count = article.like_set.count()
return HttpResponse(json.dumps({
'status': 'success',
'count': count
}), content_type="application/json")
except Article.DoesNotExist:
return HttpResponse(json.dumps({
'status': 'error',
'message': _('文章不存在')
}), content_type="application/json")
except Exception as e:
return HttpResponse(json.dumps({
'status': 'error',
'message': str(e)
}), content_type="application/json")
return HttpResponse(json.dumps({
'status': 'error',
'message': _('需要登录才能点赞')
}), content_type="application/json")
# 收藏视图
@csrf_exempt
def favorite_article(request):
'''
处理文章收藏
'''
if request.method == 'POST' and request.user.is_authenticated:
try:
data = json.loads(request.body)
article_id = data.get('article_id')
article = Article.objects.get(id=article_id)
favorite, created = Favorite.objects.get_or_create(user=request.user, article=article)
if not created:
# 如果已经收藏,则取消收藏
favorite.delete()
# 获取最新的收藏数
count = article.favorite_set.count()
return HttpResponse(json.dumps({
'status': 'success',
'count': count
}), content_type="application/json")
except Article.DoesNotExist:
return HttpResponse(json.dumps({
'status': 'error',
'message': _('文章不存在')
}), content_type="application/json")
except Exception as e:
return HttpResponse(json.dumps({
'status': 'error',
'message': str(e)
}), content_type="application/json")
return HttpResponse(json.dumps({
'status': 'error',
'message': _('需要登录才能收藏')
}), content_type="application/json")
# 我的收藏页面视图
def user_favorites(request):
'''
显示用户收藏的文章列表
'''
if not request.user.is_authenticated:
return render(request, ERROR_PAGE_TEMPLATE, {'message': _('请先登录')}, status=403)
# 获取用户收藏的文章
favorites = Favorite.objects.filter(user=request.user).order_by('-created_at')
# 提取文章并进行分页
articles = [fav.article for fav in favorites if fav.article.status == 'p']
paginator = Paginator(articles, settings.PAGINATE_BY)
page = request.GET.get('page', 1)
try:
article_list = paginator.page(page)
except Exception:
article_list = paginator.page(1)
context = {
'article_list': article_list,
'title': _('我的收藏'),
'page_type': '我的收藏',
'has_favorites': len(articles) > 0
}
return render(request, 'blog/user_favorites.html', context)

@ -42,7 +42,7 @@ class CommentsTest(TransactionTestCase):
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()

@ -91,7 +91,7 @@ class ElasticSearchBackend(BaseSearchBackend):
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.filter('term', article_type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()

@ -21,7 +21,7 @@ class DjangoBlogFeed(Feed):
return get_user_model().objects.first().get_absolute_url()
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
return Article.objects.filter(article_type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
return item.title

@ -159,7 +159,7 @@ USE_TZ = False # 不使用UTC时间
# 静态文件配置
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 静态文件收集目录
STATIC_URL = '/static/' # 静态文件URL前缀
STATICFILES = os.path.join(BASE_DIR, 'static') # 静态文件源目录
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # 静态文件源目录
# 搜索配置
HAYSTACK_CONNECTIONS = {
@ -303,8 +303,11 @@ STATICFILES_FINDERS = (
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
# 在开发环境中动态启用压缩根据DEBUG状态自动调整
COMPRESS_ENABLED = not DEBUG # 调试模式下禁用压缩
COMPRESS_OFFLINE = False # 禁用离线模式
COMPRESS_ROOT = os.path.join(BASE_DIR, 'static') # 直接指向静态文件源目录
COMPRESS_URL = STATIC_URL # 确保COMPRESS_URL与STATIC_URL一致
COMPRESS_CSS_FILTERS = [

@ -37,7 +37,7 @@ class ServerManagerTest(TestCase):
article.body = "nicecontentccc"
article.author = user
article.category = c
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
s = TextMessage([])

@ -48,7 +48,7 @@ class AccountTest(TestCase):
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
@ -96,7 +96,7 @@ class AccountTest(TestCase):
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()

@ -197,7 +197,7 @@ class ArticleDocumentManager():
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
type=article.article_type,
views=article.views,
article_order=article.article_order) for article in articles]

@ -86,7 +86,7 @@ class Article(BaseModel):
max_length=1,
choices=COMMENT_STATUS,
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
article_type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,

@ -59,7 +59,7 @@ class ArticleTest(TestCase):
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
@ -74,7 +74,7 @@ class ArticleTest(TestCase):
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)

@ -97,7 +97,7 @@ class IndexView(ArticleListView):
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
article_list = Article.objects.filter(article_type='a', status='p')
return article_list
def get_queryset_cache_key(self):
@ -216,7 +216,7 @@ class AuthorDetailView(ArticleListView):
def get_queryset_data(self):
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
author__username=author_name, article_type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
@ -238,7 +238,7 @@ class TagDetailView(ArticleListView):
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
tags__name=tag_name, article_type='a', status='p')
return article_list
def get_queryset_cache_key(self):

@ -42,7 +42,7 @@ class CommentsTest(TransactionTestCase):
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()

@ -91,7 +91,7 @@ class ElasticSearchBackend(BaseSearchBackend):
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.filter('term', article_type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()

@ -21,7 +21,7 @@ class DjangoBlogFeed(Feed):
return get_user_model().objects.first().get_absolute_url()
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
return Article.objects.filter(article_type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
return item.title

@ -37,7 +37,7 @@ class ServerManagerTest(TestCase):
article.body = "nicecontentccc"
article.author = user
article.category = c
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
s = TextMessage([])

@ -17,7 +17,7 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
<header class="archive-header">
@ -47,7 +47,7 @@
{% endfor %}
</ul>
</div>
</div><!-- #content -->
</main><!-- #content -->
</div><!-- #primary -->
{% endblock %}

@ -5,10 +5,10 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
{% load_article_detail article False user %}
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
{% if next_article %}
@ -24,7 +24,7 @@
</nav><!-- .nav-single -->
{% endif %}
</div><!-- #content -->
</main><!-- #content -->
{% if article.comment_status == "o" and OPEN_SITE_COMMENT %}

@ -18,7 +18,7 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
{% if page_type and tag_name %}
<header class="archive-header">
@ -33,7 +33,7 @@
{% load_pagination_info page_obj page_type tag_name %}
{% endif %}
</div><!-- #content -->
</main><!-- #content -->
</div><!-- #primary -->
{% endblock %}

@ -26,13 +26,13 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
<header class="archive-header">
<h1 class="archive-title">{{ message }}</h1>
</header><!-- .archive-header -->
</div><!-- #content -->
</main><!-- #content -->
</div><!-- #primary -->
{% endblock %}

@ -16,7 +16,7 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
<header class="archive-header">
@ -31,7 +31,7 @@
</li>
{% endfor %} </ul>
</div>
</div><!-- #content -->
</main><!-- #content -->
</div><!-- #primary -->
{% endblock %}

@ -37,7 +37,7 @@
</div>
</div><!-- .comments-link -->
<br/>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if not isindex %}
{% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %}

@ -8,7 +8,7 @@
</a>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if article.tags.all %}
{% trans 'and tagged' %}

@ -4,7 +4,7 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
<header class="archive-header">
@ -17,6 +17,6 @@
|
<a href="/">回到首页</a>
</header><!-- .archive-header -->
</div>
</main><!-- #content -->
</div>
{% endblock %}

@ -12,7 +12,7 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
{% if query %}
<header class="archive-header">
{% if suggestion %}
@ -54,7 +54,7 @@
<h1 class="archive-title">哎呀,关键字:<span>{{ query }}</span>没有找到结果,要不换个词再试试?</h1>
</header><!-- .archive-header -->
{% endif %}
</div><!-- #content -->
</main><!-- #content -->
</div><!-- #primary -->
{% endblock %}

@ -17,7 +17,7 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
<header class="archive-header">

@ -5,10 +5,10 @@
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<main id="content">
{% load_article_detail article False user %}
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
{% if next_article %}

@ -37,7 +37,7 @@
</div>
</div><!-- .comments-link -->
<br/>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if not isindex %}
{% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %}
@ -69,6 +69,82 @@
</div><!-- .entry-content -->
{% if not isindex %}
<div class="article-engagement" style="margin: 1rem 0; padding: 1rem; border-top: 1px solid var(--border-color);">
<button class="engagement-btn like-btn" data-article-id="{{ article.id }}">
<span class="icon">👍</span>
<span class="count">{{ article.likes_count|default:0 }}</span>
<span class="text">{% trans 'Like' %}</span>
</button>
<button class="engagement-btn favorite-btn" data-article-id="{{ article.id }}">
<span class="icon"></span>
<span class="count">{{ article.favorites_count|default:0 }}</span>
<span class="text">{% trans 'Favorite' %}</span>
</button>
{% if not user.is_authenticated %}
<div class="auth-hint">
<span><a href="{% url 'account:login' %}?next={{ request.path }}">登录</a> 后进行操作</span>
</div>
{% endif %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 点赞功能
document.querySelector('.like-btn').addEventListener('click', function() {
const articleId = this.dataset.articleId;
const countEl = this.querySelector('.count');
fetch(`/article/like/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({ article_id: articleId })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
this.classList.toggle('is-active');
countEl.textContent = data.count;
} else {
alert(data.message || '操作失败');
}
})
.catch(error => console.error('Error:', error));
});
// 收藏功能
document.querySelector('.favorite-btn').addEventListener('click', function() {
const articleId = this.dataset.articleId;
const countEl = this.querySelector('.count');
fetch(`/article/favorite/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({ article_id: articleId })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
this.classList.toggle('is-active');
countEl.textContent = data.count;
} else {
alert(data.message || '操作失败');
}
})
.catch(error => console.error('Error:', error));
});
});
</script>
{% endif %}
{% load_article_metas article user %}
</article><!-- #post -->

@ -8,7 +8,7 @@
</a>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if article.tags.all %}
{% trans 'and tagged' %}

@ -1,7 +1,7 @@
{% load blog_tags %}
{% load i18n %}
<div id="secondary" class="widget-area" role="complementary">
<aside id="search-2" class="widget widget_search">
<aside id="secondary" class="widget-area" aria-label="侧边栏">
<aside id="search-2" class="widget widget_search" aria-label="搜索功能">
<form role="search" method="get" id="searchform" class="searchform" action="/search">
<div>
<label class="screen-reader-text" for="s">{% trans 'search' %}</label>
@ -13,7 +13,7 @@
{% if extra_sidebars %}
{% for sidebar in extra_sidebars %}
<aside class="widget_text widget widget_custom_html"><p class="widget-title">
<aside class="widget_text widget widget_custom_html" aria-label="自定义侧边栏"><p class="widget-title">
{{ sidebar.name }}</p>
<div class="textwidget custom-html-widget">
{{ sidebar.content|custom_markdown|safe }}
@ -23,7 +23,7 @@
{% endif %}
{% if most_read_articles %}
<aside id="views-4" class="widget widget_views"><p class="widget-title">Views</p>
<aside id="views-4" class="widget widget_views" aria-label="热门阅读"><p class="widget-title">Views</p>
<ul>
{% for a in most_read_articles %}
<li>
@ -37,7 +37,7 @@
</aside>
{% endif %}
{% if sidebar_categorys %}
<aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms"><p class="widget-title">{% trans 'category' %}</p>
<aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms" aria-label="文章分类"><p class="widget-title">{% trans 'category' %}</p>
<ul>
{% for c in sidebar_categorys %}
<li class="cat-item cat-item-184"><a href={{ c.get_absolute_url }}>{{ c.name }}</a>
@ -47,7 +47,7 @@
</aside>
{% endif %}
{% if sidebar_comments and open_site_comment %}
<aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments"><p class="widget-title">{% trans 'recent comments' %}</p>
<aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments" aria-label="最新评论"><p class="widget-title">{% trans 'recent comments' %}</p>
<ul id="recentcomments">
{% for c in sidebar_comments %}
@ -62,7 +62,7 @@
</aside>
{% endif %}
{% if recent_articles %}
<aside id="recent-posts-2" class="widget widget_recent_entries"><p class="widget-title">{% trans 'recent articles' %}</p>
<aside id="recent-posts-2" class="widget widget_recent_entries" aria-label="最新文章"><p class="widget-title">{% trans 'recent articles' %}</p>
<ul>
{% for a in recent_articles %}
@ -74,7 +74,7 @@
</aside>
{% endif %}
{% if sidabar_links %}
<aside id="linkcat-0" class="widget widget_links"><p class="widget-title">{% trans 'bookmark' %}</p>
<aside id="linkcat-0" class="widget widget_links" aria-label="书签链接"><p class="widget-title">{% trans 'bookmark' %}</p>
<ul class='xoxo blogroll'>
{% for l in sidabar_links %}
<li>
@ -86,25 +86,24 @@
</aside>
{% endif %}
{% if show_google_adsense %}
<aside id="text-2" class="widget widget_text"><p class="widget-title">Google AdSense</p>
<aside id="text-2" class="widget widget_text" aria-label="广告"><p class="widget-title">Google AdSense</p>
<div class="textwidget">
{{ google_adsense_codes|safe }}
</div>
</aside>
{% endif %}
{% if sidebar_tags %}
<aside id="tag_cloud-2" class="widget widget_tag_cloud"><p class="widget-title">{% trans 'Tag Cloud' %}</p>
<aside id="tag_cloud-2" class="widget widget_tag_cloud" aria-label="标签云"><p class="widget-title">{% trans 'Tag Cloud' %}</p>
<div class="tagcloud">
{% for tag,count,size in sidebar_tags %}
<a href="{{ tag.get_absolute_url }}"
class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}"
style="font-size: {{ size }}pt;" title="{{ count }}个话题"> {{ tag.name }}
</a>
{% for tag, count, size in sidebar_tags %}
<a href="{{ tag.get_absolute_url }}" class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}"
style="font-size: '{{ size }}pt';"
title="{{ count }}个话题">{{ tag.name }}</a>
{% endfor %}
</div>
</aside>
{% endif %}
<aside id="text-2" class="widget widget_text"><p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p>
<aside id="text-3" class="widget widget_text" aria-label="源码链接"><p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p>
<div class="textwidget">
<p><a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
@ -115,7 +114,7 @@
</div>
</aside>
<aside id="meta-3" class="widget widget_meta"><p class="widget-title">{% trans 'Function' %}</p>
<aside id="meta-3" class="widget widget_meta" aria-label="功能链接"><p class="widget-title">{% trans 'Function' %}</p>
<ul>
<li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li>
{% if user.is_authenticated %}
@ -133,4 +132,4 @@
</aside>
<div id="rocket" class="show" title="{% trans 'Click me to return to the top' %}"></div>
</div><!-- #secondary -->
</aside><!-- #secondary -->

@ -0,0 +1,139 @@
{% extends 'blog/base.html' %}
{% load i18n %}
{% load comment_tags %}
{% load staticfiles %}
{% load custom_markdown %}
{% block title %}{% trans "我的收藏" %} - {{ SITE_NAME }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<div class="title">
<h2>{% trans "我的收藏" %}</h2>
</div>
</div>
<div class="panel-body">
{% if has_favorites %}
{% for article in article_list %}
<div class="article post-preview">
<div class="article-inner">
<h3 class="article-title">
<a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
</h3>
<div class="article-meta">
<span class="article-date">
<i class="fa fa-calendar"></i>
{{ article.created_time }}
</span>
<span class="article-category">
<i class="fa fa-folder-o"></i>
<a href="{{ article.category.get_absolute_url }}">{{ article.category.name }}</a>
</span>
<span class="article-author">
<i class="fa fa-user-o"></i>
<a href="{{ article.author.get_absolute_url }}">{{ article.author.nickname }}</a>
</span>
<span class="article-view-count">
<i class="fa fa-eye"></i>
{{ article.views }}
</span>
<span class="article-comment-count">
<i class="fa fa-comment-o"></i>
{% get_comment_count article %}
</span>
</div>
<div class="article-content">
<p>
{{ article.body|custom_markdown|truncatechars:100 }}
</p>
</div>
<div class="article-footer">
<div class="article-more-link">
<a href="{{ article.get_absolute_url }}" class="btn btn-default">
阅读全文
</a>
</div>
</div>
</div>
</div>
<hr>
{% endfor %}
{% if page_obj.has_previous or page_obj.has_next %}
<div class="pagination">
<ul class="pagination">
{% if page_obj.has_previous %}
<li>
<a href="?page={{ page_obj.previous_page_number }}">{% trans "上一页" %}</a>
</li>
{% endif %}
{% if page_obj.has_next %}
<li>
<a href="?page={{ page_obj.next_page_number }}">{% trans "下一页" %}</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{% else %}
<div class="no-favorites">
<p class="text-center">您还没有收藏任何文章</p>
<div class="text-center">
<a href="/" class="btn btn-primary">
去首页看看
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
{% include 'blog/tags.html' %}
{% include 'blog/hotest_articles.html' %}
{% include 'blog/archives.html' %}
</div>
</div>
</div>
{% endblock %}
{% block extra_footer %}
<style>
.article-meta {
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
.article-meta span {
margin-right: 15px;
}
.article-meta a {
color: #666;
}
.article-meta a:hover {
color: #337ab7;
text-decoration: none;
}
.article-title {
margin-bottom: 10px;
}
.article-title a {
color: #333;
}
.article-title a:hover {
color: #337ab7;
text-decoration: none;
}
.no-favorites {
padding: 30px 0;
}
.no-favorites p {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
</style>
{% endblock %}

@ -25,6 +25,11 @@
<a href="{% url "blog:archives" %}">{% trans 'Article archive' %}</a>
</li>
{% if user.is_authenticated %}
<li class="menu-item menu-item-type-taxonomy menu-item-object-category">
<a href="{% url "blog:user_favorites" %}">{% trans '我的收藏' %}</a>
</li>
{% endif %}
</ul>
</div>
</nav><!-- #site-navigation -->
Loading…
Cancel
Save