merge develop &手动加注释'

lunhun 3 months ago
parent 0477027a9c
commit 212afac481

@ -0,0 +1,6 @@
projectKey=DjangoBlog3
serverUrl=http://localhost:9000
serverVersion=25.11.0.114957
dashboardUrl=http://localhost:9000/dashboard?id=DjangoBlog3
ceTaskId=dda54cf7-c33c-4991-95ee-91be04fa807a
ceTaskUrl=http://localhost:9000/api/ce/task?id=dda54cf7-c33c-4991-95ee-91be04fa807a

@ -51,6 +51,7 @@ class BlogUserChangeForm(UserChangeForm):
"""
后台修改用户表单继承 Django 自带 UserChangeForm
"""
class Meta:
model = BlogUser
fields = '__all__' # 显示模型的所有字段
@ -67,7 +68,7 @@ class BlogUserAdmin(UserAdmin):
"""
自定义后台管理 BlogUser 的显示和表单配置
"""
form = BlogUserChangeForm # 修改用户时使用的表单
form = BlogUserChangeForm # 修改用户时使用的表单
add_form = BlogUserCreationForm # 创建用户时使用的表单
# 在列表页显示的字段

@ -4,6 +4,7 @@ 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

@ -2,12 +2,11 @@
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
@ -21,20 +20,35 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')),
('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')),
('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')),
('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': '用户',

@ -1,11 +1,10 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]

@ -3,6 +3,7 @@ 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

