ccy注释 #44

Closed
phm9gvnzi wants to merge 0 commits from ccy_branch into master

2
.idea/.gitignore vendored

@ -1,5 +1,3 @@
# 默认忽略的文件 # 默认忽略的文件
/shelf/ /shelf/
/workspace.xml /workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

@ -1,7 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project> </project>

@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/software-engineering-methodology-djq-branch(1).iml" filepath="$PROJECT_DIR$/.idea/software-engineering-methodology-djq-branch(1).iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/Django.iml" filepath="$PROJECT_DIR$/.idea/Django.iml" />
</modules> </modules>
</component> </component>
</project> </project>

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="$PROJECT_DIR$" vcs="Git" />
</component> </component>
</project> </project>

@ -0,0 +1,6 @@
<<<<<<< HEAD
这是我的个人分支说明
=======
# software-engineering-methodology
>>>>>>> f783378e06d6abd4513ad3220bf6f630b2fb7263

Binary file not shown.

@ -1,80 +1,59 @@
from django import forms from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
# 修正:导入 UsernameField from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UserChangeForm, UserCreationForm, UsernameField from django.contrib.auth.forms import UsernameField
from django.contrib.auth.models import Group
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# Register your models here. # Register your models here.
from .models import BlogUser from .models import BlogUser
class BlogUserCreationForm(UserCreationForm): class BlogUserCreationForm(forms.ModelForm):
""" password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
自定义用户创建表单 password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
"""
class Meta: class Meta:
model = BlogUser model = BlogUser
fields = ('username', 'email') # 根据你的模型调整 fields = ('email',)
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm): class BlogUserChangeForm(UserChangeForm):
"""
自定义用户修改表单
"""
class Meta: class Meta:
model = BlogUser model = BlogUser
fields = '__all__' fields = '__all__'
# 现在 UsernameField 已经被正确导入
field_classes = {'username': UsernameField} field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin): class BlogUserAdmin(UserAdmin):
"""
自定义用户Admin界面
"""
form = BlogUserChangeForm form = BlogUserChangeForm
add_form = BlogUserCreationForm add_form = BlogUserCreationForm
list_display = ( list_display = (
'id', 'id',
'nickname', # 确保模型中有此字段 'nickname',
'username', 'username',
'email', 'email',
'last_login', 'last_login',
'date_joined', 'date_joined',
'source' # 确保模型中有此字段 'source')
)
list_display_links = ('id', 'username') list_display_links = ('id', 'username')
ordering = ('-id',) ordering = ('-id',)
# 定义修改用户时显示的字段组
fieldsets = (
(None, {'fields': ('username', 'password')}),
# 确保模型中有 first_name, last_name, nickname 字段,否则移除
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'nickname')}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
}),
# 确保模型中有 source 字段,否则移除
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'source')}),
)
# 定义创建用户时显示的字段组
add_fieldsets = (
(None, {
'classes': ('wide',),
# 确保模型中有 nickname 字段,否则移除
'fields': ('username', 'email', 'nickname', 'password1', 'password2'),
}),
)
# 注册自定义用户模型和Admin类
admin.site.register(BlogUser, BlogUserAdmin)
# 如果你不想在Admin中管理Groups可以取消下面这行的注释
# admin.site.unregister(Group)

@ -1,46 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -1,14 +1,3 @@
# -*- coding: utf-8 -*-
"""
# 模块级注释app: accounts
作者djq
功能用户账户相关视图逻辑包含用户注册登录登出密码找回及邮箱验证等功能
关联
- 表单RegisterForm注册表单LoginForm登录表单
- 模型BlogUser自定义用户模型
- 模板account/registration_form.html注册页account/login.html登录页
- 工具函数djangoblog.utils中的邮件发送加密等工具
"""
import logging import logging
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
@ -43,55 +32,28 @@ logger = logging.getLogger(__name__)
# Create your views here. # Create your views here.
class RegisterView(FormView): class RegisterView(FormView):
""" form_class = RegisterForm
功能处理用户注册逻辑包括表单验证创建未激活用户发送邮箱验证链接 template_name = 'account/registration_form.html'
继承FormViewDjango表单处理基类
核心流程
1. 验证注册表单数据
2. 创建用户并设置为未激活状态
3. 生成邮箱验证链接包含用户ID和加密签名
4. 发送验证邮件到用户邮箱
5. 重定向到注册结果页
"""
form_class = RegisterForm # 关联注册表单类
template_name = 'account/registration_form.html' # 注册页面模板
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
"""
功能重写dispatch方法添加CSRF保护装饰器防止跨站请求伪造
参数*args, **kwargs视图函数的位置参数和关键字参数
返回父类dispatch方法的处理结果
"""
return super(RegisterView, self).dispatch(*args, **kwargs) return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
"""
功能当表单验证通过后执行的逻辑核心注册流程
参数form验证通过的注册表单实例
返回重定向到注册结果页的响应
"""
if form.is_valid(): if form.is_valid():
# djq: 创建用户但不立即保存(先设置额外属性)
user = form.save(False) user = form.save(False)
user.is_active = False # djq: 新用户默认未激活(需邮箱验证) user.is_active = False
user.source = 'Register' # djq: 标记用户来源为自主注册 user.source = 'Register'
user.save(True) # djq: 保存用户到数据库 user.save(True)
# djq: 获取当前站点域名(用于构建验证链接)
site = get_current_site().domain site = get_current_site().domain
# djq: 生成加密签名结合SECRET_KEY和用户ID防止链接被篡改
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# djq: 开发环境下使用本地域名
if settings.DEBUG: if settings.DEBUG:
site = '127.0.0.1:8000' site = '127.0.0.1:8000'
path = reverse('account:result') # djq: 验证结果页的路由 path = reverse('account:result')
# djq: 拼接邮箱验证链接包含用户ID和签名
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign) site=site, path=path, id=user.id, sign=sign)
# djq: 验证邮件内容(包含验证链接)
content = """ content = """
<p>请点击下面链接验证您的邮箱</p> <p>请点击下面链接验证您的邮箱</p>
@ -102,7 +64,6 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器 如果上面链接无法打开请将此链接复制至浏览器
{url} {url}
""".format(url=url) """.format(url=url)
# djq: 发送验证邮件到用户注册邮箱
send_email( send_email(
emailto=[ emailto=[
user.email, user.email,
@ -110,234 +71,134 @@ class RegisterView(FormView):
title='验证您的电子邮箱', title='验证您的电子邮箱',
content=content) content=content)
# djq: 重定向到注册结果页(提示用户查收验证邮件)
url = reverse('accounts:result') + \ url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id) '?type=register&id=' + str(user.id)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
# djq: 表单验证失败时,返回原页面并显示错误
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form
}) })
class LogoutView(RedirectView): class LogoutView(RedirectView):
""" url = '/login/'
功能处理用户登出逻辑清除会话并跳转至登录页
继承RedirectViewDjango重定向基类
"""
url = '/login/' # 登出后跳转的目标URL登录页
@method_decorator(never_cache) @method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""
功能重写dispatch方法添加禁止缓存装饰器确保每次登出请求都是最新的
参数*args, **kwargs视图函数的参数
返回父类dispatch方法的处理结果
"""
return super(LogoutView, self).dispatch(request, *args, **kwargs) return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
""" logout(request)
功能处理GET请求执行登出操作 delete_sidebar_cache()
参数requestHTTP请求对象
返回重定向到登录页的响应
"""
logout(request) # djq: 清除用户会话,完成登出
delete_sidebar_cache() # djq: 清除侧边栏缓存(可能包含用户相关信息)
return super(LogoutView, self).get(request, *args, **kwargs) return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView): class LoginView(FormView):
""" form_class = LoginForm
功能处理用户登录逻辑包括表单验证用户认证会话管理 template_name = 'account/login.html'
继承FormViewDjango表单处理基类 success_url = '/'
核心流程 redirect_field_name = REDIRECT_FIELD_NAME
1. 验证登录表单用户名/邮箱+密码 login_ttl = 2626560 # 一个月的时间
2. 认证用户身份
3. 根据"记住我"选项设置会话过期时间
4. 重定向到登录前的页面或首页
"""
form_class = LoginForm # 关联登录表单类
template_name = 'account/login.html' # 登录页面模板
success_url = '/' # 登录成功默认跳转页(首页)
redirect_field_name = REDIRECT_FIELD_NAME # 存储登录前URL的参数名
login_ttl = 2626560 # djq: 会话过期时间(单位:秒),此处为一个月
@method_decorator(sensitive_post_parameters('password')) @method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
@method_decorator(never_cache) @method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""
功能重写dispatch方法添加多重保护装饰器
- sensitive_post_parameters标记密码字段为敏感信息日志中隐藏
- csrf_protectCSRF保护
- never_cache禁止缓存
参数*args, **kwargs视图函数的参数
返回父类dispatch方法的处理结果
"""
return super(LoginView, self).dispatch(request, *args, **kwargs) return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self,** kwargs): def get_context_data(self, **kwargs):
"""
功能向模板传递额外上下文数据登录前的跳转URL
参数**kwargs上下文关键字参数
返回包含跳转URL的上下文字典
"""
# djq: 获取登录前的页面URL从请求参数中提取
redirect_to = self.request.GET.get(self.redirect_field_name) redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None: if redirect_to is None:
redirect_to = '/' # djq: 默认跳转至首页 redirect_to = '/'
kwargs['redirect_to'] = redirect_to # djq: 将跳转URL添加到上下文 kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(** kwargs) return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
"""
功能表单验证通过后执行的登录逻辑
参数form验证通过的登录表单实例
返回重定向到目标页面的响应
"""
# djq: 使用Django内置认证表单再次验证兼容用户名/邮箱登录)
form = AuthenticationForm(data=self.request.POST, request=self.request) form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid(): if form.is_valid():
delete_sidebar_cache() # djq: 清除侧边栏缓存(更新用户登录状态) delete_sidebar_cache()
logger.info(self.redirect_field_name) # djq: 记录跳转参数名到日志 logger.info(self.redirect_field_name)
# djq: 执行登录(将用户信息存入会话)
auth.login(self.request, form.get_user()) auth.login(self.request, form.get_user())
# djq: 如果勾选"记住我",设置会话过期时间为一个月
if self.request.POST.get("remember"): if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl) self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form) return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else: else:
# djq: 表单验证失败(如密码错误),返回原页面显示错误
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form
}) })
def get_success_url(self): def get_success_url(self):
"""
功能确定登录成功后的跳转URL优先跳转到登录前的页面
返回安全的跳转URL
"""
# djq: 从POST参数中获取登录前的URL
redirect_to = self.request.POST.get(self.redirect_field_name) redirect_to = self.request.POST.get(self.redirect_field_name)
# djq: 验证跳转URL是否安全防止跳转到外部恶意网站
if not url_has_allowed_host_and_scheme( if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[ url=redirect_to, allowed_hosts=[
self.request.get_host()]): self.request.get_host()]):
redirect_to = self.success_url # djq: 不安全则使用默认首页 redirect_to = self.success_url
return redirect_to return redirect_to
def account_result(request): def account_result(request):
""" type = request.GET.get('type')
功能处理注册结果和邮箱验证结果的展示 id = request.GET.get('id')
参数requestHTTP请求对象
返回渲染结果页面的响应
逻辑
1. 区分"注册成功""邮箱验证成功"两种场景
2. 验证场景合法性如验证链接的签名是否有效
3. 展示对应结果信息
"""
type = request.GET.get('type') # djq: 获取场景类型register/validation
id = request.GET.get('id') # djq: 获取用户ID
# djq: 获取对应的用户不存在则返回404
user = get_object_or_404(get_user_model(), id=id)
logger.info(type) # djq: 记录场景类型到日志
# djq: 如果用户已激活,直接跳转至首页(避免重复验证) user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active: if user.is_active:
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
if type and type in ['register', 'validation']: if type and type in ['register', 'validation']:
if type == 'register': if type == 'register':
# djq: 注册成功场景:提示用户查收验证邮件
content = ''' content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站 恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
''' '''
title = '注册成功' title = '注册成功'
else: else:
# djq: 邮箱验证场景:验证签名合法性 c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 计算正确签名 sign = request.GET.get('sign')
sign = request.GET.get('sign') # 获取请求中的签名
if sign != c_sign: if sign != c_sign:
return HttpResponseForbidden() # djq: 签名不匹配返回403禁止访问 return HttpResponseForbidden()
# djq: 签名验证通过,激活用户
user.is_active = True user.is_active = True
user.save() user.save()
# djq: 提示用户验证成功
content = ''' content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站 恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
''' '''
title = '验证成功' title = '验证成功'
# djq: 渲染结果页面
return render(request, 'account/result.html', { return render(request, 'account/result.html', {
'title': title, 'title': title,
'content': content 'content': content
}) })
else: else:
# djq: 场景类型不合法,跳转至首页
return HttpResponseRedirect('/') return HttpResponseRedirect('/')
class ForgetPasswordView(FormView): class ForgetPasswordView(FormView):
""" form_class = ForgetPasswordForm
功能处理密码找回逻辑通过邮箱验证后重置密码 template_name = 'account/forget_password.html'
继承FormViewDjango表单处理基类
"""
form_class = ForgetPasswordForm # 关联密码找回表单
template_name = 'account/forget_password.html' # 密码找回页面模板
def form_valid(self, form): def form_valid(self, form):
"""
功能表单验证通过后执行的密码重置逻辑
参数form验证通过的密码找回表单实例
返回重定向到登录页的响应
"""
if form.is_valid(): if form.is_valid():
# djq: 根据邮箱获取用户(假设表单已验证邮箱存在)
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# djq: 加密新密码并保存make_password自动处理哈希
blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save() blog_user.save()
# djq: 密码重置成功,跳转至登录页
return HttpResponseRedirect('/login/') return HttpResponseRedirect('/login/')
else: else:
# djq: 表单验证失败(如密码不一致),返回原页面
return self.render_to_response({'form': form}) return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View): class ForgetPasswordEmailCode(View):
"""
功能处理密码找回时的邮箱验证码发送逻辑
继承ViewDjango基础视图类
核心流程
1. 验证邮箱格式
2. 生成随机验证码
3. 发送验证码到目标邮箱
4. 存储验证码用于后续验证
"""
def post(self, request: HttpRequest): def post(self, request: HttpRequest):
"""
功能处理POST请求发送密码找回验证码
参数requestHTTP请求对象包含邮箱参数
返回"ok"字符串成功或错误提示
"""
# djq: 验证请求中的邮箱格式
form = ForgetPasswordCodeForm(request.POST) form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid(): if not form.is_valid():
return HttpResponse("错误的邮箱") # djq: 邮箱格式错误,返回提示 return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
to_email = form.cleaned_data["email"] # djq: 获取验证通过的邮箱
code = generate_code() # djq: 生成随机验证码 code = generate_code()
utils.send_verify_email(to_email, code) # djq: 发送验证码到邮箱 utils.send_verify_email(to_email, code)
utils.set_code(to_email, code) # djq: 存储验证码(如存入缓存,用于后续校验) utils.set_code(to_email, code)
return HttpResponse("ok") # djq: 发送成功,返回标识 return HttpResponse("ok")

