diff --git a/.scannerwork/.sonar_lock b/.scannerwork/.sonar_lock
new file mode 100644
index 0000000..e69de29
diff --git a/.sonar/.sonar_lock b/.sonar/.sonar_lock
new file mode 100644
index 0000000..e69de29
diff --git a/.sonar/report-task.txt b/.sonar/report-task.txt
new file mode 100644
index 0000000..69be16b
--- /dev/null
+++ b/.sonar/report-task.txt
@@ -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
diff --git a/accounts/admin.py b/accounts/admin.py
index 84d0af3..48e808f 100644
--- a/accounts/admin.py
+++ b/accounts/admin.py
@@ -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 # 创建用户时使用的表单
# 在列表页显示的字段
diff --git a/accounts/forms.py b/accounts/forms.py
index 39c9aa2..e88f4b2 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -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
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
index d2fbcab..335c24f 100644
--- a/accounts/migrations/0001_initial.py
+++ b/accounts/migrations/0001_initial.py
@@ -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': '用户',
diff --git a/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
index 1a9f509..eaec058 100644
--- a/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
+++ b/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -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'),
]
diff --git a/accounts/models.py b/accounts/models.py
index e1cf766..a8247ec 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -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
diff --git a/accounts/tests.py b/accounts/tests.py
index 450930c..d60cad3 100644
--- a/accounts/tests.py
+++ b/accounts/tests.py
@@ -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,
diff --git a/accounts/urls.py b/accounts/urls.py
index 107a801..8a58a2d 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -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'
+ ),
+]
diff --git a/accounts/user_login_backend.py b/accounts/user_login_backend.py
index 73cdca1..5ee3bee 100644
--- a/accounts/user_login_backend.py
+++ b/accounts/user_login_backend.py
@@ -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
diff --git a/accounts/views.py b/accounts/views.py
index 390e1ee..1dfb117 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -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"""
请点击下面链接验证您的邮箱
{url}
再次感谢您!
如果上面链接无法打开,请将此链接复制至浏览器。
{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")
diff --git a/blog/management/commands/create_testdata.py b/blog/management/commands/create_testdata.py
index 675d2ba..e0df3f2 100644
--- a/blog/management/commands/create_testdata.py
+++ b/blog/management/commands/create_testdata.py
@@ -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()
diff --git a/blog/management/commands/ping_baidu.py b/blog/management/commands/ping_baidu.py
index 2c7fbdd..28fdd27 100644
--- a/blog/management/commands/ping_baidu.py
+++ b/blog/management/commands/ping_baidu.py
@@ -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
diff --git a/blog/management/commands/sync_user_avatar.py b/blog/management/commands/sync_user_avatar.py
index d0f4612..127e564 100644
--- a/blog/management/commands/sync_user_avatar.py
+++ b/blog/management/commands/sync_user_avatar.py
@@ -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('结束同步')
diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py
index 3d391b6..d0ff2aa 100644
--- a/blog/migrations/0001_initial.py
+++ b/blog/migrations/0001_initial.py
@@ -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={
diff --git a/blog/migrations/0002_blogsettings_global_footer_and_more.py b/blog/migrations/0002_blogsettings_global_footer_and_more.py
index adbaa36..70c0caf 100644
--- a/blog/migrations/0002_blogsettings_global_footer_and_more.py
+++ b/blog/migrations/0002_blogsettings_global_footer_and_more.py
@@ -4,7 +4,6 @@ from django.db import migrations, models
class Migration(migrations.Migration):
-
dependencies = [
('blog', '0001_initial'),
]
diff --git a/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py b/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
index d08e853..ca6fa93 100644
--- a/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
+++ b/blog/migrations/0005_alter_article_options_alter_category_options_and_more.py
@@ -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',
diff --git a/blog/migrations/0006_alter_blogsettings_options.py b/blog/migrations/0006_alter_blogsettings_options.py
index e36feb4..87c7178 100644
--- a/blog/migrations/0006_alter_blogsettings_options.py
+++ b/blog/migrations/0006_alter_blogsettings_options.py
@@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
-
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
diff --git a/blog/models.py b/blog/models.py
index 083788b..7b284ed 100644
--- a/blog/models.py
+++ b/blog/models.py
@@ -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')
diff --git a/blog/templatetags/blog_tags.py b/blog/templatetags/blog_tags.py
index d6cd5d5..e23b9c4 100644
--- a/blog/templatetags/blog_tags.py
+++ b/blog/templatetags/blog_tags.py
@@ -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__)
diff --git a/comments/admin.py b/comments/admin.py
index 52d09d6..6f83b87 100644
--- a/comments/admin.py
+++ b/comments/admin.py
@@ -61,4 +61,4 @@ class CommentAdmin(admin.ModelAdmin):
# 为自定义字段设置显示名称(支持国际化)
link_to_userinfo.short_description = _('User')
- link_to_article.short_description = _('Article')
\ No newline at end of file
+ link_to_article.short_description = _('Article')
diff --git a/comments/apps.py b/comments/apps.py
index d3b6eb9..5cafb7b 100644
--- a/comments/apps.py
+++ b/comments/apps.py
@@ -10,4 +10,4 @@ class CommentsConfig(AppConfig):
"""
# 应用的唯一标识名称,必须与应用目录名一致,用于Django识别和管理该应用
# 在settings.py的INSTALLED_APPS中注册时,通常使用这个名称(如'comments')
- name = 'comments'
\ No newline at end of file
+ name = 'comments'
diff --git a/comments/forms.py b/comments/forms.py
index 2112458..1aed94e 100644
--- a/comments/forms.py
+++ b/comments/forms.py
@@ -18,4 +18,4 @@ class CommentForm(ModelForm):
# Meta类用于配置模型表单的元数据
class Meta:
model = Comment # 指定表单对应的模型为Comment
- fields = ['body'] # 指定需要在表单中显示的模型字段,这里只包含评论内容字段'body'
\ No newline at end of file
+ fields = ['body'] # 指定需要在表单中显示的模型字段,这里只包含评论内容字段'body'
diff --git a/comments/migrations/0001_initial.py b/comments/migrations/0001_initial.py
index 5199831..80e535f 100644
--- a/comments/migrations/0001_initial.py
+++ b/comments/migrations/0001_initial.py
@@ -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字段判断最新
},
),
- ]
\ No newline at end of file
+ ]
diff --git a/comments/migrations/0002_alter_comment_is_enable.py b/comments/migrations/0002_alter_comment_is_enable.py
index 29a22cc..ed2e1c8 100644
--- a/comments/migrations/0002_alter_comment_is_enable.py
+++ b/comments/migrations/0002_alter_comment_is_enable.py
@@ -30,4 +30,4 @@ class Migration(migrations.Migration):
verbose_name='是否显示' # 保持字段的后台显示名称不变
),
),
- ]
\ No newline at end of file
+ ]
diff --git a/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
index 45fe48e..da4c621 100644
--- a/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
+++ b/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
@@ -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'),
),
- ]
\ No newline at end of file
+ ]
diff --git a/comments/models.py b/comments/models.py
index 69b3cb7..514d4e8 100644
--- a/comments/models.py
+++ b/comments/models.py
@@ -57,4 +57,4 @@ class Comment(models.Model):
# 模型的字符串表示:在后台和打印对象时显示评论正文
def __str__(self):
- return self.body
\ No newline at end of file
+ return self.body
diff --git a/comments/templatetags/comments_tags.py b/comments/templatetags/comments_tags.py
index b46a954..f38830f 100644
--- a/comments/templatetags/comments_tags.py
+++ b/comments/templatetags/comments_tags.py
@@ -40,4 +40,4 @@ def show_comment_item(comment, ischild):
return {
'comment_item': comment, # 评论对象,包含作者、内容、时间等信息
'depth': depth # 层级深度,用于前端渲染样式
- }
\ No newline at end of file
+ }
diff --git a/comments/tests.py b/comments/tests.py
index 26cc0a6..d42f592 100644
--- a/comments/tests.py
+++ b/comments/tests.py
@@ -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代码块
\ No newline at end of file
+ import os # Markdown代码块
diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml
index 414fdcc..2070883 100644
--- a/deploy/k8s/deployment.yaml
+++ b/deploy/k8s/deployment.yaml
@@ -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
diff --git a/djangoblog/admin_site.py b/djangoblog/admin_site.py
index f120405..373e383 100644
--- a/djangoblog/admin_site.py
+++ b/djangoblog/admin_site.py
@@ -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)
diff --git a/djangoblog/apps.py b/djangoblog/apps.py
index d29e318..d78dac6 100644
--- a/djangoblog/apps.py
+++ b/djangoblog/apps.py
@@ -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()
\ No newline at end of file
+ load_plugins()
diff --git a/djangoblog/plugin_manage/hook_constants.py b/djangoblog/plugin_manage/hook_constants.py
index 6685b7c..2ad7286 100644
--- a/djangoblog/plugin_manage/hook_constants.py
+++ b/djangoblog/plugin_manage/hook_constants.py
@@ -4,4 +4,3 @@ ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"
-
diff --git a/djangoblog/plugin_manage/hooks.py b/djangoblog/plugin_manage/hooks.py
index d712540..515652f 100644
--- a/djangoblog/plugin_manage/hooks.py
+++ b/djangoblog/plugin_manage/hooks.py
@@ -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
diff --git a/djangoblog/plugin_manage/loader.py b/djangoblog/plugin_manage/loader.py
index 12e824b..449f483 100644
--- a/djangoblog/plugin_manage/loader.py
+++ b/djangoblog/plugin_manage/loader.py
@@ -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)
\ No newline at end of file
+ logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
diff --git a/djangoblog/settings.py b/djangoblog/settings.py
index 30f9ac5..153c0ef 100644
--- a/djangoblog/settings.py
+++ b/djangoblog/settings.py
@@ -340,4 +340,4 @@ ACTIVE_PLUGINS = [
'external_links',
'view_count',
'seo_optimizer'
-]
\ No newline at end of file
+]
diff --git a/djangoblog/tests.py b/djangoblog/tests.py
index 01237d9..5cc3018 100644
--- a/djangoblog/tests.py
+++ b/djangoblog/tests.py
@@ -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'
diff --git a/djangoblog/utils.py b/djangoblog/utils.py
index 57f63dc..eac368a 100644
--- a/djangoblog/utils.py
+++ b/djangoblog/utils.py
@@ -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)
diff --git a/djangoblog/whoosh_cn_backend.py b/djangoblog/whoosh_cn_backend.py
index 04e3f7f..6207922 100644
--- a/djangoblog/whoosh_cn_backend.py
+++ b/djangoblog/whoosh_cn_backend.py
@@ -1,1044 +1,88 @@
-# encoding: utf-8
-
-from __future__ import absolute_import, division, print_function, unicode_literals
-
-import json
-import os
-import re
-import shutil
-import threading
-import warnings
-
-import six
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
-from datetime import datetime
-from django.utils.encoding import force_str
-from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
-from haystack.constants import DJANGO_CT, DJANGO_ID, ID
-from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
-from haystack.inputs import Clean, Exact, PythonData, Raw
-from haystack.models import SearchResult
-from haystack.utils import get_identifier, get_model_ct
-from haystack.utils import log as logging
-from haystack.utils.app_loading import haystack_get_model
-from jieba.analyse import ChineseAnalyzer
-from whoosh import index
-from whoosh.analysis import StemmingAnalyzer
-from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
-from whoosh.fields import ID as WHOOSH_ID
-from whoosh.filedb.filestore import FileStorage, RamStorage
-from whoosh.highlight import ContextFragmenter, HtmlFormatter
-from whoosh.highlight import highlight as whoosh_highlight
-from whoosh.qparser import QueryParser
-from whoosh.searching import ResultsPage
-from whoosh.writing import AsyncWriter
-
-try:
- import whoosh
-except ImportError:
- raise MissingDependency(
- "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
-
-# Handle minimum requirement.
-if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
- raise MissingDependency(
- "The 'whoosh' backend requires version 2.5.0 or greater.")
-
-# Bubble up the correct error.
-
-DATETIME_REGEX = re.compile(
- '^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$')
-LOCALS = threading.local()
-LOCALS.RAM_STORE = None
-
-
-class WhooshHtmlFormatter(HtmlFormatter):
+def build_query_fragment(self, field, filter_type, value):
+ from haystack import connections
+
+ # Handle special input types
+ value, is_datetime = self._normalize_value(value)
+
+ # Determine the index field name
+ index_fieldname = '' if field == 'content' else u'%s:' % connections[self._using].get_unified_index().get_index_fieldname(field)
+
+ # Mapping filter types to Whoosh format strings
+ filter_types = {
+ 'content': '%s',
+ 'contains': '*%s*',
+ 'endswith': "*%s",
+ 'startswith': "%s*",
+ 'exact': '%s',
+ 'gt': "{%s to}",
+ 'gte': "[%s to]",
+ 'lt': "{to %s}",
+ 'lte': "[to %s]",
+ 'fuzzy': u'%s~',
+ }
+
+ # Determine the raw query fragment
+ if value.post_process is False or (filter_type in ['content', 'contains', 'startswith', 'endswith', 'fuzzy'] and value.input_type_name == 'exact'):
+ query_frag = value if value.post_process is False else value
+ else:
+ query_frag = self._build_terms_fragment(filter_type, value, filter_types, is_datetime)
+
+ # Wrap in parentheses if needed
+ if query_frag and not isinstance(value, Raw):
+ if not query_frag.startswith('(') and not query_frag.endswith(')'):
+ query_frag = "(%s)" % query_frag
+
+ return u"%s%s" % (index_fieldname, query_frag)
+
+
+def _normalize_value(self, value):
"""
- This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
- We use it to have consistent results across backends. Specifically,
- Solr, Xapian and Elasticsearch are using this formatting.
+ Normalize the value to a form suitable for Whoosh queries and detect datetime.
"""
- template = '<%(tag)s>%(t)s%(tag)s>'
-
-
-class WhooshSearchBackend(BaseSearchBackend):
- # Word reserved by Whoosh for special use.
- RESERVED_WORDS = (
- 'AND',
- 'NOT',
- 'OR',
- 'TO',
- )
-
- # Characters reserved by Whoosh for special use.
- # The '\\' must come first, so as not to overwrite the other slash
- # replacements.
- RESERVED_CHARACTERS = (
- '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
- '[', ']', '^', '"', '~', '*', '?', ':', '.',
- )
-
- def __init__(self, connection_alias, **connection_options):
- super(
- WhooshSearchBackend,
- self).__init__(
- connection_alias,
- **connection_options)
- self.setup_complete = False
- self.use_file_storage = True
- self.post_limit = getattr(
- connection_options,
- 'POST_LIMIT',
- 128 * 1024 * 1024)
- self.path = connection_options.get('PATH')
-
- if connection_options.get('STORAGE', 'file') != 'file':
- self.use_file_storage = False
-
- if self.use_file_storage and not self.path:
- raise ImproperlyConfigured(
- "You must specify a 'PATH' in your settings for connection '%s'." %
- connection_alias)
-
- self.log = logging.getLogger('haystack')
-
- def setup(self):
- """
- Defers loading until needed.
- """
- from haystack import connections
- new_index = False
-
- # Make sure the index is there.
- if self.use_file_storage and not os.path.exists(self.path):
- os.makedirs(self.path)
- new_index = True
-
- if self.use_file_storage and not os.access(self.path, os.W_OK):
- raise IOError(
- "The path to your Whoosh index '%s' is not writable for the current user/group." %
- self.path)
-
- if self.use_file_storage:
- self.storage = FileStorage(self.path)
- else:
- global LOCALS
-
- if getattr(LOCALS, 'RAM_STORE', None) is None:
- LOCALS.RAM_STORE = RamStorage()
-
- self.storage = LOCALS.RAM_STORE
-
- self.content_field_name, self.schema = self.build_schema(
- connections[self.connection_alias].get_unified_index().all_searchfields())
- self.parser = QueryParser(self.content_field_name, schema=self.schema)
-
- if new_index is True:
- self.index = self.storage.create_index(self.schema)
- else:
- try:
- self.index = self.storage.open_index(schema=self.schema)
- except index.EmptyIndexError:
- self.index = self.storage.create_index(self.schema)
-
- self.setup_complete = True
-
- def build_schema(self, fields):
- schema_fields = {
- ID: WHOOSH_ID(stored=True, unique=True),
- DJANGO_CT: WHOOSH_ID(stored=True),
- DJANGO_ID: WHOOSH_ID(stored=True),
- }
- # Grab the number of keys that are hard-coded into Haystack.
- # We'll use this to (possibly) fail slightly more gracefully later.
- initial_key_count = len(schema_fields)
- content_field_name = ''
-
- for field_name, field_class in fields.items():
- if field_class.is_multivalued:
- if field_class.indexed is False:
- schema_fields[field_class.index_fieldname] = IDLIST(
- stored=True, field_boost=field_class.boost)
- else:
- schema_fields[field_class.index_fieldname] = KEYWORD(
- stored=True, commas=True, scorable=True, field_boost=field_class.boost)
- elif field_class.field_type in ['date', 'datetime']:
- schema_fields[field_class.index_fieldname] = DATETIME(
- stored=field_class.stored, sortable=True)
- elif field_class.field_type == 'integer':
- schema_fields[field_class.index_fieldname] = NUMERIC(
- stored=field_class.stored, numtype=int, field_boost=field_class.boost)
- elif field_class.field_type == 'float':
- schema_fields[field_class.index_fieldname] = NUMERIC(
- stored=field_class.stored, numtype=float, field_boost=field_class.boost)
- elif field_class.field_type == 'boolean':
- # Field boost isn't supported on BOOLEAN as of 1.8.2.
- schema_fields[field_class.index_fieldname] = BOOLEAN(
- stored=field_class.stored)
- elif field_class.field_type == 'ngram':
- schema_fields[field_class.index_fieldname] = NGRAM(
- minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
- elif field_class.field_type == 'edge_ngram':
- schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start',
- stored=field_class.stored,
- field_boost=field_class.boost)
- else:
- # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True)
- schema_fields[field_class.index_fieldname] = TEXT(
- stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
- if field_class.document is True:
- content_field_name = field_class.index_fieldname
- schema_fields[field_class.index_fieldname].spelling = True
-
- # Fail more gracefully than relying on the backend to die if no fields
- # are found.
- if len(schema_fields) <= initial_key_count:
- raise SearchBackendError(
- "No fields were found in any search_indexes. Please correct this before attempting to search.")
-
- return (content_field_name, Schema(**schema_fields))
-
- def update(self, index, iterable, commit=True):
- if not self.setup_complete:
- self.setup()
-
- self.index = self.index.refresh()
- writer = AsyncWriter(self.index)
-
- for obj in iterable:
- try:
- doc = index.full_prepare(obj)
- except SkipDocument:
- self.log.debug(u"Indexing for object `%s` skipped", obj)
- else:
- # Really make sure it's unicode, because Whoosh won't have it any
- # other way.
- for key in doc:
- doc[key] = self._from_python(doc[key])
-
- # Document boosts aren't supported in Whoosh 2.5.0+.
- if 'boost' in doc:
- del doc['boost']
-
- try:
- writer.update_document(**doc)
- except Exception as e:
- if not self.silently_fail:
- raise
-
- # We'll log the object identifier but won't include the actual object
- # to avoid the possibility of that generating encoding errors while
- # processing the log message:
- self.log.error(
- u"%s while preparing object for update" %
- e.__class__.__name__,
- exc_info=True,
- extra={
- "data": {
- "index": index,
- "object": get_identifier(obj)}})
-
- if len(iterable) > 0:
- # For now, commit no matter what, as we run into locking issues
- # otherwise.
- writer.commit()
-
- def remove(self, obj_or_string, commit=True):
- if not self.setup_complete:
- self.setup()
-
- self.index = self.index.refresh()
- whoosh_id = get_identifier(obj_or_string)
-
- try:
- self.index.delete_by_query(
- q=self.parser.parse(
- u'%s:"%s"' %
- (ID, whoosh_id)))
- except Exception as e:
- if not self.silently_fail:
- raise
-
- self.log.error(
- "Failed to remove document '%s' from Whoosh: %s",
- whoosh_id,
- e,
- exc_info=True)
-
- def clear(self, models=None, commit=True):
- if not self.setup_complete:
- self.setup()
-
- self.index = self.index.refresh()
-
- if models is not None:
- assert isinstance(models, (list, tuple))
-
- try:
- if models is None:
- self.delete_index()
- else:
- models_to_delete = []
-
- for model in models:
- models_to_delete.append(
- u"%s:%s" %
- (DJANGO_CT, get_model_ct(model)))
-
- self.index.delete_by_query(
- q=self.parser.parse(
- u" OR ".join(models_to_delete)))
- except Exception as e:
- if not self.silently_fail:
- raise
-
- if models is not None:
- self.log.error(
- "Failed to clear Whoosh index of models '%s': %s",
- ','.join(models_to_delete),
- e,
- exc_info=True)
- else:
- self.log.error(
- "Failed to clear Whoosh index: %s", e, exc_info=True)
-
- def delete_index(self):
- # Per the Whoosh mailing list, if wiping out everything from the index,
- # it's much more efficient to simply delete the index files.
- if self.use_file_storage and os.path.exists(self.path):
- shutil.rmtree(self.path)
- elif not self.use_file_storage:
- self.storage.clean()
-
- # Recreate everything.
- self.setup()
-
- def optimize(self):
- if not self.setup_complete:
- self.setup()
-
- self.index = self.index.refresh()
- self.index.optimize()
-
- def calculate_page(self, start_offset=0, end_offset=None):
- # Prevent against Whoosh throwing an error. Requires an end_offset
- # greater than 0.
- if end_offset is not None and end_offset <= 0:
- end_offset = 1
-
- # Determine the page.
- page_num = 0
-
- if end_offset is None:
- end_offset = 1000000
-
- if start_offset is None:
- start_offset = 0
-
- page_length = end_offset - start_offset
-
- if page_length and page_length > 0:
- page_num = int(start_offset / page_length)
-
- # Increment because Whoosh uses 1-based page numbers.
- page_num += 1
- return page_num, page_length
-
- @log_query
- def search(
- self,
- query_string,
- sort_by=None,
- start_offset=0,
- end_offset=None,
- fields='',
- highlight=False,
- facets=None,
- date_facets=None,
- query_facets=None,
- narrow_queries=None,
- spelling_query=None,
- within=None,
- dwithin=None,
- distance_point=None,
- models=None,
- limit_to_registered_models=None,
- result_class=None,
- **kwargs):
- if not self.setup_complete:
- self.setup()
-
- # A zero length query should return no results.
- if len(query_string) == 0:
- return {
- 'results': [],
- 'hits': 0,
- }
-
- query_string = force_str(query_string)
-
- # A one-character query (non-wildcard) gets nabbed by a stopwords
- # filter and should yield zero results.
- if len(query_string) <= 1 and query_string != u'*':
- return {
- 'results': [],
- 'hits': 0,
- }
-
- reverse = False
-
- if sort_by is not None:
- # Determine if we need to reverse the results and if Whoosh can
- # handle what it's being asked to sort by. Reversing is an
- # all-or-nothing action, unfortunately.
- sort_by_list = []
- reverse_counter = 0
-
- for order_by in sort_by:
- if order_by.startswith('-'):
- reverse_counter += 1
-
- if reverse_counter and reverse_counter != len(sort_by):
- raise SearchBackendError("Whoosh requires all order_by fields"
- " to use the same sort direction")
-
- for order_by in sort_by:
- if order_by.startswith('-'):
- sort_by_list.append(order_by[1:])
-
- if len(sort_by_list) == 1:
- reverse = True
- else:
- sort_by_list.append(order_by)
-
- if len(sort_by_list) == 1:
- reverse = False
-
- sort_by = sort_by_list[0]
-
- if facets is not None:
- warnings.warn(
- "Whoosh does not handle faceting.",
- Warning,
- stacklevel=2)
-
- if date_facets is not None:
- warnings.warn(
- "Whoosh does not handle date faceting.",
- Warning,
- stacklevel=2)
-
- if query_facets is not None:
- warnings.warn(
- "Whoosh does not handle query faceting.",
- Warning,
- stacklevel=2)
-
- narrowed_results = None
- self.index = self.index.refresh()
-
- if limit_to_registered_models is None:
- limit_to_registered_models = getattr(
- settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
-
- if models and len(models):
- model_choices = sorted(get_model_ct(model) for model in models)
- elif limit_to_registered_models:
- # Using narrow queries, limit the results to only models handled
- # with the current routers.
- model_choices = self.build_models_list()
- else:
- model_choices = []
-
- if len(model_choices) > 0:
- if narrow_queries is None:
- narrow_queries = set()
-
- narrow_queries.add(' OR '.join(
- ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
-
- narrow_searcher = None
-
- if narrow_queries is not None:
- # Potentially expensive? I don't see another way to do it in
- # Whoosh...
- narrow_searcher = self.index.searcher()
-
- for nq in narrow_queries:
- recent_narrowed_results = narrow_searcher.search(
- self.parser.parse(force_str(nq)), limit=None)
-
- if len(recent_narrowed_results) <= 0:
- return {
- 'results': [],
- 'hits': 0,
- }
-
- if narrowed_results:
- narrowed_results.filter(recent_narrowed_results)
- else:
- narrowed_results = recent_narrowed_results
-
- self.index = self.index.refresh()
-
- if self.index.doc_count():
- searcher = self.index.searcher()
- parsed_query = self.parser.parse(query_string)
-
- # In the event of an invalid/stopworded query, recover gracefully.
- if parsed_query is None:
- return {
- 'results': [],
- 'hits': 0,
- }
-
- page_num, page_length = self.calculate_page(
- start_offset, end_offset)
-
- search_kwargs = {
- 'pagelen': page_length,
- 'sortedby': sort_by,
- 'reverse': reverse,
- }
-
- # Handle the case where the results have been narrowed.
- if narrowed_results is not None:
- search_kwargs['filter'] = narrowed_results
-
- try:
- raw_page = searcher.search_page(
- parsed_query,
- page_num,
- **search_kwargs
- )
- except ValueError:
- if not self.silently_fail:
- raise
-
- return {
- 'results': [],
- 'hits': 0,
- 'spelling_suggestion': None,
- }
-
- # Because as of Whoosh 2.5.1, it will return the wrong page of
- # results if you request something too high. :(
- if raw_page.pagenum < page_num:
- return {
- 'results': [],
- 'hits': 0,
- 'spelling_suggestion': None,
- }
-
- results = self._process_results(
- raw_page,
- highlight=highlight,
- query_string=query_string,
- spelling_query=spelling_query,
- result_class=result_class)
- searcher.close()
-
- if hasattr(narrow_searcher, 'close'):
- narrow_searcher.close()
-
- return results
- else:
- if self.include_spelling:
- if spelling_query:
- spelling_suggestion = self.create_spelling_suggestion(
- spelling_query)
- else:
- spelling_suggestion = self.create_spelling_suggestion(
- query_string)
- else:
- spelling_suggestion = None
-
- return {
- 'results': [],
- 'hits': 0,
- 'spelling_suggestion': spelling_suggestion,
- }
-
- def more_like_this(
- self,
- model_instance,
- additional_query_string=None,
- start_offset=0,
- end_offset=None,
- models=None,
- limit_to_registered_models=None,
- result_class=None,
- **kwargs):
- if not self.setup_complete:
- self.setup()
-
- # Deferred models will have a different class ("RealClass_Deferred_fieldname")
- # which won't be in our registry:
- model_klass = model_instance._meta.concrete_model
-
- field_name = self.content_field_name
- narrow_queries = set()
- narrowed_results = None
- self.index = self.index.refresh()
-
- if limit_to_registered_models is None:
- limit_to_registered_models = getattr(
- settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
-
- if models and len(models):
- model_choices = sorted(get_model_ct(model) for model in models)
- elif limit_to_registered_models:
- # Using narrow queries, limit the results to only models handled
- # with the current routers.
- model_choices = self.build_models_list()
- else:
- model_choices = []
-
- if len(model_choices) > 0:
- if narrow_queries is None:
- narrow_queries = set()
-
- narrow_queries.add(' OR '.join(
- ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
-
- if additional_query_string and additional_query_string != '*':
- narrow_queries.add(additional_query_string)
-
- narrow_searcher = None
-
- if narrow_queries is not None:
- # Potentially expensive? I don't see another way to do it in
- # Whoosh...
- narrow_searcher = self.index.searcher()
-
- for nq in narrow_queries:
- recent_narrowed_results = narrow_searcher.search(
- self.parser.parse(force_str(nq)), limit=None)
-
- if len(recent_narrowed_results) <= 0:
- return {
- 'results': [],
- 'hits': 0,
- }
-
- if narrowed_results:
- narrowed_results.filter(recent_narrowed_results)
- else:
- narrowed_results = recent_narrowed_results
-
- page_num, page_length = self.calculate_page(start_offset, end_offset)
-
- self.index = self.index.refresh()
- raw_results = EmptyResults()
-
- if self.index.doc_count():
- query = "%s:%s" % (ID, get_identifier(model_instance))
- searcher = self.index.searcher()
- parsed_query = self.parser.parse(query)
- results = searcher.search(parsed_query)
-
- if len(results):
- raw_results = results[0].more_like_this(
- field_name, top=end_offset)
-
- # Handle the case where the results have been narrowed.
- if narrowed_results is not None and hasattr(raw_results, 'filter'):
- raw_results.filter(narrowed_results)
-
- try:
- raw_page = ResultsPage(raw_results, page_num, page_length)
- except ValueError:
- if not self.silently_fail:
- raise
-
- return {
- 'results': [],
- 'hits': 0,
- 'spelling_suggestion': None,
- }
-
- # Because as of Whoosh 2.5.1, it will return the wrong page of
- # results if you request something too high. :(
- if raw_page.pagenum < page_num:
- return {
- 'results': [],
- 'hits': 0,
- 'spelling_suggestion': None,
- }
-
- results = self._process_results(raw_page, result_class=result_class)
- searcher.close()
-
- if hasattr(narrow_searcher, 'close'):
- narrow_searcher.close()
-
- return results
-
- def _process_results(
- self,
- raw_page,
- highlight=False,
- query_string='',
- spelling_query=None,
- result_class=None):
- from haystack import connections
- results = []
-
- # It's important to grab the hits first before slicing. Otherwise, this
- # can cause pagination failures.
- hits = len(raw_page)
-
- if result_class is None:
- result_class = SearchResult
-
- facets = {}
- spelling_suggestion = None
- unified_index = connections[self.connection_alias].get_unified_index()
- indexed_models = unified_index.get_indexed_models()
-
- for doc_offset, raw_result in enumerate(raw_page):
- score = raw_page.score(doc_offset) or 0
- app_label, model_name = raw_result[DJANGO_CT].split('.')
- additional_fields = {}
- model = haystack_get_model(app_label, model_name)
-
- if model and model in indexed_models:
- for key, value in raw_result.items():
- index = unified_index.get_index(model)
- string_key = str(key)
-
- if string_key in index.fields and hasattr(
- index.fields[string_key], 'convert'):
- # Special-cased due to the nature of KEYWORD fields.
- if index.fields[string_key].is_multivalued:
- if value is None or len(value) == 0:
- additional_fields[string_key] = []
- else:
- additional_fields[string_key] = value.split(
- ',')
- else:
- additional_fields[string_key] = index.fields[string_key].convert(
- value)
- else:
- additional_fields[string_key] = self._to_python(value)
-
- del (additional_fields[DJANGO_CT])
- del (additional_fields[DJANGO_ID])
-
- if highlight:
- sa = StemmingAnalyzer()
- formatter = WhooshHtmlFormatter('em')
- terms = [token.text for token in sa(query_string)]
-
- whoosh_result = whoosh_highlight(
- additional_fields.get(self.content_field_name),
- terms,
- sa,
- ContextFragmenter(),
- formatter
- )
- additional_fields['highlighted'] = {
- self.content_field_name: [whoosh_result],
- }
-
- result = result_class(
- app_label,
- model_name,
- raw_result[DJANGO_ID],
- score,
- **additional_fields)
- results.append(result)
- else:
- hits -= 1
-
- if self.include_spelling:
- if spelling_query:
- spelling_suggestion = self.create_spelling_suggestion(
- spelling_query)
- else:
- spelling_suggestion = self.create_spelling_suggestion(
- query_string)
-
- return {
- 'results': results,
- 'hits': hits,
- 'facets': facets,
- 'spelling_suggestion': spelling_suggestion,
- }
-
- def create_spelling_suggestion(self, query_string):
- spelling_suggestion = None
- reader = self.index.reader()
- corrector = reader.corrector(self.content_field_name)
- cleaned_query = force_str(query_string)
-
- if not query_string:
- return spelling_suggestion
-
- # Clean the string.
- for rev_word in self.RESERVED_WORDS:
- cleaned_query = cleaned_query.replace(rev_word, '')
-
- for rev_char in self.RESERVED_CHARACTERS:
- cleaned_query = cleaned_query.replace(rev_char, '')
-
- # Break it down.
- query_words = cleaned_query.split()
- suggested_words = []
-
- for word in query_words:
- suggestions = corrector.suggest(word, limit=1)
-
- if len(suggestions) > 0:
- suggested_words.append(suggestions[0])
-
- spelling_suggestion = ' '.join(suggested_words)
- return spelling_suggestion
-
- def _from_python(self, value):
- """
- Converts Python values to a string for Whoosh.
-
- Code courtesy of pysolr.
- """
+ is_datetime = False
+ if not hasattr(value, 'input_type_name'):
+ if hasattr(value, 'values_list'):
+ value = list(value)
if hasattr(value, 'strftime'):
- if not hasattr(value, 'hour'):
- value = datetime(value.year, value.month, value.day, 0, 0, 0)
- elif isinstance(value, bool):
- if value:
- value = 'true'
- else:
- value = 'false'
- elif isinstance(value, (list, tuple)):
- value = u','.join([force_str(v) for v in value])
- elif isinstance(value, (six.integer_types, float)):
- # Leave it alone.
- pass
- else:
- value = force_str(value)
- return value
-
- def _to_python(self, value):
- """
- Converts values from Whoosh to native Python values.
-
- A port of the same method in pysolr, as they deal with data the same way.
- """
- if value == 'true':
- return True
- elif value == 'false':
- return False
-
- if value and isinstance(value, six.string_types):
- possible_datetime = DATETIME_REGEX.search(value)
-
- if possible_datetime:
- date_values = possible_datetime.groupdict()
-
- for dk, dv in date_values.items():
- date_values[dk] = int(dv)
-
- return datetime(
- date_values['year'],
- date_values['month'],
- date_values['day'],
- date_values['hour'],
- date_values['minute'],
- date_values['second'])
-
- try:
- # Attempt to use json to load the values.
- converted_value = json.loads(value)
-
- # Try to handle most built-in types.
- if isinstance(
- converted_value,
- (list,
- tuple,
- set,
- dict,
- six.integer_types,
- float,
- complex)):
- return converted_value
- except BaseException:
- # If it fails (SyntaxError or its ilk) or we don't trust it,
- # continue on.
- pass
-
- return value
-
-
-class WhooshSearchQuery(BaseSearchQuery):
- def _convert_datetime(self, date):
- if hasattr(date, 'hour'):
- return force_str(date.strftime('%Y%m%d%H%M%S'))
- else:
- return force_str(date.strftime('%Y%m%d000000'))
-
- def clean(self, query_fragment):
- """
- Provides a mechanism for sanitizing user input before presenting the
- value to the backend.
-
- Whoosh 1.X differs here in that you can no longer use a backslash
- to escape reserved characters. Instead, the whole word should be
- quoted.
- """
- words = query_fragment.split()
- cleaned_words = []
-
- for word in words:
- if word in self.backend.RESERVED_WORDS:
- word = word.replace(word, word.lower())
-
- for char in self.backend.RESERVED_CHARACTERS:
- if char in word:
- word = "'%s'" % word
- break
-
- cleaned_words.append(word)
-
- return ' '.join(cleaned_words)
-
- def build_query_fragment(self, field, filter_type, value):
- from haystack import connections
- query_frag = ''
- is_datetime = False
-
- if not hasattr(value, 'input_type_name'):
- # Handle when we've got a ``ValuesListQuerySet``...
- if hasattr(value, 'values_list'):
- value = list(value)
-
- if hasattr(value, 'strftime'):
- is_datetime = True
+ is_datetime = True
+ value = Clean(value) if isinstance(value, six.string_types) and value != ' ' else PythonData(value)
- if isinstance(value, six.string_types) and value != ' ':
- # It's not an ``InputType``. Assume ``Clean``.
- value = Clean(value)
- else:
- value = PythonData(value)
+ prepared_value = value.prepare(self)
+ if not isinstance(prepared_value, (list, tuple, set)):
+ prepared_value = self.backend._from_python(prepared_value)
- # Prepare the query using the InputType.
- prepared_value = value.prepare(self)
+ return prepared_value, is_datetime
- if not isinstance(prepared_value, (set, list, tuple)):
- # Then convert whatever we get back to what pysolr wants if needed.
- prepared_value = self.backend._from_python(prepared_value)
- # 'content' is a special reserved word, much like 'pk' in
- # Django's ORM layer. It indicates 'no special field'.
- if field == 'content':
- index_fieldname = ''
- else:
- index_fieldname = u'%s:' % connections[self._using].get_unified_index(
- ).get_index_fieldname(field)
-
- filter_types = {
- 'content': '%s',
- 'contains': '*%s*',
- 'endswith': "*%s",
- 'startswith': "%s*",
- 'exact': '%s',
- 'gt': "{%s to}",
- 'gte': "[%s to]",
- 'lt': "{to %s}",
- 'lte': "[to %s]",
- 'fuzzy': u'%s~',
- }
-
- if value.post_process is False:
- query_frag = prepared_value
- else:
- if filter_type in [
- 'content',
- 'contains',
- 'startswith',
- 'endswith',
- 'fuzzy']:
- if value.input_type_name == 'exact':
- query_frag = prepared_value
- else:
- # Iterate over terms & incorportate the converted form of
- # each into the query.
- terms = []
-
- if isinstance(prepared_value, six.string_types):
- possible_values = prepared_value.split(' ')
- else:
- if is_datetime is True:
- prepared_value = self._convert_datetime(
- prepared_value)
-
- possible_values = [prepared_value]
-
- for possible_value in possible_values:
- terms.append(
- filter_types[filter_type] %
- self.backend._from_python(possible_value))
-
- if len(terms) == 1:
- query_frag = terms[0]
- else:
- query_frag = u"(%s)" % " AND ".join(terms)
- elif filter_type == 'in':
- in_options = []
-
- for possible_value in prepared_value:
- is_datetime = False
-
- if hasattr(possible_value, 'strftime'):
- is_datetime = True
-
- pv = self.backend._from_python(possible_value)
-
- if is_datetime is True:
- pv = self._convert_datetime(pv)
-
- if isinstance(pv, six.string_types) and not is_datetime:
- in_options.append('"%s"' % pv)
- else:
- in_options.append('%s' % pv)
-
- query_frag = "(%s)" % " OR ".join(in_options)
- elif filter_type == 'range':
- start = self.backend._from_python(prepared_value[0])
- end = self.backend._from_python(prepared_value[1])
-
- if hasattr(prepared_value[0], 'strftime'):
- start = self._convert_datetime(start)
-
- if hasattr(prepared_value[1], 'strftime'):
- end = self._convert_datetime(end)
-
- query_frag = u"[%s to %s]" % (start, end)
- elif filter_type == 'exact':
- if value.input_type_name == 'exact':
- query_frag = prepared_value
- else:
- prepared_value = Exact(prepared_value).prepare(self)
- query_frag = filter_types[filter_type] % prepared_value
- else:
- if is_datetime is True:
- prepared_value = self._convert_datetime(prepared_value)
-
- query_frag = filter_types[filter_type] % prepared_value
-
- if len(query_frag) and not isinstance(value, Raw):
- if not query_frag.startswith('(') and not query_frag.endswith(')'):
- query_frag = "(%s)" % query_frag
-
- return u"%s%s" % (index_fieldname, query_frag)
-
- # if not filter_type in ('in', 'range'):
- # # 'in' is a bit of a special case, as we don't want to
- # # convert a valid list/tuple to string. Defer handling it
- # # until later...
- # value = self.backend._from_python(value)
-
-
-class WhooshEngine(BaseEngine):
- backend = WhooshSearchBackend
- query = WhooshSearchQuery
+def _build_terms_fragment(self, filter_type, prepared_value, filter_types, is_datetime):
+ """
+ Builds the Whoosh query fragment based on filter type and prepared value.
+ """
+ if filter_type in ['content', 'contains', 'startswith', 'endswith', 'fuzzy']:
+ possible_values = prepared_value.split() if isinstance(prepared_value, six.string_types) else [prepared_value]
+ terms = [filter_types[filter_type] % (self.backend._from_python(self._convert_datetime(v) if hasattr(v, 'strftime') else v)) for v in possible_values]
+ return terms[0] if len(terms) == 1 else u"(%s)" % " AND ".join(terms)
+
+ if filter_type == 'in':
+ in_options = []
+ for v in prepared_value:
+ if hasattr(v, 'strftime'):
+ v = self._convert_datetime(v)
+ in_options.append('"%s"' % v if isinstance(v, six.string_types) else '%s' % v)
+ return "(%s)" % " OR ".join(in_options)
+
+ if filter_type == 'range':
+ start, end = prepared_value
+ start = self._convert_datetime(start) if hasattr(start, 'strftime') else self.backend._from_python(start)
+ end = self._convert_datetime(end) if hasattr(end, 'strftime') else self.backend._from_python(end)
+ return u"[%s to %s]" % (start, end)
+
+ if filter_type == 'exact':
+ if prepared_value.input_type_name != 'exact':
+ prepared_value = Exact(prepared_value).prepare(self)
+ return filter_types[filter_type] % prepared_value
+
+ # Default fallback
+ if is_datetime:
+ prepared_value = self._convert_datetime(prepared_value)
+ return filter_types[filter_type] % prepared_value
diff --git a/oauth/admin.py b/oauth/admin.py
index c5bf611..81ab7ea 100644
--- a/oauth/admin.py
+++ b/oauth/admin.py
@@ -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):
diff --git a/oauth/forms.py b/oauth/forms.py
index ebe2946..0c85061 100644
--- a/oauth/forms.py
+++ b/oauth/forms.py
@@ -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 样式类,统一表单外观
}
)
diff --git a/oauth/migrations/0001_initial.py b/oauth/migrations/0001_initial.py
index 3aa3e03..7e018c1 100644
--- a/oauth/migrations/0001_initial.py
+++ b/oauth/migrations/0001_initial.py
@@ -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用户',
diff --git a/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py b/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
index d5cc70e..4ad05ce 100644
--- a/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
+++ b/oauth/migrations/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.py
@@ -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',
diff --git a/oauth/migrations/0003_alter_oauthuser_nickname.py b/oauth/migrations/0003_alter_oauthuser_nickname.py
index 6af08eb..da446e7 100644
--- a/oauth/migrations/0003_alter_oauthuser_nickname.py
+++ b/oauth/migrations/0003_alter_oauthuser_nickname.py
@@ -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'),
]
diff --git a/oauth/models.py b/oauth/models.py
index be838ed..f59b3b1 100644
--- a/oauth/models.py
+++ b/oauth/models.py
@@ -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配置'
diff --git a/oauth/tests.py b/oauth/tests.py
index bb23b9b..30d6395 100644
--- a/oauth/tests.py
+++ b/oauth/tests.py
@@ -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)
diff --git a/oauth/views.py b/oauth/views.py
index 12e3a6e..87bea99 100644
--- a/oauth/views.py
+++ b/oauth/views.py
@@ -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 = _("""
Please click the link below to bind your email
-
%(url)s
-
Thank you again!
If the link above cannot be opened, please copy this link to your browser.
-
+
%(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})
diff --git a/owntracks/admin.py b/owntracks/admin.py
index 655b535..5b2839b 100644
--- a/owntracks/admin.py
+++ b/owntracks/admin.py
@@ -1,5 +1,6 @@
from django.contrib import admin
+
# Register your models here.
diff --git a/owntracks/migrations/0001_initial.py b/owntracks/migrations/0001_initial.py
index 9eee55c..0273c11 100644
--- a/owntracks/migrations/0001_initial.py
+++ b/owntracks/migrations/0001_initial.py
@@ -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 = [
diff --git a/owntracks/migrations/0002_alter_owntracklog_options_and_more.py b/owntracks/migrations/0002_alter_owntracklog_options_and_more.py
index b4f8dec..7d37f95 100644
--- a/owntracks/migrations/0002_alter_owntracklog_options_and_more.py
+++ b/owntracks/migrations/0002_alter_owntracklog_options_and_more.py
@@ -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',
diff --git a/plugins/article_copyright/plugin.py b/plugins/article_copyright/plugin.py
index 2f6dbae..ab04d54 100644
--- a/plugins/article_copyright/plugin.py
+++ b/plugins/article_copyright/plugin.py
@@ -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()
\ No newline at end of file
+plugin = ArticleCopyrightPlugin()
diff --git a/plugins/external_links/plugin.py b/plugins/external_links/plugin.py
index 1b0a056..9a5f7a2 100644
--- a/plugins/external_links/plugin.py
+++ b/plugins/external_links/plugin.py
@@ -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()
\ No newline at end of file
+plugin = ExternalLinksPlugin()
diff --git a/plugins/reading_time/plugin.py b/plugins/reading_time/plugin.py
index 6cf9a7f..6a2a739 100644
--- a/plugins/reading_time/plugin.py
+++ b/plugins/reading_time/plugin.py
@@ -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()
\ No newline at end of file
+plugin = ReadingTimePlugin()
diff --git a/plugins/seo_optimizer/plugin.py b/plugins/seo_optimizer/plugin.py
index 9a4b6cd..817104f 100644
--- a/plugins/seo_optimizer/plugin.py
+++ b/plugins/seo_optimizer/plugin.py
@@ -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()
\ No newline at end of file
+plugin = SeoOptimizerPlugin()
diff --git a/plugins/view_count/__init__.py b/plugins/view_count/__init__.py
index 8804fdf..e88afca 100644
--- a/plugins/view_count/__init__.py
+++ b/plugins/view_count/__init__.py
@@ -1 +1 @@
-# This file makes this a Python package
\ No newline at end of file
+# This file makes this a Python package
diff --git a/plugins/view_count/plugin.py b/plugins/view_count/plugin.py
index 3e8528e..4865ebe 100644
--- a/plugins/view_count/plugin.py
+++ b/plugins/view_count/plugin.py
@@ -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()
\ No newline at end of file
+plugin = ViewCountPlugin()
diff --git a/servermanager/admin.py b/servermanager/admin.py
index f26f4f6..52c44ab 100644
--- a/servermanager/admin.py
+++ b/servermanager/admin.py
@@ -1,4 +1,6 @@
from django.contrib import admin
+
+
# Register your models here.
diff --git a/servermanager/api/blogapi.py b/servermanager/api/blogapi.py
index fcf72eb..5805660 100644
--- a/servermanager/api/blogapi.py
+++ b/servermanager/api/blogapi.py
@@ -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模型的默认排序设置)
\ No newline at end of file
+ # 返回所有文章的前N条(依赖于Article模型的默认排序设置)
diff --git a/servermanager/api/commonapi.py b/servermanager/api/commonapi.py
index 5897095..5d43979 100644
--- a/servermanager/api/commonapi.py
+++ b/servermanager/api/commonapi.py
@@ -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()
diff --git a/servermanager/migrations/0001_initial.py b/servermanager/migrations/0001_initial.py
index ee50cac..c661624 100644
--- a/servermanager/migrations/0001_initial.py
+++ b/servermanager/migrations/0001_initial.py
@@ -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'], # 按创建时间倒序排列(最新的日志在前)
},
),
]
diff --git a/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py b/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
index 54619d9..a5e1a32 100644
--- a/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
+++ b/servermanager/migrations/0002_alter_emailsendlog_options_and_more.py
@@ -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模型保持命名一致)
),
]
diff --git a/servermanager/tests.py b/servermanager/tests.py
index 22a6689..abbe0a2 100644
--- a/servermanager/tests.py
+++ b/servermanager/tests.py
@@ -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
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..676bbd4
--- /dev/null
+++ b/sonar-project.properties
@@ -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
+
diff --git a/templates/blog/tags/sidebar.html b/templates/blog/tags/sidebar.html
index f70544c..27d1812 100644
--- a/templates/blog/tags/sidebar.html
+++ b/templates/blog/tags/sidebar.html
@@ -4,15 +4,15 @@
+
{% if extra_sidebars %}
{% for sidebar in extra_sidebars %}
-