Compare commits

..

No commits in common. 'wy_branch' and 'master' have entirely different histories.

Binary file not shown.

@ -0,0 +1,521 @@
/* ==========================================================================
========================================================================== */
.breadcrumb
div {
display: inline;
font-size: 13px;
margin-left: -3px;
}
/* ==========================================================================
========================================================================== */
/* 回到顶部按钮容器定位 */
#wp-auto-top {
position: fixed;
top: 45%;
right: 50%;
display: block;
margin-right: -540px;
z-index: 9999;
}
/* 回到顶部按钮通用样式 */
#wp-auto-top-top, #wp-auto-top-comment, #wp-auto-top-bottom {
background: url(https://www.lylinux.org/wp-content/plugins/wp-auto-top/img/1.png) no-repeat;
position: relative;
cursor: pointer;
height: 25px;
width: 29px;
margin: 10px 0 0;
}
/* 评论按钮特定样式 */
#wp-auto-top-comment {
background-position: left -30px;
height: 32px;
}
/* 底部按钮特定样式 */
#wp-auto-top-bottom {
background-position: left -68px;
}
/* 按钮悬停效果 */
#wp-auto-top-comment:hover {
background-position: right -30px;
}
#wp-auto-top-top:hover {
background-position: right 0;
}
#wp-auto-top-bottom:hover {
background-position: right -68px;
}
/* ==========================================================================
========================================================================== */
.widget-login {
margin-top: 15px !important;
}
/* ==========================================================================
========================================================================== */
/* 评论区域顶部间距 */
#comments {
margin-top: 20px;
}
/* 隐藏pingback列表容器 */
#pinglist-container {
display: none;
}
/* 评论标签页样式 */
.comment-tabs {
margin-bottom: 20px;
font-size: 15px;
border-bottom: 2px solid #e5e5e5;
}
.comment-tabs li {
float: left;
margin-bottom: -2px;
}
.comment-tabs li a {
display: block;
padding: 0 10px 10px;
font-weight: 600;
color: #aaa;
border-bottom: 2px solid #e5e5e5;
}
.comment-tabs li a:hover {
color: #444;
border-color: #ccc;
}
.comment-tabs li span {
margin-left: 8px;
padding: 0 6px;
border-radius: 4px;
background-color: #e5e5e5;
}
.comment-tabs li i {
margin-right: 6px;
}
.comment-tabs li.active a {
color: #e8554e;
border-bottom-color: #e8554e;
}
/* 评论列表样式 */
.commentlist, .pinglist {
margin-bottom: 20px;
}
.commentlist li, .pinglist li {
padding-left: 60px;
font-size: 14px;
line-height: 22px;
font-weight: 400;
}
.commentlist .comment-body, .pinglist li {
position: relative;
padding-bottom: 20px;
clear: both;
word-break: break-all;
}
.commentlist .comment-author,
.commentlist .comment-meta,
.commentlist .comment-awaiting-moderation {
float: left;
display: block;
font-size: 13px;
line-height: 22px;
}
.commentlist .comment-author {
margin-right: 6px;
}
.commentlist .fn, .pinglist .ping-link {
color: #444;
font-size: 13px;
font-style: normal;
font-weight: 600;
}
.commentlist .says {
display: none;
}
.commentlist .avatar {
position: absolute;
left: -60px;
top: 0;
width: 48px;
height: 48px;
border-radius: 100%;
}
.commentlist .comment-meta:before, .pinglist .ping-meta:before {
vertical-align: 4%;
margin-right: 3px;
font-size: 10px;
font-family: FontAwesome;
color: #ccc;
}
.commentlist .comment-meta a, .pinglist .ping-meta {
color: #aaa;
}
.commentlist .reply {
font-size: 13px;
line-height: 16px;
}
.commentlist .reply a,
.commentlist .comment-reply-chain {
color: #aaa;
}
.commentlist .reply a:hover,
.commentlist .comment-reply-chain:hover {
color: #444;
}
.comment-awaiting-moderation {
color: #e8554e;
font-style: normal;
}
/* pingback 列表样式 */
.pinglist li {
padding-left: 0;
}
/* 评论文本样式 */
.commentlist .comment-body p {
margin-bottom: 8px;
color: #777;
clear: both;
}
.commentlist .comment-body strong {
font-weight: 600;
}
.commentlist .comment-body ol li {
margin-left: 2em;
padding: 0;
list-style: decimal;
}
.commentlist .comment-body ul li {
margin-left: 2em;
padding: 0;
list-style: square;
}
/* 文章作者和管理员评论样式 */
.commentlist li.bypostauthor > .comment-body:after,
.commentlist li.comment-author-admin > .comment-body:after {
display: block;
position: absolute;
content: "\f040";
width: 12px;
line-height: 12px;
font-style: normal;
font-family: FontAwesome;
text-align: center;
color: #fff;
background-color: #e8554e;
}
.commentlist li.comment-author-admin > .comment-body:after {
content: "\f005"; /* 管理员使用星形图标 */
}
.commentlist li.bypostauthor > .comment-body:after,
.commentlist li.comment-author-admin > .comment-body:after {
padding: 3px;
top: 32px;
left: -28px;
font-size: 12px;
border-radius: 100%;
}
.commentlist li li.bypostauthor > .comment-body:after,
.commentlist li li.comment-author-admin > .comment-body:after {
padding: 2px;
top: 22px;
left: -26px;
font-size: 10px;
border-radius: 100%;
}
/* 子评论样式 */
.commentlist li ul {
}
.commentlist li li {
margin: 0;
padding-left: 48px;
}
.commentlist li li .avatar {
top: 0;
left: -48px;
width: 36px;
height: 36px;
}
.commentlist li li .comment-meta {
left: 70px;
}
/* 评论导航样式 */
.comments-nav {
margin-bottom: 20px;
}
.comments-nav a {
font-weight: 600;
}
.comments-nav .nav-previous {
float: left;
}
.comments-nav .nav-next {
float: right;
}
/* 评论表单样式 */
.logged-in-as,
.comment-notes,
.form-allowed-tags {
display: none;
}
/* 设置评论容器相对定位 */
#respond {
position: relative;
}
/* 回复标题的默认下边距 */
#reply-title {
margin-bottom: 20px;
}
/* 针对列表项中的回复标题进行特殊处理:隐藏并重置尺寸 */
li #reply-title {
margin: 0 !important;
padding: 0;
height: 0;
font-size: 0;
border-top: 0;
}
/* 取消回复链接的基本样式设置 */
#cancel-comment-reply-link {
float: right;
bottom: 26px;
right: 20px;
font-size: 12px;
color: #999;
}
/* 取消回复链接悬停时的颜色变化 */
#cancel-comment-reply-link:hover {
color: #777;
}
/* 评论表单整体样式 */
#commentform {
margin-bottom: 20px;
padding: 10px 20px 20px;
border-radius: 4px;
background-color: #e5e5e5;
}
/* 表单作者字段左浮动占宽48% */
#commentform p.comment-form-author {
float: left;
width: 48%;
}
/* 表单邮箱字段右浮动占宽48% */
#commentform p.comment-form-email {
float: right;
width: 48%;
}
/* URL 和评论正文字段清除浮动并独占一行 */
#commentform p.comment-form-url,
#commentform p.comment-form-comment {
clear: both;
}
/* 表单标签统一显示为块级元素,并设置上下内边距与字体加粗 */
#commentform label {
display: block;
padding: 6px 0;
font-weight: 600;
}
/* 输入框和文本域最大宽度限制为父容器100%,且默认撑满 */
#commentform input[type="text"],
#commentform textarea {
max-width: 100%;
width: 100%;
}
/* 文本域高度固定为100像素 */
#commentform textarea {
height: 100px;
}
/* 提交按钮段落上外边距调整 */
#commentform p.form-submit {
margin-top: 10px;
}
/* 登录状态下回复标题保持标准间距 */
.logged-in #reply-title {
margin-bottom: 20px;
}
/* 登录状态下的评论正文字段增加顶部间距 */
.logged-in #commentform p.comment-form-comment {
margin-top: 10px;
}
/* 登录状态下评论正文标签隐藏 */
.logged-in #commentform p.comment-form-comment label {
display: none;
}
/* 统一标题类(包括回复标题)的基础样式 */
.heading,
#reply-title {
margin-bottom: 1em;
font-size: 18px;
font-weight: 600;
text-transform: uppercase;
color: #222;
}
/* 标题图标样式设置 */
.heading i {
margin-right: 6px;
font-size: 22px;
}
/* 清除浮动伪类 before */
.group:before {
content: "";
display: table;
}
/* 清除浮动伪类 after */
.group:after {
content: "";
display: table;
clear: both;
}
/* 取消评论按钮基础样式重置 */
.cancel-comment {
margin: 0;
padding: 0;
border: 0;
font: inherit;
vertical-align: baseline;
}
/* 返回顶部火箭图标的初始位置及基本属性 */
#rocket {
position: fixed;
right: 50px;
bottom: 50px;
display: block;
visibility: hidden;
width: 26px;
height: 48px;
background: url("data:image/png;base64,...") no-repeat 50% 0;
cursor: pointer;
-webkit-transition: all 0s;
transition: all 0s;
}
/* 火箭图标鼠标悬停时背景图片偏移以切换状态 */
#rocket:hover {
background-position: 50% -62px;
}
/* 显示火箭图标 */
#rocket.show {
visibility: visible;
opacity: 1;
}
/* 触发动画时火箭图标的状态更新与动画绑定 */
#rocket.move {
background-position: 50% -62px;
-webkit-animation: toTop .8s ease-in;
animation: toTop .8s ease-in;
animation-fill-mode: forwards;
-webkit-animation-fill-mode: forwards;
}
/* Markdown提示文字样式设置 */
.comment-markdown {
float: right;
font-size: small;
}
/* 面包屑导航容器样式 */
.breadcrumb {
margin-bottom: 20px;
list-style: none;
border-radius: 4px;
}
/* 面包屑导航项横向排列 */
.breadcrumb > li {
display: inline-block;
}
/* 面包屑导航项之间的分隔符 */
.breadcrumb > li + li:before {
color: #ccc;
content: "/\00a0";
}
/* 当前激活的面包屑导航项颜色 */
.breadcrumb > .active {
color: #777;
}
/* 分割线样式(当前被注释掉) */
.break_line {
height: 1px;
border: none;
/*border-top: 1px dashed #f5d6d6;*/
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,82 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
"""
自定义用户创建表单用于在Admin后台添加新用户
继承自ModelForm提供密码验证和哈希处理功能
"""
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser # 指定使用的模型
fields = ('email',) # 表单中显示的字段,这里只显示邮箱
def clean_password2(self):
"""
验证两次输入的密码是否一致
如果密码不匹配抛出验证错误
"""
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):
"""
保存用户信息对密码进行哈希处理
commit参数控制是否立即保存到数据库
"""
user = super().save(commit=False) # 创建用户对象但不保存到数据库
user.set_password(self.cleaned_data["password1"]) # 对密码进行哈希处理
if commit:
user.source = 'adminsite' # 标记用户来源为管理员后台
user.save() # 保存到数据库
return user
class BlogUserChangeForm(UserChangeForm):
"""
自定义用户信息修改表单
继承自Django内置的UserChangeForm用于在Admin后台编辑用户信息
"""
class Meta:
model = BlogUser
fields = '__all__' # 包含所有字段
field_classes = {'username': UsernameField} # 指定用户名字段的类型
def __init__(self, *args, **kwargs):
"""初始化方法,可以在这里添加自定义的表单逻辑"""
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
"""
自定义用户管理类配置Admin后台的用户管理界面
继承自Django内置的UserAdmin类
"""
form = BlogUserChangeForm # 设置用户编辑表单
add_form = BlogUserCreationForm # 设置用户添加表单
# 列表页面显示的字段
list_display = (
'id',
'nickname', # 昵称
'username', # 用户名
'email', # 邮箱
'last_login', # 最后登录时间
'date_joined', # 注册时间
'source' # 用户来源
)
list_display_links = ('id', 'username') # 可点击跳转到编辑页面的字段
ordering = ('-id',) # 按ID倒序排列最新的用户显示在最前面