@ -4,192 +4,109 @@ from django.contrib.auth import get_user_model
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
# 导入 blog 应用下的所有模型 # Register your models here.
from .models import ( from .models import Article
Article, Category, Tag, Links, SideBar, BlogSettings,
UserProfile, Favorite # 确保导入了 UserProfile 和 Favorite
)
# ------------------------------------------------------------------------------
# Article Admin
# ------------------------------------------------------------------------------
class ArticleForm(forms.ModelForm): class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta: class Meta:
model = Article model = Article
fields = '__all__' fields = '__all__'
def make_article_publish(modeladmin, request, queryset):
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p') queryset.update(status='p')
make_article_publish.short_description = _('Publish selected articles')
def draft_article(modeladmin, request, queryset): def draft_article(modeladmin, request, queryset):
queryset.update(status='d') queryset.update(status='d')
draft_article.short_description = _('Set selected articles to draft')
def close_article_commentstatus(modeladmin, request, queryset): def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c') queryset.update(comment_status='c')
close_article_commentstatus.short_description = _('Close comments for selected articles')
def open_article_commentstatus(modeladmin, request, queryset): def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o') queryset.update(comment_status='o')
open_article_commentstatus.short_description = _('Open comments for selected articles')
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin): makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20 list_per_page = 20
search_fields = ('body', 'title') search_fields = ('body', 'title')
form = ArticleForm form = ArticleForm
list_display = ( list_display = (
'id', 'title', 'author', 'link_to_category', 'id',
'pub_time', 'views', 'status', 'type', 'article_order' 'title',
) 'author',
'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category', 'tags') list_filter = ('status', 'type', 'category')
filter_horizontal = ('tags',) filter_horizontal = ('tags',)
# 使用 fieldsets 来组织编辑页面的字段布局,更清晰
fieldsets = (
(_('Basic Information'), {
'fields': ('title', 'author', 'category', 'tags', 'status', 'type')
}),
(_('Content'), {
'fields': ('body',)
}),
(_('Settings'), {
'fields': ('article_order', 'show_toc', 'comment_status'),
'classes': ('collapse',) # 默认折叠
}),
)
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time')
view_on_site = True view_on_site = True
actions = [ actions = [
make_article_publish, draft_article, makr_article_publish,
close_article_commentstatus, open_article_commentstatus draft_article,
] close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj): def link_to_category(self, obj):
if obj.category: info = (obj.category._meta.app_label, obj.category._meta.model_name)
info = (obj.category._meta.app_label, obj.category._meta.model_name) link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
return format_html(u'<a href="{}">{}</a>', link, obj.category.name)
return _('None') link_to_category.short_description = _('category')
link_to_category.short_description = _('Category')
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj,** kwargs) form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# 限制作者只能是超级管理员 form.base_fields['author'].queryset = get_user_model(
form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) ).objects.filter(is_superuser=True)
return form return form
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None): def get_view_on_site_url(self, obj=None):
if obj: if obj:
return obj.get_full_url() url = obj.get_full_url()
return super().get_view_on_site_url(obj) return url
else:
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
# ------------------------------------------------------------------------------
# Category Admin
# ------------------------------------------------------------------------------
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'parent_category', 'index')
list_display_links = ('id', 'name')
list_filter = ('parent_category',)
search_fields = ('name',)
exclude = ('slug', 'last_mod_time', 'creation_time')
# ------------------------------------------------------------------------------
# Tag Admin
# ------------------------------------------------------------------------------
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
list_display_links = ('id', 'name')
search_fields = ('name',)
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_mod_time', 'creation_time')
# ------------------------------------------------------------------------------
# Links Admin class CategoryAdmin(admin.ModelAdmin):
# ------------------------------------------------------------------------------ list_display = ('name', 'parent_category', 'index')
@admin.register(Links) exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin): class LinksAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'link', 'sequence', 'is_enable', 'show_type')
list_display_links = ('id', 'name')
list_filter = ('is_enable', 'show_type')
search_fields = ('name', 'link')
exclude = ('last_mod_time', 'creation_time') exclude = ('last_mod_time', 'creation_time')
# ------------------------------------------------------------------------------
# SideBar Admin
# ------------------------------------------------------------------------------
@admin.register(SideBar)
class SideBarAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'is_enable', 'sequence')
list_display_links = ('id', 'name')
list_filter = ('is_enable',)
search_fields = ('name', 'content')
exclude = ('last_mod_time', 'creation_time')
# ------------------------------------------------------------------------------ class SideBarAdmin(admin.ModelAdmin):
# BlogSettings Admin (Singleton Pattern) list_display = ('name', 'content', 'is_enable', 'sequence')
# ------------------------------------------------------------------------------
@admin.register(BlogSettings)
class BlogSettingsAdmin(admin.ModelAdmin):
list_display = ('id', 'site_name')
exclude = ('last_mod_time', 'creation_time') exclude = ('last_mod_time', 'creation_time')
def has_add_permission(self, request):
"""
限制只能有一个配置实例
如果已经存在一条记录则禁用添加按钮
"""
if BlogSettings.objects.exists():
return False
return True
def save_model(self, request, obj, form, change): class BlogSettingsAdmin(admin.ModelAdmin):
""" pass
确保始终只有一个配置实例
"""
if not change and BlogSettings.objects.exists():
raise ValidationError(_('There can be only one Blog Settings instance.'))
super().save_model(request, obj, form, change)
# ------------------------------------------------------------------------------
# UserProfile Admin
# ------------------------------------------------------------------------------
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
list_display = ('id', 'user', 'created_at')
list_display_links = ('id', 'user')
list_filter = ('created_at',)
search_fields = ('user__username', 'user__email', 'bio')
fieldsets = (
(_('User'), {
'fields': ('user',)
}),
(_('Profile Information'), {
'fields': ('bio', 'avatar')
}),
(_('Social Links'), {
'fields': ('website', 'github', 'twitter', 'weibo'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'updated_at') # 时间戳设为只读
# ------------------------------------------------------------------------------
# Favorite Admin (Optional)
# ------------------------------------------------------------------------------
@admin.register(Favorite)
class FavoriteAdmin(admin.ModelAdmin):
list_display = ('id', 'user', 'article', 'created_at')
list_display_links = ('id',)
list_filter = ('created_at',)
search_fields = ('user__username', 'article__title')
readonly_fields = ('created_at',)
# 通常不希望管理员手动创建或修改收藏,所以可以禁用相关权限
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False

@ -1,253 +1,213 @@
import time # 用于生成时间戳作为文档ID import time
import elasticsearch.client # Elasticsearch客户端工具
from django.conf import settings # 导入Django项目配置 import elasticsearch.client
# 导入Elasticsearch DSL相关模块用于定义文档结构和字段类型 from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections # 用于创建Elasticsearch连接 from elasticsearch_dsl.connections import connections
from blog.models import Article # 导入Django博客文章模型 from blog.models import Article
# 检查是否启用了Elasticsearch通过判断配置中是否有ELASTICSEARCH_DSL
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
# 创建Elasticsearch连接连接地址从Django配置中获取
connections.create_connection( connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch # 导入Elasticsearch客户端 from elasticsearch import Elasticsearch
# 初始化Elasticsearch客户端
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient # 导入Ingest API客户端用于处理数据管道 from elasticsearch.client import IngestClient
c = IngestClient(es) c = IngestClient(es)
try: try:
# 检查是否存在名为'geoip'的数据管道用于解析IP地址的地理位置信息
c.get_pipeline('geoip') c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError: except elasticsearch.exceptions.NotFoundError:
# 若不存在,则创建'geoip'管道通过IP地址添加地理位置信息
c.put_pipeline('geoip', body='''{ c.put_pipeline('geoip', body='''{
"description" : "Add geoip info", # 管道描述添加IP的地理信息 "description" : "Add geoip info",
"processors" : [ "processors" : [
{ {
"geoip" : { "geoip" : {
"field" : "ip" # 基于文档中的'ip'字段解析地理信息 "field" : "ip"
} }
} }
] ]
}''') }''')
# 内部文档类存储IP地址解析后的地理位置信息嵌套在ElapsedTimeDocument中
class GeoIp(InnerDoc): class GeoIp(InnerDoc):
continent_name = Keyword() # 大陆名称Keyword类型精确匹配不分词 continent_name = Keyword()
country_iso_code = Keyword() # 国家ISO代码如CN、US country_iso_code = Keyword()
country_name = Keyword() # 国家名称 country_name = Keyword()
location = GeoPoint() # 经纬度坐标Elasticsearch的地理点类型 location = GeoPoint()
# 内部文档类存储用户代理中的浏览器信息嵌套在UserAgent中
class UserAgentBrowser(InnerDoc): class UserAgentBrowser(InnerDoc):
Family = Keyword() # 浏览器家族如Chrome、Firefox Family = Keyword()
Version = Keyword() # 浏览器版本 Version = Keyword()
# 内部文档类:存储用户代理中的操作系统信息(继承浏览器信息结构)
class UserAgentOS(UserAgentBrowser): class UserAgentOS(UserAgentBrowser):
pass # 结构与浏览器一致包含Family系统家族和Version系统版本 pass
# 内部文档类存储用户代理中的设备信息嵌套在UserAgent中
class UserAgentDevice(InnerDoc): class UserAgentDevice(InnerDoc):
Family = Keyword() # 设备家族如iPhone、Windows Family = Keyword()
Brand = Keyword() # 设备品牌如Apple、Samsung Brand = Keyword()
Model = Keyword() # 设备型号如iPhone 13 Model = Keyword()
# 内部文档类存储用户代理User-Agent完整信息嵌套在ElapsedTimeDocument中
class UserAgent(InnerDoc): class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False) # 浏览器信息(可选) browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False) # 操作系统信息(可选) os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False) # 设备信息(可选) device = Object(UserAgentDevice, required=False)
string = Text() # 原始User-Agent字符串 string = Text()
is_bot = Boolean() # 是否为爬虫机器人 is_bot = Boolean()
# Elasticsearch文档类记录性能耗时信息如接口响应时间
class ElapsedTimeDocument(Document): class ElapsedTimeDocument(Document):
url = Keyword() # 请求URL精确匹配 url = Keyword()
time_taken = Long() # 耗时(毫秒) time_taken = Long()
log_datetime = Date() # 日志记录时间 log_datetime = Date()
ip = Keyword() # 访问者IP地址 ip = Keyword()
geoip = Object(GeoIp, required=False) # 地理位置信息由geoip管道解析可选 geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False) # 用户代理信息(可选) useragent = Object(UserAgent, required=False)
class Index: class Index:
name = 'performance' # 索引名称:存储性能数据 name = 'performance'
settings = { settings = {
"number_of_shards": 1, # 主分片数量 "number_of_shards": 1,
"number_of_replicas": 0 # 副本分片数量单节点环境设为0 "number_of_replicas": 0
} }
class Meta: class Meta:
doc_type = 'ElapsedTime' # 文档类型Elasticsearch 7.x后可省略 doc_type = 'ElapsedTime'
# 管理类处理ElapsedTimeDocument的索引创建、删除和数据插入
class ElaspedTimeDocumentManager: class ElaspedTimeDocumentManager:
@staticmethod @staticmethod
def build_index(): def build_index():
"""创建performance索引若不存在"""
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 检查索引是否存在
res = client.indices.exists(index="performance") res = client.indices.exists(index="performance")
if not res: if not res:
# 初始化索引根据ElapsedTimeDocument的定义创建映射
ElapsedTimeDocument.init() ElapsedTimeDocument.init()
@staticmethod @staticmethod
def delete_index(): def delete_index():
"""删除performance索引"""
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 忽略400索引不存在和404请求错误的错误
es.indices.delete(index='performance', ignore=[400, 404]) es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod @staticmethod
def create(url, time_taken, log_datetime, useragent, ip): def create(url, time_taken, log_datetime, useragent, ip):
"""创建一条性能日志文档并保存到Elasticsearch"""
# 确保索引已创建
ElaspedTimeDocumentManager.build_index() ElaspedTimeDocumentManager.build_index()
# 构建用户代理信息对象
ua = UserAgent() ua = UserAgent()
ua.browser = UserAgentBrowser() ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family # 浏览器家族 ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string # 浏览器版本 ua.browser.Version = useragent.browser.version_string
ua.os = UserAgentOS() ua.os = UserAgentOS()
ua.os.Family = useragent.os.family # 操作系统家族 ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string # 操作系统版本 ua.os.Version = useragent.os.version_string
ua.device = UserAgentDevice() ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family # 设备家族 ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand # 设备品牌 ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model # 设备型号 ua.device.Model = useragent.device.model
ua.string = useragent.ua_string # 原始User-Agent字符串 ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot # 是否为爬虫 ua.is_bot = useragent.is_bot
# 创建性能日志文档
doc = ElapsedTimeDocument( doc = ElapsedTimeDocument(
meta={ meta={
# 用当前时间戳毫秒级作为文档ID确保唯一性 'id': int(
'id': int(round(time.time() * 1000)) round(
time.time() *
1000))
}, },
url=url, # 请求URL url=url,
time_taken=time_taken, # 耗时 time_taken=time_taken,
log_datetime=log_datetime, # 记录时间 log_datetime=log_datetime,
useragent=ua, # 用户代理信息 useragent=ua, ip=ip)
ip=ip # 访问IP
)
# 保存文档时应用'geoip'管道自动解析IP的地理位置
doc.save(pipeline="geoip") doc.save(pipeline="geoip")
# Elasticsearch文档类存储博客文章信息用于全文搜索
class ArticleDocument(Document): class ArticleDocument(Document):
# 文章内容使用IK分词器ik_max_word最大粒度分词ik_smart智能分词
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 文章标题(同上,支持中文分词搜索)
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 作者信息(嵌套对象)
author = Object(properties={ author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 作者昵称 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() # 作者ID 'id': Integer()
}) })
# 分类信息(嵌套对象)
category = Object(properties={ category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 分类名称 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() # 分类ID 'id': Integer()
}) })
# 标签信息(嵌套对象列表)
tags = Object(properties={ tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 标签名称 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer() # 标签ID 'id': Integer()
}) })
pub_time = Date() # 发布时间 pub_time = Date()
status = Text() # 文章状态(如发布、草稿) status = Text()
comment_status = Text() # 评论状态(如允许、关闭) comment_status = Text()
type = Text() # 文章类型(如原创、转载) type = Text()
views = Integer() # 浏览量 views = Integer()
article_order = Integer() # 文章排序权重 article_order = Integer()
class Index: class Index:
name = 'blog' # 索引名称:存储博客文章数据 name = 'blog'
settings = { settings = {
"number_of_shards": 1, "number_of_shards": 1,
"number_of_replicas": 0 "number_of_replicas": 0
} }
class Meta: class Meta:
doc_type = 'Article' # 文档类型 doc_type = 'Article'
# 管理类处理ArticleDocument的索引创建、删除、数据同步
class ArticleDocumentManager(): class ArticleDocumentManager():
def __init__(self): def __init__(self):
"""初始化时创建blog索引若不存在"""
self.create_index() self.create_index()
def create_index(self): def create_index(self):
"""创建blog索引根据ArticleDocument的定义"""
ArticleDocument.init() ArticleDocument.init()
def delete_index(self): def delete_index(self):
"""删除blog索引"""
from elasticsearch import Elasticsearch from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404]) es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles): def convert_to_doc(self, articles):
"""将Django的Article模型对象列表转换为ArticleDocument列表"""
return [ return [
ArticleDocument( ArticleDocument(
meta={'id': article.id}, # 用文章ID作为文档ID meta={
body=article.body, # 文章内容 'id': article.id},
title=article.title, # 文章标题 body=article.body,
title=article.title,
author={ author={
'nickname': article.author.username, # 作者用户名 'nickname': article.author.username,
'id': article.author.id # 作者ID 'id': article.author.id},
},
category={ category={
'name': article.category.name, # 分类名称 'name': article.category.name,
'id': article.category.id # 分类ID 'id': article.category.id},
}, tags=[
# 标签列表遍历文章的tags多对多字段 {
tags=[{'name': t.name, 'id': t.id} for t in article.tags.all()], 'name': t.name,
pub_time=article.pub_time, # 发布时间 'id': t.id} for t in article.tags.all()],
status=article.status, # 文章状态 pub_time=article.pub_time,
comment_status=article.comment_status, # 评论状态 status=article.status,
type=article.type, # 文章类型 comment_status=article.comment_status,
views=article.views, # 浏览量 type=article.type,
article_order=article.article_order # 排序权重 views=article.views,
) for article in articles article_order=article.article_order) for article in articles]
]
def rebuild(self, articles=None): def rebuild(self, articles=None):
"""重建blog索引将文章数据同步到Elasticsearch默认同步所有文章""" ArticleDocument.init()
ArticleDocument.init() # 确保索引结构正确
# 若未指定文章列表,则同步所有文章
articles = articles if articles else Article.objects.all() articles = articles if articles else Article.objects.all()
# 转换为文档列表
docs = self.convert_to_doc(articles) docs = self.convert_to_doc(articles)
# 批量保存文档
for doc in docs: for doc in docs:
doc.save() doc.save()
def update_docs(self, docs): def update_docs(self, docs):
"""更新文档列表(批量保存)"""
for doc in docs: for doc in docs:
doc.save() doc.save()

@ -17,22 +17,3 @@ class BlogSearchForm(SearchForm):
if self.cleaned_data['querydata']: if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata']) logger.info(self.cleaned_data['querydata'])
return datas return datas
# blog/forms.py
from django import forms
from .models import UserProfile
class UserProfileUpdateForm(forms.ModelForm):
"""
用户资料更新表单
"""
class Meta:
model = UserProfile
fields = ['avatar', 'bio', 'website', 'github', 'twitter', 'weibo']
widgets = {
'bio': forms.Textarea(attrs={'rows': 5, 'placeholder': 'Tell us about yourself...'}),
'website': forms.URLInput(attrs={'placeholder': 'https://'}),
'github': forms.URLInput(attrs={'placeholder': 'https://github.com/'}),
'twitter': forms.URLInput(attrs={'placeholder': 'https://twitter.com/'}),
'weibo': forms.URLInput(attrs={'placeholder': 'https://weibo.com/'}),
}

@ -1,88 +1,42 @@
import logging import logging
import time import time
# 用于获取客户端IP地址的工具
from ipware import get_client_ip from ipware import get_client_ip
# 用于解析用户代理(浏览器/设备信息)的工具
from user_agents import parse from user_agents import parse
# 导入Elasticsearch相关配置和文档管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# 初始化日志记录器,用于记录中间件运行过程中的信息和错误
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class OnlineMiddleware(object): class OnlineMiddleware(object):
"""
自定义Django中间件用于
1. 计算页面渲染耗时
2. 收集访问日志IP用户代理访问时间等
3. 当启用Elasticsearch时将访问性能数据存入搜索引擎
4. 在响应内容中替换特定标记为页面加载时间
"""
def __init__(self, get_response=None): def __init__(self, get_response=None):
"""
中间件初始化方法
:param get_response: Django框架传入的下一个响应处理函数用于构建中间件链
"""
self.get_response = get_response self.get_response = get_response
# 调用父类初始化方法兼容Python 2.x在Python 3中可省略
super().__init__() super().__init__()
def __call__(self, request): def __call__(self, request):
""" ''' page render time '''
中间件核心处理方法在请求到达视图前和响应返回客户端前执行
:param request: Django请求对象包含客户端请求的所有信息
:return: 经过处理的Django响应对象
"""
# 记录请求处理开始时间(用于计算页面渲染耗时)
start_time = time.time() start_time = time.time()
# 调用下一个中间件或视图函数,获取响应对象
response = self.get_response(request) response = self.get_response(request)
# 从请求头中获取用户代理字符串(包含浏览器、设备等信息)
http_user_agent = request.META.get('HTTP_USER_AGENT', '') http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# 通过ipware工具获取客户端IP地址返回元组(ip地址, 是否为公开IP)
ip, _ = get_client_ip(request) ip, _ = get_client_ip(request)
# 解析用户代理字符串,生成结构化的用户代理对象(方便提取浏览器)
user_agent = parse(http_user_agent) user_agent = parse(http_user_agent)
# 非流式响应如普通HTML页面排除文件下载等流式响应才进行处理
if not response.streaming: if not response.streaming:
try: try:
# 计算页面渲染总耗时(当前时间 - 开始时间)
cast_time = time.time() - start_time cast_time = time.time() - start_time
# 如果启用了Elasticsearch将访问性能数据存入搜索引擎
if ELASTICSEARCH_ENABLED: if ELASTICSEARCH_ENABLED:
# 转换耗时为毫秒并保留两位小数
time_taken = round((cast_time) * 1000, 2) time_taken = round((cast_time) * 1000, 2)
# 获取当前请求的URL路径
url = request.path url = request.path
# 导入Django时区工具获取当前时间
from django.utils import timezone from django.utils import timezone
# 通过文档管理器创建并保存访问记录
ElaspedTimeDocumentManager.create( ElaspedTimeDocumentManager.create(
url=url, # 访问的URL url=url,
time_taken=time_taken, # 页面加载耗时(毫秒) time_taken=time_taken,
log_datetime=timezone.now(), # 访问时间 log_datetime=timezone.now(),
useragent=user_agent, # 用户代理信息(浏览器/设备) useragent=user_agent,
ip=ip # 客户端IP地址 ip=ip)
)
# 将响应内容中的<!!LOAD_TIMES!!>标记替换为实际渲染耗时保留前5位字符
# 注意仅适用于文本类型响应如HTML二进制响应会跳过
response.content = response.content.replace( response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', # 待替换的二进制标记 b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
str.encode(str(cast_time)[:5]) # 转换为二进制的耗时字符串
)
# 捕获处理过程中的所有异常,避免中间件错误导致请求失败
except Exception as e: except Exception as e:
logger.error("Error in OnlineMiddleware: %s" % e) logger.error("Error OnlineMiddleware: %s" % e)
# 返回处理后的响应对象
return response return response

@ -1,30 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-22 23:35
import django.db.models.deletion
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.CreateModel(
name='Favorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='blog.article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_articles', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': '收藏',
'verbose_name_plural': '收藏',
'unique_together': {('article', 'user')},
},
),
]

@ -1,37 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-23 00:03
import blog.models
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0007_favorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('avatar', models.ImageField(blank=True, help_text='Upload your avatar image (recommended size: 100x100px)', null=True, upload_to=blog.models.user_avatar_path, verbose_name='Avatar')),
('bio', models.TextField(blank=True, help_text='Tell us a little about yourself', null=True, verbose_name='Biography')),
('website', models.URLField(blank=True, null=True, verbose_name='Website')),
('github', models.URLField(blank=True, null=True, verbose_name='GitHub')),
('twitter', models.URLField(blank=True, null=True, verbose_name='Twitter')),
('weibo', models.URLField(blank=True, null=True, verbose_name='Weibo')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'User Profile',
'verbose_name_plural': 'User Profiles',
'ordering': ['-created_at'],
},
),
]