@ -1,16 +1,14 @@
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 django.conf import settings
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from djangoblog.utils import get_current_site, get_sha256, delete_sidebar_cache
from . import utils
# Create your tests here.
class AccountTest(TestCase):
"""
针对账户注册登录密码找回邮箱验证等功能的测试类
@ -19,10 +17,9 @@ class AccountTest(TestCase):
def setUp(self):
"""
初始化测试所需的对象
每个测试方法运行前都会执行
初始化测试所需的对象每个测试方法运行前都会执行
"""
self.client = Client() # Django 测试客户端,用于模拟请求
self.client = Client() # Django 测试客户端,用于模拟 HTTP 请求
self.factory = RequestFactory() # 用于构造请求对象
# 创建一个普通测试用户
self.blog_user = BlogUser.objects.create_user(
@ -36,58 +33,53 @@ class AccountTest(TestCase):
"""
测试账户验证登录以及文章管理功能
"""
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) # 登录应成功
# 测试访问 Django admin
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
# 测试访问 Django admin 后台页面
_ = self.client.get('/admin/')
# 创建一个文章分类
# 创建文章分类
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.type = 'a' # 文章类型 'a' 代表普通文章
article.status = 'p' # 状态 'p' 代表发布状态
article.save()
# 测试访问文章的后台管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试访问文章后台管理页面
_ = self.client.get(article.get_admin_url())
def test_validate_register(self):
"""
测试用户注册激活登录及文章管理流程
"""
# 验证注册前用户不存在
# 确保注册前没有该邮箱用户
self.assertEquals(
0, len(BlogUser.objects.filter(email='user123@user.com'))
)
# 通过客户端 POST 请求模拟用户注册
# 模拟用户注册请求
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
@ -95,37 +87,37 @@ class AccountTest(TestCase):
'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
)
_ = self.client.get(url)
# 测试访问邮箱验证链接
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# 登录用户
# 模拟用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache() # 清理缓存,避免后台界面异常
# 创建分类与文章
# 清理侧边栏缓存
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"
@ -135,48 +127,41 @@ class AccountTest(TestCase):
article.status = 'p'
article.save()
# 测试访问文章后台管理页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 测试文章后台管理页面访问
_ = self.client.get(article.get_admin_url())
# 测试注销
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
# 测试注销登录
_ = self.client.get(reverse('account:logout'))
# 注销后访问后台页面可能重定向
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
# 测试未登录访问文章后台管理页面
_ = self.client.get(article.get_admin_url())
# 测试错误密码登录
response = self.client.post(reverse('account:login'), {
_ = 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])
# 测试登录失败后访问后台文章页面
_ = self.client.get(article.get_admin_url())
def test_verify_email_code(self):
"""
测试邮箱验证码生成发送与验证
测试邮箱验证码的生成发送和验证
"""
to_email = "admin@admin.com"
code = generate_code() # 生成验证码
utils.set_code(to_email, code) # 设置验证码缓存
code = utils.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)
# 验证正确邮箱和验证码应返回 None
self.assertEqual(utils.verify("admin@admin.com", code), None)
# 验证不存在的邮箱应返回字符串错误信息
self.assertEqual(type(utils.verify("admin@123.com", code)), str)
def test_forget_password_email_code_success(self):
"""
测试发送忘记密码验证码成功
测试忘记密码发送验证码成功场景
"""
resp = self.client.post(
path=reverse("account:forget_password_code"),
@ -187,14 +172,16 @@ class AccountTest(TestCase):
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")
@ -203,10 +190,11 @@ class AccountTest(TestCase):
def test_forget_password_email_success(self):
"""
测试通过邮箱验证码重置密码成功
测试通过邮箱验证码成功重置密码
"""
code = generate_code()
code = utils.generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
@ -217,16 +205,16 @@ class AccountTest(TestCase):
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp.status_code, 302) # 成功重置密码应跳转
# 验证用户密码是否修改成功
# 验证数据库中用户密码已更新
blog_user = BlogUser.objects.filter(email=self.blog_user.email).first()
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,
@ -242,10 +230,11 @@ class AccountTest(TestCase):
def test_forget_password_email_code_error(self):
"""
测试密码重置时使用错误验证码
测试通过邮箱重置密码时验证码错误的场景
"""
code = generate_code()
code = utils.generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,

@ -4,25 +4,121 @@ from django.urls import re_path
from . import views
from .forms import LoginForm
# 命名空间,用于在模板或 reverse() 函数中引用 URL
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]
# URL 路由配置列表
urlpatterns = [
# 登录页面路由
# 使用 LoginView 视图类,登录成功后重定向到首页 ('/')
# kwargs 用于指定自定义的认证表单 LoginForm
re_path(
r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}
),
# 用户注册页面路由
# 使用 RegisterView 视图类,注册成功后重定向到首页 ('/')
re_path(
r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'
),
# 用户登出路由
# 使用 LogoutView 视图类,处理用户登出操作
re_path(
r'^logout/$',
views.LogoutView.as_view(),
name='logout'
),
# 账户操作结果页面路由
# 用于显示注册激活、密码重置等操作的结果页面
# 注意这里用 path 而不是 re_path直接指定 URL
path(
r'account/result.html',
views.account_result,
name='result'
),
# 忘记密码页面路由
# 使用 ForgetPasswordView 视图类,处理用户重置密码的表单提交
re_path(
r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'
),
# 忘记密码验证码接口路由
# 使用 ForgetPasswordEmailCode 视图类,处理通过邮箱发送验证码的请求
re_path(
r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'
),
]
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
# 命名空间,用于在模板或 reverse() 函数中引用 URL
app_name = "accounts"
# URL 路由配置列表
urlpatterns = [
# 登录页面路由
# 使用 LoginView 视图类,登录成功后重定向到首页 ('/')
# kwargs 用于指定自定义的认证表单 LoginForm
re_path(
r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}
),
# 用户注册页面路由
# 使用 RegisterView 视图类,注册成功后重定向到首页 ('/')
re_path(
r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'
),
# 用户登出路由
# 使用 LogoutView 视图类,处理用户登出操作
re_path(
r'^logout/$',
views.LogoutView.as_view(),
name='logout'
),
# 账户操作结果页面路由
# 用于显示注册激活、密码重置等操作的结果页面
# 注意这里用 path 而不是 re_path直接指定 URL
path(
r'account/result.html',
views.account_result,
name='result'
),
# 忘记密码页面路由
# 使用 ForgetPasswordView 视图类,处理用户重置密码的表单提交
re_path(
r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'
),
# 忘记密码验证码接口路由
# 使用 ForgetPasswordEmailCode 视图类,处理通过邮箱发送验证码的请求
re_path(
r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'
),
]

@ -8,19 +8,22 @@ class EmailOrUsernameModelBackend(ModelBackend):
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if '@' in username:
kwargs = {'email': username}
# 使用局部变量 credentials 避免覆盖 kwargs
credentials = {}
if username and '@' in username:
credentials['email'] = username
else:
kwargs = {'username': username}
credentials['username'] = username
try:
user = get_user_model().objects.get(**kwargs)
user = get_user_model().objects.get(**credentials)
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
def get_user(self, user_id):
try:
return get_user_model().objects.get(pk=username)
return get_user_model().objects.get(pk=user_id)
except get_user_model().DoesNotExist:
return None

@ -1,12 +1,8 @@
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.contrib.auth import REDIRECT_FIELD_NAME, get_user_model, logout, forms as auth_forms, hashers as auth_hashers
from django.http import HttpResponseRedirect, HttpResponseForbidden, HttpResponse
from django.http.request import HttpRequest
from django.shortcuts import get_object_or_404, render
@ -24,122 +20,100 @@ from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
logger = logging.getLogger(__name__) # 初始化日志记录器
logger = logging.getLogger(__name__)
# ------------------------- 注册视图 -------------------------
class RegisterView(FormView):
"""用户注册视图"""
form_class = RegisterForm
template_name = 'account/registration_form.html' # 注册页面模板路径
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect) # 防止CSRF攻击
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
return super().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) # 保存到数据库
user_obj = form.save(commit=False)
user_obj.is_active = False
user_obj.source = 'Register'
user_obj.save()
site = get_current_site().domain # 获取当前站点域名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 生成验证签名
site_domain = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user_obj.id)))
if settings.DEBUG: # 开发环境下替换站点域名
site = '127.0.0.1:8000'
if settings.DEBUG:
site_domain = '127.0.0.1:8000'
# 生成邮箱验证链接
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
url = f"http://{site_domain}{path}?type=validation&id={user_obj.id}&sign={sign}"
# 邮件内容
content = """
content = f"""
<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)
"""
send_email(emailto=[user_obj.email], title='验证您的电子邮箱', content=content)
redirect_url = reverse('accounts:result') + f'?type=register&id={user_obj.id}'
return HttpResponseRedirect(redirect_url)
else:
# 验证失败重新渲染表单
return self.render_to_response({'form': form})
# ------------------------- 登出视图 -------------------------
class LogoutView(RedirectView):
"""用户登出视图"""
url = '/login/' # 登出后跳转页面
url = '/login/'
@method_decorator(never_cache) # 禁止页面缓存
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request) # 注销当前用户
delete_sidebar_cache() # 清理缓存
return super(LogoutView, self).get(request, *args, **kwargs)
logout(request)
delete_sidebar_cache()
return super().get(request, *args, **kwargs)
# ------------------------- 登录视图 -------------------------
class LoginView(FormView):
"""用户登录视图"""
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/' # 登录成功默认跳转首页
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # “记住我”有效期30天
login_ttl = 2626560
@method_decorator(sensitive_post_parameters('password')) # 标记敏感字段
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""为模板提供重定向参数"""
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
redirect_to = self.request.GET.get(self.redirect_field_name, '/')
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
return super().get_context_data(**kwargs)
def form_valid(self, form):
"""登录逻辑"""
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"): # 如果勾选“记住我”
auth_form = auth_forms.AuthenticationForm(data=self.request.POST, request=self.request)
if auth_form.is_valid():
delete_sidebar_cache()
auth.login(self.request, auth_form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
return super().form_valid(auth_form)
else:
# 登录失败,重新渲染页面
return self.render_to_response({'form': form})
return self.render_to_response({'form': auth_form})
def get_success_url(self):
"""登录成功后跳转路径校验"""
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[self.request.get_host()]
):
if not url_has_allowed_host_and_scheme(url=redirect_to, allowed_hosts=[self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
@ -147,73 +121,59 @@ class LoginView(FormView):
# ------------------------- 注册与验证结果视图 -------------------------
def account_result(request):
"""注册或邮箱验证结果页"""
type = request.GET.get('type')
id = request.GET.get('id')
result_type = request.GET.get('type')
user_id = request.GET.get('id')
user = get_object_or_404(get_user_model(), id=id) # 获取用户对象
logger.info(type)
user_obj = get_object_or_404(get_user_model(), id=user_id)
if user.is_active: # 若已激活,直接跳转首页
if user_obj.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
if result_type in ['register', 'validation']:
if result_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 = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
expected_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user_obj.id)))
provided_sign = request.GET.get('sign')
if provided_sign != expected_sign:
return HttpResponseForbidden()
user_obj.is_active = True
user_obj.save()
content = '恭喜您已经成功完成邮箱验证,现在可以使用账号登录。'
title = '验证成功'
# 渲染结果模板
return render(request, 'account/result.html', {
'title': title,
'content': content
})
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()
# 更新加密后的新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/') # 修改成功后跳转登录页
blog_user_obj = BlogUser.objects.filter(email=form.cleaned_data.get("email")).first()
if blog_user_obj:
blog_user_obj.password = auth_hashers.make_password(form.cleaned_data["new_password2"])
blog_user_obj.save()
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
# ------------------------- 发送邮箱验证码视图 -------------------------
class ForgetPasswordEmailCode(View):
"""用于发送找回密码邮箱验证码"""
def post(self, request: HttpRequest):
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") # 返回成功响应
code_form = ForgetPasswordCodeForm(request.POST)
if not code_form.is_valid():
return HttpResponse("错误的邮箱")
to_email = code_form.cleaned_data["email"]
verification_code = generate_code()
utils.send_verify_email(to_email, verification_code)
utils.set_code(to_email, verification_code)
return HttpResponse("ok")

@ -1,3 +1,5 @@
import os
import secrets
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
@ -9,8 +11,13 @@ class Command(BaseCommand):
help = 'create test datas'
def handle(self, *args, **options):
# 使用随机生成的密码,保证安全
random_password = secrets.token_urlsafe(16)
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
email='test@test.com',
username='测试用户',
defaults={'password': make_password(random_password)}
)[0]
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
@ -22,12 +29,14 @@ class Command(BaseCommand):
basetag = Tag()
basetag.name = "标签"
basetag.save()
for i in range(1, 20):
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
author=user
)[0]
tag = Tag()
tag.name = "标签" + str(i)
tag.save()

@ -1,8 +1,8 @@
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
site = get_current_site().domain

@ -10,38 +10,38 @@ from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
help = 'sync user avatar'
def test_picture(self, url):
DEFAULT_AVATAR = static('blog/img/avatar.png') # 定义常量
STATIC_PREFIX = static("../")
def test_picture(self, url: str) -> bool:
"""测试 URL 是否可访问"""
try:
if requests.get(url, timeout=2).status_code == 200:
return True
except:
pass
response = requests.get(url, timeout=2)
return response.status_code == 200
except requests.RequestException:
return False # 明确捕获 requests 请求异常
def handle(self, *args, **options):
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
total = len(users)
self.stdout.write(f'开始同步 {total} 个用户头像')
for u in users:
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
if url.startswith(static_url):
if self.test_picture(url):
continue
else:
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
else:
url = save_user_avatar(url)
self.stdout.write(f'开始同步: {u.nickname}')
url = u.picture or self.DEFAULT_AVATAR
if url.startswith(self.STATIC_PREFIX):
if not self.test_picture(url) and u.metadata:
manager = get_manager_by_type(u.type)
url = save_user_avatar(manager.get_picture(u.metadata))
elif not self.test_picture(url):
url = self.DEFAULT_AVATAR
else:
url = static('blog/img/avatar.png')
url = save_user_avatar(url)
if url:
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
self.stdout.write(f'结束同步: {u.nickname}, url: {url}')
u.picture = url
u.save()
self.stdout.write('结束同步')

@ -1,14 +1,12 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
@ -29,12 +27,15 @@ class Migration(migrations.Migration):
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
('google_adsense_codes',
models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
('beiancode',
models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
('gongan_beiancode',
models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
'verbose_name': '网站配置',
@ -49,7 +50,9 @@ class Migration(migrations.Migration):
('link', models.URLField(verbose_name='链接地址')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('show_type', models.CharField(
choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')],
default='i', max_length=1, 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='修改时间')),
],
@ -100,7 +103,9 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
('parent_category',
models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='blog.category', verbose_name='父级分类')),
],
options={
'verbose_name': '分类',
@ -117,14 +122,19 @@ class Migration(migrations.Migration):
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1,
verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1,
verbose_name='评论状态')),
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1,
verbose_name='类型')),
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL,
verbose_name='作者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category',
verbose_name='分类')),
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]

@ -1,14 +1,12 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
@ -17,7 +15,8 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article',
'verbose_name_plural': 'article'},
),
migrations.AlterModelOptions(
name='category',
@ -115,7 +114,8 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL,
verbose_name='author'),
),
migrations.AlterField(
model_name='article',
@ -125,12 +125,14 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category',
verbose_name='category'),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1,
verbose_name='comment status'),
),
migrations.AlterField(
model_name='article',
@ -145,7 +147,8 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1,
verbose_name='status'),
),
migrations.AlterField(
model_name='article',
@ -160,7 +163,8 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1,
verbose_name='type'),
),
migrations.AlterField(
model_name='article',
@ -235,7 +239,8 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='blog.category', verbose_name='parent category'),
),
migrations.AlterField(
model_name='links',
@ -265,7 +270,8 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')],
default='i', max_length=1, verbose_name='show type'),
),
migrations.AlterField(
model_name='sidebar',

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]

@ -32,22 +32,18 @@ class BaseModel(models.Model):
def save(self, *args, **kwargs):
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
slug = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs)
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
url = f"https://{site}{self.get_absolute_url()}"
return url
class Meta:
@ -72,38 +68,24 @@ class Article(BaseModel):
('a', _('Article')),
('p', _('Page')),
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
pub_time = models.DateTimeField(_('publish time'), default=now)
status = models.CharField(_('status'), max_length=1, choices=STATUS_CHOICES, default='p')
# 修改字段名以消除冲突
comment_status_field = models.CharField(
_('comment status'), max_length=1, choices=COMMENT_STATUS, default='o'
)
type_field = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
article_order = models.IntegerField(_('order'), default=0)
show_toc = models.BooleanField(_('show toc'), default=False)
category = models.ForeignKey('Category', on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True)
def body_to_string(self):
return self.body
@ -128,8 +110,7 @@ class Article(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
names = [(c.name, c.get_absolute_url()) for c in tree]
return names
def save(self, *args, **kwargs):
@ -140,15 +121,15 @@ class Article(BaseModel):
self.save(update_fields=['views'])
def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
cache_key = f'article_comments_{self.id}'
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
logger.info(f'get article comments:{self.id}')
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
logger.info(f'set article comments:{self.id}')
return comments
def get_admin_url(self):
@ -157,20 +138,13 @@ class Article(BaseModel):
@cache_decorator(expiration=60 * 100)
def next_article(self):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
@ -178,14 +152,8 @@ class Article(BaseModel):
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
parent_category = models.ForeignKey('self', blank=True, null=True, on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index'))
@ -195,19 +163,13 @@ class Category(BaseModel):
verbose_name_plural = verbose_name
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
return reverse('blog:category_detail', kwargs={'category_name': self.slug})
def __str__(self):
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
categorys = []
def parse(category):
@ -220,10 +182,6 @@ class Category(BaseModel):
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
categorys = []
all_categorys = Category.objects.all()
@ -241,7 +199,6 @@ class Category(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)
@ -261,19 +218,13 @@ class Tag(BaseModel):
verbose_name_plural = verbose_name
# 以下 Links / SideBar / BlogSettings 不涉及字段冲突,可保持不变
class Links(models.Model):
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
is_enable = models.BooleanField(_('is show'), default=True)
show_type = models.CharField(_('show type'), max_length=1, choices=LinkShowType.choices, default=LinkShowType.I)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
@ -287,7 +238,6 @@ class Links(models.Model):
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
@ -305,59 +255,24 @@ class SideBar(models.Model):
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
site_name = models.CharField(_('site name'), max_length=200, default='')
site_description = models.TextField(_('site description'), max_length=1000, default='')
site_seo_description = models.TextField(_('site seo description'), max_length=1000, default='')
site_keywords = models.TextField(_('site keywords'), max_length=1000, default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
article_comment_count = models.IntegerField(_('article comment count'), default=5)
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
google_adsense_codes = models.TextField(_('adsense code'), max_length=2000, default='', blank=True)
open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='')
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
global_header = models.TextField("公共头部", default='', blank=True, null=True)
global_footer = models.TextField("公共尾部", default='', blank=True, null=True)
beian_code = models.CharField('备案号', max_length=2000, blank=True, null=True, default='')
analytics_code = models.TextField("网站统计代码", max_length=1000, default='')
show_gongan_code = models.BooleanField('是否显示公安备案号', default=False)
gongan_beiancode = models.TextField('公安备案号', max_length=2000, blank=True, null=True, default='')
comment_need_review = models.BooleanField('评论是否需要审核', default=False)
class Meta:
verbose_name = _('Website configuration')

@ -14,11 +14,11 @@ from django.utils.safestring import mark_safe
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
from comments.models import Comment
from djangoblog.plugin_manage import hooks
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__)

@ -61,4 +61,4 @@ class CommentAdmin(admin.ModelAdmin):
# 为自定义字段设置显示名称(支持国际化)
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')
link_to_article.short_description = _('Article')

@ -10,4 +10,4 @@ class CommentsConfig(AppConfig):
"""
# 应用的唯一标识名称必须与应用目录名一致用于Django识别和管理该应用
# 在settings.py的INSTALLED_APPS中注册时通常使用这个名称如'comments'
name = 'comments'
name = 'comments'