@ -0,0 +1,28 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
"""
账户应用的配置类
Django应用配置类用于配置accounts应用的元数据和行为
继承自Django的AppConfig基类
"""
# 应用的Python路径Django使用这个属性来识别应用
# 这应该与应用的目录名一致
name = 'accounts'
# 其他常用但未在此定义的配置选项包括:
# - verbose_name: 应用的易读名称(用于管理后台显示)
# - default_auto_field: 默认的主键字段类型
# - label: 应用的简短标签用于替代name
# - path: 应用的文件系统路径
# 示例如果需要配置verbose_name可以这样添加
# verbose_name = '用户账户管理'
# 示例如果需要自定义ready方法可以这样添加
# def ready(self):
# # 应用启动时执行的代码
# # 通常用于信号注册等初始化操作
# import accounts.signals

@ -0,0 +1,147 @@
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
"""自定义登录表单继承自Django的AuthenticationForm"""
def __init__(self, *args, **kwargs):
"""初始化方法设置表单字段的widget属性"""
super(LoginForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入框属性
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 设置密码字段的输入框属性
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
"""自定义用户注册表单继承自Django的UserCreationForm"""
def __init__(self, *args, **kwargs):
"""初始化方法设置所有表单字段的widget属性"""
super(RegisterForm, self).__init__(*args, **kwargs)
# 设置用户名字段的输入框属性
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 设置邮箱字段的输入框属性
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
# 设置密码字段的输入框属性
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# 设置确认密码字段的输入框属性
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
"""邮箱字段验证方法"""
email = self.cleaned_data['email']
# 检查邮箱是否已存在
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
"""表单的元数据配置"""
model = get_user_model() # 使用当前激活的用户模型
fields = ("username", "email") # 表单包含的字段
class ForgetPasswordForm(forms.Form):
"""忘记密码重置表单"""
# 新密码字段
new_password1 = forms.CharField(
label=_("New password"), # 字段标签
widget=forms.PasswordInput( # 密码输入框
attrs={
"class": "form-control", # CSS类
'placeholder': _("New password") # 占位符文本
}
),
)
# 确认新密码字段
new_password2 = forms.CharField(
label="确认密码", # 字段标签
widget=forms.PasswordInput( # 密码输入框
attrs={
"class": "form-control", # CSS类
'placeholder': _("Confirm password") # 占位符文本
}
),
)
# 邮箱字段
email = forms.EmailField(
label='邮箱', # 字段标签
widget=forms.TextInput( # 文本输入框
attrs={
'class': 'form-control', # CSS类
'placeholder': _("Email") # 占位符文本
}
),
)
# 验证码字段
code = forms.CharField(
label=_('Code'), # 字段标签
widget=forms.TextInput( # 文本输入框
attrs={
'class': 'form-control', # CSS类
'placeholder': _("Code") # 占位符文本
}
),
)
def clean_new_password2(self):
"""确认密码字段验证方法"""
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
# 检查两次输入的密码是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
# 使用Django的密码验证器验证密码强度
password_validation.validate_password(password2)
return password2
def clean_email(self):
"""邮箱字段验证方法"""
user_email = self.cleaned_data.get("email")
# 检查邮箱对应的用户是否存在
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
return user_email
def clean_code(self):
"""验证码字段验证方法"""
code = self.cleaned_data.get("code")
# 使用utils模块验证邮箱和验证码是否匹配
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
"""忘记密码验证码请求表单(仅包含邮箱字段)"""
email = forms.EmailField(
label=_('Email'), # 邮箱字段标签
)

@ -0,0 +1,95 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
"""
Django数据库迁移文件
用于创建BlogUser模型的数据库表结构
这是一个初始迁移文件initial migration
"""
# 标记为初始迁移Django使用这个标志来识别应用的第一个迁移
initial = True
# 依赖关系此迁移依赖于auth应用的特定迁移
# 确保在创建用户表之前,权限相关的表已经存在
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
# 迁移操作列表:定义要执行的具体数据库操作
operations = [
# 创建BlogUser模型的数据库表
migrations.CreateModel(
name='BlogUser', # 模型名称
fields=[
# 主键字段使用BigAutoField作为自增主键
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 密码字段存储加密后的密码最大长度128字符
('password', models.CharField(max_length=128, verbose_name='password')),
# 最后登录时间:记录用户最后一次登录的时间
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
# 超级用户标志:标记用户是否拥有所有权限
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
# 用户名字段:唯一标识用户,有严格的字符限制和验证
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
# 名字字段:用户的名,可选
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
# 姓氏字段:用户的姓,可选
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
# 邮箱字段:用户的邮箱地址,可选
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
# 员工状态:标记用户是否可以登录管理后台
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
# 活跃状态:标记用户账户是否激活
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
# 加入日期:用户注册的时间,默认为当前时间
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
# 昵称字段:自定义字段,用户显示名称,可选
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
# 创建时间:自定义字段,记录用户创建时间
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 最后修改时间:自定义字段,记录用户信息最后修改时间
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
# 来源字段:自定义字段,记录用户创建来源(如注册渠道)
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
# 组关联:多对多关系,用户所属的权限组
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
# 用户权限:多对多关系,用户特有的权限
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
# 模型元选项
options={
'verbose_name': '用户', # 单数名称(用于管理后台)
'verbose_name_plural': '用户', # 复数名称(用于管理后台)
'ordering': ['-id'], # 默认排序按ID降序最新的在前
'get_latest_by': 'id', # 指定获取最新记录时使用的字段
},
# 模型管理器
managers=[
# 使用Django默认的UserManager来管理用户对象
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -0,0 +1,86 @@
# 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):
"""
Django数据库迁移文件
用于修改BlogUser模型的结构和字段定义
这是一个数据模型重构迁移主要更新字段命名和国际化
"""
# 依赖关系此迁移依赖于accounts应用的初始迁移
# 确保在修改表结构之前,初始表已经创建
dependencies = [
('accounts', '0001_initial'), # 依赖于accounts应用的第一个迁移文件
]
# 迁移操作列表:定义要执行的具体数据库结构修改
operations = [
# 修改模型的元选项(主要是国际化显示名称)
migrations.AlterModelOptions(
name='bloguser', # 目标模型名称
options={
'get_latest_by': 'id', # 保持按id获取最新记录
'ordering': ['-id'], # 保持按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, # 保持最大长度100
verbose_name='nick name' # 更新为英文显示名称
),
),
# 修改来源字段的显示名称(国际化)
migrations.AlterField(
model_name='bloguser', # 目标模型
name='source', # 要修改的字段
field=models.CharField(
blank=True, # 保持允许为空
max_length=100, # 保持最大长度100
verbose_name='create source' # 更新为英文显示名称
),
),
]

@ -0,0 +1,69 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
"""
自定义用户模型继承自Django的AbstractUser基类
扩展了博客系统的用户功能
"""
# 昵称字段,允许为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 用户创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 用户创建来源(如:网站注册、第三方登录等),允许为空
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
"""
获取用户的绝对URL用于Django的通用视图和模板中
返回用户详情页的URL
"""
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
"""
定义模型的字符串表示形式
在管理后台和其他显示对象的地方使用
这里使用邮箱作为标识
"""
return self.email
def get_full_url(self):
"""
获取用户的完整URL包含域名
用于生成完整的用户主页链接
"""
site = get_current_site().domain # 获取当前站点域名
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
"""模型的元数据配置"""
# 默认按ID降序排列最新的用户排在前面
ordering = ['-id']
# 在管理后台中显示的单数名称
verbose_name = _('user')
# 在管理后台中显示的复数名称
verbose_name_plural = verbose_name
# 指定获取最新记录时使用的字段
get_latest_by = 'id'

@ -0,0 +1,249 @@
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
class AccountTest(TestCase):
def setUp(self):
"""测试用例初始化方法,每个测试方法执行前都会运行"""
self.client = Client() # 创建测试客户端用于模拟HTTP请求
self.factory = RequestFactory() # 创建请求工厂,用于构建请求对象
# 创建测试用户
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" # 测试用的新密码
def test_validate_account(self):
"""测试账户验证功能,包括登录、管理员权限和文章管理"""
site = get_current_site().domain # 获取当前站点域名
# 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
# 测试登录功能
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) # 断言登录成功
# 测试管理员页面访问
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200) # 断言可以访问管理员页面
# 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a' # 文章类型
article.status = 'p' # 发布状态
article.save()
# 测试文章管理页面访问
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) # 断言可以访问文章管理页面
def test_validate_register(self):
"""测试用户注册流程,包括注册、邮箱验证、登录和权限管理"""
# 验证注册前用户不存在
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
# 发送注册请求
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
# 验证注册后用户已创建
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 生成邮箱验证签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
# 测试验证页面访问
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# 测试登录功能
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
# 提升用户权限为管理员
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache() # 清理侧边栏缓存
# 创建测试分类
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
# 创建测试文章
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
# 测试文章管理页面访问
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试登出功能
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) # 登出通常会有重定向
# 登出后测试文章管理页面访问(应该被拒绝)
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200]) # 应该重定向到登录页
# 重新登录测试(使用错误密码)
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123' # 错误的密码
})
self.assertIn(response.status_code, [301, 302, 200])
# 登录后再次测试文章管理页面访问
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
"""测试邮箱验证码功能"""
to_email = "admin@admin.com"
code = generate_code() # 生成验证码
# 设置验证码到缓存
utils.set_code(to_email, code)
# 发送验证邮件
utils.send_verify_email(to_email, code)
# 测试正确的验证码验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) # 应该没有错误
# 测试错误的邮箱验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) # 应该返回错误信息字符串
def test_forget_password_email_code_success(self):
"""测试成功发送忘记密码验证码"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok") # 断言返回成功消息
def test_forget_password_email_code_fail(self):
"""测试忘记密码验证码发送失败的情况"""
# 测试空邮箱参数
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
# 测试无效邮箱格式
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com") # 无效的邮箱格式
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
"""测试成功重置密码"""
code = generate_code()
utils.set_code(self.blog_user.email, code) # 设置验证码到缓存
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
# 提交密码重置请求
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302) # 重置成功应该重定向
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None) # 用户应该存在
self.assertEqual(blog_user.check_password(data["new_password1"]), True) # 密码应该匹配
def test_forget_password_email_not_user(self):
"""测试重置密码时用户不存在的情况"""
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com", # 不存在的邮箱
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) # 应该返回错误页面而不是重定向
def test_forget_password_email_code_error(self):
"""测试重置密码时验证码错误的情况"""
code = generate_code()
utils.set_code(self.blog_user.email, code) # 设置正确的验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", # 错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) # 应该返回错误页面而不是重定向

@ -0,0 +1,49 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
# 定义应用的命名空间用于URL反向解析
# 在模板中使用如:{% url 'accounts:login' %}
app_name = "accounts"
# URL配置列表定义所有用户账户相关的路由
urlpatterns = [
# 登录路由 - 使用正则表达式匹配以login/结尾的URL
re_path(r'^login/$',
# 使用基于类的视图,登录成功后重定向到首页
views.LoginView.as_view(success_url='/'),
name='login', # URL名称用于反向解析
kwargs={'authentication_form': LoginForm}), # 传递自定义登录表单类
# 注册路由 - 使用正则表达式匹配以register/结尾的URL
re_path(r'^register/$',
# 注册视图,注册成功后重定向到首页
views.RegisterView.as_view(success_url="/"),
name='register'), # URL名称
# 登出路由 - 使用正则表达式匹配以logout/结尾的URL
re_path(r'^logout/$',
# 登出视图,处理用户退出登录
views.LogoutView.as_view(),
name='logout'), # URL名称
# 账户操作结果页面 - 使用path匹配精确路径
path(r'account/result.html',
# 使用函数视图显示账户操作结果(如注册成功、密码重置成功等)
views.account_result,
name='result'), # URL名称
# 忘记密码页面 - 使用正则表达式匹配以forget_password/结尾的URL
re_path(r'^forget_password/$',
# 忘记密码视图,显示密码重置页面
views.ForgetPasswordView.as_view(),
name='forget_password'), # URL名称
# 忘记密码验证码接口 - 使用正则表达式匹配以forget_password_code/结尾的URL
re_path(r'^forget_password_code/$',
# 处理忘记密码的邮箱验证码发送和验证
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'), # URL名称
]

@ -0,0 +1,61 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
自定义认证后端允许用户使用用户名或邮箱登录
Extends ModelBackend to allow authentication using either username or email.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
用户认证方法
Authenticate a user based on username/email and password.
Args:
request: HTTP请求对象
username: 用户输入的用户名或邮箱
password: 用户输入的密码
**kwargs: 其他参数
Returns:
User: 认证成功的用户对象
None: 认证失败
"""
# 判断输入的是邮箱还是用户名
if '@' in username:
# 如果包含@符号,按邮箱处理
kwargs = {'email': username}
else:
# 否则按用户名处理
kwargs = {'username': username}
try:
# 根据用户名或邮箱查找用户
user = get_user_model().objects.get(**kwargs)
# 验证密码是否正确
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
# 用户不存在时返回None
return None
def get_user(self, user_id):
"""
根据用户ID获取用户对象
Get a user by their primary key.
Args:
user_id: 用户ID
Returns:
User: 用户对象
None: 用户不存在
"""
try:
# 通过主键查找用户
return get_user_model().objects.get(pk=user_id)
except get_user_model().DoesNotExist:
# 用户不存在时返回None
return None