@ -1,7 +1,7 @@
import logging import logging
import re import re
import os
from abc import abstractmethod from abc import abstractmethod
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
@ -14,118 +14,131 @@ from uuslug import slugify
from djangoblog.utils import cache_decorator, cache from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
# 确保导入了 post_save 信号和 receiver 装饰器
from django.db.models.signals import post_save
from django.dispatch import receiver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def user_avatar_path(instance, filename):
"""
定义用户头像的上传路径
"""
# 文件将被上传到 MEDIA_ROOT/user_<id>/avatar/<filename>
return os.path.join(f'user_{instance.user.id}', 'avatar', filename)
class LinkShowType(models.TextChoices): class LinkShowType(models.TextChoices):
# 定义友情链接显示类型的枚举类,分别对应首页、列表页、文章页、所有页面、轮播
I = ('i', _('index')) I = ('i', _('index'))
L = ('l', _('list')) L = ('l', _('list'))
P = ('p', _('post')) P = ('p', _('post'))
A = ('a', _('all')) A = ('a', _('all'))
S = ('s', _('slide')) S = ('s', _('slide'))
class BaseModel(models.Model): class BaseModel(models.Model):
id = models.AutoField(primary_key=True) """
creation_time = models.DateTimeField(_('creation time'), default=now) 基础模型类为其他模型提供通用的字段和方法
last_modify_time = models.DateTimeField(_('modify time'), default=now) """
id = models.AutoField(primary_key=True) # 自增主键
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""
重写save方法处理slug字段如果模型有slug和title/name字段并调用父类save方法
同时处理仅更新views字段的特殊情况
"""
is_update_views = isinstance( is_update_views = isinstance(
self, self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views: if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views) Article.objects.filter(pk=self.pk).update(views=self.views)
else: else:
# 如果模型有slug字段生成slug基于title或name字段
if 'slug' in self.__dict__: if 'slug' in self.__dict__:
slug = getattr( slug_source = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name')
self, 'title') if 'title' in self.__dict__ else getattr( setattr(self, 'slug', slugify(slug_source))
self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_full_url(self): def get_full_url(self):
"""
获取模型对象的完整URL包含域名
"""
site = get_current_site().domain site = get_current_site().domain
url = "https://{site}{path}".format(site=site, url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url()) path=self.get_absolute_url())
return url return url
class Meta: class Meta:
abstract = True abstract = True # 抽象模型,不生成数据库表
@abstractmethod @abstractmethod
def get_absolute_url(self): def get_absolute_url(self):
"""
抽象方法子类必须实现用于获取模型对象的绝对URL
"""
pass pass
class Article(BaseModel): class Article(BaseModel):
"""文章""" """
文章模型类存储文章的相关信息
"""
# 文章状态:草稿、已发布
STATUS_CHOICES = ( STATUS_CHOICES = (
('d', _('Draft')), ('d', _('Draft')),
('p', _('Published')), ('p', _('Published')),
) )
# 评论状态:开启、关闭
COMMENT_STATUS = ( COMMENT_STATUS = (
('o', _('Open')), ('o', _('Open')),
('c', _('Close')), ('c', _('Close')),
) )
# 文章类型:文章、页面
TYPE = ( TYPE = (
('a', _('Article')), ('a', _('Article')),
('p', _('Page')), ('p', _('Page')),
) )
title = models.CharField(_('title'), max_length=200, unique=True) title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题,唯一
body = MDTextField(_('body')) body = MDTextField(_('body')) # 文章内容使用MDTextField支持markdown
pub_time = models.DateTimeField( pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) _('publish time'), blank=False, null=False, default=now) # 发布时间
status = models.CharField( status = models.CharField(
_('status'), _('status'),
max_length=1, max_length=1,
choices=STATUS_CHOICES, choices=STATUS_CHOICES,
default='p') default='p') # 文章状态
comment_status = models.CharField( comment_status = models.CharField(
_('comment status'), _('comment status'),
max_length=1, max_length=1,
choices=COMMENT_STATUS, choices=COMMENT_STATUS,
default='o') default='o') # 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 文章类型
views = models.PositiveIntegerField(_('views'), default=0) views = models.PositiveIntegerField(_('views'), default=0) # 文章浏览量
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
blank=False, blank=False,
null=False, null=False,
on_delete=models.CASCADE) on_delete=models.CASCADE) # 文章作者,外键关联用户模型
article_order = models.IntegerField( article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0) _('order'), blank=False, null=False, default=0) # 文章排序序号
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录
category = models.ForeignKey( category = models.ForeignKey(
'Category', 'Category',
verbose_name=_('category'), verbose_name=_('category'),
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) null=False) # 文章分类外键关联Category模型
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 文章标签多对多关联Tag模型
def body_to_string(self): def body_to_string(self):
"""将文章内容转换为字符串返回"""
return self.body return self.body
def __str__(self): def __str__(self):
"""自定义字符串表示,返回文章标题"""
return self.title return self.title
class Meta: class Meta:
ordering = ['-article_order', '-pub_time'] ordering = ['-article_order', '-pub_time'] # 排序规则先按article_order降序再按pub_time降序
verbose_name = _('article') verbose_name = _('article')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
get_latest_by = 'id' get_latest_by = 'id'
def get_absolute_url(self): def get_absolute_url(self):
"""获取文章的绝对URL用于生成文章详情页链接"""
return reverse('blog:detailbyid', kwargs={ return reverse('blog:detailbyid', kwargs={
'article_id': self.id, 'article_id': self.id,
'year': self.creation_time.year, 'year': self.creation_time.year,
@ -135,15 +148,26 @@ class Article(BaseModel):
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_category_tree(self): def get_category_tree(self):
"""
获取文章分类的树形结构包含当前分类及其所有父级分类并缓存
"""
tree = self.category.get_category_tree() tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names return names
def save(self, *args, **kwargs):
"""重写save方法调用父类save方法"""
super().save(*args, **kwargs)
def viewed(self): def viewed(self):
"""文章被浏览时浏览量加1并保存"""
self.views += 1 self.views += 1
self.save(update_fields=['views']) self.save(update_fields=['views'])
def comment_list(self): def comment_list(self):
"""
获取文章的评论列表优先从缓存获取缓存不存在则查询数据库并缓存
"""
cache_key = 'article_comments_{id}'.format(id=self.id) cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key) value = cache.get(cache_key)
if value: if value:
@ -156,102 +180,65 @@ class Article(BaseModel):
return comments return comments
def get_admin_url(self): def get_admin_url(self):
"""获取文章在admin后台的编辑URL"""
info = (self._meta.app_label, self._meta.model_name) info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,)) return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100) @cache_decorator(expiration=60 * 100)
def next_article(self): def next_article(self):
# 下一篇 """获取下一篇文章id大于当前文章且已发布的第一篇并缓存"""
return Article.objects.filter( return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first() id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100) @cache_decorator(expiration=60 * 100)
def prev_article(self): def prev_article(self):
# 前一篇 """获取前一篇文章id小于当前文章且已发布的第一篇并缓存"""
return Article.objects.filter(id__lt=self.id, status='p').first() return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self): def get_first_image_url(self):
""" """
Get the first image url from article.body. 从文章内容中提取第一张图片的URL
:return: 通过正则表达式匹配markdown图片语法中的图片链接
""" """
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match: if match:
return match.group(1) return match.group(1)
return "" return ""
@cache_decorator(60 * 60 * 24) # 缓存24小时
def get_related_articles(self, limit=5):
"""
根据当前文章的标签和分类获取相关推荐文章
:param limit: 推荐文章数量上限
:return: 相关文章列表
"""
# 1. 获取当前文章的所有标签ID
tag_ids = self.tags.values_list('id', flat=True)
# 2. 查询有相同标签的已发布文章(排除当前文章),并去重
related_by_tag = Article.objects.filter(
tags__id__in=tag_ids,
status='p'
).exclude(id=self.id).distinct()
# 3. 如果标签匹配的文章不足,补充同分类的已发布文章
if related_by_tag.count() < limit:
# 计算还需要补充的文章数量
need = limit - related_by_tag.count()
# 查询同分类的文章(排除当前文章和已通过标签匹配的文章)
related_by_category = Article.objects.filter(
category=self.category,
status='p'
).exclude(
id__in=list(related_by_tag.values_list('id', flat=True)) + [self.id]
).order_by('-pub_time')[:need]
# 合并结果(标签匹配的文章在前,分类匹配的在后)
related_articles = list(related_by_tag) + list(related_by_category)
else:
# 标签匹配的文章足够,直接取前 limit 篇
related_articles = list(related_by_tag)[:limit]
return related_articles
# 新增:获取文章的收藏数
def get_favorite_count(self):
"""获取文章的收藏数"""
return self.favorites.count()
class Category(BaseModel): class Category(BaseModel):
"""文章分类""" """
name = models.CharField(_('category name'), max_length=30, unique=True) 文章分类模型类
"""
name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称,唯一
parent_category = models.ForeignKey( parent_category = models.ForeignKey(
'self', 'self',
verbose_name=_('parent category'), verbose_name=_('parent category'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE) # 父分类,自关联
slug = models.SlugField(default='no-slug', max_length=60, blank=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 分类的slug用于URL
index = models.IntegerField(default=0, verbose_name=_('index')) index = models.IntegerField(default=0, verbose_name=_('index')) # 分类排序序号
class Meta: class Meta:
ordering = ['-index'] ordering = ['-index'] # 按index降序排序
verbose_name = _('category') verbose_name = _('category')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def get_absolute_url(self): def get_absolute_url(self):
"""获取分类的绝对URL用于生成分类页链接"""
return reverse( return reverse(
'blog:category_detail', kwargs={ 'blog:category_detail', kwargs={
'category_name': self.slug}) 'category_name': self.slug})
def __str__(self): def __str__(self):
"""自定义字符串表示,返回分类名称"""
return self.name return self.name
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_category_tree(self): def get_category_tree(self):
""" """
递归获得分类目录的父级 递归获取分类的树形结构当前分类及其所有父级分类并缓存
:return:
""" """
categorys = [] categorys = []
@ -266,8 +253,7 @@ class Category(BaseModel):
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_sub_categorys(self): def get_sub_categorys(self):
""" """
获得当前分类目录所有子集 递归获取当前分类的所有子分类包括子分类的子分类等并缓存
:return:
""" """
categorys = [] categorys = []
all_categorys = Category.objects.all() all_categorys = Category.objects.all()
@ -284,209 +270,158 @@ class Category(BaseModel):
parse(self) parse(self)
return categorys return categorys
class Tag(BaseModel): class Tag(BaseModel):
"""文章标签""" """
name = models.CharField(_('tag name'), max_length=30, unique=True) 文章标签模型类
slug = models.SlugField(default='no-slug', max_length=60, blank=True) """
name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称,唯一
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 标签的slug用于URL
def __str__(self): def __str__(self):
"""自定义字符串表示,返回标签名称"""
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
"""获取标签的绝对URL用于生成标签页链接"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_article_count(self): def get_article_count(self):
"""获取该标签下的文章数量,并缓存"""
return Article.objects.filter(tags__name=self.name).distinct().count() return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta: class Meta:
ordering = ['name'] ordering = ['name'] # 按名称排序
verbose_name = _('tag') verbose_name = _('tag')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
class Links(models.Model):
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True) class Links(models.Model):
link = models.URLField(_('link')) """
sequence = models.IntegerField(_('order'), unique=True) 友情链接模型类
"""
name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称,唯一
link = models.URLField(_('link')) # 链接URL
sequence = models.IntegerField(_('order'), unique=True) # 排序序号,唯一
is_enable = models.BooleanField( is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False) _('is show'), default=True, blank=False, null=False) # 是否显示
show_type = models.CharField( show_type = models.CharField(
_('show type'), _('show type'),
max_length=1, max_length=1,
choices=LinkShowType.choices, choices=LinkShowType.choices,
default=LinkShowType.I) default=LinkShowType.I) # 显示类型关联LinkShowType枚举
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta: class Meta:
ordering = ['sequence'] ordering = ['sequence'] # 按sequence排序
verbose_name = _('link') verbose_name = _('link')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def __str__(self): def __str__(self):
"""自定义字符串表示,返回链接名称"""
return self.name return self.name
class SideBar(models.Model): class SideBar(models.Model):
"""侧边栏,可以展示一些html内容""" """
name = models.CharField(_('title'), max_length=100) 侧边栏模型类用于展示自定义HTML内容
content = models.TextField(_('content')) """
sequence = models.IntegerField(_('order'), unique=True) name = models.CharField(_('title'), max_length=100) # 侧边栏标题
is_enable = models.BooleanField(_('is enable'), default=True) content = models.TextField(_('content')) # 侧边栏内容HTML
creation_time = models.DateTimeField(_('creation time'), default=now) sequence = models.IntegerField(_('order'), unique=True) # 排序序号,唯一
last_mod_time = models.DateTimeField(_('modify time'), default=now) is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
class Meta: class Meta:
ordering = ['sequence'] ordering = ['sequence'] # 按sequence排序
verbose_name = _('sidebar') verbose_name = _('sidebar')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def __str__(self): def __str__(self):
"""自定义字符串表示,返回侧边栏标题"""
return self.name return self.name
class BlogSettings(models.Model): class BlogSettings(models.Model):
"""blog的配置""" """
博客配置模型类存储网站的各种配置信息
"""
site_name = models.CharField( site_name = models.CharField(
_('site name'), _('site name'),
max_length=200, max_length=200,
null=False, null=False,
blank=False, blank=False,
default='') default='') # 网站名称
site_description = models.TextField( site_description = models.TextField(
_('site description'), _('site description'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='') # 网站描述
site_seo_description = models.TextField( site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='') _('site seo description'), max_length=1000, null=False, blank=False, default='') # 网站SEO描述
site_keywords = models.TextField( site_keywords = models.TextField(
_('site keywords'), _('site keywords'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='') # 网站关键词
article_sub_length = models.IntegerField(_('article sub length'), default=300) article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示Google广告
google_adsense_codes = models.TextField( google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='') _('adsense code'), max_length=2000, null=True, blank=True, default='') # Google广告代码
open_site_comment = models.BooleanField(_('open site comment'), default=True) open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启网站评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') global_header = models.TextField("公共头部", null=True, blank=True, default='') # 公共头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 公共尾部HTML
beian_code = models.CharField( beian_code = models.CharField(
'备案号', '备案号',
max_length=2000, max_length=2000,
null=True, null=True,
blank=True, blank=True,
default='') default='') # 网站备案号
analytics_code = models.TextField( analytics_code = models.TextField(
"网站统计代码", "网站统计代码",
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='') # 网站统计代码
show_gongan_code = models.BooleanField( show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False) '是否显示公安备案号', default=False, null=False) # 是否显示公安备案号
gongan_beiancode = models.TextField( gongan_beiancode = models.TextField(
'公安备案号', '公安备案号',
max_length=2000, max_length=2000,
null=True, null=True,
blank=True, blank=True,
default='') default='') # 公安备案号
comment_need_review = models.BooleanField( comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) '评论是否需要审核', default=False, null=False) # 评论是否需要审核
class Meta: class Meta:
verbose_name = _('Website configuration') verbose_name = _('Website configuration')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def __str__(self): def __str__(self):
"""自定义字符串表示,返回网站名称"""
return self.site_name return self.site_name
def clean(self): def clean(self):
"""
模型验证方法确保只能有一个配置实例
如果存在其他配置实例排除当前实例则抛出验证错误
"""
if BlogSettings.objects.exclude(id=self.id).count(): if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration')) raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
class Favorite(models.Model):
"""
文章收藏模型
"""
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='favorites')
# 使用 settings.AUTH_USER_MODEL
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='favorite_articles')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('article', 'user') # 一个用户只能收藏同一篇文章一次
verbose_name = "收藏"
verbose_name_plural = "收藏"
def __str__(self):
return f'{self.user.username} favorites {self.article.title}'
class UserProfile(models.Model):
"""
用户资料扩展模型
用于存储用户头像个人简介等额外信息
"""
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile', verbose_name=_('User'))
# 基本信息
avatar = models.ImageField(
upload_to=user_avatar_path,
blank=True,
null=True,
verbose_name=_('Avatar'),
help_text=_('Upload your avatar image (recommended size: 100x100px)')
)
bio = models.TextField(
blank=True,
null=True,
verbose_name=_('Biography'),
help_text=_('Tell us a little about yourself')
)
# 可选的社交链接
website = models.URLField(blank=True, null=True, verbose_name=_('Website'))
github = models.URLField(blank=True, null=True, verbose_name=_('GitHub'))
twitter = models.URLField(blank=True, null=True, verbose_name=_('Twitter'))
weibo = models.URLField(blank=True, null=True, verbose_name=_('Weibo'))
# 时间戳
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created At'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated At'))
class Meta:
verbose_name = _('User Profile')
verbose_name_plural = _('User Profiles')
ordering = ['-created_at']
def __str__(self):
return f"{self.user.username}'s Profile"
def get_absolute_url(self):
""" """
定义用户资料页的绝对URL 重写save方法保存后清除缓存使配置变更立即生效
""" """
return reverse('blog:user_profile', kwargs={'username': self.user.username}) super().save(*args, **kwargs)
from djangoblog.utils import cache
# 信号当一个新的自定义用户settings.AUTH_USER_MODEL被创建时自动创建一个关联的UserProfile cache.clear()
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def save_user_profile(sender, instance, **kwargs):
# 使用 get_or_create 防止在某些情况下如手动创建用户时profile不存在
UserProfile.objects.get_or_create(user=instance)
instance.profile.save()

@ -341,4 +341,4 @@ def query(qs, **kwargs):
@register.filter @register.filter
def addstr(arg1, arg2): def addstr(arg1, arg2):
"""concatenate arg1 & arg2""" """concatenate arg1 & arg2"""
return str(arg1) + str(arg2) return str(arg1) + str(arg2)