@ -18,4 +18,4 @@ class CommentForm(ModelForm):
# Meta类用于配置模型表单的元数据
class Meta:
model = Comment # 指定表单对应的模型为Comment
fields = ['body'] # 指定需要在表单中显示的模型字段,这里只包含评论内容字段'body'
fields = ['body'] # 指定需要在表单中显示的模型字段,这里只包含评论内容字段'body'

@ -1,11 +1,10 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 以上注释为Django自动生成显示生成该迁移文件的Django版本和时间
import django.utils.timezone # 用于处理时间相关字段
# 导入Django必要的模块
from django.conf import settings # 用于获取项目设置,特别是用户模型配置
from django.db import migrations, models # 数据库迁移和模型字段相关模块
import django.db.models.deletion # 用于定义外键删除行为
import django.utils.timezone # 用于处理时间相关字段
class Migration(migrations.Migration):
@ -32,28 +31,28 @@ class Migration(migrations.Migration):
# 主键字段,自动增长的大整数类型
('id', models.BigAutoField(
auto_created=True, # 自动创建
primary_key=True, # 设为主键
serialize=False, # 不序列化
verbose_name='ID' # 后台显示名称
primary_key=True, # 设为主键
serialize=False, # 不序列化
verbose_name='ID' # 后台显示名称
)),
# 评论正文字段
('body', models.TextField(
max_length=300, # 最大长度限制
max_length=300, # 最大长度限制
verbose_name='正文' # 后台显示名称
)),
# 评论创建时间字段
('created_time', models.DateTimeField(
default=django.utils.timezone.now, # 默认值为当前时间
verbose_name='创建时间' # 后台显示名称
verbose_name='创建时间' # 后台显示名称
)),
# 评论最后修改时间字段
('last_mod_time', models.DateTimeField(
default=django.utils.timezone.now, # 默认值为当前时间
verbose_name='修改时间' # 后台显示名称
verbose_name='修改时间' # 后台显示名称
)),
# 评论是否显示的状态字段
('is_enable', models.BooleanField(
default=True, # 默认显示
default=True, # 默认显示
verbose_name='是否显示' # 后台显示名称
)),
# 外键关联到文章模型
@ -66,12 +65,12 @@ class Migration(migrations.Migration):
('author', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, # 用户删除时,关联评论也删除
to=settings.AUTH_USER_MODEL, # 关联到项目配置的用户模型
verbose_name='作者' # 后台显示名称
verbose_name='作者' # 后台显示名称
)),
# 外键关联到自身,实现评论嵌套(回复功能)
('parent_comment', models.ForeignKey(
blank=True, # 允许为空
null=True, # 数据库中允许为NULL
blank=True, # 允许为空
null=True, # 数据库中允许为NULL
on_delete=django.db.models.deletion.CASCADE, # 上级评论删除时,子评论也删除
to='comments.comment', # 关联到当前应用的Comment模型
verbose_name='上级评论' # 后台显示名称
@ -79,10 +78,10 @@ class Migration(migrations.Migration):
],
# 模型的元数据配置
options={
'verbose_name': '评论', # 模型的单数显示名称
'verbose_name_plural': '评论', # 模型的复数显示名称(中文单复数相同)
'ordering': ['-id'], # 默认排序方式按id倒序新评论在前
'get_latest_by': 'id', # 使用latest()方法时按id字段判断最新
'verbose_name': '评论', # 模型的单数显示名称
'verbose_name_plural': '评论', # 模型的复数显示名称(中文单复数相同)
'ordering': ['-id'], # 默认排序方式按id倒序新评论在前
'get_latest_by': 'id', # 使用latest()方法时按id字段判断最新
},
),
]
]

@ -30,4 +30,4 @@ class Migration(migrations.Migration):
verbose_name='是否显示' # 保持字段的后台显示名称不变
),
),
]
]

@ -1,11 +1,10 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
# Django自动生成的注释显示生成该迁移文件的Django版本和时间
import django.utils.timezone # 时间处理工具
# 导入必要的模块
from django.conf import settings # 用于获取用户模型配置
from django.db import migrations, models # 数据库迁移和模型字段相关
import django.db.models.deletion # 外键删除行为定义
import django.utils.timezone # 时间处理工具
class Migration(migrations.Migration):
@ -90,4 +89,4 @@ class Migration(migrations.Migration):
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to='comments.comment', verbose_name='parent comment'),
),
]
]

@ -57,4 +57,4 @@ class Comment(models.Model):
# 模型的字符串表示:在后台和打印对象时显示评论正文
def __str__(self):
return self.body
return self.body

@ -40,4 +40,4 @@ def show_comment_item(comment, ischild):
return {
'comment_item': comment, # 评论对象,包含作者、内容、时间等信息
'depth': depth # 层级深度,用于前端渲染样式
}
}