@ -0,0 +1,64 @@
import typing
from datetime import timedelta
from django.core.cache import cache
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
# 验证码的生存时间Time To Live设置为5分钟
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送验证邮件
Args:
to_mail: 接收邮箱地址
subject: 邮件主题默认为"Verify Email"
code: 需要发送的验证码
"""
# 生成邮件HTML内容包含验证码信息并支持国际化
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
# 调用邮件发送工具函数发送邮件
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证邮箱和验证码是否匹配
Args:
email: 需要验证的邮箱地址
code: 用户输入的验证码
Return:
如果验证失败返回错误信息字符串验证成功返回None
Note:
当前错误处理方式不够合理建议改为抛出异常的方式
这样调用方可以通过try-except来处理错误而不是检查返回值
"""
# 从缓存中获取该邮箱对应的验证码
cache_code = get_code(email)
# 比较缓存中的验证码和用户输入的验证码
if cache_code != code:
return gettext("Verification code error")
# 验证成功返回None
def set_code(email: str, code: str):
"""将验证码存储到缓存中
Args:
email: 作为缓存键的邮箱地址
code: 需要存储的验证码
"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""从缓存中获取验证码
Args:
email: 作为缓存键的邮箱地址
Return:
返回缓存中的验证码如果不存在或已过期则返回None
"""
return cache.get(email)