@ -1,130 +1,173 @@
# blog/urls.py <<<<<<< HEAD
# 导入 Django 内置的路径配置工具和缓存装饰器 # 导入 Django 内置的路径配置工具和缓存装饰器
from django.urls import path from django.urls import path
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
# 导入当前应用blog的视图模块 # 导入当前应用blog的视图模块,用于关联路由与视图逻辑
from . import views from . import views
# 定义应用命名空间 # 定义应用命名空间namespace用于在模板或反向解析时区分不同应用的路由
# 例如:在模板中使用 {% url 'blog:index' %} 生成首页链接
app_name = "blog" app_name = "blog"
# 路由配置列表 # 路由配置列表,每个 path 对应一个 URL 规则与视图的映射
urlpatterns = [ urlpatterns = [
# 首页路由 # 首页路由:匹配根路径(网站域名/
path( path(
'', r'', # URL 路径表达式,空字符串表示根路径
views.IndexView.as_view(), views.IndexView.as_view(), # 关联的视图类IndexView通过 as_view() 转换为可调用视图
name='index' name='index' # 路由名称,用于反向解析(如 reverse('blog:index')
), ),
# 分页首页路由:匹配带页码的首页(如 /page/2/
path( path(
'page/<int:page>/', r'page/<int:page>/', # <int:page> 是路径参数int 表示接收整数类型page 是参数名
views.IndexView.as_view(), views.IndexView.as_view(), # 复用首页视图类,视图中会通过 page 参数处理分页
name='index_page' name='index_page'
), ),
# 文章详情页路由 # 文章详情页路由按日期和文章ID匹配如 /article/2023/10/20/100.html
path( path(
'article/<int:article_id>/', r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(), # 路径参数year、month、day、article_id文章ID均为整数
name='article_detail' views.ArticleDetailView.as_view(), # 文章详情视图类,处理文章展示逻辑
),
# 原始的带日期的URL保持不变
path(
'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid' name='detailbyid'
), ),
# 文章收藏功能路由 # 分类详情页路由:按分类名匹配(如 /category/tech.html
path( path(
'article/<int:article_id>/favorite/', r'category/<slug:category_name>.html',
views.ArticleFavoriteView.as_view(), # <slug:category_name>slug 类型表示接收字母、数字、下划线和连字符组成的字符串适合URL友好的名称
name='article_favorite' views.CategoryDetailView.as_view(), # 分类详情视图类,展示该分类下的文章
),
# 分类详情页路由
path(
'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail' name='category_detail'
), ),
# 分类详情分页路由:带页码的分类页(如 /category/tech/2.html
path( path(
'category/<slug:category_name>/<int:page>.html', r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(), # 复用分类视图类,通过 page 参数分页
name='category_detail_page' name='category_detail_page'
), ),
# 作者详情页路由 # 作者详情页路由:按作者名匹配(如 /author/alice.html
path( path(
'author/<author_name>.html', r'author/<author_name>.html',
views.AuthorDetailView.as_view(), # <author_name>:未指定类型,默认接收字符串(除特殊字符外)
views.AuthorDetailView.as_view(), # 作者详情视图类,展示该作者的文章
name='author_detail' name='author_detail'
), ),
# 作者详情分页路由:带页码的作者页(如 /author/alice/2.html
path( path(
'author/<author_name>/<int:page>.html', r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(), # 复用作者视图类,通过 page 参数分页
name='author_detail_page' name='author_detail_page'
), ),
# 标签详情页路由 # 标签详情页路由:按标签名匹配(如 /tag/python.html
path( path(
'tag/<slug:tag_name>.html', r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(), # 标签详情视图类,展示该标签下的文章
name='tag_detail' name='tag_detail'
), ),
# 标签详情分页路由:带页码的标签页(如 /tag/python/2.html
path( path(
'tag/<slug:tag_name>/<int:page>.html', r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(), # 复用标签视图类,通过 page 参数分页
name='tag_detail_page' name='tag_detail_page'
), ),
# 归档页路由 # 归档页路由:匹配 /archives.html
path( path(
'archives.html', 'archives.html',
# 缓存装饰器cache_page(60*60) 表示缓存该页面1小时60秒*60减轻服务器压力
cache_page(60 * 60)(views.ArchivesView.as_view()), cache_page(60 * 60)(views.ArchivesView.as_view()),
name='archives' name='archives' # 归档视图,通常展示按日期分组的文章列表
), ),
# 友情链接页路由 # 友情链接页路由:匹配 /links.html
path( path(
'links.html', 'links.html',
views.LinkListView.as_view(), views.LinkListView.as_view(), # 友情链接视图类,展示网站链接列表
name='links' name='links'
), ),
# ==================== 关键修正:调整 URL 顺序 ==================== # 文件上传路由:匹配 /upload
# 用户资料相关路由
# 将具体的路径放在通用路径之前
path(
'profile/edit/',
views.UserProfileUpdateView.as_view(),
name='user_profile_update'
),
# 我的收藏页面路由
path( path(
'profile/favorites/', r'upload',
views.UserFavoritesView.as_view(), views.fileupload, # 关联函数视图(非类视图),处理文件上传逻辑
name='user_favorites' name='upload'
), ),
# 通用的用户资料路径放在最后
# 缓存清理路由:匹配 /clean
path( path(
'profile/<str:username>/', r'clean',
views.UserProfileDetailView.as_view(), views.clean_cache_view, # 关联缓存清理视图,用于手动触发缓存清理
name='user_profile' name='clean'
), ),
# ================================================================= ]
=======
from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
# 其他功能路由 app_name = "blog"
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
path( path(
'upload', r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
path(
r'upload',
views.fileupload, views.fileupload,
name='upload' name='upload'),
),
path( path(
'clean', r'clean',
views.clean_cache_view, views.clean_cache_view,
name='clean' name='clean'),
), ]
] >>>>>>> ccy_branch

@ -4,26 +4,18 @@ import uuid
from django.conf import settings from django.conf import settings
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.shortcuts import render, redirect from django.shortcuts import render
from django.templatetags.static import static from django.templatetags.static import static
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.views.generic import View, TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
from haystack.views import SearchView from haystack.views import SearchView
from django.contrib.auth.decorators import login_required from blog.models import Article, Category, LinkShowType, Links, Tag
from django.contrib import messages
from django.http import HttpResponseRedirect
# 导入自定义用户模型
from accounts.models import BlogUser
from blog.models import Article, Category, LinkShowType, Links, Tag, Favorite, UserProfile
from comments.forms import CommentForm from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -31,6 +23,7 @@ from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ArticleListView(ListView): class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染 # template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html' template_name = 'blog/article_index.html'
@ -45,7 +38,7 @@ class ArticleListView(ListView):
link_type = LinkShowType.L link_type = LinkShowType.L
def get_view_cache_key(self): def get_view_cache_key(self):
return self.request.GET.get('page', 1) return self.request.get['pages']
@property @property
def page_number(self): def page_number(self):
@ -93,15 +86,8 @@ class ArticleListView(ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type kwargs['linktype'] = self.link_type
# 新增:添加热门文章数据(所有列表页共享) return super(ArticleListView, self).get_context_data(**kwargs)
hot_articles_cache_key = 'hot_articles'
hot_articles = cache.get(hot_articles_cache_key)
if not hot_articles:
hot_articles = Article.objects.filter(type='a', status='p').order_by('-views')[:5]
cache.set(hot_articles_cache_key, hot_articles, 60 * 60) # 缓存1小时
logger.info('set hot articles cache')
kwargs['hot_articles'] = hot_articles
return super(ArticleListView, self).get_context_data(** kwargs)
class IndexView(ArticleListView): class IndexView(ArticleListView):
''' '''
@ -118,6 +104,7 @@ class IndexView(ArticleListView):
cache_key = 'index_{page}'.format(page=self.page_number) cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key return cache_key
class ArticleDetailView(DetailView): class ArticleDetailView(DetailView):
''' '''
文章详情页面 文章详情页面
@ -160,36 +147,20 @@ class ArticleDetailView(DetailView):
kwargs['comment_count'] = len( kwargs['comment_count'] = len(
article_comments) if article_comments else 0 article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article() kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article() kwargs['prev_article'] = self.object.prev_article
# 新增:获取相关推荐文章
related_articles = self.object.get_related_articles(limit=5)
kwargs['related_articles'] = related_articles
# 新增:添加热门文章数据(详情页也显示) context = super(ArticleDetailView, self).get_context_data(**kwargs)
hot_articles_cache_key = 'hot_articles'
hot_articles = cache.get(hot_articles_cache_key)
if not hot_articles:
hot_articles = Article.objects.filter(type='a', status='p').order_by('-views')[:5]
cache.set(hot_articles_cache_key, hot_articles, 60 * 60)
logger.info('set hot articles cache')
kwargs['hot_articles'] = hot_articles
# 新增:判断当前用户是否收藏了该文章
if self.request.user.is_authenticated:
kwargs['is_favorited'] = Favorite.objects.filter(article=self.object, user=self.request.user).exists()
context = super(ArticleDetailView, self).get_context_data(** kwargs)
article = self.object article = self.object
# Action Hook, 通知插件"文章详情已获取" # Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request) hooks.run_action('after_article_body_get', article=article, request=self.request)
# Filter Hook, 允许插件修改文章正文 # # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article, article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request) request=self.request)
return context return context
class CategoryDetailView(ArticleListView): class CategoryDetailView(ArticleListView):
''' '''
分类目录列表 分类目录列表
@ -218,6 +189,7 @@ class CategoryDetailView(ArticleListView):
return cache_key return cache_key
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
categoryname = self.categoryname categoryname = self.categoryname
try: try:
categoryname = categoryname.split('/')[-1] categoryname = categoryname.split('/')[-1]
@ -225,7 +197,8 @@ class CategoryDetailView(ArticleListView):
pass pass
kwargs['page_type'] = CategoryDetailView.page_type kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(** kwargs) return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView): class AuthorDetailView(ArticleListView):
''' '''
@ -242,7 +215,6 @@ class AuthorDetailView(ArticleListView):
def get_queryset_data(self): def get_queryset_data(self):
author_name = self.kwargs['author_name'] author_name = self.kwargs['author_name']
# 这里使用 BlogUser 或 settings.AUTH_USER_MODEL 都是安全的
article_list = Article.objects.filter( article_list = Article.objects.filter(
author__username=author_name, type='a', status='p') author__username=author_name, type='a', status='p')
return article_list return article_list
@ -251,7 +223,8 @@ class AuthorDetailView(ArticleListView):
author_name = self.kwargs['author_name'] author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(** kwargs) return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView): class TagDetailView(ArticleListView):
''' '''
@ -278,10 +251,12 @@ class TagDetailView(ArticleListView):
return cache_key return cache_key
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(** kwargs) return super(TagDetailView, self).get_context_data(**kwargs)
class ArchivesView(ArticleListView): class ArchivesView(ArticleListView):
''' '''
@ -299,16 +274,6 @@ class ArchivesView(ArticleListView):
cache_key = 'archives' cache_key = 'archives'
return cache_key return cache_key
def get_context_data(self, **kwargs):
# 归档页单独添加热门文章因继承自ArticleListView但需确保显示
hot_articles_cache_key = 'hot_articles'
hot_articles = cache.get(hot_articles_cache_key)
if not hot_articles:
hot_articles = Article.objects.filter(type='a', status='p').order_by('-views')[:5]
cache.set(hot_articles_cache_key, hot_articles, 60 * 60)
logger.info('set hot articles cache')
kwargs['hot_articles'] = hot_articles
return super(ArchivesView, self).get_context_data(** kwargs)
class LinkListView(ListView): class LinkListView(ListView):
model = Links model = Links
@ -317,30 +282,24 @@ class LinkListView(ListView):
def get_queryset(self): def get_queryset(self):
return Links.objects.filter(is_enable=True) return Links.objects.filter(is_enable=True)
def get_context_data(self, **kwargs):
# 链接页添加热门文章
hot_articles_cache_key = 'hot_articles'
hot_articles = cache.get(hot_articles_cache_key)
if not hot_articles:
hot_articles = Article.objects.filter(type='a', status='p').order_by('-views')[:5]
cache.set(hot_articles_cache_key, hot_articles, 60 * 60)
logger.info('set hot articles cache')
kwargs['hot_articles'] = hot_articles
return super(LinkListView, self).get_context_data(** kwargs)
class EsSearchView(SearchView): class EsSearchView(SearchView):
def get_context(self): def get_context(self):
context = super().get_context() paginator, page = self.build_page()
# 搜索页添加热门文章 context = {
hot_articles_cache_key = 'hot_articles' "query": self.query,
hot_articles = cache.get(hot_articles_cache_key) "form": self.form,
if not hot_articles: "page": page,
hot_articles = Article.objects.filter(type='a', status='p').order_by('-views')[:5] "paginator": paginator,
cache.set(hot_articles_cache_key, hot_articles, 60 * 60) "suggestion": None,
logger.info('set hot articles cache') }
context['hot_articles'] = hot_articles if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context return context
@csrf_exempt @csrf_exempt
def fileupload(request): def fileupload(request):
""" """
@ -360,7 +319,7 @@ def fileupload(request):
imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename)) fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES_DIRS[0], "files" if not isimage else "image", timestr) base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir): if not os.path.exists(base_dir):
os.makedirs(base_dir) os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}")) savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
@ -373,15 +332,14 @@ def fileupload(request):
from PIL import Image from PIL import Image
image = Image.open(savepath) image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True) image.save(savepath, quality=20, optimize=True)
# 修正静态文件URL路径 url = static(savepath)
relative_path = os.path.relpath(savepath, settings.STATICFILES_DIRS[0])
url = static(relative_path)
response.append(url) response.append(url)
return HttpResponse(response) return HttpResponse(response)
else: else:
return HttpResponse("only for post") return HttpResponse("only for post")
def page_not_found_view( def page_not_found_view(
request, request,
exception, exception,
@ -395,6 +353,7 @@ def page_not_found_view(
'statuscode': '404'}, 'statuscode': '404'},
status=404) status=404)
def server_error_view(request, template_name='blog/error_page.html'): def server_error_view(request, template_name='blog/error_page.html'):
return render(request, return render(request,
template_name, template_name,
@ -402,6 +361,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
'statuscode': '500'}, 'statuscode': '500'},
status=500) status=500)
def permission_denied_view( def permission_denied_view(
request, request,
exception, exception,
@ -413,181 +373,7 @@ def permission_denied_view(
'message': _('Sorry, you do not have permission to access this page?'), 'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403) 'statuscode': '403'}, status=403)
def clean_cache_view(request): def clean_cache_view(request):
cache.clear() cache.clear()
return HttpResponse('ok') return HttpResponse('ok')
# ==============================================================
# 以下为新增的 DjangoBlogFeed 类
# ==============================================================
from django.contrib.syndication.views import Feed
from django.urls import reverse
class DjangoBlogFeed(Feed):
"""
自定义的 RSS Feed
用于生成网站的 RSS 订阅源
"""
# 订阅源的标题
title = _("Django Blog")
# 订阅源的链接
link = "/"
# 订阅源的描述
description = _("Latest articles from Django Blog")
def items(self):
"""
返回要在订阅中显示的项目列表
这里我们返回最新的 10 篇已发布的文章
"""
return Article.objects.filter(status='p').order_by('-creation_time')[:10]
def item_title(self, item):
"""
返回单个项目文章的标题
"""
return item.title
def item_description(self, item):
"""
返回单个项目文章的描述
这里我们使用文章的摘要如果没有摘要则使用正文的前 200 个字符
"""
if item.summary:
return item.summary
# 为了安全,确保不返回 None
return item.body[:200] + "..." if item.body else ""
def item_link(self, item):
"""
返回单个项目文章的绝对链接
"""
return reverse('blog:detailbyid', kwargs={
'article_id': item.pk,
'year': item.creation_time.year,
'month': item.creation_time.month,
'day': item.creation_time.day
})
def item_pubdate(self, item):
"""
返回单个项目文章的发布日期
这是可选的但推荐添加以符合 RSS 规范
"""
return item.creation_time
# ======================================================================
# 收藏功能视图 (修正和统一)
# ======================================================================
class ArticleFavoriteView(LoginRequiredMixin, View):
"""
处理文章收藏/取消收藏的视图 (统一处理)
"""
http_method_names = ['post'] # 只允许 POST 请求
def post(self, request, *args, **kwargs):
article_id = kwargs.get('article_id')
article = get_object_or_404(Article, pk=article_id)
# get_or_create 是一个原子操作,能有效防止并发问题
favorite, created = Favorite.objects.get_or_create(article=article, user=request.user)
if not created:
# 如果记录已存在,则删除(取消收藏)
favorite.delete()
is_favorite = False
else:
# 如果是新创建,则表示收藏成功
is_favorite = True
# 返回更新后的收藏数和状态
favorite_count = article.get_favorite_count()
return JsonResponse({'is_favorite': is_favorite, 'favorite_count': favorite_count})
class UserFavoritesView(LoginRequiredMixin, ListView):
"""
展示用户收藏的所有文章 (基于类的视图)
"""
model = Article
template_name = 'blog/user_favorites.html'
context_object_name = 'favorite_articles'
paginate_by = 10 # 每页显示10篇
def get_queryset(self):
# 获取当前用户的所有收藏,并按收藏时间倒序排列
# 使用 select_related 和 prefetch_related 优化数据库查询
return Article.objects.filter(
favorites__user=self.request.user
).select_related('author', 'category').prefetch_related('tags').order_by('-favorites__created_at')
def get_context_data(self,** kwargs):
context = super().get_context_data(**kwargs)
# 添加 profile_user 用于复用个人资料页面的侧边栏
context['profile_user'] = self.request.user
return context
# ======================================================================
# 用户资料视图
# ======================================================================
from django.views.generic import UpdateView
from django.urls import reverse_lazy
from .forms import UserProfileUpdateForm # 确保你有这个表单文件
class UserProfileDetailView(DetailView):
"""
显示用户公开资料的视图
"""
model = UserProfile
template_name = 'blog/user_profile_detail.html'
context_object_name = 'profile'
def get_object(self, queryset=None):
"""通过用户名查找用户,然后返回其 profile"""
username = self.kwargs.get('username')
# 修正:使用自定义的 BlogUser 模型进行查询
user = get_object_or_404(BlogUser, username=username)
return get_object_or_404(UserProfile, user=user)
def get_context_data(self, **kwargs):
"""添加额外的上下文数据,例如用户发布的文章"""
context = super().get_context_data(** kwargs)
user = self.object.user # 从 profile 对象获取 user
# 获取该用户发布的所有公开文章,并按发布时间排序
context['user_articles'] = Article.objects.filter(author=user, status='p').order_by('-pub_time')[:10]
return context
class UserProfileUpdateView(LoginRequiredMixin, UpdateView):
"""
用户编辑自己资料的视图
"""
model = UserProfile
form_class = UserProfileUpdateForm
template_name = 'blog/user_profile_update.html'
def get_queryset(self):
"""
重写此方法以确保用户只能编辑自己的资料
这是对象级权限控制的最佳实践
"""
# 只返回与当前登录用户关联的 UserProfile 对象
return UserProfile.objects.filter(user=self.request.user)
def get_object(self, queryset=None):
"""
用户只能编辑自己的 profile
此方法与 get_queryset 结合提供了双重保障
"""
return self.request.user.profile
def get_success_url(self):
"""编辑成功后重定向到自己的资料页"""
return reverse_lazy('blog:user_profile', kwargs={'username': self.request.user.username})
def form_valid(self, form):
"""表单验证成功后,显示成功消息"""
messages.success(self.request, 'Your profile has been updated successfully!')
return super().form_valid(form)

@ -1,87 +0,0 @@
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: auto
threshold: 1%
informational: true
patch:
default:
target: auto
threshold: 1%
informational: true
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "reach,diff,flags,tree"
behavior: default
require_changes: no
ignore:
# Django 相关
- "*/migrations/*"
- "manage.py"
- "*/settings.py"
- "*/wsgi.py"
- "*/asgi.py"
# 测试相关
- "*/tests/*"
- "*/test_*.py"
- "*/*test*.py"
# 静态文件和模板
- "*/static/*"
- "*/templates/*"
- "*/collectedstatic/*"
# 国际化文件
- "*/locale/*"
- "**/*.po"
- "**/*.mo"
# 文档和部署
- "*/docs/*"
- "*/deploy/*"
- "README*.md"
- "LICENSE"
- "Dockerfile"
- "docker-compose*.yml"
- "*.yaml"
- "*.yml"
# 开发环境
- "*/venv/*"
- "*/__pycache__/*"
- "*.pyc"
- ".coverage"
- "coverage.xml"
# 日志文件
- "*/logs/*"
- "*.log"
# 特定文件
- "*/whoosh_cn_backend.py" # 搜索后端
- "*/elasticsearch_backend.py" # 搜索后端
- "*/MemcacheStorage.py" # 缓存存储
- "*/robot.py" # 机器人相关
# 配置文件
- "codecov.yml"
- ".coveragerc"
- "requirements*.txt"

@ -36,8 +36,4 @@ class Comment(models.Model):
get_latest_by = 'id' get_latest_by = 'id'
def __str__(self): def __str__(self):
return self.body
return self.body return self.body

@ -8,78 +8,61 @@ from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid from djangoblog.utils import get_max_articleid_commentid
# 评论功能测试类继承TransactionTestCase以支持事务管理的测试 # Create your tests here.
class CommentsTest(TransactionTestCase): class CommentsTest(TransactionTestCase):
def setUp(self): def setUp(self):
"""测试前的初始化设置"""
# 创建测试客户端,用于模拟用户请求
self.client = Client() self.client = Client()
# 创建请求工厂,用于构造测试请求
self.factory = RequestFactory() self.factory = RequestFactory()
# 配置博客评论设置(需要审核才能显示)
from blog.models import BlogSettings from blog.models import BlogSettings
value = BlogSettings() value = BlogSettings()
value.comment_need_review = True # 评论需要审核 value.comment_need_review = True
value.save() value.save()
# 创建超级用户用于测试登录状态
self.user = BlogUser.objects.create_superuser( self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
password="liangliangyy1") password="liangliangyy1")
def update_article_comment_status(self, article): def update_article_comment_status(self, article):
"""更新文章所有评论为启用状态(通过审核)"""
comments = article.comment_set.all() comments = article.comment_set.all()
for comment in comments: for comment in comments:
comment.is_enable = True # 设置评论为启用 comment.is_enable = True
comment.save() comment.save()
def test_validate_comment(self): def test_validate_comment(self):
"""测试评论功能的各种场景"""
# 登录测试用户
self.client.login(username='liangliangyy1', password='liangliangyy1') self.client.login(username='liangliangyy1', password='liangliangyy1')
# 创建测试分类
category = Category() category = Category()
category.name = "categoryccc" category.name = "categoryccc"
category.save() category.save()
# 创建测试文章
article = Article() article = Article()
article.title = "nicetitleccc" article.title = "nicetitleccc"
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = self.user article.author = self.user
article.category = category article.category = category
article.type = 'a' # 假设'a'表示文章类型 article.type = 'a'
article.status = 'p' # 假设'p'表示已发布 article.status = 'p'
article.save() article.save()
# 获取评论提交的URL
comment_url = reverse( comment_url = reverse(
'comments:postcomment', kwargs={ 'comments:postcomment', kwargs={
'article_id': article.id}) 'article_id': article.id})
# 测试提交第一条评论
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': '123ffffffffff' # 评论内容 'body': '123ffffffffff'
}) })
# 验证提交成功302重定向
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# 重新获取文章对象
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk)
# 因为评论需要审核,初始状态下评论列表应为空
self.assertEqual(len(article.comment_list()), 0) self.assertEqual(len(article.comment_list()), 0)
# 更新评论状态为通过审核
self.update_article_comment_status(article) self.update_article_comment_status(article)
# 审核后评论列表应包含1条评论
self.assertEqual(len(article.comment_list()), 1) self.assertEqual(len(article.comment_list()), 1)
# 测试提交第二条评论
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': '123ffffffffff', 'body': '123ffffffffff',
@ -87,19 +70,40 @@ class CommentsTest(TransactionTestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# 验证第二条评论
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article) self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2) self.assertEqual(len(article.comment_list()), 2)
# 获取第一条评论ID作为父评论测试回复功能
parent_comment_id = article.comment_list()[0].id parent_comment_id = article.comment_list()[0].id
# 测试提交带格式的回复评论(包含标题、代码块、链接等)
response = self.client.post(comment_url, response = self.client.post(comment_url,
{ {
'body': ''' 'body': '''
# Title1 # Title1
```python ```python
import os import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
})
self.assertEqual(response.status_code, 302)
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
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)
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
from comments.utils import send_comment_email
send_comment_email(comment)

@ -13,51 +13,66 @@ from .models import Comment
class CommentPostView(FormView): class CommentPostView(FormView):
form_class = CommentForm """处理评论提交的视图类继承自Django的FormView"""
template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect) form_class = CommentForm # 指定使用的表单类
template_name = 'blog/article_detail.html' # 指定渲染的模板
@method_decorator(csrf_protect) # 添加CSRF保护装饰器防止跨站请求伪造
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
"""重写dispatch方法在请求处理前后执行额外逻辑"""
return super(CommentPostView, self).dispatch(*args, **kwargs) return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id'] """处理GET请求 - 重定向到文章详情页的评论区域"""
article = get_object_or_404(Article, pk=article_id) article_id = self.kwargs['article_id'] # 从URL参数获取文章ID
url = article.get_absolute_url() article = get_object_or_404(Article, pk=article_id) # 获取文章对象不存在则返回404
return HttpResponseRedirect(url + "#comments") url = article.get_absolute_url() # 获取文章的绝对URL
return HttpResponseRedirect(url + "#comments") # 重定向到文章页的评论锚点
def form_invalid(self, form): def form_invalid(self, form):
article_id = self.kwargs['article_id'] """表单验证失败时的处理逻辑"""
article = get_object_or_404(Article, pk=article_id) article_id = self.kwargs['article_id'] # 获取文章ID
article = get_object_or_404(Article, pk=article_id) # 获取文章对象
# 重新渲染页面,显示表单错误信息
return self.render_to_response({ return self.render_to_response({
'form': form, 'form': form, # 包含错误信息的表单
'article': article 'article': article # 文章对象
}) })
def form_valid(self, form): def form_valid(self, form):
"""提交的数据验证合法后的逻辑""" """提交的数据验证合法后的逻辑"""
user = self.request.user user = self.request.user # 获取当前登录用户
author = BlogUser.objects.get(pk=user.pk) author = BlogUser.objects.get(pk=user.pk) # 获取对应的博客用户对象
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id'] # 从URL参数获取文章ID
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id) # 获取文章对象
# 检查文章是否允许评论
if article.comment_status == 'c' or article.status == 'c': if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") raise ValidationError("该文章评论已关闭.") # 抛出验证错误
comment = form.save(False)
comment.article = article comment = form.save(False) # 创建评论对象但不保存到数据库
comment.article = article # 设置评论关联的文章
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
settings = get_blog_setting() settings = get_blog_setting() # 获取博客设置
# 如果博客设置不需要审核评论,则直接启用评论
if not settings.comment_need_review: if not settings.comment_need_review:
comment.is_enable = True comment.is_enable = True
comment.author = author
comment.author = author # 设置评论作者
# 处理父级评论(回复功能)
if form.cleaned_data['parent_comment_id']: if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get( parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) pk=form.cleaned_data['parent_comment_id']) # 获取父级评论对象
comment.parent_comment = parent_comment comment.parent_comment = parent_comment # 设置评论的父级评论
comment.save(True) # 保存评论到数据库
comment.save(True) # 重定向到文章页并跳转到新评论的位置
return HttpResponseRedirect( return HttpResponseRedirect(
"%s#div-comment-%d" % "%s#div-comment-%d" % # 使用锚点跳转到特定评论
(article.get_absolute_url(), comment.pk)) (article.get_absolute_url(), comment.pk))