@ -5,9 +5,6 @@ from django.urls import reverse # 用于反向解析URL
# 导入相关模型和工具
from accounts.models import BlogUser # 用户模型
from blog.models import Category, Article # 博客分类和文章模型
from comments.models import Comment # 评论模型
from comments.templatetags.comments_tags import * # 评论相关的模板标签
from djangoblog.utils import get_max_articleid_commentid # 工具函数
# 创建测试类继承TransactionTestCase支持事务的测试类适合涉及数据库事务的测试
@ -101,4 +98,4 @@ class CommentsTest(TransactionTestCase):
# Title1 Markdown标题
```python
import os # Markdown代码块
import os # Markdown代码块

@ -15,6 +15,7 @@ spec:
labels:
app: djangoblog
spec:
automountServiceAccountToken: false
containers:
- name: djangoblog
image: liangliangyy/djangoblog:latest
@ -40,9 +41,11 @@ spec:
requests:
cpu: 10m
memory: 100Mi
ephemeral-storage: 500Mi
limits:
cpu: "2"
memory: 2Gi
ephemeral-storage: 2Gi
volumeMounts:
- name: djangoblog
mountPath: /code/djangoblog/collectedstatic
@ -57,6 +60,7 @@ spec:
claimName: resource-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
@ -74,6 +78,7 @@ spec:
labels:
app: redis
spec:
automountServiceAccountToken: false
containers:
- name: redis
image: redis:latest
@ -84,11 +89,14 @@ spec:
requests:
cpu: 10m
memory: 100Mi
ephemeral-storage: 500Mi
limits:
cpu: 200m
memory: 2Gi
ephemeral-storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
@ -106,6 +114,7 @@ spec:
labels:
app: db
spec:
automountServiceAccountToken: false
containers:
- name: db
image: mysql:latest
@ -143,9 +152,11 @@ spec:
requests:
cpu: 10m
memory: 100Mi
ephemeral-storage: 500Mi
limits:
cpu: "2"
memory: 2Gi
ephemeral-storage: 2Gi
volumeMounts:
- name: db-data
mountPath: /var/lib/mysql
@ -153,8 +164,9 @@ spec:
- name: db-data
persistentVolumeClaim:
claimName: db-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
@ -172,6 +184,7 @@ spec:
labels:
app: nginx
spec:
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:latest
@ -182,9 +195,11 @@ spec:
requests:
cpu: 10m
memory: 100Mi
ephemeral-storage: 500Mi
limits:
cpu: "2"
memory: 2Gi
ephemeral-storage: 2Gi
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
@ -214,6 +229,7 @@ spec:
claimName: resource-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
@ -231,6 +247,7 @@ spec:
labels:
app: elasticsearch
spec:
automountServiceAccountToken: false
containers:
- name: elasticsearch
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
@ -250,9 +267,11 @@ spec:
requests:
cpu: 10m
memory: 100Mi
ephemeral-storage: 500Mi
limits:
cpu: "2"
memory: 2Gi
ephemeral-storage: 2Gi
readinessProbe:
httpGet:
path: /
@ -269,6 +288,6 @@ spec:
- name: elasticsearch-data
mountPath: /usr/share/elasticsearch/data/
volumes:
- name: elasticsearch-data
persistentVolumeClaim:
claimName: elasticsearch-pvc
- name: elasticsearch-data
persistentVolumeClaim:
claimName: elasticsearch-pvc

@ -3,18 +3,32 @@ from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
# accounts
from accounts.admin import BlogUserAdmin
from accounts.models import BlogUser
# blog
from blog.admin import ArticlelAdmin, CategoryAdmin, TagAdmin, LinksAdmin, SideBarAdmin, BlogSettingsAdmin, CommandsAdmin, EmailSendLogAdmin
from blog.models import Article, Category, Tag, Links, SideBar, BlogSettings, commands, EmailSendLog
# comments
from comments.admin import CommentAdmin
from comments.models import Comment
# logentryadmin
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
# oauth
from oauth.admin import OAuthUserAdmin, OAuthConfigAdmin
from oauth.models import OAuthUser, OAuthConfig
# owntracks
from owntracks.admin import OwnTrackLogsAdmin
from owntracks.models import OwnTrackLog
# servermanager
# 如果 servermanager 中有 Admin 类或模型需要注册,再单独 import
# 目前原代码注册中未用到 servermanager 的类,可以暂时不导入
class DjangoBlogAdminSite(AdminSite):
@ -40,25 +54,31 @@ class DjangoBlogAdminSite(AdminSite):
admin_site = DjangoBlogAdminSite(name='admin')
# blog
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# accounts
admin_site.register(BlogUser, BlogUserAdmin)
# comments
admin_site.register(Comment, CommentAdmin)
# oauth
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
# owntracks
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
# sites
admin_site.register(Site, SiteAdmin)
# log entry
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,5 +1,6 @@
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
@ -8,4 +9,4 @@ class DjangoblogAppConfig(AppConfig):
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()
load_plugins()

@ -4,4 +4,3 @@ ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -26,7 +26,8 @@ def run_action(hook_name: str, *args, **kwargs):
try:
callback(*args, **kwargs)
except Exception as e:
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
@ -40,5 +41,6 @@ def apply_filters(hook_name: str, value, *args, **kwargs):
try:
value = callback(value, *args, **kwargs)
except Exception as e:
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}",
exc_info=True)
return value

@ -1,9 +1,11 @@
import os
import logging
import os
from django.conf import settings
logger = logging.getLogger(__name__)
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
@ -16,4 +18,4 @@ def load_plugins():
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -340,4 +340,4 @@ ACTIVE_PLUGINS = [
'external_links',
'view_count',
'seo_optimizer'
]
]

@ -1,15 +1,21 @@
from django.test import TestCase
from djangoblog.utils import *
# 只导入实际使用的函数和类
from djangoblog.utils import get_sha256, CommonMarkdown, parse_dict_to_url
class DjangoBlogTest(TestCase):
def setUp(self):
# setUp 空方法保留用于初始化测试环境
# 目前没有额外初始化逻辑
pass
def test_utils(self):
# 测试 get_sha256 函数
md5 = get_sha256('test')
self.assertIsNotNone(md5)
# 测试 CommonMarkdown.get_markdown 方法
c = CommonMarkdown.get_markdown('''
# Title1
@ -20,10 +26,10 @@ class DjangoBlogTest(TestCase):
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
# 测试 parse_dict_to_url 函数
d = {
'd': 'key1',
'd2': 'key2'

@ -1,60 +1,23 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
def wrapper(func):
def news(*args, **kwargs):
try:
view = args[0]
key = view.get_cache_key()
except:
key = None
if not key:
except AttributeError:
# view 没有 get_cache_key 方法,使用哈希值生成缓存 key
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
if value is not None:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
logger.debug('cache_decorator set cache:%s key:%s' % (func.__name__, key))
value = func(*args, **kwargs)
if value is None:
cache.set(key, '__default_cache_value__', expiration)
@ -67,166 +30,29 @@ def cache_decorator(expiration=3 * 60):
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
site = Site.objects.get_current()
return site
class CommonMarkdown:
@staticmethod
def _convert_markdown(value):
md = markdown.Markdown(
extensions=[
'extra',
'codehilite',
'toc',
'tables',
]
)
body = md.convert(value)
toc = md.toc
return body, toc
@staticmethod
def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
return value
def save_user_avatar(url):
'''
"""
保存用户头像
:param url:头像url
:return: 本地路径
'''
logger.info(url)
"""
logger.info(f"save_user_avatar: {url}")
basedir = os.path.join(settings.STATICFILES, 'avatar')
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename)
except Exception as e:
logger.error(e)
rsp.raise_for_status()
if not os.path.exists(basedir):
os.makedirs(basedir)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = any(url.endswith(ext) for ext in image_extensions)
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像: ' + os.path.join(basedir, save_filename))
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename)
except (requests.RequestException, OSError) as e:
logger.error(f"save_user_avatar failed: {e}")
return static('blog/img/avatar.png')
def delete_sidebar_cache():
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

File diff suppressed because it is too large Load Diff

@ -20,7 +20,7 @@ class OAuthUserAdmin(admin.ModelAdmin):
'id',
'nickname',
'link_to_usermodel', # 自定义方法:显示关联的用户链接
'show_user_image', # 自定义方法:显示头像缩略图
'show_user_image', # 自定义方法:显示头像缩略图
'type',
'email',
)
@ -34,8 +34,8 @@ class OAuthUserAdmin(admin.ModelAdmin):
# 动态设置只读字段:让所有字段都变为只读,禁止编辑
def get_readonly_fields(self, request, obj=None):
return list(self.readonly_fields) + \
[field.name for field in obj._meta.fields] + \
[field.name for field in obj._meta.many_to_many]
[field.name for field in obj._meta.fields] + \
[field.name for field in obj._meta.many_to_many]
# 禁止在后台手动添加 OAuthUser
def has_add_permission(self, request):

@ -16,7 +16,7 @@ class RequireEmailForm(forms.Form):
# 自定义 email 输入框的 HTML 样式与属性
self.fields['email'].widget = widgets.EmailInput(
attrs={
'placeholder': "email", # 输入框占位提示文字
"class": "form-control" # Bootstrap 样式类,统一表单外观
'placeholder': "email", # 输入框占位提示文字
"class": "form-control" # Bootstrap 样式类,统一表单外观
}
)

@ -1,13 +1,11 @@
# Generated by Django 4.1.7 on 2023-03-07 09:53
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
@ -19,10 +17,13 @@ class Migration(migrations.Migration):
name='OAuthConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
('type', models.CharField(
choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'),
('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),
('callback_url',
models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),
('is_enable', models.BooleanField(default=True, 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='修改时间')),
@ -46,7 +47,8 @@ class Migration(migrations.Migration):
('metadata', models.TextField(blank=True, null=True)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': 'oauth用户',

@ -1,13 +1,11 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('oauth', '0001_initial'),
@ -71,12 +69,15 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='oauthconfig',
name='type',
field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
field=models.CharField(
choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'),
('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
),
migrations.AlterField(
model_name='oauthuser',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
migrations.AlterField(
model_name='oauthuser',

@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'),
]

@ -17,7 +17,10 @@ class OAuthUser(models.Model):
nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
token = models.CharField(max_length=150, null=True, blank=True)
picture = models.CharField(max_length=350, blank=True, null=True)
type = models.CharField(blank=False, null=False, max_length=50)
# 修改字段名,消除与 TYPE 常量冲突
type_field = models.CharField(blank=False, null=False, max_length=50)
email = models.CharField(max_length=50, null=True, blank=True)
metadata = models.TextField(null=True, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
@ -40,7 +43,9 @@ class OAuthConfig(models.Model):
('facebook', 'FaceBook'),
('qq', 'QQ'),
)
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
# 修改字段名
type_field = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
appkey = models.CharField(max_length=200, verbose_name='AppKey')
appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
callback_url = models.CharField(
@ -55,11 +60,11 @@ class OAuthConfig(models.Model):
def clean(self):
if OAuthConfig.objects.filter(
type=self.type).exclude(id=self.id).count():
raise ValidationError(_(self.type + _('already exists')))
type_field=self.type_field).exclude(id=self.id).count():
raise ValidationError(_(self.type_field + _(' already exists')))
def __str__(self):
return self.type
return self.type_field
class Meta:
verbose_name = 'oauth配置'

@ -11,7 +11,6 @@ from oauth.models import OAuthConfig
from oauth.oauthmanager import BaseOauthManager
# Create your tests here.
class OAuthConfigTest(TestCase):
def setUp(self):
self.client = Client()
@ -34,7 +33,7 @@ class OAuthConfigTest(TestCase):
class OauthLoginTest(TestCase):
def setUp(self) -> None:
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.apps = self.init_apps()
@ -49,9 +48,9 @@ class OauthLoginTest(TestCase):
c.save()
return applications
def get_app_by_type(self, type):
def get_app_by_type(self, type_name):
for app in self.apps:
if app.ICON_NAME.lower() == type:
if app.ICON_NAME.lower() == type_name:
return app
@patch("oauth.oauthmanager.WBOauthManager.do_post")
@ -59,10 +58,8 @@ class OauthLoginTest(TestCase):
def test_weibo_login(self, mock_do_get, mock_do_post):
weibo_app = self.get_app_by_type('weibo')
assert weibo_app
url = weibo_app.get_authorization_url()
mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid"
})
mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid"})
mock_do_get.return_value = json.dumps({
"avatar_large": "avatar_large",
"screen_name": "screen_name",
@ -78,18 +75,15 @@ class OauthLoginTest(TestCase):
def test_google_login(self, mock_do_get, mock_do_post):
google_app = self.get_app_by_type('google')
assert google_app
url = google_app.get_authorization_url()
mock_do_post.return_value = json.dumps({
"access_token": "access_token",
"id_token": "id_token",
})
mock_do_post.return_value = json.dumps({"access_token": "access_token", "id_token": "id_token"})
mock_do_get.return_value = json.dumps({
"picture": "picture",
"name": "name",
"sub": "sub",
"email": "email",
})
token = google_app.get_access_token_by_code('code')
google_app.get_access_token_by_code('code')
userinfo = google_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token')
self.assertEqual(userinfo.openid, 'sub')
@ -99,9 +93,7 @@ class OauthLoginTest(TestCase):
def test_github_login(self, mock_do_get, mock_do_post):
github_app = self.get_app_by_type('github')
assert github_app
url = github_app.get_authorization_url()
self.assertTrue("github.com" in url)
self.assertTrue("client_id" in url)
mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer"
mock_do_get.return_value = json.dumps({
"avatar_url": "avatar_url",
@ -109,7 +101,7 @@ class OauthLoginTest(TestCase):
"id": "id",
"email": "email",
})
token = github_app.get_access_token_by_code('code')
github_app.get_access_token_by_code('code')
userinfo = github_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a')
self.assertEqual(userinfo.openid, 'id')
@ -119,57 +111,36 @@ class OauthLoginTest(TestCase):
def test_facebook_login(self, mock_do_get, mock_do_post):
facebook_app = self.get_app_by_type('facebook')
assert facebook_app
url = facebook_app.get_authorization_url()
self.assertTrue("facebook.com" in url)
mock_do_post.return_value = json.dumps({
"access_token": "access_token",
})
mock_do_post.return_value = json.dumps({"access_token": "access_token"})
mock_do_get.return_value = json.dumps({
"name": "name",
"id": "id",
"email": "email",
"picture": {
"data": {
"url": "url"
}
}
"picture": {"data": {"url": "url"}}
})
token = facebook_app.get_access_token_by_code('code')
facebook_app.get_access_token_by_code('code')
userinfo = facebook_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token')
@patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[
'access_token=access_token&expires_in=3600',
'callback({"client_id":"appid","openid":"openid"} );',
json.dumps({
"nickname": "nickname",
"email": "email",
"figureurl": "figureurl",
"openid": "openid",
})
json.dumps({"nickname": "nickname", "email": "email", "figureurl": "figureurl", "openid": "openid"})
])
def test_qq_login(self, mock_do_get):
qq_app = self.get_app_by_type('qq')
assert qq_app
url = qq_app.get_authorization_url()
self.assertTrue("qq.com" in url)
token = qq_app.get_access_token_by_code('code')
qq_app.get_access_token_by_code('code')
userinfo = qq_app.get_oauth_userinfo()
self.assertEqual(userinfo.token, 'access_token')
@patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post):
mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid"
})
mock_user_info = {
"avatar_large": "avatar_large",
"screen_name": "screen_name1",
"id": "id",
"email": "email",
}
mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid"})
mock_user_info = {"avatar_large": "avatar_large", "screen_name": "screen_name1", "id": "id", "email": "email"}
mock_do_get.return_value = json.dumps(mock_user_info)
response = self.client.get('/oauth/oauthlogin?type=weibo')
@ -181,34 +152,16 @@ class OauthLoginTest(TestCase):
self.assertEqual(response.url, '/')
user = auth.get_user(self.client)
assert user.is_authenticated
self.assertTrue(user.is_authenticated)
self.assertEqual(user.username, mock_user_info['screen_name'])
self.assertEqual(user.email, mock_user_info['email'])
self.client.logout()
response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/')
user = auth.get_user(self.client)
assert user.is_authenticated
self.assertTrue(user.is_authenticated)
self.assertEqual(user.username, mock_user_info['screen_name'])
self.assertEqual(user.email, mock_user_info['email'])
@patch("oauth.oauthmanager.WBOauthManager.do_post")
@patch("oauth.oauthmanager.WBOauthManager.do_get")
def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post):
mock_do_post.return_value = json.dumps({"access_token": "access_token",
"uid": "uid"
})
mock_user_info = {
"avatar_large": "avatar_large",
"screen_name": "screen_name1",
"id": "id",
}
mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid"})
mock_user_info = {"avatar_large": "avatar_large", "screen_name": "screen_name1", "id": "id"}
mock_do_get.return_value = json.dumps(mock_user_info)
response = self.client.get('/oauth/oauthlogin?type=weibo')
@ -216,30 +169,20 @@ class OauthLoginTest(TestCase):
self.assertTrue("api.weibo.com" in response.url)
response = self.client.get('/oauth/authorize?type=weibo&code=code')
self.assertEqual(response.status_code, 302)
oauth_user_id = int(response.url.split('/')[-1].split('.')[0])
self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html')
response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id})
self.assertEqual(response.status_code, 302)
sign = get_sha256(settings.SECRET_KEY +
str(oauth_user_id) + settings.SECRET_KEY)
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauth_user_id,
})
self.assertEqual(response.url, f'{url}?type=email')
path = reverse('oauth:email_confirm', kwargs={
'id': oauth_user_id,
'sign': sign
})
sign = get_sha256(settings.SECRET_KEY + str(oauth_user_id) + settings.SECRET_KEY)
path = reverse('oauth:email_confirm', kwargs={'id': oauth_user_id, 'sign': sign})
response = self.client.get(path)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success')
user = auth.get_user(self.client)
from oauth.models import OAuthUser
oauth_user = OAuthUser.objects.get(author=user)

@ -1,24 +1,19 @@
import logging
# Create your views here.
from urllib.parse import urlparse
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth import login
from django.contrib.auth import get_user_model, login
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.http import HttpResponseForbidden
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from djangoblog.blog_signals import oauth_user_login_signal
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email, get_sha256
from djangoblog.utils import get_current_site, send_email, get_sha256
from oauth.forms import RequireEmailForm
from .models import OAuthUser
from .oauthmanager import get_manager_by_type, OAuthAccessTokenException
@ -28,35 +23,34 @@ logger = logging.getLogger(__name__)
def get_redirecturl(request):
nexturl = request.GET.get('next_url', None)
if not nexturl or nexturl == '/login/' or nexturl == '/login':
nexturl = '/'
return nexturl
p = urlparse(nexturl)
if p.netloc:
if not nexturl or nexturl in ['/login/', '/login']:
return '/'
parsed_url = urlparse(nexturl)
if parsed_url.netloc:
site = get_current_site().domain
if not p.netloc.replace('www.', '') == site.replace('www.', ''):
if parsed_url.netloc.replace('www.', '') != site.replace('www.', ''):
logger.info('非法url:' + nexturl)
return "/"
return nexturl
def oauthlogin(request):
type = request.GET.get('type', None)
if not type:
oauth_type = request.GET.get('type', None)
if not oauth_type:
return HttpResponseRedirect('/')
manager = get_manager_by_type(type)
manager = get_manager_by_type(oauth_type)
if not manager:
return HttpResponseRedirect('/')
nexturl = get_redirecturl(request)
authorizeurl = manager.get_authorization_url(nexturl)
return HttpResponseRedirect(authorizeurl)
authorize_url = manager.get_authorization_url(nexturl)
return HttpResponseRedirect(authorize_url)
def authorize(request):
type = request.GET.get('type', None)
if not type:
oauth_type = request.GET.get('type', None)
if not oauth_type:
return HttpResponseRedirect('/')
manager = get_manager_by_type(type)
manager = get_manager_by_type(oauth_type)
if not manager:
return HttpResponseRedirect('/')
code = request.GET.get('code', None)
@ -71,83 +65,73 @@ def authorize(request):
nexturl = get_redirecturl(request)
if not rsp:
return HttpResponseRedirect(manager.get_authorization_url(nexturl))
user = manager.get_oauth_userinfo()
if user:
if not user.nickname or not user.nickname.strip():
user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
try:
temp = OAuthUser.objects.get(type=type, openid=user.openid)
temp.picture = user.picture
temp.metadata = user.metadata
temp.nickname = user.nickname
user = temp
except ObjectDoesNotExist:
pass
# facebook的token过长
if type == 'facebook':
user.token = ''
if user.email:
with transaction.atomic():
author = None
try:
author = get_user_model().objects.get(id=user.author_id)
except ObjectDoesNotExist:
pass
if not author:
result = get_user_model().objects.get_or_create(email=user.email)
author = result[0]
if result[1]:
try:
get_user_model().objects.get(username=user.nickname)
except ObjectDoesNotExist:
author.username = user.nickname
else:
author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.source = 'authorize'
author.save()
user.author = author
user.save()
oauth_user_login_signal.send(
sender=authorize.__class__, id=user.id)
login(request, author)
return HttpResponseRedirect(nexturl)
else:
user.save()
url = reverse('oauth:require_email', kwargs={
'oauthid': user.id
})
if not user:
return HttpResponseRedirect(nexturl)
if not user.nickname or not user.nickname.strip():
user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
return HttpResponseRedirect(url)
try:
temp = OAuthUser.objects.get(type=oauth_type, openid=user.openid)
temp.picture = user.picture
temp.metadata = user.metadata
temp.nickname = user.nickname
user = temp
except ObjectDoesNotExist:
pass
if oauth_type == 'facebook':
user.token = ''
if user.email:
with transaction.atomic():
author = None
try:
author = get_user_model().objects.get(id=user.author_id)
except ObjectDoesNotExist:
pass
if not author:
author, created = get_user_model().objects.get_or_create(email=user.email)
if created:
try:
get_user_model().objects.get(username=user.nickname)
author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
except ObjectDoesNotExist:
author.username = user.nickname
author.source = 'authorize'
author.save()
user.author = author
user.save()
oauth_user_login_signal.send(sender=authorize.__class__, id=user.id)
login(request, author)
return HttpResponseRedirect(nexturl)
else:
return HttpResponseRedirect(nexturl)
user.save()
redirect_url = reverse('oauth:require_email', kwargs={'oauthid': user.id})
return HttpResponseRedirect(redirect_url)
def emailconfirm(request, id, sign):
if not sign:
return HttpResponseForbidden()
if not get_sha256(settings.SECRET_KEY +
str(id) +
settings.SECRET_KEY).upper() == sign.upper():
if get_sha256(settings.SECRET_KEY + str(id) + settings.SECRET_KEY).upper() != sign.upper():
return HttpResponseForbidden()
oauthuser = get_object_or_404(OAuthUser, pk=id)
with transaction.atomic():
if oauthuser.author:
author = get_user_model().objects.get(pk=oauthuser.author_id)
else:
result = get_user_model().objects.get_or_create(email=oauthuser.email)
author = result[0]
if result[1]:
author, created = get_user_model().objects.get_or_create(email=oauthuser.email)
if created:
author.source = 'emailconfirm'
author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip(
) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip() else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.save()
oauthuser.author = author
oauthuser.save()
oauth_user_login_signal.send(
sender=emailconfirm.__class__,
id=oauthuser.id)
oauth_user_login_signal.send(sender=emailconfirm.__class__, id=oauthuser.id)
login(request, author)
site = 'http://' + get_current_site().domain
@ -162,40 +146,23 @@ def emailconfirm(request, id, sign):
%(site)s
''') % {'oauthuser_type': oauthuser.type, 'site': site}
send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content)
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': id
})
url = url + '?type=success'
return HttpResponseRedirect(url)
send_email(emailto=[oauthuser.email], title=_('Congratulations on your successful binding!'), content=content)
redirect_url = reverse('oauth:bindsuccess', kwargs={'oauthid': id}) + '?type=success'
return HttpResponseRedirect(redirect_url)
class RequireEmailView(FormView):
form_class = RequireEmailForm
template_name = 'oauth/require_email.html'
def get(self, request, *args, **kwargs):
oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
if oauthuser.email:
pass
# return HttpResponseRedirect('/')
return super(RequireEmailView, self).get(request, *args, **kwargs)
def get_initial(self):
oauthid = self.kwargs['oauthid']
return {
'email': '',
'oauthid': oauthid
}
return {'email': '', 'oauthid': self.kwargs['oauthid']}
def get_context_data(self, **kwargs):
oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
oauthuser = get_object_or_404(OAuthUser, pk=self.kwargs['oauthid'])
if oauthuser.picture:
kwargs['picture'] = oauthuser.picture
return super(RequireEmailView, self).get_context_data(**kwargs)
return super().get_context_data(**kwargs)
def form_valid(self, form):
email = form.cleaned_data['email']
@ -203,51 +170,36 @@ class RequireEmailView(FormView):
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
oauthuser.email = email
oauthuser.save()
sign = get_sha256(settings.SECRET_KEY +
str(oauthuser.id) + settings.SECRET_KEY)
sign = get_sha256(settings.SECRET_KEY + str(oauthuser.id) + settings.SECRET_KEY)
site = get_current_site().domain
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('oauth:email_confirm', kwargs={
'id': oauthid,
'sign': sign
})
url = "http://{site}{path}".format(site=site, path=path)
path = reverse('oauth:email_confirm', kwargs={'id': oauthid, 'sign': sign})
url = f"http://{site}{path}"
content = _("""
<p>Please click the link below to bind your email</p>
<a href="%(url)s" rel="bookmark">%(url)s</a>
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
<br />
<br />
%(url)s
""") % {'url': url}
send_email(emailto=[email, ], title=_('Bind your email'), content=content)
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauthid
})
url = url + '?type=email'
return HttpResponseRedirect(url)
""") % {'url': url}
send_email(emailto=[email], title=_('Bind your email'), content=content)
redirect_url = reverse('oauth:bindsuccess', kwargs={'oauthid': oauthid}) + '?type=email'
return HttpResponseRedirect(redirect_url)
def bindsuccess(request, oauthid):
type = request.GET.get('type', None)
oauth_type = request.GET.get('type', None)
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
if type == 'email':
if oauth_type == 'email':
title = _('Bind your email')
content = _(
'Congratulations, the binding is just one step away. '
'Please log in to your email to check the email to complete the binding. Thank you.')
content = _('Congratulations, the binding is just one step away. Please log in to your email to check the email to complete the binding. Thank you.')
else:
title = _('Binding successful')
content = _(
"Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s"
" to directly log in to this website without a password. You are welcome to continue to follow this site." % {
'oauthuser_type': oauthuser.type})
return render(request, 'oauth/bindsuccess.html', {
'title': title,
'content': content
})
content = _("Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s to directly log in to this website without a password. You are welcome to continue to follow this site." % {'oauthuser_type': oauthuser.type})
return render(request, 'oauth/bindsuccess.html', {'title': title, 'content': content})

@ -1,5 +1,6 @@
from django.contrib import admin
# Register your models here.

@ -1,11 +1,10 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.db import migrations, models
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('owntracks', '0001_initial'),
]
@ -12,7 +11,8 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='owntracklog',
options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'},
options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs',
'verbose_name_plural': 'OwnTrackLogs'},
),
migrations.RenameField(
model_name='owntracklog',

@ -1,17 +1,15 @@
# 1. 导入所需的基础类和模块
# 从插件管理模块导入基类 BasePlugin
# 所有自定义插件都应该继承这个基类,它提供了插件的基本结构和生命周期管理
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入钩子管理模块
# 这个模块提供了注册和触发钩子的功能
from djangoblog.plugin_manage import hooks
# 从插件管理模块导入基类 BasePlugin
# 所有自定义插件都应该继承这个基类,它提供了插件的基本结构和生命周期管理
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 从常量定义文件导入文章内容钩子的名称
# 使用常量可以避免硬编码字符串,增加代码的可读性和可维护性
# ARTICLE_CONTENT_HOOK_NAME 的值很可能就是 'article_content' 或类似的字符串
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 2. 定义插件主类
@ -19,10 +17,10 @@ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 定义一个名为 ArticleCopyrightPlugin 的类,并让它继承自 BasePlugin
class ArticleCopyrightPlugin(BasePlugin):
# 定义插件的元数据,这些信息通常会在后台管理界面显示
PLUGIN_NAME = '文章结尾版权声明' # 插件的显示名称
PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。' # 插件的详细描述
PLUGIN_VERSION = '0.2.0' # 插件的版本号
PLUGIN_AUTHOR = 'liangliangyy' # 插件的作者
PLUGIN_NAME = '文章结尾版权声明' # 插件的显示名称
PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。' # 插件的详细描述
PLUGIN_VERSION = '0.2.0' # 插件的版本号
PLUGIN_AUTHOR = 'liangliangyy' # 插件的作者
# 3. 实现钩子注册方法 (关键步骤)
def register_hooks(self):
@ -72,4 +70,4 @@ class ArticleCopyrightPlugin(BasePlugin):
# 2. 这个实例化过程会调用父类 BasePlugin 的 __init__ 构造方法。
# 3. 在 BasePlugin 的 __init__ 方法中,会自动调用我们上面定义的 register_hooks() 方法。
# 4. 这样,插件就完成了自身的注册,静静地等待着文章内容钩子被触发。
plugin = ArticleCopyrightPlugin()
plugin = ArticleCopyrightPlugin()

@ -2,10 +2,11 @@
import re
# 导入URL解析模块用于解析链接的域名等信息
from urllib.parse import urlparse
# 导入Django博客系统的插件基类当前插件需继承此类实现标准化功能
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于注册和触发插件功能
from djangoblog.plugin_manage import hooks
# 导入Django博客系统的插件基类当前插件需继承此类实现标准化功能
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入文章内容钩子常量,指定插件要作用的具体钩子位置
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -73,4 +74,4 @@ class ExternalLinksPlugin(BasePlugin):
# 实例化插件类,使插件系统能识别并加载该插件
plugin = ExternalLinksPlugin()
plugin = ExternalLinksPlugin()

@ -2,10 +2,11 @@
import math
# 导入正则表达式模块用于处理HTML内容和文本分词
import re
# 导入Django博客插件基类当前插件需继承此类
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于注册插件功能
from djangoblog.plugin_manage import hooks
# 导入Django博客插件基类当前插件需继承此类
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入文章内容钩子常量,指定插件作用的位置
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -66,4 +67,4 @@ class ReadingTimePlugin(BasePlugin):
# 实例化插件,使插件系统能够识别并加载
plugin = ReadingTimePlugin()
plugin = ReadingTimePlugin()

@ -1,15 +1,16 @@
# 导入JSON模块用于将结构化数据转换为JSON格式
import json
# 导入Django工具函数用于移除HTML标签提取纯文本
from django.utils.html import strip_tags
# 导入博客数据模型,用于获取文章、分类、标签等数据
from blog.models import Article, Category
# 导入插件钩子管理模块,用于注册插件功能到指定钩子
from djangoblog.plugin_manage import hooks
# 导入Django模板过滤器当前未使用预留用于文本截断
from django.template.defaultfilters import truncatewords
# 导入插件基类,所有插件需继承此类实现标准化接口
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于注册插件功能到指定钩子
from djangoblog.plugin_manage import hooks
# 导入博客数据模型,用于获取文章、分类、标签等数据
from blog.models import Article, Category, Tag
# 导入工具函数,用于获取博客站点的基础配置(如站点名称、关键词等)
from djangoblog.utils import get_blog_setting
@ -222,4 +223,4 @@ class SeoOptimizerPlugin(BasePlugin):
# 实例化插件,使插件系统能够识别并加载该插件
plugin = SeoOptimizerPlugin()
plugin = SeoOptimizerPlugin()

@ -1 +1 @@
# This file makes this a Python package
# This file makes this a Python package

@ -1,7 +1,7 @@
# 导入Django博客系统的插件基类所有自定义插件需继承此类以实现标准化接口
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 导入插件钩子管理模块,用于将插件功能绑定到系统预设的钩子点
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.base_plugin import BasePlugin
# 定义文章浏览次数统计插件类继承自插件基类BasePlugin
@ -41,4 +41,4 @@ class ViewCountPlugin(BasePlugin):
# 实例化插件类:
# 插件系统会扫描并加载该实例,使上述注册的钩子和功能生效
plugin = ViewCountPlugin()
plugin = ViewCountPlugin()

@ -1,4 +1,6 @@
from django.contrib import admin
# Register your models here.

@ -5,24 +5,24 @@ from blog.models import Article, Category
class BlogApi:
def __init__(self):
self.searchqueryset = SearchQuerySet() # 初始化搜索查询集,用于处理文章搜索功能
self.searchqueryset.auto_query('') # 执行空查询,初始化搜索结果集(可能用于后续叠加过滤条件)
self.__max_takecount__ = 8 # 定义私有变量限制各类查询的最大返回数量为8条
self.searchqueryset = SearchQuerySet() # 初始化搜索查询集,用于处理文章搜索功能
self.searchqueryset.auto_query('') # 执行空查询,初始化搜索结果集(可能用于后续叠加过滤条件)
self.__max_takecount__ = 8 # 定义私有变量限制各类查询的最大返回数量为8条
def search_articles(self, query):
sqs = self.searchqueryset.auto_query(query) # 使用搜索查询集执行自动查询(可能包含分词、过滤等处理)
sqs = sqs.load_all() # 预加载所有关联数据,减少数据库查询次数(优化性能)
return sqs[:self.__max_takecount__] # 限制返回结果数量返回前N条匹配的文章
sqs = self.searchqueryset.auto_query(query) # 使用搜索查询集执行自动查询(可能包含分词、过滤等处理)
sqs = sqs.load_all() # 预加载所有关联数据,减少数据库查询次数(优化性能)
return sqs[:self.__max_takecount__] # 限制返回结果数量返回前N条匹配的文章
def get_category_lists(self):
return Category.objects.all() # 返回所有分类对象(未限制数量,通常分类数量较少)
return Category.objects.all() # 返回所有分类对象(未限制数量,通常分类数量较少)
def get_category_articles(self, categoryname):
articles = Article.objects.filter(category__name=categoryname) # 过滤出指定分类下的所有文章(通过外键关联查询)
articles = Article.objects.filter(category__name=categoryname) # 过滤出指定分类下的所有文章(通过外键关联查询)
if articles:
return articles[:self.__max_takecount__]
return None # 若存在符合条件的文章返回前N条否则返回None
return None # 若存在符合条件的文章返回前N条否则返回None
def get_recent_articles(self):
return Article.objects.all()[:self.__max_takecount__]
# 返回所有文章的前N条依赖于Article模型的默认排序设置
# 返回所有文章的前N条依赖于Article模型的默认排序设置

@ -1,14 +1,14 @@
import logging # 导入日志模块,用于记录程序运行过程中的日志信息
import logging # 导入日志模块,用于记录程序运行过程中的日志信息
import os # 导入os模块用于与操作系统交互如获取环境变量、执行系统命令等
import openai # 导入openai模块用于调用OpenAI的API服务
import openai # 导入openai模块用于调用OpenAI的API服务
from servermanager.models import commands # 从servermanager应用的models模块中导入commands模型用于操作命令相关的数据
from servermanager.models import commands # 从servermanager应用的models模块中导入commands模型用于操作命令相关的数据
logger = logging.getLogger(__name__) # 创建日志记录器,名称为当前模块名,用于记录该模块的日志
logger = logging.getLogger(__name__) # 创建日志记录器,名称为当前模块名,用于记录该模块的日志
openai.api_key = os.environ.get('OPENAI_API_KEY') # 从环境变量中获取OpenAI的API密钥并设置为openai模块的API密钥
if os.environ.get('HTTP_PROXY'): # 检查环境变量中是否设置了HTTP代理如果有则为openai模块设置代理
openai.api_key = os.environ.get('OPENAI_API_KEY') # 从环境变量中获取OpenAI的API密钥并设置为openai模块的API密钥
if os.environ.get('HTTP_PROXY'): # 检查环境变量中是否设置了HTTP代理如果有则为openai模块设置代理
openai.proxy = os.environ.get('HTTP_PROXY')
@ -16,6 +16,7 @@ class ChatGPT:
"""
ChatGPT类用于与OpenAI的GPT模型进行交互实现聊天功能
"""
@staticmethod
def chat(prompt):
"""
@ -42,6 +43,7 @@ class CommandHandler:
"""
命令处理器类用于处理和执行系统命令以及提供命令帮助信息
"""
def __init__(self):
"""
初始化方法加载所有的命令数据
@ -96,6 +98,7 @@ class CommandHandler:
rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe)
return rsp
# 当该模块作为主程序运行时执行以下代码
if __name__ == '__main__':
chatbot = ChatGPT()

@ -2,6 +2,8 @@
# 说明此文件由Django 4.1.7版本自动生成生成时间为2023年3月2日7:14
# 迁移文件用于记录数据库模型的创建和修改通过Django的migrate命令同步到数据库
from django.db import migrations, models
# 导入Django迁移模块和模型字段模块
class Migration(migrations.Migration):
@ -9,13 +11,13 @@ class Migration(migrations.Migration):
initial = True # 标记为初始迁移(第一次创建模型时生成)
dependencies = [
] # 依赖的其他迁移文件列表,初始迁移无依赖,所以为空
# 若后续迁移依赖其他应用的迁移,会在此处列出,如:['appname.0001_initial']
] # 依赖的其他迁移文件列表,初始迁移无依赖,所以为空
# 若后续迁移依赖其他应用的迁移,会在此处列出,如:['appname.0001_initial']
operations = [ # 迁移操作列表,包含模型的创建、修改等操作
migrations.CreateModel( # 创建名为"commands"的模型(对应数据库表)
name='commands',
fields=[ # 定义模型的字段(对应数据库表的列)
fields=[ # 定义模型的字段(对应数据库表的列)
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=300, verbose_name='命令标题')),
('command', models.CharField(max_length=2000, verbose_name='命令')),
@ -23,10 +25,10 @@ class Migration(migrations.Migration):
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
],
options={ # 模型的额外配置
options={ # 模型的额外配置
'verbose_name': '命令', # 模型单数显示名称(后台管理用)
'verbose_name_plural': '命令', # 模型复数显示名称(后台管理用)
}, # 若未指定ordering默认按主键id排序
'verbose_name_plural': '命令', # 模型复数显示名称(后台管理用)
}, # 若未指定ordering默认按主键id排序
),
migrations.CreateModel( # 创建名为"EmailSendLog"的模型(邮件发送日志)
name='EmailSendLog',
@ -41,7 +43,7 @@ class Migration(migrations.Migration):
options={
'verbose_name': '邮件发送log',
'verbose_name_plural': '邮件发送log',
'ordering': ['-created_time'], # 按创建时间倒序排列(最新的日志在前)
'ordering': ['-created_time'], # 按创建时间倒序排列(最新的日志在前)
},
),
]

@ -2,11 +2,13 @@
# 说明此文件由Django 4.2.5版本自动生成生成时间为2023年9月6日13:19
# 作用:记录数据库模型的修改操作(字段重命名、配置调整等),用于同步数据库结构变更
from django.db import migrations
# 导入Django迁移模块
class Migration(migrations.Migration):
# 迁移类,所有数据库变更操作在此定义
dependencies = [ # 依赖的前置迁移文件:表示必须先执行'servermanager'应用的'0001_initial'迁移
dependencies = [ # 依赖的前置迁移文件:表示必须先执行'servermanager'应用的'0001_initial'迁移
# 才能执行当前迁移(确保修改的是已存在的模型)
('servermanager', '0001_initial'),
]
@ -14,14 +16,15 @@ class Migration(migrations.Migration):
operations = [ # 迁移操作列表:包含对模型的修改操作
migrations.AlterModelOptions( # 修改'EmailSendLog'模型的元配置
name='emailsendlog',
options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'},
), # 1. 排序方式变更:按'creation_time'字段倒序排列(最新记录在前)
# (原配置可能是按其他字段排序,此处同步字段名变更后的排序)
options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log',
'verbose_name_plural': '邮件发送log'},
), # 1. 排序方式变更:按'creation_time'字段倒序排列(最新记录在前)
# (原配置可能是按其他字段排序,此处同步字段名变更后的排序)
# 2. 模型显示名称(单数和复数)保持不变
migrations.RenameField( # 重命名'commands'模型的字段
migrations.RenameField( # 重命名'commands'模型的字段
model_name='commands',
old_name='created_time', # 原字段名:创建时间
new_name='creation_time', # 新字段名:创建时间(更简洁的命名)
old_name='created_time', # 原字段名:创建时间
new_name='creation_time', # 新字段名:创建时间(更简洁的命名)
),
migrations.RenameField( # 重命名'commands'模型的另一个字段
model_name='commands',
@ -30,7 +33,7 @@ class Migration(migrations.Migration):
),
migrations.RenameField( # 重命名'commands'模型的另一个字段
model_name='emailsendlog',
old_name='created_time', # 原字段名:创建时间
old_name='created_time', # 原字段名:创建时间
new_name='creation_time', # 新字段名创建时间与commands模型保持命名一致
),
]

@ -1,5 +1,4 @@
from django.test import Client, RequestFactory, TestCase
from django.utils import timezone
from werobot.messages.messages import TextMessage
from accounts.models import BlogUser

@ -0,0 +1,11 @@
sonar.projectKey=DjangoBlog
sonar.projectName=DjangoBlog
sonar.projectVersion=1.0
sonar.sources=.
sonar.language=py
sonar.sourceEncoding=UTF-8
sonar.python.coverage.reportPaths=coverage.xml
# ?? SonarQube ??
sonar.host.url=http://localhost:9000

@ -4,15 +4,15 @@
<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>
<label class="screen-reader-text" for="q">{% 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">
@ -21,8 +21,8 @@
</aside>
{% endfor %}
{% endif %}
{% if most_read_articles %}
{% if most_read_articles %}
<aside id="views-4" class="widget widget_views"><p class="widget-title">Views</p>
<ul>
{% for a in most_read_articles %}
@ -33,65 +33,61 @@
</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 'category' %}</p>
<ul>
{% for c in sidebar_categorys %}
<li class="cat-item cat-item-184"><a href={{ c.get_absolute_url }}>{{ c.name }}</a>
</li>
<li class="cat-item cat-item-184"><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 'recent comments' %}</p>
{% if sidebar_comments and open_site_comment %}
<aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments"><p class="widget-title">{% trans 'recent comments' %}</p>
<ul id="recentcomments">
{% for c in sidebar_comments %}
<li class="recentcomments">
<span class="comment-author-link">
{{ c.author.username }}</span>
<span class="comment-author-link">{{ c.author.username }}</span>
{% trans 'published on' %}《
<a href="{{ c.article.get_absolute_url }}#comment-{{ c.pk }}">{{ c.article.title }}</a>
<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 'recent articles' %}</p>
<ul>
{% for a in recent_articles %}
<li><a href="{{ a.get_absolute_url }}" title="{{ a.title }}">
{{ a.title }}
</a></li>
{% 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 'bookmark' %}</p>
<ul class='xoxo blogroll'>
{% for l in sidabar_links %}
<li>
<a href="{{ l.link }}" target="_blank" title="{{ l.name }}">{{ l.name }}</a>
</li>
<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>
<aside id="text-adsense" 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 'Tag Cloud' %}</p>
<div class="tagcloud">
@ -104,14 +100,17 @@
</div>
</aside>
{% endif %}
<aside id="text-2" class="widget widget_text"><p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p>
<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>
<aside id="text-welcome" class="widget widget_text"><p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p>
<div class="textwidget">
<p>
<a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow">
<img 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>
@ -119,11 +118,9 @@
<ul>
<li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li>
{% if user.is_authenticated %}
<li><a href="{% url "account:logout" %}" rel="nofollow">{% trans 'logout' %}</a>
</li>
<li><a href="{% url 'account:logout' %}" rel="nofollow">{% trans 'logout' %}</a></li>
{% else %}
<li><a href="{% url "account:login" %}" rel="nofollow">{% trans 'login' %}</a></li>
<li><a href="{% url 'account:login' %}" rel="nofollow">{% trans 'login' %}</a></li>
{% endif %}
{% if user.is_superuser %}
<li><a href="{% url 'owntracks:show_dates' %}" target="_blank">{% trans 'Track record' %}</a></li>

@ -1,34 +1,40 @@
{% 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=""
src="{{ comment_item.author.email|gravatar_url:150 }}"
srcset="{{ comment_item.author.email|gravatar_url:150 }}"
class="avatar avatar-96 photo" height="96" width="96">
<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>
<ul class="comment-list">
<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=""
src="{{ comment_item.author.email|gravatar_url:150 }}"
srcset="{{ comment_item.author.email|gravatar_url:150 }}"
class="avatar avatar-96 photo" height="96" width="96">
<cite class="fn">
{% if comment_item.author.is_superuser %}
<a href="{{ comment_item.author.get_absolute_url }}" rel="external nofollow" class="url">
{{ comment_item.author.username }}
</a>
{% else %}
<span class="url">{{ comment_item.author.username }}</span>
{% endif %}
</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>
<div class="comment-meta commentmetadata">
<div>{{ comment_item.creation_time }}</div>
{% if comment_item.author.parent_comment %}
<div>回复给: @{{ comment_item.author.parent_comment.username }}</div>
{% endif %}
</div>
</li><!-- #comment-## -->
<p>{{ comment_item.body|escape|comment_markdown }}</p>
<div class="reply">
<button type="button" class="comment-reply-button"
onclick="do_reply({{ comment_item.pk }})"
aria-label="回复给{{ comment_item.author.username }}">
回复
</button>
</div>
</div>
</li>
</ul>

@ -1,45 +1,48 @@
{% 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=""
src="{{ comment_item.author.email|gravatar_url:150 }}"
srcset="{{ comment_item.author.email|gravatar_url:150 }}"
class="avatar avatar-96 photo" height="96" width="96">
<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>
<ul class="comment-list" style="margin-left: {% widthratio depth 1 3 %}rem">
<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=""
src="{{ comment_item.author.email|gravatar_url:150 }}"
srcset="{{ comment_item.author.email|gravatar_url:150 }}"
class="avatar avatar-96 photo" height="96" width="96">
<cite class="fn">
{% if comment_item.author.is_superuser %}
<a href="{{ comment_item.author.get_absolute_url }}" rel="external nofollow" class="url">
{{ comment_item.author.username }}
</a>
{% else %}
<span class="url">{{ comment_item.author.username }}</span>
{% endif %}
</cite>
</div>
<div class="comment-meta commentmetadata">
{{ comment_item.creation_time }}
</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>回复 <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>
<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>
<div class="reply">
<button type="button" class="comment-reply-button"
data-pk="{{ comment_item.pk }}"
onclick="do_reply({{ comment_item.pk }})"
aria-label="回复给{{ comment_item.author.username }}">
回复
</button>
</div>
</div>
</li>
</ul>
</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" %}
@ -51,4 +54,4 @@
{% endwith %}
{% endif %}
{% endwith %}
{% endfor %}
{% endfor %}

@ -1,15 +1,17 @@
<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 id="reply-title" class="comment-reply-title">
发表评论
<small>
<button type="button" id="cancel-comment-reply-link-top" style="display:none;"
onclick="cancel_reply()" aria-label="取消回复">取消回复</button>
</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>
@ -22,7 +24,7 @@
{% 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>
<button type="button" id="cancel-comment-reply-link-bottom" onclick="cancel_reply()" aria-label="取消回复">取消回复</button>
</small>
</div>
</form>
@ -30,4 +32,30 @@
</div><!-- #comments .comments-area -->
<script type="text/javascript">
function cancel_reply() {
// 隐藏底部取消回复按钮
var cancelLinkBottom = document.getElementById('cancel-comment-reply-link-bottom');
if (cancelLinkBottom) {
cancelLinkBottom.style.display = 'none';
}
// 隐藏顶部取消回复按钮
var cancelLinkTop = document.getElementById('cancel-comment-reply-link-top');
if (cancelLinkTop) {
cancelLinkTop.style.display = 'none';
}
// 清空 parent_comment_id
var parentInput = document.querySelector('input[name="parent_comment_id"]');
if (parentInput) {
parentInput.value = '';
}
// 重置回复标题
var replyTitle = document.getElementById('reply-title');
if (replyTitle) {
replyTitle.innerHTML = '发表评论<small><button type="button" id="cancel-comment-reply-link-top" style="display:none;" onclick="cancel_reply()" aria-label="取消回复">取消回复</button></small>';
}
}
</script>

@ -1,37 +1,34 @@
<!DOCTYPE html>
<html>
<html lang="zh-CN" xml:lang="zh-CN">
<head>
{% load static %}
{% load static compress %}
<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">
<link rel="icon" href="{% static '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 'account/css/account.css' %}" rel="stylesheet">
<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>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js" integrity="sha384-3fN0Ejh2zZbqT2KtrwQ6JfBp6F2U8ppxqNsnJ3y6U5YosjWQKpkPzZVjJ1sXn/tp" crossorigin="anonymous"></script>
<![endif]-->
</head>
@ -39,9 +36,9 @@
{% block content %}
{% endblock %}
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<!-- jQuery and account JS -->
<script src="{% static 'blog/js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'account/js/account.js' %}"></script>
</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>
</html>

@ -1,19 +1,19 @@
<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>
{% load blog_tags %}
<ul class="menu">
<li id="menu-item-{{ node.pk }}"
class="menu-item menu-item-type-taxonomy menu-item-object-category {% if child_categorys %}menu-item-has-children{% endif %} menu-item-{{ node.pk }}">
<a href="{{ node.get_absolute_url }}">{{ node.name }}</a>
{% 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>
</ul>

Loading…
Cancel
Save