@ -0,0 +1,283 @@
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
logger = logging.getLogger(__name__)
class RegisterView(FormView):
"""
用户注册视图
处理用户注册流程包括表单验证用户创建和发送验证邮件
"""
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
"""确保视图受到CSRF保护"""
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
"""
处理有效的注册表单
创建非活跃用户发送邮箱验证邮件
"""
if form.is_valid():
# 创建用户但不立即保存到数据库
user = form.save(False)
user.is_active = False # 邮箱验证前用户不可用
user.source = 'Register' # 标记用户来源
user.save(True) # 保存用户到数据库
# 获取当前站点信息
site = get_current_site().domain
# 生成验证签名,用于验证链接的安全性
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# 调试模式下使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
# 构建验证URL
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
# 构建邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
# 重定向到结果页面
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# 表单无效,重新渲染表单页面
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
"""
用户登出视图
处理用户登出操作并清理相关缓存
"""
url = '/login/' # 登出后重定向的URL
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
"""确保登出页面不被缓存"""
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""处理GET请求的登出操作"""
logout(request) # 执行登出操作
delete_sidebar_cache() # 清理侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
"""
用户登录视图
处理用户认证和登录会话管理
"""
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/' # 登录成功后默认重定向的URL
redirect_field_name = REDIRECT_FIELD_NAME # 重定向字段名
login_ttl = 2626560 # 一个月的时间(秒),用于"记住我"功能
@method_decorator(sensitive_post_parameters('password')) # 保护密码参数
@method_decorator(csrf_protect) # CSRF保护
@method_decorator(never_cache) # 禁止缓存
def dispatch(self, request, *args, **kwargs):
"""应用装饰器到视图分发方法"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""向模板上下文添加重定向URL"""
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/' # 默认重定向到首页
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""处理有效的登录表单"""
# 使用Django的AuthenticationForm进行认证
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
# 认证成功,清理缓存并记录日志
delete_sidebar_cache()
logger.info(self.redirect_field_name)
# 登录用户
auth.login(self.request, form.get_user())
# 处理"记住我"功能
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
else:
# 认证失败,重新显示表单
return self.render_to_response({
'form': form
})
def get_success_url(self):
"""获取登录成功后重定向的URL"""
redirect_to = self.request.POST.get(self.redirect_field_name)
# 验证重定向URL的安全性
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url # 不安全的URL使用默认URL
return redirect_to
def account_result(request):
"""
账户操作结果页面
处理注册结果和邮箱验证
"""
type = request.GET.get('type') # 操作类型register或validation
id = request.GET.get('id') # 用户ID
# 获取用户对象如果不存在返回404
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
# 如果用户已激活,直接重定向到首页
if user.is_active:
return HttpResponseRedirect('/')
# 处理注册和验证操作
if type and type in ['register', 'validation']:
if type == 'register':
# 注册成功页面
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# 邮箱验证处理
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
# 验证签名安全性
if sign != c_sign:
return HttpResponseForbidden() # 签名不匹配,禁止访问
# 激活用户账户
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
# 无效的操作类型,重定向到首页
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
"""
忘记密码视图
处理密码重置请求
"""
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
"""处理有效的密码重置表单"""
if form.is_valid():
# 根据邮箱查找用户
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# 使用Django的密码哈希器设置新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save() # 保存新密码
# 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
# 表单无效,重新显示表单
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
"""
发送忘记密码验证码视图
处理密码重置验证码的发送
"""
def post(self, request: HttpRequest):
"""处理POST请求发送密码重置验证码"""
form = ForgetPasswordCodeForm(request.POST)
# 验证表单数据
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
# 生成并发送验证码
code = generate_code()
utils.send_verify_email(to_email, code) # 发送验证邮件
utils.set_code(to_email, code) # 存储验证码(通常在缓存中)
return HttpResponse("ok") # 返回成功响应

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = 'blog'
/*备注*/

@ -0,0 +1,344 @@
import hashlib
import logging
import random
import urllib
from django import template
from django.conf import settings
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.template.defaultfilters import stringfilter
from django.templatetags.static import static
from django.urls import reverse
from django.utils.safestring import mark_safe
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
from comments.models import Comment
from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__)
register = template.Library()
@register.simple_tag(takes_context=True)
def head_meta(context):
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.simple_tag
def datetimeformat(data):
try:
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.filter()
@stringfilter
def custom_markdown(content):
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@register.filter()
@stringfilter
def comment_markdown(content):
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
"""
获得文章内容的摘要
:param content:
:return:
"""
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return truncatechars_html(content, blogsetting.article_sub_length)
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
from django.utils.html import strip_tags
return strip_tags(content)[:150]
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
获得文章面包屑
:param article:
:return:
"""
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
site = get_current_site().domain
names.append((blogsetting.site_name, '/'))
names = names[::-1]
return {
'names': names,
'title': article.title,
'count': len(names) + 1
}
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
文章标签
:param article:
:return:
"""
tags = article.tags.all()
tags_list = []
for tag in tags:
url = tag.get_absolute_url()
count = tag.get_article_count()
tags_list.append((
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
))
return {
'article_tags_list': tags_list
}
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
加载侧边栏
:return:
"""
value = cache.get("sidebar" + linktype)
if value:
value['user'] = user
return value
else:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = Category.objects.all()
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
if tags and len(tags) > 0:
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags)
value = {
'recent_articles': recent_articles,
'sidebar_categorys': sidebar_categorys,
'most_read_articles': most_read_articles,
'article_dates': dates,
'sidebar_comments': commment_list,
'sidabar_links': links,
'show_google_adsense': blogsetting.show_google_adsense,
'google_adsense_codes': blogsetting.google_adsense_codes,
'open_site_comment': blogsetting.open_site_comment,
'show_gongan_code': blogsetting.show_gongan_code,
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
value['user'] = user
return value
@register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user):
"""
获得文章meta信息
:param article:
:return:
"""
return {
'article': article,
'user': user
}
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
previous_url = ''
next_url = ''
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': next_number,
'tag_name': tag.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': previous_number,
'tag_name': tag.slug})
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:author_detail_page',
kwargs={
'page': next_number,
'author_name': tag_name})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:author_detail_page',
kwargs={
'page': previous_number,
'author_name': tag_name})
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:category_detail_page',
kwargs={
'page': next_number,
'category_name': category.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:category_detail_page',
kwargs={
'page': previous_number,
'category_name': category.slug})
return {
'previous_url': previous_url,
'next_url': next_url,
'page_obj': page_obj
}
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
"""
加载文章详情
:param article:
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
else:
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
email = email.encode('utf-8')
default = static('blog/img/avatar.png')
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
"""获得gravatar头像"""
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' %
(url, size, size))
@register.simple_tag
def query(qs, **kwargs):
""" template tag which allows queryset filtering. Usage:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
"""
return qs.filter(**kwargs)
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)

@ -0,0 +1,49 @@
from django.contrib import admin# 导入Django admin相关模块用于自定义后台管理
from django.urls import reverse# 导入reverse函数用于生成URL反向解析
from django.utils.html import format_html# 导入format_html用于安全生成HTML内容防止XSS攻击
from django.utils.translation import gettext_lazy as _# 导入gettext_lazy用于国际化翻译惰性加载优化性能
def disable_commentstatus(modeladmin, request, queryset):# 自定义批量操作:禁用评论状态
queryset.update(is_enable=False)# 将选中的评论记录批量更新is_enable字段为False禁用
def enable_commentstatus(modeladmin, request, queryset):# 自定义批量操作:启用评论状态
queryset.update(is_enable=True) # 将选中的评论记录批量更新is_enable字段为True启用
disable_commentstatus.short_description = _('Disable comments')# 为批量操作设置显示名称(支持国际化)
enable_commentstatus.short_description = _('Enable comments')
# 自定义评论模型的Admin管理类控制后台评论的展示和操作
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20# 每页显示20条评论记录
list_display = (# 列表页展示的字段(自定义字段需在类中定义对应方法)
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
list_display_links = ('id', 'body', 'is_enable')# 列表页中可点击跳转编辑页的字段
list_filter = ('is_enable',)# 右侧过滤条件(按是否启用筛选)
exclude = ('creation_time', 'last_modify_time')# 编辑页排除的字段(创建时间和最后修改时间通常自动生成,不允许手动编辑)
actions = [disable_commentstatus, enable_commentstatus]# 注册批量操作(将上面定义的两个函数加入到后台操作中)
def link_to_userinfo(self, obj):# 自定义字段:生成关联用户的后台编辑链接
info = (obj.author._meta.app_label, obj.author._meta.model_name)# 获取用户模型的app标签和模型名称用于生成admin URL
# obj.author表示评论关联的用户对象
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))# 反向解析生成用户模型的编辑页URL格式admin:app_label_model_name_change
return format_html( # 生成HTML链接显示用户昵称若无昵称则显示邮箱
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
def link_to_article(self, obj):# 自定义字段:生成关联文章的后台编辑链接
info = (obj.article._meta.app_label, obj.article._meta.model_name)# 获取文章模型的app标签和模型名称
# obj.article表示评论关联的文章对象
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))# 反向解析生成文章模型的编辑页URL
return format_html(# 生成HTML链接显示文章标题
u'<a href="%s">%s</a>' % (link, obj.article.title))
# 自定义字段的显示名称(支持国际化)
link_to_userinfo.short_description = _('User')# 显示为"用户"
link_to_article.short_description = _('Article')# 显示为"文章"

@ -0,0 +1,80 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.pot
# Django stuff:
*.log
logs/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# PyCharm
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
static/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html
BingSiteAuth.xml
cb9339dbe2ff86a5aa169d28dba5f615.txt
werobot_session.*
django.jpg
uploads/
settings_production.py
werobot_session.db
bin/datas/

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

Loading…
Cancel
Save