@ -40,7 +40,7 @@ class DjangoBlogAdminSite(AdminSite):
admin_site = DjangoBlogAdminSite(name='admin') admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticleAdmin) admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin) admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin) admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin) admin_site.register(Links, LinksAdmin)

@ -12,8 +12,15 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -23,17 +30,18 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get( SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DJANGO_DEBUG', 'True') == 'True' DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = [] # ALLOWED_HOSTS = []
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split(',') ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置 # django 4.0新增配置
CSRF_TRUSTED_ORIGINS = os.environ.get('DJANGO_CSRF_TRUSTED_ORIGINS', 'http://localhost,http://127.0.0.1').split(',') CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
# 'django.contrib.admin', # 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig', 'django.contrib.admin.apps.SimpleAdminConfig',
@ -57,6 +65,7 @@ INSTALLED_APPS = [
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
@ -95,6 +104,8 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
@ -102,16 +113,15 @@ DATABASES = {
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '123456', 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '123456',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(os.environ.get('DJANGO_MYSQL_PORT') or 3306), 'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': { 'OPTIONS': {
'charset': 'utf8mb4', 'charset': 'utf8mb4'},
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", }}
},
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@ -137,31 +147,18 @@ LOCALE_PATHS = (
) )
LANGUAGE_CODE = 'zh-hans' LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai' TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = False USE_TZ = False
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
# --- MODIFICATION: 自动创建静态文件目录 ---
STATIC_DIR = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [STATIC_DIR]
# 如果 static 目录不存在,则自动创建
if not os.path.exists(STATIC_DIR):
os.makedirs(STATIC_DIR)
# --- MODIFICATION END ---
# 媒体文件配置 (用户上传的文件,如头像)
# 确保此配置已正确设置
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# 同样,自动创建媒体文件目录
if not os.path.exists(MEDIA_ROOT):
os.makedirs(MEDIA_ROOT)
HAYSTACK_CONNECTIONS = { HAYSTACK_CONNECTIONS = {
'default': { 'default': {
@ -171,11 +168,15 @@ HAYSTACK_CONNECTIONS = {
} }
# Automatically update searching index # Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password # Allow user login with username and password
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend'] 'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
AUTH_USER_MODEL = 'accounts.BlogUser' AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/' LOGIN_URL = '/login/'
@ -191,7 +192,6 @@ BOOTSTRAP_COLOR_TYPES = [
PAGINATE_BY = 10 PAGINATE_BY = 10
# http cache timeout # http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000 CACHE_CONTROL_MAX_AGE = 2592000
# cache setting # cache setting
CACHES = { CACHES = {
'default': { 'default': {
@ -215,18 +215,16 @@ BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
# Email: # Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = os.environ.get('DJANGO_EMAIL_TLS', 'False') == 'True' EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = os.environ.get('DJANGO_EMAIL_SSL', 'True') == 'True' EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = EMAIL_HOST_USER
# Setting debug=false did NOT handle except email notifications # Setting debug=false did NOT handle except email notifications
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5) # WX ADMIN password(Two times md5)
WXADMIN = os.environ.get( WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
@ -302,14 +300,10 @@ STATICFILES_FINDERS = (
# other # other
'compressor.finders.CompressorFinder', 'compressor.finders.CompressorFinder',
) )
COMPRESS_ENABLED = True
# --- MODIFICATION: 动态设置压缩开关 ---
# 在开发环境(DEBUG=True)关闭压缩,在生产环境(DEBUG=False)开启压缩
COMPRESS_ENABLED = not DEBUG
# --- MODIFICATION END ---
# COMPRESS_OFFLINE = True # COMPRESS_OFFLINE = True
COMPRESS_CSS_FILTERS = [ COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones # creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter', 'compressor.filters.css_default.CssAbsoluteFilter',
@ -320,6 +314,8 @@ COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter' 'compressor.filters.jsmin.JSMinFilter'
] ]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
@ -344,17 +340,4 @@ ACTIVE_PLUGINS = [
'external_links', 'external_links',
'view_count', 'view_count',
'seo_optimizer' 'seo_optimizer'
] ]
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# ... 其他配置 ...
# 媒体文件(用户上传的文件)的存储根目录
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# 媒体文件的 URL 前缀。浏览器通过这个 URL 来访问媒体文件。
MEDIA_URL = '/media/'

@ -1,15 +1,19 @@
from django.test import TestCase from django.test import TestCase
from djangoblog.utils import * from djangoblog.utils import *
# 定义测试类继承自TestCase用于测试Django博客项目中的工具类/函数
class DjangoBlogTest(TestCase): class DjangoBlogTest(TestCase):
# 本测试用例无需前置初始化操作,故保持空实现
def setUp(self): def setUp(self):
pass pass
# 1. 测试SHA256加密工具函数get_sha256
# 对字符串'test'进行SHA256加密获取加密结果
def test_utils(self): def test_utils(self):
md5 = get_sha256('test') md5 = get_sha256('test')
self.assertIsNotNone(md5) self.assertIsNotNone(md5)
# 2. 测试Markdown解析工具类CommonMarkdown
# 调用get_markdown方法解析一段包含多种元素的Markdown文本
c = CommonMarkdown.get_markdown(''' c = CommonMarkdown.get_markdown('''
# Title1 # Title1

@ -17,35 +17,23 @@ from django.conf import settings
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap
from django.urls import path, re_path, include from django.urls import path, include
from django.contrib import admin from django.urls import re_path
from haystack.views import search_view_factory
# 从 blog.views 导入所需视图(包括我们之前添加的 DjangoBlogFeed
from blog.views import (
page_not_found_view,
server_error_view,
permission_denied_view,
DjangoBlogFeed,
EsSearchView # 假设你在 blog 应用中有一个 EsSearchView
)
# 使用你自定义的 admin_site from blog.views import EsSearchView
from djangoblog.admin_site import admin_site from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
# 导入站点地图 from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ( from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
ArticleSiteMap, from django.contrib import admin
CategorySiteMap, from django.urls import path, re_path, include
StaticViewSitemap, from django.conf.urls.i18n import i18n_patterns
TagSiteMap, from django.contrib.sitemaps.views import sitemap
UserSiteMap from blog.views import page_not_found_view, server_error_view, permission_denied_view, DjangoBlogFeed
) from search.views import search_view_factory
from es_search.views import EsSearchView
# --- 暂时注释掉需要 'es_search' 或 'search' 应用的导入 --- from es_search.forms import ElasticSearchForm
# from haystack.views import search_view_factory
# from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
# from es_search.forms import ElasticSearchForm
# from search.views import search_view_factory
sitemaps = { sitemaps = {
'blog': ArticleSiteMap, 'blog': ArticleSiteMap,
@ -64,28 +52,20 @@ urlpatterns = [
] ]
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
# 使用自定义的 admin_site re_path(r'^admin/', admin.site.urls),
re_path(r'^admin/', admin_site.urls),
re_path(r'', include('blog.urls', namespace='blog')), re_path(r'', include('blog.urls', namespace='blog')),
re_path(r'mdeditor/', include('mdeditor.urls')), re_path(r'mdeditor/', include('mdeditor.urls')),
re_path(r'', include('comments.urls', namespace='comment')), re_path(r'', include('comments.urls', namespace='comment')),
re_path(r'', include('accounts.urls', namespace='account')), re_path(r'', include('accounts.urls', namespace='account')),
re_path(r'', include('oauth.urls', namespace='oauth')), re_path(r'', include('oauth.urls', namespace='oauth')),
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='sitemap'), re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed(), name='feed'), re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed(), name='rss'), re_path(r'^rss/$', DjangoBlogFeed()),
re_path(r'^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchForm), name='search'),
# --- 暂时注释掉需要 'es_search' 应用的搜索路由 ---
# 如果你的搜索功能在 blog 应用中实现,可以这样配置:
# re_path(r'^search/', EsSearchView.as_view(), name='search'),
# --- 以下是需要 'es_search' 或 'search' 应用的原始配置,现已注释 ---
# re_path(r'^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchForm), name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')), re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks')), re_path(r'', include('owntracks.urls', namespace='owntracks')),
prefix_default_language=False prefix_default_language=False
) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 755 KiB

@ -1,30 +0,0 @@
{% extends 'share_layout/base_account.html' %}
{% load i18n %}
{% load static %}
{% block content %}
<div class="container">
<h2 class="form-signin-heading text-center">{% trans 'forget the password' %}</h2>
<div class="card card-signin">
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="">
<form class="form-signin" action="{% url 'account:forget_password' %}" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field }}
{{ field.errors }}
{% endfor %}
<input type="button" class="button" id="btn" value="{% trans 'get verification code' %}">
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans 'submit' %}</button>
</form>
</div>
<p class="text-center">
<a href="/">Home Page</a>
|
<a href="{% url "account:login" %}">login page</a>
</p>
</div> <!-- /container -->
{% endblock %}

@ -1,46 +0,0 @@
{% extends 'share_layout/base_account.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="container">
<h2 class="form-signin-heading text-center">Sign in with your Account</h2>
<div class="card card-signin">
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="">
<form class="form-signin" action="{% url 'account:login' %}" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field }}
{{ field.errors }}
{% endfor %}
<input type="hidden" name="next" value="{{ redirect_to }}">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<div class="checkbox">
{% comment %}<a class="pull-right">Need help?</a>{% endcomment %}
<label>
<input type="checkbox" value="remember-me" name="remember"> Stay signed in
</label>
</div>
{% load oauth_tags %}
{% load_oauth_applications request%}
</form>
</div>
<p class="text-center">
<a href="{% url "account:register" %}">
{% trans 'Create Account' %}
</a>
|
<a href="/">Home Page</a>
|
<a href="{% url "account:forget_password" %}">
{% trans 'Forget Password' %}
</a>
</p>
</div> <!-- /container -->
{% endblock %}

@ -1,29 +0,0 @@
{% extends 'share_layout/base_account.html' %}
{% load static %}
{% block content %}
<div class="container">
<h2 class="form-signin-heading text-center">Create Your Account</h2>
<div class="card card-signin">
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="">
<form class="form-signin" action="{% url 'account:register' %}" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field }}
{{ field.errors }}
{% endfor %}
<button class="btn btn-lg btn-primary btn-block" type="submit">Create Your Account</button>
</form>
</div>
<p class="text-center">
<a href="{% url "account:login" %}">Sign In</a>
</p>
</div> <!-- /container -->
{% endblock %}

@ -1,27 +0,0 @@
{% extends 'share_layout/base.html' %}
{% load i18n %}
{% block header %}
<title> {{ title }}</title>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<header class="archive-header">
<h2 class="archive-title"> {{ content }}</h2>
</header><!-- .archive-header -->
<br/>
<header class="archive-header" style="text-align: center">
<a href="{% url "account:login" %}">
{% trans 'login' %}
</a>
|
<a href="/">
{% trans 'back to the homepage' %}
</a>
</header><!-- .archive-header -->
</div>
</div>
{% endblock %}

@ -1,60 +0,0 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load cache %}
{% load i18n %}
{% block header %}
<title>{% trans 'article archive' %} | {{ SITE_DESCRIPTION }}</title>
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/>
<meta property="og:type" content="blog"/>
<meta property="og:title" content="{{ SITE_NAME }}"/>
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<header class="archive-header">
<p class="archive-title">{% trans 'article archive' %}</p>
</header><!-- .archive-header -->
<div class="entry-content">
{% regroup article_list by pub_time.year as year_post_group %}
<ul>
{% for year in year_post_group %}
<li>{{ year.grouper }} {% trans 'year' %}
{% regroup year.list by pub_time.month as month_post_group %}
<ul>
{% for month in month_post_group %}
<li>{{ month.grouper }} {% trans 'month' %}
<ul>
{% for article in month.list %}
<li><a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
</div><!-- #content -->
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar user 'i' %}
{% endblock %}

@ -1,166 +0,0 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load static %}
{% block header %}
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<!-- 修复:确保标签参数完整 -->
<div id="article-content">
{% load_article_detail article False user %}
</div>
{% if article.type == 'a' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
{% if next_article %}
<span class="nav-previous"><a href="{{ next_article.get_absolute_url }}" rel="prev"><span
class="meta-nav">&larr;</span> {{ next_article.title }}</a></span>
{% endif %}
{% if prev_article %}
<span class="nav-next"><a href="{{ prev_article.get_absolute_url }}"
rel="next">{{ prev_article.title }} <span
class="meta-nav">&rarr;</span></a></span>
{% endif %}
</nav><!-- .nav-single -->
{% endif %}
<!-- 文章收藏功能 -->
<div style="margin: 20px 0; padding: 10px; border-top: 1px solid #eee; border-bottom: 1px solid #eee;">
{% if user.is_authenticated %}
{# 为按钮添加一个 data-action 属性用于JS判断当前操作 #}
<button id="favorite-btn"
data-article-id="{{ article.id }}"
data-action="{% if article in user.favorite_articles.all %}remove{% else %}add{% endif %}"
style="background: none; border: none; cursor: pointer; color: #333; font-size: 1em;">
{% if article in user.favorite_articles.all %}
<i class="fas fa-bookmark" style="color: #f0ad4e;"></i>
<span>已收藏</span>
{% else %}
<i class="far fa-bookmark"></i>
<span>收藏</span>
{% endif %}
<span id="favorite-count" style="margin-left: 5px;">({{ article.get_favorite_count }})</span>
</button>
{% else %}
<span style="color: #999;">
<i class="far fa-bookmark"></i>
<span>收藏</span>
<span style="margin-left: 5px;">({{ article.get_favorite_count }})</span>
</span>
<p style="margin-top: 5px; font-size: 0.9em; color: #999;">
<a href="{% url "account:login" %}?next={{ request.get_full_path }}">登录</a> 后收藏文章
</p>
{% endif %}
</div>
<!-- 相关推荐文章区域 -->
{% if related_articles %}
<section class="related-posts">
<h3 class="related-title">相关推荐</h3>
<ul class="related-posts-list">
{% for rel_article in related_articles %}
<li>
<article class="related-post">
<header>
<h4 class="related-post-title">
<a href="{{ rel_article.get_absolute_url }}" rel="bookmark">
{{ rel_article.title }}
</a>
</h4>
<div class="related-post-meta">
<time datetime="{{ rel_article.pub_time|date:"c" }}">
{{ rel_article.pub_time|date:"Y年m月d日" }}
</time>
<span class="views"> | 阅读量:{{ rel_article.views }}</span>
</div>
</header>
</article>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
</div><!-- #content -->
{% if article.comment_status == "o" and OPEN_SITE_COMMENT %}
{% include 'comments/tags/comment_list.html' %}
{% if user.is_authenticated %}
{% include 'comments/tags/post_comment.html' %}
{% else %}
<div class="comments-area">
<h3 class="comment-meta">您还没有登录,请您<a
href="{% url "account:login" %}?next={{ request.get_full_path }}" rel="nofollow">登录</a>后发表评论。
</h3>
{% load oauth_tags %}
{% load_oauth_applications request %}
</div>
{% endif %}
{% endif %}
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar user "p" %}
{% endblock %}
{% block footer %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script>
$(document).ready(function() {
// 收藏功能 JS
$('#favorite-btn').on('click', function() {
var $btn = $(this);
var articleId = $btn.data('article-id');
var action = $btn.data('action'); // 获取当前操作add 或 remove
// 动态生成URL
// 修改后的代码
// 无论 add 还是 remove都使用同一个 URL 名称 article_favorite
var url = "{% url 'blog:article_favorite' article_id=999999999 %}".replace('999999999', articleId);
$.ajax({
url: url,
type: 'POST',
dataType: 'json',
beforeSend: function(xhr) {
xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");
},
success: function(data) {
var $icon = $btn.find('i');
var $textSpan = $btn.find('span:eq(0)'); // 找到第一个span'收藏'/'已收藏'
var $countSpan = $('#favorite-count');
// 更新收藏数
$countSpan.text('(' + data.favorite_count + ')');
// 更新按钮状态和文本
if (action === 'add') {
$icon.removeClass('far').addClass('fas').css('color', '#f0ad4e');
$textSpan.text('已收藏');
$btn.data('action', 'remove'); // 切换action状态
} else {
$icon.removeClass('fas').addClass('far').css('color', '#333');
$textSpan.text('收藏');
$btn.data('action', 'add'); // 切换action状态
}
},
error: function(xhr, status, error) {
console.error("收藏操作失败:", error);
// 尝试解析错误信息
var errorMsg = "操作失败,请稍后重试。";
if (xhr.responseJSON && xhr.responseJSON.message) {
errorMsg = xhr.responseJSON.message;
}
alert(errorMsg);
}
});
});
});
</script>
{% endblock %}

@ -1,42 +0,0 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load cache %}
{% block header %}
{% if tag_name %}
<title>{{ page_type }}:{{ tag_name }} | {{ SITE_DESCRIPTION }}</title>
{% comment %}<meta name="description" content="{{ page_type }}:{{ tag_name }}"/>{% endcomment %}
{% else %}
<title>{{ SITE_NAME }} | {{ SITE_DESCRIPTION }}</title>
{% endif %}
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/>
<meta property="og:type" content="blog"/>
<meta property="og:title" content="{{ SITE_NAME }}"/>
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
{% if page_type and tag_name %}
<header class="archive-header">
<p class="archive-title">{{ page_type }}<span>{{ tag_name }}</span></p>
</header><!-- .archive-header -->
{% endif %}
{% for article in article_list %}
{% load_article_detail article True user %}
{% endfor %}
{% if is_paginated %}
{% load_pagination_info page_obj page_type tag_name %}
{% endif %}
</div><!-- #content -->
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar user linktype %}
{% endblock %}

@ -1,45 +0,0 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load cache %}
{% block header %}
{% if tag_name %}
{% if statuscode == '404' %}
<title>404 NotFound</title>
{% elif statuscode == '403' %}
<title>Permission Denied</title>
{% elif statuscode == '500' %}
<title>500 Error</title>
{% else %}
<title></title>
{% endif %}
{% comment %}<meta name="description" content="{{ page_type }}:{{ tag_name }}"/>{% endcomment %}
{% else %}
<title>{{ SITE_NAME }} | {{ SITE_DESCRIPTION }}</title>
{% endif %}
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/>
<meta property="og:type" content="blog"/>
<meta property="og:title" content="{{ SITE_NAME }}"/>
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<header class="archive-header">
<h1 class="archive-title">{{ message }}</h1>
</header><!-- .archive-header -->
</div><!-- #content -->
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar user 'i' %}
{% endblock %}

@ -1,44 +0,0 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load cache %}
{% block header %}
<title>友情链接 | {{ SITE_DESCRIPTION }}</title>
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/>
<meta property="og:type" content="blog"/>
<meta property="og:title" content="{{ SITE_NAME }}"/>
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<header class="archive-header">
<p class="archive-title">友情链接</p>
</header><!-- .archive-header -->
<div class="entry-content">
<ul>
{% for obj in object_list %}
<li>
<a href="{{ obj.link }}">{{ obj.name }}</a>
</li>
{% endfor %} </ul>
</div>
</div><!-- #content -->
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar user 'i' %}
{% endblock %}

@ -1,86 +0,0 @@
{% load blog_tags %}
{% load cache %}
{% load i18n %}
<article id="post-{{ article.pk }} "
class="post-{{ article.pk }} post type-post status-publish format-standard hentry">
<header class="entry-header">
<h1 class="entry-title">
{% if isindex %}
{% if article.article_order > 0 %}
<a href="{{ article.get_absolute_url }}"
rel="bookmark">【{% trans 'pin to top' %}】{{ article.title }}</a>
{% else %}
<a href="{{ article.get_absolute_url }}"
rel="bookmark">{{ article.title }}</a>
{% endif %}
{% else %}
{{ article.title }}
{% endif %}
</h1>
<div class="comments-link">
{% if article.comment_status == "o" and open_site_comment %}
<a href="{{ article.get_absolute_url }}#comments" class="ds-thread-count" data-thread-key="3815"
rel="nofollow">
<span class="leave-reply">
{% if article.comment_set and article.comment_set.count %}
{{ article.comment_set.count }} {% trans 'comments' %}
{% else %}
{% trans 'comment' %}
{% endif %}
</span>
</a>
{% endif %}
<div style="float:right">
{{ article.views }} views
</div>
</div><!-- .comments-link -->
<br/>
{% if article.type == 'a' %}
{% if not isindex %}
{% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %}
{% endcache %}
{% endif %}
{% endif %}
</header><!-- .entry-header -->
<div class="entry-content" itemprop="articleBody">
{% if isindex %}
{% if isindex %}
{# 如果是列表页isindex=True只显示摘要 #}
{{ article.summary|default:article.body|truncatechars:200|safe }}
{% else %}
{# 如果是详情页isindex=False显示完整内容 #}
{{ article.body|safe }}
{% endif %}
<p class='read-more'><a
href=' {{ article.get_absolute_url }}'>Read more</a></p>
{% else %}
{% if article.show_toc %}
{% get_markdown_toc article.body as toc %}
<b>{% trans 'toc' %}:</b>
{{ toc|safe }}
<hr class="break_line"/>
{% endif %}
<div class="article">
{% if isindex %}
{# 列表页显示摘要或正文前200字符 #}
{{ article.summary|default:article.body|truncatechars:200|safe }}
{% else %}
{# 详情页显示完整正文(因传入的是 False对应详情页 #}
{{ article.body|safe }}
{% endif %}
</div>
{% endif %}
</div><!-- .entry-content -->
{% load_article_metas article user %}
</article><!-- #post -->

@ -1,54 +0,0 @@
{% load i18n %}
{% load blog_tags %}
<footer class="entry-meta">
{% trans 'posted in' %}
<a href="{{ article.category.get_absolute_url }}" rel="category tag">{{ article.category.name }}</a>
{% if article.type == 'a' %}
{% if article.tags.all %}
{% trans 'and tagged' %}
{% for t in article.tags.all %}
<a href="{{ t.get_absolute_url }}" rel="tag">{{ t.name }}</a>
{% if t != article.tags.all.last %}
,
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
. {% trans 'by ' %}
<span class="by-author">
<span class="author vcard">
<a class="url fn n" href="{{ article.author.get_absolute_url }}"
{% blocktranslate %}
title="View all articles published by {{ article.author.username }}"
{% endblocktranslate %}
rel="author">
<span itemprop="author" itemscope itemtype="http://schema.org/Person">
<span itemprop="name" itemprop="publisher">
{{ article.author.username }}
</span>
</span>
</a>
</span>
</span>
{% trans 'on' %}
<a href="{{ article.get_absolute_url }}"
title="{% datetimeformat article.pub_time %}"
itemprop="datePublished" content="{% datetimeformat article.pub_time %}"
rel="bookmark">
<time class="entry-date updated"
datetime="{{ article.pub_time }}">
{% datetimeformat article.pub_time %}
</time>
</a>
<!-- 新增:显示阅读量 -->
<span class="meta-sep"> | </span>
<span class="views">
{% trans 'Views' %}: <span itemprop="interactionCount">{{ article.views }}</span>
</span>
{% if user.is_superuser %}
<span class="meta-sep"> | </span>
<a href="{{ article.get_admin_url }}">{% trans 'edit' %}</a>
{% endif %}
</footer><!-- .entry-meta -->

@ -1,17 +0,0 @@
{% load i18n %}
<nav id="nav-below" class="navigation" role="navigation">
<h3 class="assistive-text">
{% trans 'article navigation' %}
</h3>
{% if page_obj.has_next and next_url%}
<div class="nav-previous"><a
href="{{ next_url }}"><span
class="meta-nav">&larr;</span> {% trans 'earlier articles' %}</a></div>
{% endif %}
{% if page_obj.has_previous and previous_url %}
<div class="nav-next"><a href="{{ previous_url }}">{% trans 'newer articles' %}
<span
class="meta-nav">→</span></a>
</div>
{% endif %}
</nav><!-- .navigation -->

@ -1,19 +0,0 @@
{% load i18n %}
{% if article_tags_list %}
<div class="panel panel-default">
<div class="panel-heading">
{% trans 'tags' %}
</div>
<div class="panel-body">
{% for url,count,tag,color in article_tags_list %}
<a class="label label-{{ color }}" style="display: inline-block;" href="{{ url }}"
title="{{ tag.name }}">
{{ tag.name }}
<span class="badge">{{ count }}</span>
</a>
{% endfor %}
</div>
</div>
{% endif %}

@ -1,19 +0,0 @@
<ul itemscope itemtype="https://schema.org/BreadcrumbList" class="breadcrumb">
{% for name,url in names %}
<li itemprop="itemListElement" itemscope
itemtype="https://schema.org/ListItem">
<a href="{{ url }}" itemprop="item" >
<span itemprop="name">{{ name }}</span></a>
<meta itemprop="position" content="{{ forloop.counter }}"/>
</li>
{% endfor %}
<li class="active" itemprop="itemListElement" itemscope
itemtype="https://schema.org/ListItem">
<span itemprop="name">{{ title }}</span>
<meta itemprop="position" content="{{ count }}"/>
</li>
</ul>

@ -1,129 +0,0 @@
{% load blog_tags %}
{% load i18n %}
<div id="secondary" class="widget-area" role="complementary">
<aside id="search-2" class="widget widget_search">
<form role="search" method="get" id="searchform" class="searchform" action="/search">
<div>
<label class="screen-reader-text" for="s">{% trans 'search' %}</label>
<input type="text" value="" name="q" id="q"/>
<input type="submit" id="searchsubmit" />
</div>
</form>
</aside>
{% if extra_sidebars %}
{% for sidebar in extra_sidebars %}
<aside class="widget_text widget widget_custom_html"><p class="widget-title">{{ sidebar.name }}</p>
<div class="textwidget custom-html-widget">
{{ sidebar.content|safe }}
</div>
</aside>
{% endfor %}
{% endif %}
<!-- 热门文章模块优化 -->
{% if most_read_articles %}
<aside id="views-4" class="widget widget_views">
<p class="widget-title">{% trans '热门文章' %}</p>
<ul>
{% for a in most_read_articles %}
<li>
<a href="{{ a.get_absolute_url }}" title="{{ a.title }}" class="hot-article-link">
{{ a.title }}
</a>
<span class="post-views">
<i class="fa fa-eye" aria-hidden="true"></i> {{ a.views }}
</span>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
{% if sidebar_categorys %}
<aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms"><p class="widget-title">{% trans '分类' %}</p>
<ul>
{% for c in sidebar_categorys %}
<li class="cat-item"><a href="{{ c.get_absolute_url }}">{{ c.name }}</a></li>
{% endfor %}
</ul>
</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 '最新评论' %}</p>
<ul id="recentcomments">
{% for c in sidebar_comments %}
<li class="recentcomments">
<span class="comment-author-link">{{ c.author.username }}</span>
{% trans '发表于' %}《<a href="{{ c.article.get_absolute_url }}#comment-{{ c.pk }}">{{ c.article.title }}</a>
</li>
{% endfor %}
</ul>
</aside>
{% endif %}
{% if recent_articles %}
<aside id="recent-posts-2" class="widget widget_recent_entries"><p class="widget-title">{% trans '最新文章' %}</p>
<ul>
{% for a in recent_articles %}
<li><a href="{{ a.get_absolute_url }}" title="{{ a.title }}">{{ a.title }}</a></li>
{% endfor %}
</ul>
</aside>
{% endif %}
{% if sidabar_links %}
<aside id="linkcat-0" class="widget widget_links"><p class="widget-title">{% trans '友情链接' %}</p>
<ul class='xoxo blogroll'>
{% for l in sidabar_links %}
<li><a href="{{ l.link }}" target="_blank" title="{{ l.name }}">{{ l.name }}</a></li>
{% endfor %}
</ul>
</aside>
{% endif %}
{% if show_google_adsense %}
<aside id="text-2" class="widget widget_text"><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 '标签云' %}</p>
<div class="tagcloud">
{% for tag,count,size in sidebar_tags %}
<a href="{{ tag.get_absolute_url }}" class="tag-link-{{ 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 '欢迎Star或Fork源码' %}</p>
<div class="textwidget">
<p><a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
src="https://resource.lylinux.net/img.shields.io/github/stars/liangliangyy/djangoblog.svg?style=social&amp;label=Star"
alt="GitHub stars"></a>
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
src="https://resource.lylinux.net/img.shields.io/github/forks/liangliangyy/djangoblog.svg?style=social&amp;label=Fork"
alt="GitHub forks"></a></p>
</div>
</aside>
<aside id="meta-3" class="widget widget_meta"><p class="widget-title">{% trans '功能导航' %}</p>
<ul>
<li><a href="/admin/" rel="nofollow">{% trans '管理后台' %}</a></li>
{% if user.is_authenticated %}
<li><a href="{% url "account:logout" %}" rel="nofollow">{% trans '退出登录' %}</a></li>
{% else %}
<li><a href="{% url "account:login" %}" rel="nofollow">{% trans '登录' %}</a></li>
{% endif %}
{% if user.is_superuser %}
<li><a href="{% url 'owntracks:show_dates' %}" target="_blank">{% trans '轨迹记录' %}</a></li>
{% endif %}
<li><a href="http://gitbook.lylinux.net" target="_blank" rel="nofollow">GitBook</a></li>
</ul>
</aside>
<div id="rocket" class="show" title="{% trans '返回顶部' %}"></div>
</div><!-- #secondary -->

@ -1,97 +0,0 @@
<!-- blog/templates/blog/user_favorites.html -->
{% extends 'share_layout/base.html' %}
{% load static %}
{% load blog_tags %}
{% block header %}
<title>我的收藏 | {{ SITE_NAME }}</title>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<article class="hentry">
<header class="entry-header">
<h1 class="entry-title">我的收藏</h1>
<p>这里是你收藏的所有文章。</p>
</header>
<div class="entry-content">
{% if favorite_articles %}
<ul class="article-list">
{% for article in favorite_articles %}
<li>
<h2><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h2>
<div class="meta">
<time datetime="{{ article.pub_time|date:"c" }}">
{{ article.pub_time|date:"F j, Y" }}
</time>
<span>作者: <a href="{% url 'blog:author_detail' article.author.username %}">{{ article.author.username }}</a></span>
<!-- 修正第 31 行的 URL 名称 -->
<span>分类:<a href="{% url 'blog:category_detail' article.category.slug %}">{{ article.category.name }}</a></span>
</div>
<div class="summary">
{{ article.body|striptags|truncatechars:150 }}
</div>
<a href="{{ article.get_absolute_url }}" class="read-more">阅读全文</a>
</li>
{% endfor %}
</ul>
{% else %}
<p class="no-favorites">你还没有收藏任何文章。快去 <a href="{% url 'blog:index' %}">文章列表</a> 看看吧!</p>
{% endif %}
</div>
</article>
</div>
</div>
{% endblock %}
{% block sidebar %}
{% load_sidebar profile_user "p" %}
{% endblock %}
{% block extra_footer %}
<style>
.article-list {
list-style: none;
padding: 0;
}
.article-list li {
padding: 20px 0;
border-bottom: 1px solid #eee;
}
.article-list li:last-child {
border-bottom: none;
}
.article-list h2 {
margin-top: 0;
}
.meta {
color: #777;
font-size: 0.9em;
margin-bottom: 10px;
}
.summary {
margin-bottom: 15px;
}
.read-more {
display: inline-block;
padding: 5px 10px;
background-color: #007bff;
color: white;
border-radius: 3px;
text-decoration: none;
}
.read-more:hover {
background-color: #0056b3;
color: white;
}
.no-favorites {
text-align: center;
padding: 50px 0;
color: #555;
}
</style>
{% endblock %}

@ -1,230 +0,0 @@
<!-- blog/templates/blog/user_profile_detail.html -->
{% extends 'share_layout/base.html' %}
{% load static %}
{% load blog_tags %} <!-- 添加这一行,加载自定义标签库 -->
{% block header %}
<title>{{ profile.user.username }}'s Profile | {{ SITE_NAME }}</title>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<article class="hentry">
<header class="entry-header profile-header">
<div class="profile-avatar">
{% if profile.avatar %}
<img src="{{ profile.avatar.url }}" alt="{{ profile.user.username }}'s avatar">
{% else %}
<img src="{% static 'blog/images/default-avatar.png' %}" alt="Default Avatar">
{% endif %}
</div>
<div class="profile-info">
<h1 class="entry-title">{{ profile.user.username }}</h1>
<div class="profile-meta">
<span>Joined on {{ profile.created_at|date:"F j, Y" }}</span>
{% if user.is_authenticated and user == profile.user %}
<a href="{% url 'blog:user_profile_update' %}" class="edit-profile-btn">
Edit Profile
</a>
{% endif %}
</div>
</div>
</header>
<div class="entry-content profile-content">
{% if profile.bio %}
<section class="profile-bio">
<h3>About Me</h3>
<p>{{ profile.bio|linebreaks }}</p>
</section>
{% endif %}
{% if profile.website or profile.github or profile.twitter or profile.weibo %}
<section class="profile-links">
<h3>Connect with Me</h3>
<ul>
{% if profile.website %}
<li><a href="{{ profile.website }}" target="_blank" rel="noopener noreferrer"><i class="fas fa-globe"></i> {{ profile.website }}</a></li>
{% endif %}
{% if profile.github %}
<li><a href="{{ profile.github }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-github"></i> GitHub</a></li>
{% endif %}
{% if profile.twitter %}
<li><a href="{{ profile.twitter }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-twitter"></i> Twitter</a></li>
{% endif %}
{% if profile.weibo %}
<li><a href="{{ profile.weibo }}" target="_blank" rel="noopener noreferrer"><i class="fab fa-weibo"></i> Weibo</a></li>
{% endif %}
</ul>
</section>
{% endif %}
<!-- ==================== 新增:我的收藏链接 ==================== -->
{% if user.is_authenticated and user == profile.user %}
<section class="profile-actions">
<a href="{% url 'blog:user_favorites' %}" class="btn-favorite">
<i class="fas fa-heart"></i> 我的收藏 ({{ user.favorite_articles.count }})
</a>
</section>
{% endif %}
<!-- ========================================================== -->
</div>
</article>
{% if user_articles %}
<section class="profile-articles">
<h2>Articles by {{ profile.user.username }} <span>({{ user_articles|length }})</span></h2>
<ul>
{% for article in user_articles %}
<li>
<a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
<time datetime="{{ article.pub_time|date:"c" }}">{{ article.pub_time|date:"F j, Y" }}</time>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
</div>
</div>
{% endblock %}
{% block sidebar %}
{% load_sidebar user "p" %}
{% endblock %}
{% block extra_footer %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
<style>
/* 个人资料头部布局 */
.profile-header {
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
margin-bottom: 30px; /* 增加了下边距,与内容区隔开 */
}
/* 头像样式调整 */
.profile-avatar img {
width: 80px; /* 从 120px 调整为 80px */
height: 80px; /* 从 120px 调整为 80px */
border-radius: 50%;
object-fit: cover;
margin-right: 20px; /* 稍微减少了右边距 */
border: 3px solid #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1); /* 增强了一点阴影 */
}
/* 个人信息区域 */
.profile-info h1 {
margin-bottom: 5px; /* 减少了标题下边距 */
}
.edit-profile-btn {
display: inline-block;
margin-top: 10px;
padding: 5px 15px;
background-color: #007bff;
color: white;
border-radius: 4px;
text-decoration: none;
font-size: 0.9em; /* 字体稍小 */
}
.edit-profile-btn:hover {
background-color: #0056b3;
color: white;
}
/* 内容区块通用样式 */
.profile-bio, .profile-links, .profile-articles, .profile-actions { /* 新增 .profile-actions */
margin-bottom: 30px;
padding: 20px;
background: #f9f9f9;
border-radius: 4px;
border: 1px solid #eee; /* 增加了一个边框 */
}
/* ==================== 新增:我的收藏链接样式 ==================== */
.btn-favorite {
display: inline-flex; /* 使用 flex 布局让图标和文字垂直居中 */
align-items: center;
padding: 10px 20px;
background-color: #f0ad4e; /* 使用一个醒目的颜色,如橙色 */
color: white;
border-radius: 4px;
text-decoration: none;
font-weight: bold;
transition: background-color 0.2s ease;
}
.btn-favorite i {
margin-right: 8px;
font-size: 1.1em;
}
.btn-favorite:hover {
background-color: #ec971f; /* hover 时颜色加深 */
color: white;
}
/* ================================================================ */
/* 链接列表 */
.profile-links ul {
list-style: none;
padding: 0;
}
.profile-links li {
margin-bottom: 10px;
}
.profile-links a {
display: flex;
align-items: center;
color: #007bff;
text-decoration: none;
transition: color 0.2s; /* 增加了过渡效果 */
}
.profile-links a:hover {
text-decoration: underline;
color: #0056b3; /* hover 时颜色加深 */
}
.profile-links i {
margin-right: 10px;
font-size: 1.2em;
width: 20px;
text-align: center;
color: #555; /* 图标颜色稍微暗一点 */
}
/* 文章列表 */
.profile-articles ul {
list-style: none;
padding: 0;
}
.profile-articles li {
padding: 10px 0;
border-bottom: 1px dotted #ddd;
display: flex;
justify-content: space-between;
align-items: center; /* 垂直居中对齐 */
}
.profile-articles li:last-child {
border-bottom: none; /* 去掉最后一个的边框 */
}
.profile-articles li time {
color: #777;
font-size: 0.9em;
}
</style>
{% endblock %}

@ -1,104 +0,0 @@
<!-- blog/templates/blog/user_profile_update.html -->
{% extends 'share_layout/base.html' %}
{% load static %}
{% load blog_tags %} <!-- 加载自定义标签库 -->
{% block header %}
<title>Edit Profile | {{ SITE_NAME }}</title>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<article class="hentry">
<header class="entry-header">
<h1 class="entry-title">Edit Your Profile</h1>
</header>
<div class="entry-content">
<form enctype="multipart/form-data" method="post" action="{% url 'blog:user_profile_update' %}">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger">
<strong>Please correct the errors below.</strong>
</div>
{% endif %}
<div class="form-group">
<label for="{{ form.avatar.id_for_label }}">Current Avatar:</label><br>
{% if user.profile.avatar %}
<img src="{{ user.profile.avatar.url }}" alt="Current Avatar" style="width: 100px; border-radius: 50%; margin-bottom: 10px;">
{% else %}
<img src="{% static 'blog/images/default-avatar.png' %}" alt="Default Avatar" style="width: 100px; border-radius: 50%; margin-bottom: 10px;">
{% endif %}
<br>
{{ form.avatar.label_tag }} {{ form.avatar }}
<small class="form-text text-muted">{{ form.avatar.help_text }}</small>
{{ form.avatar.errors }}
</div>
<div class="form-group">
{{ form.bio.label_tag }}
{{ form.bio }}
{{ form.bio.errors }}
</div>
<div class="form-group">
{{ form.website.label_tag }}
{{ form.website }}
{{ form.website.errors }}
</div>
<div class="form-group">
{{ form.github.label_tag }}
{{ form.github }}
{{ form.github.errors }}
</div>
<div class="form-group">
{{ form.twitter.label_tag }}
{{ form.twitter }}
{{ form.twitter.errors }}
</div>
<div class="form-group">
{{ form.weibo.label_tag }}
{{ form.weibo }}
{{ form.weibo.errors }}
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="{% url 'blog:user_profile' username=user.username %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</article>
</div>
</div>
{% endblock %}
{% block sidebar %}
{% load_sidebar user "p" %}
{% endblock %}
{% block extra_footer %}
<style>
.form-group { margin-bottom: 20px; }
label { font-weight: bold; display: block; margin-bottom: 5px; }
input[type="text"], input[type="url"], textarea {
width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;
}
textarea { resize: vertical; }
.btn {
display: inline-block; padding: 10px 16px; margin-bottom: 0; font-size: 14px; font-weight: 400; line-height: 1.42857143; text-align: center; white-space: nowrap; vertical-align: middle; cursor: pointer; background-image: none; border: 1px solid transparent; border-radius: 4px; text-decoration: none;
}
.btn-primary { color: #fff; background-color: #337ab7; border-color: #2e6da4; }
.btn-primary:hover { background-color: #286090; border-color: #204d74; color: #fff; }
.btn-secondary { color: #333; background-color: #fff; border-color: #ccc; }
.btn-secondary:hover { background-color: #e6e6e6; border-color: #adadad; color: #333; }
.alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; }
.alert-danger { color: #a94442; background-color: #f2dede; border-color: #ebccd1; }
</style>
{% endblock %}

@ -1,37 +0,0 @@
{% load blog_tags %}
<li class="comment even thread-even depth-{{ depth }} parent" id="comment-{{ comment_item.pk }}">
<div id="div-comment-{{ comment_item.pk }}" class="comment-body">
<div class="comment-author vcard">
<img alt="{{ comment_item.author.username }}的头像"
src="{{ comment_item.author.email|gravatar_url:150 }}"
srcset="{{ comment_item.author.email|gravatar_url:150 }}"
class="avatar avatar-96 photo"
loading="lazy"
decoding="async"
style="max-width:100%;height:auto;">
<cite class="fn">
<a rel="nofollow"
{% if comment_item.author.is_superuser %}
href="{{ comment_item.author.get_absolute_url }}"
{% else %}
href="#"
{% endif %}
rel="external nofollow"
class="url">{{ comment_item.author.username }}
</a>
</cite>
</div>
<div class="comment-meta commentmetadata">
<div>{{ comment_item.creation_time }}</div>
<div>回复给:@{{ comment_item.author.parent_comment.username }}</div>
</div>
<p>{{ comment_item.body|escape|comment_markdown }}</p>
<div class="reply"><a rel="nofollow" class="comment-reply-link"
href="javascript:void(0)"
onclick="do_reply({{ comment_item.pk }})"
aria-label="回复给{{ comment_item.author.username }}">回复</a></div>
</div>
</li><!-- #comment-## -->

@ -1,57 +0,0 @@
{% load blog_tags %}
<li class="comment even thread-even depth-{{ depth }} parent" id="comment-{{ comment_item.pk }}"
style="margin-left: {% widthratio depth 1 3 %}rem">
<div id="div-comment-{{ comment_item.pk }}" class="comment-body">
<div class="comment-author vcard">
<img alt="{{ comment_item.author.username }}的头像"
src="{{ comment_item.author.email|gravatar_url:150 }}"
srcset="{{ comment_item.author.email|gravatar_url:150 }}"
class="avatar avatar-96 photo"
loading="lazy"
decoding="async"
style="max-width:100%;height:auto;">
<cite class="fn">
<a rel="nofollow"
{% if comment_item.author.is_superuser %}
href="{{ comment_item.author.get_absolute_url }}"
{% else %}
href="#"
{% endif %}
rel="external nofollow"
class="url">{{ comment_item.author.username }}
</a>
</cite>
</div>
<div class="comment-meta commentmetadata">
{{ comment_item.creation_time }}
</div>
<p>
{% if comment_item.parent_comment %}
<div>回复 <a
href="#comment-{{ comment_item.parent_comment.pk }}">@{{ comment_item.parent_comment.author.username }}</a>
</div>
{% endif %}
</p>
<p>{{ comment_item.body|escape|comment_markdown }}</p>
<div class="reply"><a rel="nofollow" class="comment-reply-link"
href="javascript:void(0)" data-pk="{{ comment_item.pk }}"
aria-label="回复给{{ comment_item.author.username }}">回复</a></div>
</div>
</li><!-- #comment-## -->
{% query article_comments parent_comment=comment_item as cc_comments %}
{% for cc in cc_comments %}
{% with comment_item=cc template_name="comments/tags/comment_item_tree.html" %}
{% if depth >= 1 %}
{% include template_name %}
{% else %}
{% with depth=depth|add:1 %}
{% include template_name %}
{% endwith %}
{% endif %}
{% endwith %}
{% endfor %}

@ -1,45 +0,0 @@
<dev>
<section id="comments" class="themeform">
{% load blog_tags %}
{% load comments_tags %}
{% load cache %}
<ul class="comment-tabs group">
<li class="active"><a href="#commentlist-container"><i
class="fa fa-comments-o"></i>评论<span>{{ comment_count }}</span></a></li>
</ul>
{% if article_comments %}
<div id="commentlist-container" class="comment-tab" style="display: block;">
<ol class="commentlist">
{# {% query article_comments parent_comment=None as parent_comments %}#}
{% for comment_item in p_comments %}
{% with 0 as depth %}
{% include "comments/tags/comment_item_tree.html" %}
{% endwith %}
{% endfor %}
</ol><!--/.commentlist-->
<div class="navigation">
<nav class="nav-single">
{% if comment_prev_page_url %}
<div class="nav-previous">
<span><a href="{{ comment_prev_page_url }}" rel="prev"><span
class="meta-nav">←</span> 上一页</a></span>
</div>
{% endif %}
{% if comment_next_page_url %}
<div class="nav-next">
<span><a href="{{ comment_next_page_url }}" rel="next">下一页 <span
class="meta-nav">→</span></a></span>
</div>
{% endif %}
</nav>
</div>
<br/>
</div>
{% endif %}
</section>
</dev>

@ -1,33 +0,0 @@
<div id="comments" class="comments-area">
<div id="respond" class="comment-respond">
<h3 id="reply-title" class="comment-reply-title">发表评论
<small><a rel="nofollow" id="cancel-comment-reply-link" href="/wordpress/?p=3786#respond"
style="display:none;">取消回复</a></small>
</h3>
<form action="{% url 'comments:postcomment' article.pk %}" method="post" id="commentform"
class="comment-form">{% csrf_token %}
<p class="comment-form-comment">
{{ form.body.label_tag }}
{{ form.body }}
{{ form.body.errors }}
</p>
{{ form.parent_comment_id }}
<div class="form-submit">
{% if COMMENT_NEED_REVIEW %}
<span class="comment-markdown"> 支持markdown评论经审核后才会显示。</span>
{% else %}
<span class="comment-markdown"> 支持markdown。</span>
{% endif %}
<input name="submit" type="submit" id="submit" class="submit" value="发表评论"/>
<small class="cancel-comment" id="cancel_comment" style="display: none">
<a href="javascript:void(0)" id="cancel-comment-reply-link" onclick="cancel_reply()">取消回复</a>
</small>
</div>
</form>
</div><!-- #respond -->
</div><!-- #comments .comments-area -->

@ -1,22 +0,0 @@
{% extends 'share_layout/base.html' %}
{% block header %}
<title> {{ title }}</title>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<header class="archive-header">
<h2 class="archive-title"> {{ content }}</h2>
</header><!-- .archive-header -->
<br/>
<header class="archive-header" style="text-align: center">
<a href="{% url "account:login" %}">登录</a>
|
<a href="/">回到首页</a>
</header><!-- .archive-header -->
</div>
</div>
{% endblock %}

@ -1,13 +0,0 @@
{% load i18n %}
<div class="widget-login">
{% if apps %}
<small>
{% trans 'quick login' %}:
</small>
{% for icon,url in apps %}
<a href="{{ url }}" rel="nofollow">
<span class="icon-sn-{{ icon }}"></span>
</a>
{% endfor %}
{% endif %}
</div>

@ -1,46 +0,0 @@
{% extends 'share_layout/base_account.html' %}
{% load static %}
{% block content %}
<div class="container">
<h2 class="form-signin-heading text-center">绑定您的邮箱账号</h2>
<div class="card card-signin">
{% if picture %}
<img class="img-circle profile-img" src="{{ picture }}" alt="">
{% else %}
<img class="img-circle profile-img" src="{% static 'blog/img/avatar.png' %}" alt="">
{% endif %}
<form class="form-signin" action="" method="post">
{% csrf_token %}
{% comment %}<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" id="inputEmail" class="form-control" placeholder="Email" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="inputPassword" class="form-control" placeholder="Password" required>{% endcomment %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field }}
{{ field.errors }}
{% endfor %}
<button class="btn btn-lg btn-primary btn-block" type="submit">提交</button>
{% comment %}
<div class="checkbox">
<a class="pull-right">Need help?</a>
<label>
<input type="checkbox" value="remember-me"> Stay signed in
</label>
</div>
{% endcomment %}
</form>
</div>
<p class="text-center">
<a href="{% url "account:login" %}">登录</a>
</p>
</div> <!-- /container -->
{% endblock %}

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>记录日期</title>
</head>
<body>
<ul>
{% for date in results %}
<li>
<a href="{% url 'owntracks:show_maps' %}?date={{ date }}" target="_blank">{{ date }}</a>
</li>
{% endfor %}
</ul>
</body>
</html>

@ -1,135 +0,0 @@
<!doctype html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<style>
html,
body,
#container {
width: 100%;
height: 100%;
margin: 0px;
}
#loadingTip {
position: absolute;
z-index: 9999;
top: 0;
left: 0;
padding: 3px 10px;
background: red;
color: #fff;
font-size: 14px;
}
</style>
<title>运动轨迹</title>
</head>
<body>
<div id="container"></div>
<script type="text/javascript" src='//webapi.amap.com/maps?v=1.4.4&key=9c89950bdfbcecd46f814309384655cd'></script>
<!-- UI组件库 1.0 -->
<script src="//webapi.amap.com/ui/1.0/main.js?v=1.0.11"></script>
<script type="text/javascript">
//创建地图
var map = new AMap.Map('container', {
zoom: 4
});
AMapUI.load(['ui/misc/PathSimplifier', 'lib/$'], function (PathSimplifier, $) {
if (!PathSimplifier.supportCanvas) {
alert('当前环境不支持 Canvas');
return;
}
//just some colors
var colors = [
"#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00",
"#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707",
"#651067", "#329262", "#5574a6", "#3b3eac"
];
var pathSimplifierIns = new PathSimplifier({
zIndex: 100,
//autoSetFitView:false,
map: map, //所属的地图实例
getPath: function (pathData, pathIndex) {
return pathData.path;
},
getHoverTitle: function (pathData, pathIndex, pointIndex) {
if (pointIndex >= 0) {
//point
return pathData.name + ',点:' + pointIndex + '/' + pathData.path.length;
}
return pathData.name + ',点数量' + pathData.path.length;
},
renderOptions: {
pathLineStyle: {
dirArrowStyle: true
},
getPathStyle: function (pathItem, zoom) {
var color = colors[pathItem.pathIndex % colors.length],
lineWidth = Math.round(4 * Math.pow(1.1, zoom - 3));
return {
pathLineStyle: {
strokeStyle: color,
lineWidth: lineWidth
},
pathLineSelectedStyle: {
lineWidth: lineWidth + 2
},
pathNavigatorStyle: {
fillStyle: color
}
};
}
}
});
window.pathSimplifierIns = pathSimplifierIns;
$('<div id="loadingTip">加载数据,请稍候...</div>').appendTo(document.body);
$.getJSON('/owntracks/get_datas?date={{ date }}', function (d) {
if (!d || !d.length) {
$("#loadingTip").text("没有数据...")
return;
}
$('#loadingTip').remove();
pathSimplifierIns.setData(d);
//initRoutesContainer(d);
function onload() {
pathSimplifierIns.renderLater();
}
function onerror(e) {
alert('图片加载失败!');
}
d.forEach(function (item, index) {
var navg1 = pathSimplifierIns.createPathNavigator(index, {
loop: true,
speed: 1000,
});
navg1.start();
})
});
});
</script>
</body>
</html>

@ -1,3 +0,0 @@
{{ object.title }}
{{ object.author.username }}
{{ object.body }}

@ -1,66 +0,0 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% block header %}
<title>{{ SITE_NAME }} | {{ SITE_DESCRIPTION }}</title>
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/>
<meta property="og:type" content="blog"/>
<meta property="og:title" content="{{ SITE_NAME }}"/>
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
{% if query %}
<header class="archive-header">
{% if suggestion %}
<h2 class="archive-title">
已显示<span style="color: red"> “{{ suggestion }}” </span>的搜索结果。&nbsp;&nbsp;
仍然搜索:<a style="text-transform: none;" href="/search/?q={{ query }}&is_suggest=no">{{ query }}</a> <br>
</h2>
{% else %}
<h2 class="archive-title">
搜索:<span style="color: red">{{ query }} </span> &nbsp;&nbsp;
</h2>
{% endif %}
</header><!-- .archive-header -->
{% endif %}
{% if query and page.object_list %}
{% for article in page.object_list %}
{% load_article_detail article.object True user %}
{% endfor %}
{% if page.has_previous or page.has_next %}
<nav id="nav-below" class="navigation" role="navigation">
<h3 class="assistive-text">文章导航</h3>
{% if page.has_previous %}
<div class="nav-previous"><a
href="?q={{ query }}&amp;page={{ page.previous_page_number }}"><span
class="meta-nav">&larr;</span> 早期文章</a></div>
{% endif %}
{% if page.has_next %}
<div class="nav-next"><a href="?q={{ query }}&amp;page={{ page.next_page_number }}">较新文章
<span
class="meta-nav">→</span></a>
</div>
{% endif %}
</nav><!-- .navigation -->
{% endif %}
{% else %}
<header class="archive-header">
<h1 class="archive-title">哎呀,关键字:<span>{{ query }}</span>没有找到结果,要不换个词再试试?</h1>
</header><!-- .archive-header -->
{% endif %}
</div><!-- #content -->
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar request.user 'i' %}
{% endblock %}

@ -1,6 +0,0 @@
<aside id="text-2" class="widget widget_text"><h3 class="widget-title">Google AdSense</h3>
<div class="textwidget">
{{ GOOGLE_ADSENSE_CODES }}
</div>
</aside>

@ -1,157 +0,0 @@
{% load static %}
{% load cache %}
{% load i18n %}
{% load compress %}
<!DOCTYPE html>
<!--[if IE 7]>
<html class="ie ie7" lang="zh-CN"
prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<![endif]-->
<!--[if IE 8]>
<html class="ie ie8" lang="zh-CN"
prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<![endif]-->
<!--[if !(IE 7) & !(IE 8)]><!-->
<html lang="zh-CN" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<!--<![endif]-->
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="theme-color" content="#21759b"/>
{% load blog_tags %}
{% head_meta %}
{% block header %}
<!-- SEO插件会自动生成title、description、keywords等标签 -->
{% endblock %}
<link rel="profile" href="http://gmpg.org/xfn/11"/>
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//cdn.mathjax.org"/>
<link rel="dns-prefetch" href="//cdn.jsdelivr.net"/>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin/>
<!--[if lt IE 9]>
<script src="{% static 'blog/js/html5.js' %}" type="text/javascript"></script>
<![endif]-->
<!-- RSS和图标 -->
<link rel="alternate" type="application/rss+xml" title="{{ SITE_NAME }} &raquo; Feed" href="/feed"/>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
<link rel="icon" href="/favicon.ico" type="image/x-icon"/>
<link rel="apple-touch-icon" href="/favicon.ico"/>
<!-- 本地字体加载 -->
<link rel="stylesheet" href="{% static 'blog/fonts/open-sans.css' %}">
<!-- 新增:阅读进度条样式 -->
<style>
#reading-progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background-color: #007bff; /* 你可以更改进度条颜色 */
z-index: 9999; /* 确保在最上层 */
width: 0%;
transition: width 0.1s ease; /* 平滑过渡效果 */
}
</style>
{% compress css %}
<link rel='stylesheet' id='twentytwelve-style-css' href='{% static 'blog/css/style.css' %}' type='text/css'
media='all'/>
<link href="{% static 'blog/css/oauth_style.css' %}" rel="stylesheet">
{% comment %}<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>{% endcomment %}
<!--[if lt IE 9]>
<link rel='stylesheet' id='twentytwelve-ie-css' href='{% static 'blog/css/ie.css' %}' type='text/css' media='all' />
<![endif]-->
<link rel="stylesheet" href="{% static 'pygments/default.css' %}"/>
<link rel="stylesheet" href="{% static 'blog/css/nprogress.css' %}">
{% block compress_css %}
{% endblock %}
{% endcompress %}
{% if GLOBAL_HEADER %}
{{ GLOBAL_HEADER|safe }}
{% endif %}
</head>
<body class="home blog custom-font-enabled">
<!-- 新增:阅读进度条 DOM 元素 -->
<div id="reading-progress-bar"></div>
<div id="page" class="hfeed site">
<header id="masthead" class="site-header" role="banner">
<hgroup>
<h1 class="site-title"><a href="/" title="{{ SITE_NAME }}" rel="home">{{ SITE_NAME }}</a>
</h1>
<h2 class="site-description">{{ SITE_DESCRIPTION }}</h2>
</hgroup>
{% load i18n %}
{% include 'share_layout/nav.html' %}
</header><!-- #masthead -->
<div id="main" class="wrapper">
{% block content %}
{% endblock %}
{% block sidebar %}
{% endblock %}
</div><!-- #main .wrapper -->
{% include 'share_layout/footer.html' %}
</div><!-- #page -->
<!-- JavaScript资源 -->
{% compress js %}
<script src="{% static 'blog/js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'blog/js/nprogress.js' %}"></script>
<script src="{% static 'blog/js/blog.js' %}"></script>
<script src="{% static 'blog/js/navigation.js' %}"></script>
{% block compress_js %}
{% endblock %}
{% endcompress %}
<!-- MathJax智能加载器 -->
<script src="{% static 'blog/js/mathjax-loader.js' %}" async defer></script>
<!-- 新增:阅读进度条 JavaScript -->
<script>
(function($) {
$(document).ready(function() {
var $progressBar = $('#reading-progress-bar');
var $articleContent = $('#article-content'); // 假设文章内容容器的ID是 'article-content'
// 仅在文章详情页执行
if ($articleContent.length > 0) {
var articleTop = $articleContent.offset().top;
var articleHeight = $articleContent.outerHeight();
var windowHeight = $(window).height();
var scrollableDistance = articleHeight - windowHeight;
$(window).on('scroll', function() {
var scrollPosition = $(window).scrollTop();
// 计算阅读进度百分比
var scrollPercent = (scrollPosition - articleTop) / scrollableDistance;
// 确保百分比在 0 到 1 之间
scrollPercent = Math.max(0, Math.min(1, scrollPercent));
// 更新进度条宽度
$progressBar.css('width', (scrollPercent * 100) + '%');
});
// 初始化时触发一次滚动事件,以设置初始状态
$(window).trigger('scroll');
}
});
})(jQuery);
</script>
{% block footer %}
{% endblock %}
</body>
</html>

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html>
<head>
{% load static %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="../../favicon.ico">
<meta name="robots" content="noindex">
<title>{{ SITE_NAME }} | {{ SITE_DESCRIPTION }}</title>
<link href="{% static 'account/css/account.css' %}" rel="stylesheet">
{% load compress %}
{% compress css %}
<!-- Bootstrap core CSS -->
<link href="{% static 'assets/css/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'blog/css/oauth_style.css' %}" rel="stylesheet">
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<link href="{% static 'assets/css/ie10-viewport-bug-workaround.css' %}" rel="stylesheet">
<!-- TODC Bootstrap core CSS -->
<link href="{% static 'assets/css/todc-bootstrap.min.css' %}" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="{% static 'assets/css/signin.css' %}" rel="stylesheet">
{% endcompress %}
{% compress js %}
<script src="{% static 'assets/js/ie10-viewport-bug-workaround.js' %}"></script>
<script src="{% static 'assets/js/ie-emulation-modes-warning.js' %}"></script>
{% endcompress %}
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
{% block content %}
{% endblock %}
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
</body>
<script type="text/javascript" src="{% static 'blog/js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'account/js/account.js' %}" type="text/javascript"></script>
</html>

@ -1,56 +0,0 @@
<footer id="colophon" role="contentinfo">
<div class="site-info" style="text-align: center">
Copyright&copy;&nbsp;{{ CURRENT_YEAR }}&nbsp;
<a href="/" target="blank">{{ SITE_NAME }}</a>
&nbsp;|&nbsp;
<a href="/sitemap.xml" title="SiteMap" target="_blank">
SiteMap
</a>
&nbsp;|&nbsp;
<a href="/feed" title="RSS Feed" target="_blank">
RSS Feed
</a>
&nbsp;|&nbsp;
<a href="/links.html" title="友情链接" rel="nofollow" target="_blank">
友情链接
</a>
|&nbsp; Hosting On&nbsp;
<a href="https://www.linode.com/?r=b0d38794d05ef8816b357a929106e89b7c6452f9" target="blank" rel="nofollow">Linode</a>
|&nbsp;
<a href="https://tongji.baidu.com/sc-web/3478620/home/ico?siteId=11261596" target="_blank"
rel="nofollow">百度统计</a>
</div>
<div class="site-info" style="text-align: center">
Powered by
<a href="https://www.djangoproject.com/" rel="nofollow" target="blank">Django</a>
&nbsp;|&nbsp;
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow" target="blank">liangliangyy</a>
|
<a href="https://www.lylinux.net" target="blank">lylinux</a>
|
本页面加载耗时:<!!LOAD_TIMES!!>s
</div>
{% if BEIAN_CODE %}
<div class="site-info" style="text-align: center">
<a href="https://beian.miit.gov.cn/" rel="nofollow" target="_blank">
<p style=" height:20px;line-height:20px;margin: 0px 0px 0px 5px; color:#939393;">
{{ BEIAN_CODE }}
</p>
</a>
{% if BEIAN_CODE_GONGAN and SHOW_GONGAN_CODE %}
{{ BEIAN_CODE_GONGAN |safe }}
{% endif %}
</div>
{% endif %}
{% if ANALYTICS_CODE %}
{{ ANALYTICS_CODE| safe }}
{% endif %}
{% if GLOBAL_FOOTER %}
{{ GLOBAL_FOOTER|safe }}
{% endif %}
</footer><!-- #colophon -->

@ -1,131 +0,0 @@
{% load i18n %}
{% load blog_tags %} <!-- 确保 blog_tags 已加载 -->
<nav id="site-navigation" class="main-navigation" role="navigation">
<div class="menu-%e8%8f%9c%e5%8d%95-container">
<ul id="menu-%e8%8f%9c%e5%8d%95" class="nav-menu">
<li id="menu-item-3498"
class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-3498">
<a href="/">{% trans 'index' %}</a></li>
{% query nav_category_list parent_category=None as root_categorys %}
{% for node in root_categorys %}
{% include 'share_layout/nav_node.html' %}
{% endfor %}
{% if nav_pages %}
{% for node in nav_pages %}
<li id="menu-item-{{ node.pk }}"
class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-has-children menu-item-{{ node.pk }}">
<a href="{{ node.get_absolute_url }}">{{ node.title }}</a>
</li>
{% endfor %}
{% endif %}
<li class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-has-children">
<a href="{% url "blog:archives" %}">{% trans 'Article archive' %}</a>
</li>
<!-- 新增:用户菜单 -->
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-has-children user-menu">
{% if user.is_authenticated %}
<a href="javascript:void(0);">
{% if user.profile.avatar %}
<img src="{{ user.profile.avatar.url }}" alt="{{ user.username }}" class="avatar">
{% else %}
<i class="fas fa-user"></i>
{% endif %}
{{ user.username }}
</a>
<ul class="sub-menu">
<li class="menu-item">
<a href="{% url 'blog:user_profile' username=user.username %}">
<i class="fas fa-id-card"></i> {% trans 'My Profile' %}
</a>
</li>
<li class="menu-item">
<a href="{% url 'blog:user_profile_update' %}">
<i class="fas fa-edit"></i> {% trans 'Edit Profile' %}
</a>
</li>
<li class="menu-item">
<a href="{% url 'account:logout' %}"> <!-- 假设你的登出URL名称是 account:logout -->
<i class="fas fa-sign-out-alt"></i> {% trans 'Logout' %}
</a>
</li>
</ul>
{% else %}
<a href="{% url 'account:login' %}"> <!-- 假设你的登录URL名称是 account:login -->
<i class="fas fa-sign-in-alt"></i> {% trans 'Login / Register' %}
</a>
{% endif %}
</li>
<!-- 用户菜单结束 -->
</ul>
</div>
</nav><!-- #site-navigation -->
<!-- 新增:引入 Font Awesome 用于显示图标 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
<!-- 新增:一些简单的 CSS 来美化用户菜单 -->
<style>
/* 让用户菜单靠右浮动 */
.nav-menu .user-menu {
float: right;
position: relative;
}
/* 美化头像 */
.nav-menu .user-menu .avatar {
width: 24px;
height: 24px;
border-radius: 50%;
vertical-align: middle;
margin-right: 8px;
}
/* 子菜单样式 */
.nav-menu .user-menu .sub-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
background: #fff;
min-width: 180px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 10px 0;
z-index: 1000;
border-radius: 4px;
}
.nav-menu .user-menu:hover .sub-menu {
display: block;
}
.nav-menu .user-menu .sub-menu li {
padding: 0;
margin: 0;
list-style: none;
}
.nav-menu .user-menu .sub-menu a {
display: block;
padding: 10px 20px;
color: #333;
text-decoration: none;
white-space: nowrap;
}
.nav-menu .user-menu .sub-menu a:hover {
background-color: #f5f5f5;
color: #007bff;
}
.nav-menu .user-menu .sub-menu i {
width: 20px; /* 图标宽度固定,使文字对齐 */
text-align: center;
margin-right: 8px;
}
</style>

@ -1,19 +0,0 @@
<li id="menu-item-{{ node.pk }}"
class="menu-item menu-item-type-taxonomy menu-item-object-category menu-item-has-children menu-item-{{ node.pk }}">
<a href="{{ node.get_absolute_url }}">{{ node.name }}</a>
{% load blog_tags %}
{% query nav_category_list parent_category=node as child_categorys %}
{% if child_categorys %}
<ul class="sub-menu">
{% for child in child_categorys %}
{% with node=child template_name="share_layout/nav_node.html" %}
{% include template_name %}
{% endwith %}
{% endfor %}
</ul>
{% endif %}
</li>
Loading…
Cancel
Save