diff --git a/.gitmodules.save b/.gitmodules.save
deleted file mode 100644
index 88c7c4f..0000000
--- a/.gitmodules.save
+++ /dev/null
@@ -1,13 +0,0 @@
-R
-^R
-
-
-
-
-
-
-:wq
-
-x
-
-
diff --git a/accounts--dyh/accounts质量分析.docx b/accounts--dyh/accounts质量分析.docx
deleted file mode 100644
index 50a57f2..0000000
Binary files a/accounts--dyh/accounts质量分析.docx and /dev/null differ
diff --git a/accounts--dyh/admin.py b/accounts--dyh/admin.py
deleted file mode 100644
index ae53414..0000000
--- a/accounts--dyh/admin.py
+++ /dev/null
@@ -1,103 +0,0 @@
-# 导入Django表单模块
-from django import forms
-# 导入Django默认用户管理类
-from django.contrib.auth.admin import UserAdmin
-# 导入用户修改表单
-from django.contrib.auth.forms import UserChangeForm
-# 导入用户名字段
-from django.contrib.auth.forms import UsernameField
-# 导入国际化翻译函数
-from django.utils.translation import gettext_lazy as _
-
-# 注册模型到管理后台
-# 导入自定义用户模型
-from .models import BlogUser
-
-
-class BlogUserCreationForm(forms.ModelForm):
- """自定义用户创建表单,用于管理员后台创建用户"""
-
- # 密码字段1 - 输入密码
- password1 = forms.CharField(
- label=_('password'), # 字段标签:密码
- widget=forms.PasswordInput # 使用密码输入控件
- )
- # 密码字段2 - 确认密码
- password2 = forms.CharField(
- label=_('Enter password again'), # 字段标签:再次输入密码
- widget=forms.PasswordInput # 使用密码输入控件
- )
-
- class Meta:
- # 指定使用的模型
- model = BlogUser
- # 表单包含的字段:仅邮箱
- fields = ('email',)
-
- def clean_password2(self):
- """清理和验证密码确认字段"""
- # 从已清理数据中获取密码1
- password1 = self.cleaned_data.get("password1")
- # 从已清理数据中获取密码2
- password2 = self.cleaned_data.get("password2")
- # 检查两个密码是否存在且匹配
- if password1 and password2 and password1 != password2:
- # 如果不匹配,抛出验证错误
- raise forms.ValidationError(_("passwords do not match"))
- # 返回验证通过的密码2
- return password2
-
- def save(self, commit=True):
- """保存用户实例,处理密码哈希"""
- # 调用父类save方法但不立即提交到数据库
- user = super().save(commit=False)
- # 使用Django的密码哈希方法设置密码
- user.set_password(self.cleaned_data["password1"])
- # 如果设置为立即提交
- if commit:
- # 设置用户来源为管理员站点
- user.source = 'adminsite'
- # 保存用户到数据库
- user.save()
- # 返回用户实例
- return user
-
-
-class BlogUserChangeForm(UserChangeForm):
- """自定义用户信息修改表单"""
-
- class Meta:
- # 指定使用的模型
- model = BlogUser
- # 包含所有字段
- fields = '__all__'
- # 字段类映射,用户名使用特定字段类
- field_classes = {'username': UsernameField}
-
- def __init__(self, *args, **kwargs):
- """初始化表单"""
- # 调用父类初始化方法
- super().__init__(*args, **kwargs)
-
-
-class BlogUserAdmin(UserAdmin):
- """自定义用户管理类,配置Django管理后台的用户界面"""
-
- # 指定修改用户时使用的表单
- form = BlogUserChangeForm
- # 指定创建用户时使用的表单
- add_form = BlogUserCreationForm
- # 列表页面显示的字段
- list_display = (
- 'id', # 用户ID
- 'nickname', # 昵称
- 'username', # 用户名
- 'email', # 邮箱
- 'last_login', # 最后登录时间
- 'date_joined', # 注册时间
- 'source' # 用户来源
- )
- # 列表中可作为链接点击的字段
- list_display_links = ('id', 'username')
- # 默认排序字段:按ID降序排列
- ordering = ('-id',)
\ No newline at end of file
diff --git a/accounts--dyh/apps.py b/accounts--dyh/apps.py
deleted file mode 100644
index 75f1ad1..0000000
--- a/accounts--dyh/apps.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# 导入Django应用配置基类
-from django.apps import AppConfig
-
-
-class AccountsConfig(AppConfig):
- """账户应用的配置类"""
-
- # 应用的名称(Python路径)
- name = 'accounts'
\ No newline at end of file
diff --git a/accounts--dyh/forms.py b/accounts--dyh/forms.py
deleted file mode 100644
index a7bada7..0000000
--- a/accounts--dyh/forms.py
+++ /dev/null
@@ -1,194 +0,0 @@
-# 导入Django表单模块
-from django import forms
-# 导入用户模型获取函数和密码验证工具
-from django.contrib.auth import get_user_model, password_validation
-# 导入Django内置认证表单
-from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
-# 导入验证异常
-from django.core.exceptions import ValidationError
-# 导入表单控件
-from django.forms import widgets
-# 导入国际化翻译函数
-from django.utils.translation import gettext_lazy as _
-# 导入工具函数
-from . import utils
-# 导入用户模型
-from .models import BlogUser
-
-
-class LoginForm(AuthenticationForm):
- """用户登录表单,继承自Django内置认证表单"""
-
- def __init__(self, *args, **kwargs):
- """初始化表单,自定义字段控件"""
- # 调用父类初始化方法
- super(LoginForm, self).__init__(*args, **kwargs)
- # 自定义用户名字段控件:文本输入框,带占位符和CSS类
- self.fields['username'].widget = widgets.TextInput(
- attrs={
- 'placeholder': "username", # 输入框占位符文本
- "class": "form-control" # CSS类名,用于样式
- }
- )
- # 自定义密码字段控件:密码输入框,带占位符和CSS类
- self.fields['password'].widget = widgets.PasswordInput(
- attrs={
- 'placeholder': "password", # 输入框占位符文本
- "class": "form-control" # CSS类名,用于样式
- }
- )
-
-
-class RegisterForm(UserCreationForm):
- """用户注册表单,继承自Django内置用户创建表单"""
-
- def __init__(self, *args, **kwargs):
- """初始化表单,自定义所有字段的控件"""
- # 调用父类初始化方法
- super(RegisterForm, self).__init__(*args, **kwargs)
- # 自定义用户名字段控件
- self.fields['username'].widget = widgets.TextInput(
- attrs={
- 'placeholder': "username", # 占位符:用户名
- "class": "form-control" # CSS类
- }
- )
- # 自定义邮箱字段控件
- self.fields['email'].widget = widgets.EmailInput(
- attrs={
- 'placeholder': "email", # 占位符:邮箱
- "class": "form-control" # CSS类
- }
- )
- # 自定义密码字段控件
- self.fields['password1'].widget = widgets.PasswordInput(
- attrs={
- 'placeholder': "password", # 占位符:密码
- "class": "form-control" # CSS类
- }
- )
- # 自定义密码确认字段控件
- self.fields['password2'].widget = widgets.PasswordInput(
- attrs={
- 'placeholder': "repeat password", # 占位符:重复密码
- "class": "form-control" # CSS类
- }
- )
-
- def clean_email(self):
- """清理和验证邮箱字段,确保邮箱唯一性"""
- # 从已清理数据中获取邮箱
- email = self.cleaned_data['email']
- # 检查数据库中是否已存在该邮箱
- if get_user_model().objects.filter(email=email).exists():
- # 如果邮箱已存在,抛出验证错误
- raise ValidationError(_("email already exists"))
- # 返回验证通过的邮箱
- return email
-
- class Meta:
- """表单元数据配置"""
- # 指定表单关联的模型
- model = get_user_model()
- # 表单包含的字段:用户名和邮箱
- fields = ("username", "email")
-
-
-class ForgetPasswordForm(forms.Form):
- """忘记密码重置表单"""
-
- # 新密码字段1
- new_password1 = forms.CharField(
- label=_("New password"), # 字段标签:新密码
- widget=forms.PasswordInput( # 使用密码输入控件
- attrs={
- "class": "form-control", # CSS类
- 'placeholder': _("New password") # 占位符:新密码
- }
- ),
- )
-
- # 新密码字段2 - 确认密码
- new_password2 = forms.CharField(
- label="确认密码", # 字段标签:确认密码(硬编码中文)
- widget=forms.PasswordInput( # 使用密码输入控件
- attrs={
- "class": "form-control", # CSS类
- 'placeholder': _("Confirm password") # 占位符:确认密码
- }
- ),
- )
-
- # 邮箱字段
- email = forms.EmailField(
- label='邮箱', # 字段标签:邮箱(硬编码中文)
- widget=forms.TextInput( # 使用文本输入控件
- attrs={
- 'class': 'form-control', # CSS类
- 'placeholder': _("Email") # 占位符:邮箱
- }
- ),
- )
-
- # 验证码字段
- code = forms.CharField(
- label=_('Code'), # 字段标签:验证码
- widget=forms.TextInput( # 使用文本输入控件
- attrs={
- 'class': 'form-control', # CSS类
- 'placeholder': _("Code") # 占位符:验证码
- }
- ),
- )
-
- def clean_new_password2(self):
- """清理和验证密码确认字段"""
- # 从原始数据中获取新密码1(不使用cleaned_data因为可能还未验证)
- password1 = self.data.get("new_password1")
- # 从原始数据中获取新密码2
- password2 = self.data.get("new_password2")
- # 检查两个密码是否存在且匹配
- if password1 and password2 and password1 != password2:
- # 如果不匹配,抛出验证错误
- raise ValidationError(_("passwords do not match"))
- # 使用Django内置密码验证器验证密码强度
- password_validation.validate_password(password2)
- # 返回验证通过的密码
- return password2
-
- def clean_email(self):
- """清理和验证邮箱字段,确保邮箱已注册"""
- # 从已清理数据中获取邮箱
- user_email = self.cleaned_data.get("email")
- # 检查数据库中是否存在该邮箱的用户
- if not BlogUser.objects.filter(email=user_email).exists():
- # 安全提示:这里会暴露邮箱是否注册,可根据安全需求修改
- # 如果邮箱不存在,抛出验证错误
- raise ValidationError(_("email does not exist"))
- # 返回验证通过的邮箱
- return user_email
-
- def clean_code(self):
- """清理和验证验证码字段"""
- # 从已清理数据中获取验证码
- code = self.cleaned_data.get("code")
- # 调用工具函数验证验证码是否正确
- error = utils.verify(
- email=self.cleaned_data.get("email"), # 传入邮箱
- code=code, # 传入验证码
- )
- # 如果验证返回错误信息
- if error:
- # 抛出验证错误
- raise ValidationError(error)
- # 返回验证通过的验证码
- return code
-
-
-class ForgetPasswordCodeForm(forms.Form):
- """获取忘记密码验证码的表单"""
-
- # 邮箱字段
- email = forms.EmailField(
- label=_('Email'), # 字段标签:邮箱
- )
\ No newline at end of file
diff --git a/accounts--dyh/models.py b/accounts--dyh/models.py
deleted file mode 100644
index 0ec9c5c..0000000
--- a/accounts--dyh/models.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# 导入Django抽象用户基类
-from django.contrib.auth.models import AbstractUser
-# 导入Django数据库模型
-from django.db import models
-# 导入URL反向解析函数
-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
-
-
-# 在这里创建模型
-
-class BlogUser(AbstractUser):
- """自定义用户模型,继承自Django抽象用户基类"""
-
- # 昵称字段,最大长度100字符,允许为空
- nickname = models.CharField(
- _('nick name'), # 字段显示名称:昵称
- max_length=100, # 最大长度
- blank=True # 允许为空
- )
- # 创建时间字段,默认值为当前时间
- creation_time = models.DateTimeField(
- _('creation time'), # 字段显示名称:创建时间
- default=now # 默认值:当前时间
- )
- # 最后修改时间字段,默认值为当前时间
- last_modify_time = models.DateTimeField(
- _('last modify time'), # 字段显示名称:最后修改时间
- default=now # 默认值:当前时间
- )
- # 用户来源字段,最大长度100字符,允许为空
- source = models.CharField(
- _('create source'), # 字段显示名称:创建来源
- max_length=100, # 最大长度
- blank=True # 允许为空
- )
-
- def get_absolute_url(self):
- """获取用户的绝对URL,用于生成用户详情页链接"""
- # 使用reverse反向解析URL,传入用户名作为参数
- return reverse(
- 'blog:author_detail', # URL模式名称
- kwargs={'author_name': self.username} # URL参数:作者用户名
- )
-
- def __str__(self):
- """对象的字符串表示,返回邮箱地址"""
- return self.email
-
- def get_full_url(self):
- """获取用户的完整URL(包含域名)"""
- # 获取当前站点域名
- site = get_current_site().domain
- # 构建完整URL:https://域名 + 用户详情页路径
- url = "https://{site}{path}".format(
- site=site, # 站点域名
- path=self.get_absolute_url() # 用户详情页路径
- )
- # 返回完整URL
- return url
-
- class Meta:
- """模型的元数据配置"""
- ordering = ['-id'] # 默认排序:按ID降序排列
- verbose_name = _('user') # 单数显示名称:用户
- verbose_name_plural = verbose_name # 复数显示名称:与单数相同
- get_latest_by = 'id' # 获取最新记录的依据字段:ID
\ No newline at end of file
diff --git a/accounts--dyh/tests.py b/accounts--dyh/tests.py
deleted file mode 100644
index 32c2b7e..0000000
--- a/accounts--dyh/tests.py
+++ /dev/null
@@ -1,294 +0,0 @@
-# 导入Django测试客户端、请求工厂、测试用例
-from django.test import Client, RequestFactory, TestCase
-# 导入URL反向解析
-from django.urls import reverse
-# 导入时区工具
-from django.utils import timezone
-# 导入延迟翻译函数
-from django.utils.translation import gettext_lazy as _
-
-# 导入账户模型
-from accounts.models import BlogUser
-# 导入博客模型
-from blog.models import Article, Category
-# 导入项目工具函数
-from djangoblog.utils import *
-# 导入当前应用的工具函数
-from . import utils
-
-
-# 在这里创建测试
-
-class AccountTest(TestCase):
- """账户功能测试类"""
-
- def setUp(self):
- """测试前置设置,每个测试方法执行前都会调用"""
- # 创建测试客户端
- self.client = Client()
- # 创建请求工厂
- self.factory = RequestFactory()
- # 创建测试用户
- self.blog_user = BlogUser.objects.create_user(
- username="test", # 用户名
- email="admin@admin.com", # 邮箱
- password="12345678" # 密码
- )
- # 设置新测试密码
- self.new_test = "xxx123--="
-
- def test_validate_account(self):
- """测试账户验证功能"""
- # 获取当前站点
- site = get_current_site().domain
- # 创建超级用户
- user = BlogUser.objects.create_superuser(
- email="liangliangyy1@gmail.com", # 邮箱
- username="liangliangyy1", # 用户名
- password="qwer!@#$ggg" # 密码
- )
- # 从数据库获取刚创建的用户
- testuser = BlogUser.objects.get(username='liangliangyy1')
-
- # 尝试登录
- loginresult = self.client.login(
- username='liangliangyy1', # 用户名
- password='qwer!@#$ggg' # 密码
- )
- # 断言登录成功
- self.assertEqual(loginresult, True)
- # 访问管理员页面
- response = self.client.get('/admin/')
- # 断言页面访问成功
- self.assertEqual(response.status_code, 200)
-
- # 创建分类
- category = Category()
- category.name = "categoryaaa" # 分类名称
- category.creation_time = timezone.now() # 创建时间
- category.last_modify_time = timezone.now() # 最后修改时间
- category.save()
-
- # 创建文章
- article = Article()
- article.title = "nicetitleaaa" # 文章标题
- article.body = "nicecontentaaa" # 文章内容
- article.author = user # 文章作者
- article.category = category # 文章分类
- article.type = 'a' # 文章类型
- article.status = 'p' # 文章状态:发布
- article.save()
-
- # 访问文章管理页面
- response = self.client.get(article.get_admin_url())
- # 断言页面访问成功
- self.assertEqual(response.status_code, 200)
-
- def test_validate_register(self):
- """测试用户注册功能"""
- # 断言注册前邮箱不存在
- self.assertEquals(
- 0, len(BlogUser.objects.filter(email='user123@user.com')))
- # 发送注册POST请求
- response = self.client.post(reverse('account:register'), {
- 'username': 'user1233', # 用户名
- 'email': 'user123@user.com', # 邮箱
- 'password1': 'password123!q@wE#R$T', # 密码
- 'password2': 'password123!q@wE#R$T', # 确认密码
- })
- # 断言注册后邮箱存在
- self.assertEquals(
- 1, len(BlogUser.objects.filter(email='user123@user.com')))
- # 获取刚注册的用户
- user = BlogUser.objects.filter(email='user123@user.com')[0]
- # 生成验证签名
- sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
- # 构建验证URL路径
- path = reverse('accounts:result')
- # 构建完整验证URL
- url = '{path}?type=validation&id={id}&sign={sign}'.format(
- path=path, # 路径
- id=user.id, # 用户ID
- sign=sign # 签名
- )
- # 访问验证URL
- response = self.client.get(url)
- # 断言页面访问成功
- self.assertEqual(response.status_code, 200)
-
- # 登录用户
- self.client.login(username='user1233', password='password123!q@wE#R$T')
- # 获取用户并设置为管理员
- user = BlogUser.objects.filter(email='user123@user.com')[0]
- user.is_superuser = True # 设置为超级用户
- user.is_staff = True # 设置为工作人员
- user.save()
- # 删除侧边栏缓存
- delete_sidebar_cache()
-
- # 创建分类
- category = Category()
- category.name = "categoryaaa" # 分类名称
- category.creation_time = timezone.now() # 创建时间
- category.last_modify_time = timezone.now() # 最后修改时间
- category.save()
-
- # 创建文章
- article = Article()
- article.category = category # 文章分类
- article.title = "nicetitle333" # 文章标题
- article.body = "nicecontentttt" # 文章内容
- article.author = user # 文章作者
- article.type = 'a' # 文章类型
- article.status = 'p' # 文章状态:发布
- article.save()
-
- # 访问文章管理页面
- response = self.client.get(article.get_admin_url())
- # 断言页面访问成功
- self.assertEqual(response.status_code, 200)
-
- # 登出用户
- response = self.client.get(reverse('account:logout'))
- # 断言登出成功(重定向状态码)
- self.assertIn(response.status_code, [301, 302, 200])
-
- # 再次访问文章管理页面(应该被重定向到登录页)
- response = self.client.get(article.get_admin_url())
- # 断言被重定向
- self.assertIn(response.status_code, [301, 302, 200])
-
- # 使用错误密码尝试登录
- response = self.client.post(reverse('account:login'), {
- 'username': 'user1233', # 用户名
- 'password': 'password123' # 错误密码
- })
- # 断言登录失败(重定向状态码)
- self.assertIn(response.status_code, [301, 302, 200])
-
- # 再次访问文章管理页面(应该仍然被重定向)
- response = self.client.get(article.get_admin_url())
- # 断言被重定向
- self.assertIn(response.status_code, [301, 302, 200])
-
- def test_verify_email_code(self):
- """测试邮箱验证码功能"""
- # 测试邮箱
- to_email = "admin@admin.com"
- # 生成验证码
- code = generate_code()
- # 设置验证码到缓存
- utils.set_code(to_email, code)
- # 发送验证邮件
- utils.send_verify_email(to_email, code)
-
- # 验证正确验证码
- err = utils.verify("admin@admin.com", code)
- # 断言验证成功(返回None)
- self.assertEqual(err, None)
-
- # 验证错误验证码
- err = utils.verify("admin@123.com", code)
- # 断言验证失败(返回错误信息字符串)
- self.assertEqual(type(err), str)
-
- def test_forget_password_email_code_success(self):
- """测试成功获取忘记密码验证码"""
- # 发送获取验证码的POST请求
- resp = self.client.post(
- path=reverse("account:forget_password_code"), # URL路径
- data=dict(email="admin@admin.com") # 请求数据:邮箱
- )
-
- # 断言响应状态码为200
- self.assertEqual(resp.status_code, 200)
- # 断言响应内容为"ok"
- self.assertEqual(resp.content.decode("utf-8"), "ok")
-
- def test_forget_password_email_code_fail(self):
- """测试获取忘记密码验证码失败情况"""
- # 发送空数据的POST请求
- resp = self.client.post(
- path=reverse("account:forget_password_code"), # URL路径
- data=dict() # 空数据
- )
- # 断言返回错误信息
- self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
-
- # 发送无效邮箱的POST请求
- resp = self.client.post(
- path=reverse("account:forget_password_code"), # URL路径
- data=dict(email="admin@com") # 无效邮箱格式
- )
- # 断言返回错误信息
- self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
-
- def test_forget_password_email_success(self):
- """测试成功重置密码"""
- # 生成验证码
- code = generate_code()
- # 设置验证码到缓存
- utils.set_code(self.blog_user.email, code)
- # 准备请求数据
- data = dict(
- new_password1=self.new_test, # 新密码
- new_password2=self.new_test, # 确认密码
- email=self.blog_user.email, # 邮箱
- code=code, # 验证码
- )
- # 发送重置密码的POST请求
- resp = self.client.post(
- path=reverse("account:forget_password"), # URL路径
- data=data # 请求数据
- )
- # 断言重定向响应(状态码302)
- self.assertEqual(resp.status_code, 302)
-
- # 验证用户密码是否修改成功
- blog_user = BlogUser.objects.filter(
- email=self.blog_user.email,
- ).first() # 类型注解:BlogUser
- # 断言用户存在
- self.assertNotEqual(blog_user, None)
- # 断言新密码验证通过
- self.assertEqual(blog_user.check_password(data["new_password1"]), True)
-
- def test_forget_password_email_not_user(self):
- """测试重置密码时邮箱不存在的情况"""
- # 准备请求数据(不存在的邮箱)
- data = dict(
- new_password1=self.new_test, # 新密码
- new_password2=self.new_test, # 确认密码
- email="123@123.com", # 不存在的邮箱
- code="123456", # 验证码
- )
- # 发送重置密码的POST请求
- resp = self.client.post(
- path=reverse("account:forget_password"), # URL路径
- data=data # 请求数据
- )
-
- # 断言返回表单页面(状态码200)
- self.assertEqual(resp.status_code, 200)
-
- def test_forget_password_email_code_error(self):
- """测试重置密码时验证码错误的情况"""
- # 生成验证码
- code = generate_code()
- # 设置验证码到缓存
- utils.set_code(self.blog_user.email, code)
- # 准备请求数据(错误的验证码)
- data = dict(
- new_password1=self.new_test, # 新密码
- new_password2=self.new_test, # 确认密码
- email=self.blog_user.email, # 邮箱
- code="111111", # 错误的验证码
- )
- # 发送重置密码的POST请求
- resp = self.client.post(
- path=reverse("account:forget_password"), # URL路径
- data=data # 请求数据
- )
-
- # 断言返回表单页面(状态码200)
- self.assertEqual(resp.status_code, 200)
\ No newline at end of file
diff --git a/accounts--dyh/urls.py b/accounts--dyh/urls.py
deleted file mode 100644
index 4374124..0000000
--- a/accounts--dyh/urls.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# 导入URL路径函数
-from django.urls import path
-# 导入正则URL路径函数(兼容老版本)
-from django.urls import re_path
-
-# 导入当前应用的视图
-from . import views
-# 导入登录表单
-from .forms import LoginForm
-
-# 应用命名空间,用于URL反向解析
-app_name = "accounts"
-
-# URL模式列表
-urlpatterns = [
- # 登录URL - 使用正则表达式匹配 /login/ 路径
- re_path(r'^login/$',
- # 使用LoginView视图类,登录成功后重定向到首页
- views.LoginView.as_view(success_url='/'),
- name='login', # URL名称:login
- # 传入额外参数:指定认证表单类
- kwargs={'authentication_form': LoginForm}),
-
- # 注册URL - 使用正则表达式匹配 /register/ 路径
- re_path(r'^register/$',
- # 使用RegisterView视图类,注册成功后重定向到首页
- views.RegisterView.as_view(success_url="/"),
- name='register'), # URL名称:register
-
- # 登出URL - 使用正则表达式匹配 /logout/ 路径
- re_path(r'^logout/$',
- # 使用LogoutView视图类
- views.LogoutView.as_view(),
- name='logout'), # URL名称:logout
-
- # 账户结果页面URL - 使用path匹配固定路径
- path(r'account/result.html',
- # 使用account_result函数视图
- views.account_result,
- name='result'), # URL名称:result
-
- # 忘记密码URL - 使用正则表达式匹配 /forget_password/ 路径
- re_path(r'^forget_password/$',
- # 使用ForgetPasswordView视图类
- views.ForgetPasswordView.as_view(),
- name='forget_password'), # URL名称:forget_password
-
- # 获取忘记密码验证码URL - 使用正则表达式匹配 /forget_password_code/ 路径
- re_path(r'^forget_password_code/$',
- # 使用ForgetPasswordEmailCode视图类
- views.ForgetPasswordEmailCode.as_view(),
- name='forget_password_code'), # URL名称:forget_password_code
-]
\ No newline at end of file
diff --git a/accounts--dyh/user_login_backend.py b/accounts--dyh/user_login_backend.py
deleted file mode 100644
index 7179809..0000000
--- a/accounts--dyh/user_login_backend.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# 导入获取用户模型的函数
-from django.contrib.auth import get_user_model
-# 导入模型后端认证基类
-from django.contrib.auth.backends import ModelBackend
-
-
-class EmailOrUsernameModelBackend(ModelBackend):
- """
- 自定义认证后端,允许使用用户名或邮箱登录
- 继承自Django的ModelBackend
- """
-
- def authenticate(self, request, username=None, password=None, **kwargs):
- """用户认证方法"""
- # 检查用户名中是否包含@符号(判断是否为邮箱)
- if '@' in username:
- # 如果是邮箱,设置查询参数为邮箱
- kwargs = {'email': username}
- else:
- # 如果是用户名,设置查询参数为用户名
- kwargs = {'username': username}
- try:
- # 根据查询参数获取用户对象
- user = get_user_model().objects.get(**kwargs)
- # 检查密码是否正确
- if user.check_password(password):
- # 密码正确,返回用户对象
- return user
- # 捕获用户不存在的异常
- except get_user_model().DoesNotExist:
- # 用户不存在,返回None
- return None
-
- def get_user(self, username):
- """根据用户ID获取用户对象"""
- try:
- # 根据主键(用户ID)获取用户对象
- return get_user_model().objects.get(pk=username)
- # 捕获用户不存在的异常
- except get_user_model().DoesNotExist:
- # 用户不存在,返回None
- return None
\ No newline at end of file
diff --git a/accounts--dyh/utils.py b/accounts--dyh/utils.py
deleted file mode 100644
index 607b172..0000000
--- a/accounts--dyh/utils.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# 导入类型提示模块
-import typing
-# 导入时间间隔类
-from datetime import timedelta
-
-# 导入Django缓存框架
-from django.core.cache import cache
-# 导入国际化翻译函数
-from django.utils.translation import gettext
-# 导入延迟翻译函数
-from django.utils.translation import gettext_lazy as _
-
-# 导入发送邮件的工具函数
-from djangoblog.utils import send_email
-
-# 验证码有效期:5分钟
-_code_ttl = timedelta(minutes=5)
-
-
-def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
- """发送验证邮件,用于密码重置等场景
-
- Args:
- to_mail: 接收邮件的邮箱地址
- subject: 邮件主题,默认为"验证邮箱"
- code: 验证码内容
- """
- # 构建邮件HTML内容,包含验证码信息
- html_content = _(
- # 翻译文本:您正在重置密码,验证码是:{code},5分钟内有效,请妥善保管
- "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it "
- "properly"
- ) % {'code': code} # 将code插入到格式化字符串中
- # 调用发送邮件函数
- send_email([to_mail], subject, html_content)
-
-
-def verify(email: str, code: str) -> typing.Optional[str]:
- """验证验证码是否有效
-
- Args:
- email: 请求验证的邮箱地址
- code: 用户输入的验证码
-
- Return:
- 如果验证失败返回错误信息字符串,验证成功返回None
-
- Note:
- 这里的错误处理不太合理,应该采用raise抛出异常
- 否则调用方也需要对error进行处理
- """
- # 从缓存中获取该邮箱对应的验证码
- cache_code = get_code(email)
- # 比较缓存中的验证码和用户输入的验证码
- if cache_code != code:
- # 如果不匹配,返回错误信息
- return gettext("Verification code error")
- # 验证成功,返回None
-
-
-def set_code(email: str, code: str):
- """将验证码设置到缓存中"""
- # 使用cache.set方法,key为邮箱,value为验证码,设置过期时间
- cache.set(email, code, _code_ttl.seconds)
-
-
-def get_code(email: str) -> typing.Optional[str]:
- """从缓存中获取验证码"""
- # 使用cache.get方法,根据邮箱获取验证码
- return cache.get(email)
\ No newline at end of file
diff --git a/accounts--dyh/views.py b/accounts--dyh/views.py
deleted file mode 100644
index 35ceaf3..0000000
--- a/accounts--dyh/views.py
+++ /dev/null
@@ -1,327 +0,0 @@
-# 导入日志模块
-import logging
-# 导入延迟翻译函数
-from django.utils.translation import gettext_lazy as _
-# 导入Django设置
-from django.conf import settings
-# 导入Django认证框架
-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
-# 导入Django内置认证表单
-from django.contrib.auth.forms import AuthenticationForm
-# 导入密码哈希函数
-from django.contrib.auth.hashers import make_password
-# 导入HTTP响应重定向和禁止访问响应
-from django.http import HttpResponseRedirect, HttpResponseForbidden
-# 导入HTTP请求类型
-from django.http.request import HttpRequest
-# 导入HTTP响应类型
-from django.http.response import HttpResponse
-# 导入快捷函数:获取对象或404错误
-from django.shortcuts import get_object_or_404
-# 导入快捷函数:渲染模板
-from django.shortcuts import render
-# 导入URL反向解析
-from django.urls import reverse
-# 导入方法装饰器
-from django.utils.decorators import method_decorator
-# 导入URL安全验证函数
-from django.utils.http import url_has_allowed_host_and_scheme
-# 导入基于类的视图基类
-from django.views import View
-# 导入禁止缓存装饰器
-from django.views.decorators.cache import never_cache
-# 导入CSRF保护装饰器
-from django.views.decorators.csrf import csrf_protect
-# 导入敏感参数保护装饰器
-from django.views.decorators.debug import sensitive_post_parameters
-# 导入通用视图类
-from django.views.generic import FormView, RedirectView
-
-# 导入项目工具函数
-from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
-# 导入当前应用的工具函数
-from . import utils
-# 导入自定义表单
-from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
-# 导入用户模型
-from .models import BlogUser
-
-# 获取当前模块的日志器
-logger = logging.getLogger(__name__)
-
-
-# 在这里创建视图
-
-class RegisterView(FormView):
- """用户注册视图"""
-
- # 指定使用的表单类
- form_class = RegisterForm
- # 指定使用的模板
- template_name = 'account/registration_form.html'
-
- @method_decorator(csrf_protect) # CSRF保护装饰器
- def dispatch(self, *args, **kwargs):
- """处理请求分发"""
- # 调用父类的dispatch方法
- return super(RegisterView, self).dispatch(*args, **kwargs)
-
- def form_valid(self, form):
- """处理表单验证通过的情况"""
- # 检查表单是否有效
- if form.is_valid():
- # 保存表单数据但不提交到数据库(commit=False)
- user = form.save(False)
- # 设置用户为未激活状态(需要邮箱验证)
- user.is_active = False
- # 设置用户来源为注册页面
- user.source = 'Register'
- # 保存用户到数据库
- user.save(True)
- # 获取当前站点域名
- site = get_current_site().domain
- # 生成验证签名:对密钥+用户ID进行双重SHA256哈希
- sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
-
- # 如果是调试模式,使用本地地址
- if settings.DEBUG:
- site = '127.0.0.1:8000'
- # 获取结果页面的URL路径
- path = reverse('account:result')
- # 构建完整的验证URL
- url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
- site=site, # 站点地址
- path=path, # 结果页面路径
- id=user.id, # 用户ID
- sign=sign # 验证签名
- )
-
- # 构建邮件内容
- content = """
-
请点击下面链接验证您的邮箱
-
- {url}
-
- 再次感谢您!
-
- 如果上面链接无法打开,请将此链接复制至浏览器。
- {url}
- """.format(url=url)
- # 发送验证邮件
- send_email(
- emailto=[user.email], # 收件人邮箱
- title='验证您的电子邮箱', # 邮件标题
- content=content # 邮件内容
- )
-
- # 构建注册结果页面URL
- url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
- # 重定向到结果页面
- return HttpResponseRedirect(url)
- else:
- # 表单无效,重新渲染表单并显示错误
- return self.render_to_response({'form': form})
-
-
-class LogoutView(RedirectView):
- """用户登出视图"""
-
- # 登出后重定向的URL
- url = '/login/'
-
- @method_decorator(never_cache) # 禁止缓存装饰器
- def dispatch(self, request, *args, **kwargs):
- """处理请求分发"""
- # 调用父类的dispatch方法
- return super(LogoutView, self).dispatch(request, *args, **kwargs)
-
- def get(self, request, *args, **kwargs):
- """处理GET请求(登出操作)"""
- # 调用Django登出函数
- logout(request)
- # 删除侧边栏缓存
- delete_sidebar_cache()
- # 调用父类的get方法进行重定向
- return super(LogoutView, self).get(request, *args, **kwargs)
-
-
-class LoginView(FormView):
- """用户登录视图"""
-
- # 指定使用的表单类
- form_class = LoginForm
- # 指定使用的模板
- template_name = 'account/login.html'
- # 登录成功后的重定向URL
- success_url = '/'
- # 重定向字段名
- redirect_field_name = REDIRECT_FIELD_NAME
- # 登录会话保持时间:一个月(秒数)
- login_ttl = 2626560
-
- # 方法装饰器:保护敏感参数、CSRF保护、禁止缓存
- @method_decorator(sensitive_post_parameters('password'))
- @method_decorator(csrf_protect)
- @method_decorator(never_cache)
- def dispatch(self, request, *args, **kwargs):
- """处理请求分发"""
- # 调用父类的dispatch方法
- return super(LoginView, self).dispatch(request, *args, **kwargs)
-
- def get_context_data(self, **kwargs):
- """获取模板上下文数据"""
- # 从GET参数中获取重定向URL
- redirect_to = self.request.GET.get(self.redirect_field_name)
- # 如果重定向URL为空,设置为首页
- if redirect_to is None:
- redirect_to = '/'
- # 将重定向URL添加到上下文数据中
- kwargs['redirect_to'] = redirect_to
- # 调用父类方法获取基础上下文数据
- return super(LoginView, self).get_context_data(**kwargs)
-
- def form_valid(self, form):
- """处理表单验证通过的情况"""
- # 创建认证表单实例(使用POST数据)
- form = AuthenticationForm(data=self.request.POST, request=self.request)
-
- # 检查表单是否有效
- if form.is_valid():
- # 删除侧边栏缓存
- delete_sidebar_cache()
- # 记录日志
- logger.info(self.redirect_field_name)
-
- # 登录用户
- auth.login(self.request, form.get_user())
- # 如果用户选择了"记住我"
- if self.request.POST.get("remember"):
- # 设置会话过期时间为一个月
- self.request.session.set_expiry(self.login_ttl)
- # 调用父类的form_valid方法(会处理重定向)
- return super(LoginView, self).form_valid(form)
- else:
- # 表单无效,重新渲染表单并显示错误
- return self.render_to_response({'form': form})
-
- def get_success_url(self):
- """获取登录成功后的重定向URL"""
- # 从POST数据中获取重定向URL
- redirect_to = self.request.POST.get(self.redirect_field_name)
- # 验证重定向URL是否安全(同源策略)
- if not url_has_allowed_host_and_scheme(
- url=redirect_to,
- allowed_hosts=[self.request.get_host()]):
- # 如果不安全,使用默认的成功URL
- redirect_to = self.success_url
- # 返回重定向URL
- return redirect_to
-
-
-def account_result(request):
- """账户操作结果页面视图函数"""
- # 从GET参数获取操作类型
- type = request.GET.get('type')
- # 从GET参数获取用户ID
- id = request.GET.get('id')
-
- # 根据ID获取用户对象,如果不存在返回404
- user = get_object_or_404(get_user_model(), id=id)
- # 记录日志
- logger.info(type)
- # 如果用户已激活,重定向到首页
- if user.is_active:
- return HttpResponseRedirect('/')
- # 检查操作类型是否为注册或验证
- if type and type in ['register', 'validation']:
- # 如果是注册操作
- if type == 'register':
- # 设置注册成功的内容和标题
- content = '''
- 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。
- '''
- title = '注册成功'
- else:
- # 生成验证签名
- c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
- # 从GET参数获取签名
- sign = request.GET.get('sign')
- # 验证签名是否正确
- if sign != c_sign:
- # 签名错误,返回禁止访问
- return HttpResponseForbidden()
- # 激活用户账户
- user.is_active = True
- # 保存用户
- user.save()
- # 设置验证成功的内容和标题
- content = '''
- 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。
- '''
- title = '验证成功'
- # 渲染结果页面
- return render(request, 'account/result.html', {
- 'title': title, # 页面标题
- 'content': content # 页面内容
- })
- else:
- # 无效的操作类型,重定向到首页
- return HttpResponseRedirect('/')
-
-
-class ForgetPasswordView(FormView):
- """忘记密码重置视图"""
-
- # 指定使用的表单类
- form_class = ForgetPasswordForm
- # 指定使用的模板
- template_name = 'account/forget_password.html'
-
- def form_valid(self, form):
- """处理表单验证通过的情况"""
- # 检查表单是否有效
- if form.is_valid():
- # 根据邮箱获取用户对象
- blog_user = BlogUser.objects.filter(
- email=form.cleaned_data.get("email")
- ).get()
- # 使用新密码的哈希值更新用户密码
- blog_user.password = make_password(form.cleaned_data["new_password2"])
- # 保存用户
- blog_user.save()
- # 重定向到登录页面
- return HttpResponseRedirect('/login/')
- else:
- # 表单无效,重新渲染表单并显示错误
- return self.render_to_response({'form': form})
-
-
-class ForgetPasswordEmailCode(View):
- """获取忘记密码验证码的API视图"""
-
- def post(self, request: HttpRequest):
- """处理POST请求"""
- # 创建表单实例并验证数据
- form = ForgetPasswordCodeForm(request.POST)
- # 检查表单是否有效
- if not form.is_valid():
- # 表单无效,返回错误响应
- return HttpResponse("错误的邮箱")
- # 从已验证数据中获取邮箱
- to_email = form.cleaned_data["email"]
-
- # 生成验证码
- code = generate_code()
- # 发送验证邮件
- utils.send_verify_email(to_email, code)
- # 将验证码保存到缓存
- utils.set_code(to_email, code)
-
- # 返回成功响应
- return HttpResponse("ok")
\ No newline at end of file
diff --git a/accounts--dyh/__init__.py b/blog-lsh(优化后)/__init__.py
similarity index 100%
rename from accounts--dyh/__init__.py
rename to blog-lsh(优化后)/__init__.py
diff --git a/blog-lsh/admin.py b/blog-lsh(优化后)/admin.py
similarity index 52%
rename from blog-lsh/admin.py
rename to blog-lsh(优化后)/admin.py
index 0f61314..46c3420 100644
--- a/blog-lsh/admin.py
+++ b/blog-lsh(优化后)/admin.py
@@ -1,8 +1,3 @@
-"""
-LJX: Django后台管理配置模块
-负责blog应用中各模型在Django admin后台的显示和操作配置
-包括文章、分类、标签、友情链接、侧边栏等模型的后台管理界面设置
-"""
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
@@ -15,7 +10,6 @@ from .models import Article
class ArticleForm(forms.ModelForm):
- """LJX: 文章表单类,用于后台文章编辑"""
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
@@ -24,26 +18,21 @@ class ArticleForm(forms.ModelForm):
def makr_article_publish(modeladmin, request, queryset):
- """LJX: 批量发布文章的管理动作"""
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
- """LJX: 批量将文章设为草稿的管理动作"""
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
- """LJX: 批量关闭文章评论的管理动作"""
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
- """LJX: 批量开启文章评论的管理动作"""
queryset.update(comment_status='o')
-# LJX: 设置管理动作的描述信息
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
@@ -51,33 +40,31 @@ open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
- """LJX: 文章模型的后台管理配置"""
- list_per_page = 20 # LJX: 每页显示20条记录
- search_fields = ('body', 'title') # LJX: 可搜索的字段
- form = ArticleForm # LJX: 使用自定义表单
+ list_per_page = 20
+ search_fields = ('body', 'title')
+ form = ArticleForm
list_display = (
'id',
'title',
'author',
- 'link_to_category', # LJX: 自定义字段显示分类链接
+ 'link_to_category',
'creation_time',
'views',
'status',
'type',
'article_order')
- list_display_links = ('id', 'title') # LJX: 可点击的字段
- list_filter = ('status', 'type', 'category') # LJX: 右侧过滤器
- filter_horizontal = ('tags',) # LJX: 水平选择器用于多对多字段
- exclude = ('creation_time', 'last_modify_time') # LJX: 排除的字段
- view_on_site = True # LJX: 显示"在站点查看"按钮
- actions = [ # LJX: 可用的批量动作
+ list_display_links = ('id', 'title')
+ list_filter = ('status', 'type', 'category')
+ filter_horizontal = ('tags',)
+ exclude = ('creation_time', 'last_modify_time')
+ view_on_site = True
+ actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj):
- """LJX: 自定义方法,显示分类名称并链接到分类编辑页面"""
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'%s' % (link, obj.category.name))
@@ -85,20 +72,17 @@ class ArticlelAdmin(admin.ModelAdmin):
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
- """LJX: 重写获取表单方法,限制作者只能选择超级用户"""
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
- """LJX: 重写保存模型方法"""
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
- """LJX: 获取在站点查看的URL"""
if obj:
- url = obj.get_full_url() # LJX: 使用文章的完整URL
+ url = obj.get_full_url()
return url
else:
from djangoblog.utils import get_current_site
@@ -107,27 +91,22 @@ class ArticlelAdmin(admin.ModelAdmin):
class TagAdmin(admin.ModelAdmin):
- """LJX: 标签模型的后台管理配置"""
- exclude = ('slug', 'last_mod_time', 'creation_time') # LJX: 排除自动生成的字段
+ exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
- """LJX: 分类模型的后台管理配置"""
- list_display = ('name', 'parent_category', 'index') # LJX: 列表显示字段
- exclude = ('slug', 'last_mod_time', 'creation_time') # LJX: 排除自动生成的字段
+ list_display = ('name', 'parent_category', 'index')
+ exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
- """LJX: 友情链接模型的后台管理配置"""
- exclude = ('last_mod_time', 'creation_time') # LJX: 排除时间字段
+ exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
- """LJX: 侧边栏模型的后台管理配置"""
- list_display = ('name', 'content', 'is_enable', 'sequence') # LJX: 列表显示字段
- exclude = ('last_mod_time', 'creation_time') # LJX: 排除时间字段
+ list_display = ('name', 'content', 'is_enable', 'sequence')
+ exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
- """LJX: 博客设置模型的后台管理配置"""
- pass
\ No newline at end of file
+ pass
diff --git a/blog-lsh(优化后)/apps.py b/blog-lsh(优化后)/apps.py
new file mode 100644
index 0000000..7930587
--- /dev/null
+++ b/blog-lsh(优化后)/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ name = 'blog'
diff --git a/blog-lsh(优化后)/context_processors.py b/blog-lsh(优化后)/context_processors.py
new file mode 100644
index 0000000..73e3088
--- /dev/null
+++ b/blog-lsh(优化后)/context_processors.py
@@ -0,0 +1,43 @@
+import logging
+
+from django.utils import timezone
+
+from djangoblog.utils import cache, get_blog_setting
+from .models import Category, Article
+
+logger = logging.getLogger(__name__)
+
+
+def seo_processor(requests):
+ key = 'seo_processor'
+ value = cache.get(key)
+ if value:
+ return value
+ else:
+ logger.info('set processor cache.')
+ setting = get_blog_setting()
+ value = {
+ 'SITE_NAME': setting.site_name,
+ 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
+ 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
+ 'SITE_SEO_DESCRIPTION': setting.site_seo_description,
+ 'SITE_DESCRIPTION': setting.site_description,
+ 'SITE_KEYWORDS': setting.site_keywords,
+ 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
+ 'ARTICLE_SUB_LENGTH': setting.article_sub_length,
+ 'nav_category_list': Category.objects.all(),
+ 'nav_pages': Article.objects.filter(
+ type='p',
+ status='p'),
+ 'OPEN_SITE_COMMENT': setting.open_site_comment,
+ 'BEIAN_CODE': setting.beian_code,
+ 'ANALYTICS_CODE': setting.analytics_code,
+ "BEIAN_CODE_GONGAN": setting.gongan_beiancode,
+ "SHOW_GONGAN_CODE": setting.show_gongan_code,
+ "CURRENT_YEAR": timezone.now().year,
+ "GLOBAL_HEADER": setting.global_header,
+ "GLOBAL_FOOTER": setting.global_footer,
+ "COMMENT_NEED_REVIEW": setting.comment_need_review,
+ }
+ cache.set(key, value, 60 * 60 * 10)
+ return value
diff --git a/blog-lsh/documents.py b/blog-lsh(优化后)/documents.py
similarity index 56%
rename from blog-lsh/documents.py
rename to blog-lsh(优化后)/documents.py
index 2d56e39..0f1db7b 100644
--- a/blog-lsh/documents.py
+++ b/blog-lsh(优化后)/documents.py
@@ -1,8 +1,3 @@
-"""
-LJX: Elasticsearch文档定义模块
-定义Elasticsearch索引的文档结构和数据模型
-用于博客文章的全文搜索和性能监控数据的存储
-"""
import time
import elasticsearch.client
@@ -12,11 +7,9 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
-# LJX: 检查是否启用Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
- # LJX: 创建Elasticsearch连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@@ -26,9 +19,8 @@ if ELASTICSEARCH_ENABLED:
c = IngestClient(es)
try:
- c.get_pipeline('geoip') # LJX: 检查geoip管道是否存在
+ c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
- # LJX: 创建geoip处理管道,用于IP地址地理位置解析
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@@ -42,84 +34,72 @@ if ELASTICSEARCH_ENABLED:
class GeoIp(InnerDoc):
- """LJX: IP地理位置信息内嵌文档"""
- continent_name = Keyword() # LJX: 大洲名称
- country_iso_code = Keyword() # LJX: 国家ISO代码
- country_name = Keyword() # LJX: 国家名称
- location = GeoPoint() # LJX: 地理位置坐标
+ continent_name = Keyword()
+ country_iso_code = Keyword()
+ country_name = Keyword()
+ location = GeoPoint()
class UserAgentBrowser(InnerDoc):
- """LJX: 用户代理浏览器信息"""
- Family = Keyword() # LJX: 浏览器家族
- Version = Keyword() # LJX: 浏览器版本
+ Family = Keyword()
+ Version = Keyword()
class UserAgentOS(UserAgentBrowser):
- """LJX: 用户代理操作系统信息"""
pass
class UserAgentDevice(InnerDoc):
- """LJX: 用户代理设备信息"""
- Family = Keyword() # LJX: 设备家族
- Brand = Keyword() # LJX: 设备品牌
- Model = Keyword() # LJX: 设备型号
+ Family = Keyword()
+ Brand = Keyword()
+ Model = Keyword()
class UserAgent(InnerDoc):
- """LJX: 完整的用户代理信息"""
- browser = Object(UserAgentBrowser, required=False) # LJX: 浏览器信息
- os = Object(UserAgentOS, required=False) # LJX: 操作系统信息
- device = Object(UserAgentDevice, required=False) # LJX: 设备信息
- string = Text() # LJX: 原始用户代理字符串
- is_bot = Boolean() # LJX: 是否是爬虫
+ browser = Object(UserAgentBrowser, required=False)
+ os = Object(UserAgentOS, required=False)
+ device = Object(UserAgentDevice, required=False)
+ string = Text()
+ is_bot = Boolean()
class ElapsedTimeDocument(Document):
- """LJX: 性能监控耗时文档,记录页面加载时间等性能数据"""
- url = Keyword() # LJX: 请求URL
- time_taken = Long() # LJX: 耗时(毫秒)
- log_datetime = Date() # LJX: 日志时间
- ip = Keyword() # LJX: IP地址
- geoip = Object(GeoIp, required=False) # LJX: 地理位置信息
- useragent = Object(UserAgent, required=False) # LJX: 用户代理信息
+ url = Keyword()
+ time_taken = Long()
+ log_datetime = Date()
+ ip = Keyword()
+ geoip = Object(GeoIp, required=False)
+ useragent = Object(UserAgent, required=False)
class Index:
- """LJX: 索引配置"""
- name = 'performance' # LJX: 索引名称
+ name = 'performance'
settings = {
- "number_of_shards": 1, # LJX: 分片数量
- "number_of_replicas": 0 # LJX: 副本数量
+ "number_of_shards": 1,
+ "number_of_replicas": 0
}
class Meta:
- doc_type = 'ElapsedTime' # LJX: 文档类型
+ doc_type = 'ElapsedTime'
class ElaspedTimeDocumentManager:
- """LJX: 性能监控文档管理器"""
@staticmethod
def build_index():
- """LJX: 构建性能监控索引"""
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
- ElapsedTimeDocument.init() # LJX: 初始化索引
+ ElapsedTimeDocument.init()
@staticmethod
def delete_index():
- """LJX: 删除性能监控索引"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
- """LJX: 创建性能监控记录"""
ElaspedTimeDocumentManager.build_index()
- # LJX: 构建用户代理信息对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
@@ -136,13 +116,12 @@ class ElaspedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
- # LJX: 创建文档并保存,使用geoip管道处理IP地址
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
- 1000)) # LJX: 使用时间戳作为文档ID
+ 1000))
},
url=url,
time_taken=time_taken,
@@ -152,64 +131,57 @@ class ElaspedTimeDocumentManager:
class ArticleDocument(Document):
- """LJX: 博客文章搜索文档,用于全文搜索"""
- body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # LJX: 文章内容,使用IK分词器
- title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # LJX: 文章标题
- author = Object(properties={ # LJX: 作者信息
+ body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
+ author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
- category = Object(properties={ # LJX: 分类信息
+ category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
- tags = Object(properties={ # LJX: 标签信息
+ tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
- # LJX: 文章元数据字段
- pub_time = Date() # LJX: 发布时间
- status = Text() # LJX: 状态
- comment_status = Text() # LJX: 评论状态
- type = Text() # LJX: 类型
- views = Integer() # LJX: 浏览量
- article_order = Integer() # LJX: 文章排序
+ pub_time = Date()
+ status = Text()
+ comment_status = Text()
+ type = Text()
+ views = Integer()
+ article_order = Integer()
class Index:
- """LJX: 文章索引配置"""
- name = 'blog' # LJX: 索引名称
+ name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
- doc_type = 'Article' # LJX: 文档类型
+ doc_type = 'Article'
class ArticleDocumentManager():
- """LJX: 文章文档管理器,处理文章搜索索引的创建和更新"""
def __init__(self):
- self.create_index() # LJX: 初始化时创建索引
+ self.create_index()
def create_index(self):
- """LJX: 创建文章索引"""
ArticleDocument.init()
def delete_index(self):
- """LJX: 删除文章索引"""
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
- """LJX: 将文章对象转换为搜索文档对象"""
return [
ArticleDocument(
meta={
- 'id': article.id}, # LJX: 使用文章ID作为文档ID
+ 'id': article.id},
body=article.body,
title=article.title,
author={
@@ -221,7 +193,7 @@ class ArticleDocumentManager():
tags=[
{
'name': t.name,
- 'id': t.id} for t in article.tags.all()], # LJX: 转换标签列表
+ 'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
@@ -230,14 +202,12 @@ class ArticleDocumentManager():
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
- """LJX: 重建搜索索引"""
ArticleDocument.init()
- articles = articles if articles else Article.objects.all() # LJX: 如果没有指定文章,则使用所有文章
+ articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
for doc in docs:
- doc.save() # LJX: 保存所有文档到索引
+ doc.save()
def update_docs(self, docs):
- """LJX: 更新搜索文档"""
for doc in docs:
- doc.save()
\ No newline at end of file
+ doc.save()
diff --git a/blog-lsh(优化后)/forms.py b/blog-lsh(优化后)/forms.py
new file mode 100644
index 0000000..715be76
--- /dev/null
+++ b/blog-lsh(优化后)/forms.py
@@ -0,0 +1,19 @@
+import logging
+
+from django import forms
+from haystack.forms import SearchForm
+
+logger = logging.getLogger(__name__)
+
+
+class BlogSearchForm(SearchForm):
+ querydata = forms.CharField(required=True)
+
+ def search(self):
+ datas = super(BlogSearchForm, self).search()
+ if not self.is_valid():
+ return self.no_query_found()
+
+ if self.cleaned_data['querydata']:
+ logger.info(self.cleaned_data['querydata'])
+ return datas
diff --git a/blog-lsh/__init__.py b/blog-lsh(优化后)/management/__init__.py
similarity index 100%
rename from blog-lsh/__init__.py
rename to blog-lsh(优化后)/management/__init__.py
diff --git a/blog-lsh/management/__init__.py b/blog-lsh(优化后)/management/commands/__init__.py
similarity index 100%
rename from blog-lsh/management/__init__.py
rename to blog-lsh(优化后)/management/commands/__init__.py
diff --git a/blog-lsh/management/commands/build_index.py b/blog-lsh(优化后)/management/commands/build_index.py
similarity index 100%
rename from blog-lsh/management/commands/build_index.py
rename to blog-lsh(优化后)/management/commands/build_index.py
diff --git a/blog-lsh/management/commands/build_search_words.py b/blog-lsh(优化后)/management/commands/build_search_words.py
similarity index 100%
rename from blog-lsh/management/commands/build_search_words.py
rename to blog-lsh(优化后)/management/commands/build_search_words.py
diff --git a/blog-lsh/management/commands/clear_cache.py b/blog-lsh(优化后)/management/commands/clear_cache.py
similarity index 100%
rename from blog-lsh/management/commands/clear_cache.py
rename to blog-lsh(优化后)/management/commands/clear_cache.py
diff --git a/blog-lsh/management/commands/create_testdata.py b/blog-lsh(优化后)/management/commands/create_testdata.py
similarity index 100%
rename from blog-lsh/management/commands/create_testdata.py
rename to blog-lsh(优化后)/management/commands/create_testdata.py
diff --git a/blog-lsh/management/commands/ping_baidu.py b/blog-lsh(优化后)/management/commands/ping_baidu.py
similarity index 100%
rename from blog-lsh/management/commands/ping_baidu.py
rename to blog-lsh(优化后)/management/commands/ping_baidu.py
diff --git a/blog-lsh/management/commands/sync_user_avatar.py b/blog-lsh(优化后)/management/commands/sync_user_avatar.py
similarity index 100%
rename from blog-lsh/management/commands/sync_user_avatar.py
rename to blog-lsh(优化后)/management/commands/sync_user_avatar.py
diff --git a/blog-lsh(优化后)/middleware.py b/blog-lsh(优化后)/middleware.py
new file mode 100644
index 0000000..94dd70c
--- /dev/null
+++ b/blog-lsh(优化后)/middleware.py
@@ -0,0 +1,42 @@
+import logging
+import time
+
+from ipware import get_client_ip
+from user_agents import parse
+
+from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
+
+logger = logging.getLogger(__name__)
+
+
+class OnlineMiddleware(object):
+ def __init__(self, get_response=None):
+ self.get_response = get_response
+ super().__init__()
+
+ def __call__(self, request):
+ ''' page render time '''
+ start_time = time.time()
+ response = self.get_response(request)
+ http_user_agent = request.META.get('HTTP_USER_AGENT', '')
+ ip, _ = get_client_ip(request)
+ user_agent = parse(http_user_agent)
+ if not response.streaming:
+ try:
+ cast_time = time.time() - start_time
+ if ELASTICSEARCH_ENABLED:
+ time_taken = round((cast_time) * 1000, 2)
+ url = request.path
+ from django.utils import timezone
+ ElaspedTimeDocumentManager.create(
+ url=url,
+ time_taken=time_taken,
+ log_datetime=timezone.now(),
+ useragent=user_agent,
+ ip=ip)
+ response.content = response.content.replace(
+ b'', str.encode(str(cast_time)[:5]))
+ except Exception as e:
+ logger.error("Error OnlineMiddleware: %s" % e)
+
+ return response
diff --git a/blog-lsh/migrations/0001_initial.py b/blog-lsh(优化后)/migrations/0001_initial.py
similarity index 100%
rename from blog-lsh/migrations/0001_initial.py
rename to blog-lsh(优化后)/migrations/0001_initial.py
diff --git a/blog-lsh/migrations/0002_blogsettings_global_footer_and_more.py b/blog-lsh(优化后)/migrations/0002_blogsettings_global_footer_and_more.py
similarity index 100%
rename from blog-lsh/migrations/0002_blogsettings_global_footer_and_more.py
rename to blog-lsh(优化后)/migrations/0002_blogsettings_global_footer_and_more.py
diff --git a/blog-lsh/migrations/0003_blogsettings_comment_need_review.py b/blog-lsh(优化后)/migrations/0003_blogsettings_comment_need_review.py
similarity index 100%
rename from blog-lsh/migrations/0003_blogsettings_comment_need_review.py
rename to blog-lsh(优化后)/migrations/0003_blogsettings_comment_need_review.py
diff --git a/blog-lsh/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py b/blog-lsh(优化后)/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
similarity index 100%
rename from blog-lsh/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
rename to blog-lsh(优化后)/migrations/0004_rename_analyticscode_blogsettings_analytics_code_and_more.py
diff --git a/blog-lsh/migrations/0005_alter_article_options_alter_category_options_and_more.py b/blog-lsh(优化后)/migrations/0005_alter_article_options_alter_category_options_and_more.py
similarity index 100%
rename from blog-lsh/migrations/0005_alter_article_options_alter_category_options_and_more.py
rename to blog-lsh(优化后)/migrations/0005_alter_article_options_alter_category_options_and_more.py
diff --git a/blog-lsh/migrations/0006_alter_blogsettings_options.py b/blog-lsh(优化后)/migrations/0006_alter_blogsettings_options.py
similarity index 100%
rename from blog-lsh/migrations/0006_alter_blogsettings_options.py
rename to blog-lsh(优化后)/migrations/0006_alter_blogsettings_options.py
diff --git a/blog-lsh/management/commands/__init__.py b/blog-lsh(优化后)/migrations/__init__.py
similarity index 100%
rename from blog-lsh/management/commands/__init__.py
rename to blog-lsh(优化后)/migrations/__init__.py
diff --git a/blog-lsh(优化后)/models.py b/blog-lsh(优化后)/models.py
new file mode 100644
index 0000000..083788b
--- /dev/null
+++ b/blog-lsh(优化后)/models.py
@@ -0,0 +1,376 @@
+import logging
+import re
+from abc import abstractmethod
+
+from django.conf import settings
+from django.core.exceptions import ValidationError
+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 mdeditor.fields import MDTextField
+from uuslug import slugify
+
+from djangoblog.utils import cache_decorator, cache
+from djangoblog.utils import get_current_site
+
+logger = logging.getLogger(__name__)
+
+
+class LinkShowType(models.TextChoices):
+ I = ('i', _('index'))
+ L = ('l', _('list'))
+ P = ('p', _('post'))
+ A = ('a', _('all'))
+ S = ('s', _('slide'))
+
+
+class BaseModel(models.Model):
+ id = models.AutoField(primary_key=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_modify_time = models.DateTimeField(_('modify time'), default=now)
+
+ def save(self, *args, **kwargs):
+ is_update_views = isinstance(
+ 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')
+ 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())
+ return url
+
+ class Meta:
+ abstract = True
+
+ @abstractmethod
+ def get_absolute_url(self):
+ pass
+
+
+class Article(BaseModel):
+ """文章"""
+ STATUS_CHOICES = (
+ ('d', _('Draft')),
+ ('p', _('Published')),
+ )
+ COMMENT_STATUS = (
+ ('o', _('Open')),
+ ('c', _('Close')),
+ )
+ TYPE = (
+ ('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')
+ 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)
+
+ def body_to_string(self):
+ return self.body
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ ordering = ['-article_order', '-pub_time']
+ verbose_name = _('article')
+ verbose_name_plural = verbose_name
+ get_latest_by = 'id'
+
+ def get_absolute_url(self):
+ return reverse('blog:detailbyid', kwargs={
+ 'article_id': self.id,
+ 'year': self.creation_time.year,
+ 'month': self.creation_time.month,
+ 'day': self.creation_time.day
+ })
+
+ @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))
+
+ return names
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+
+ def viewed(self):
+ self.views += 1
+ self.save(update_fields=['views'])
+
+ def comment_list(self):
+ cache_key = 'article_comments_{id}'.format(id=self.id)
+ value = cache.get(cache_key)
+ if value:
+ logger.info('get article comments:{id}'.format(id=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))
+ return comments
+
+ def get_admin_url(self):
+ info = (self._meta.app_label, self._meta.model_name)
+ return reverse('admin:%s_%s_change' % info, args=(self.pk,))
+
+ @cache_decorator(expiration=60 * 100)
+ def next_article(self):
+ # 下一篇
+ 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)
+ return ""
+
+
+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)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+ index = models.IntegerField(default=0, verbose_name=_('index'))
+
+ class Meta:
+ ordering = ['-index']
+ verbose_name = _('category')
+ verbose_name_plural = verbose_name
+
+ def get_absolute_url(self):
+ 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):
+ categorys.append(category)
+ if category.parent_category:
+ parse(category.parent_category)
+
+ parse(self)
+ return categorys
+
+ @cache_decorator(60 * 60 * 10)
+ def get_sub_categorys(self):
+ """
+ 获得当前分类目录所有子集
+ :return:
+ """
+ categorys = []
+ all_categorys = Category.objects.all()
+
+ def parse(category):
+ if category not in categorys:
+ categorys.append(category)
+ childs = all_categorys.filter(parent_category=category)
+ for child in childs:
+ if category not in categorys:
+ categorys.append(child)
+ parse(child)
+
+ parse(self)
+ return categorys
+
+
+class Tag(BaseModel):
+ """文章标签"""
+ name = models.CharField(_('tag name'), max_length=30, unique=True)
+ slug = models.SlugField(default='no-slug', max_length=60, blank=True)
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
+
+ @cache_decorator(60 * 60 * 10)
+ def get_article_count(self):
+ return Article.objects.filter(tags__name=self.name).distinct().count()
+
+ class Meta:
+ ordering = ['name']
+ verbose_name = _('tag')
+ verbose_name_plural = verbose_name
+
+
+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)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ class Meta:
+ ordering = ['sequence']
+ verbose_name = _('link')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.name
+
+
+class SideBar(models.Model):
+ """侧边栏,可以展示一些html内容"""
+ name = models.CharField(_('title'), max_length=100)
+ content = models.TextField(_('content'))
+ sequence = models.IntegerField(_('order'), unique=True)
+ is_enable = models.BooleanField(_('is enable'), default=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_mod_time = models.DateTimeField(_('modify time'), default=now)
+
+ class Meta:
+ ordering = ['sequence']
+ verbose_name = _('sidebar')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.name
+
+
+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='')
+ 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='')
+ 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)
+
+ class Meta:
+ verbose_name = _('Website configuration')
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.site_name
+
+ def clean(self):
+ if BlogSettings.objects.exclude(id=self.id).count():
+ raise ValidationError(_('There can only be one configuration'))
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+ from djangoblog.utils import cache
+ cache.clear()
diff --git a/blog-lsh(优化后)/search_indexes.py b/blog-lsh(优化后)/search_indexes.py
new file mode 100644
index 0000000..7f1dfac
--- /dev/null
+++ b/blog-lsh(优化后)/search_indexes.py
@@ -0,0 +1,13 @@
+from haystack import indexes
+
+from blog.models import Article
+
+
+class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
+ text = indexes.CharField(document=True, use_template=True)
+
+ def get_model(self):
+ return Article
+
+ def index_queryset(self, using=None):
+ return self.get_model().objects.filter(status='p')
diff --git a/blog-lsh/static/account/css/account.css b/blog-lsh(优化后)/static/account/css/account.css
similarity index 100%
rename from blog-lsh/static/account/css/account.css
rename to blog-lsh(优化后)/static/account/css/account.css
diff --git a/blog-lsh/static/account/js/account.js b/blog-lsh(优化后)/static/account/js/account.js
similarity index 100%
rename from blog-lsh/static/account/js/account.js
rename to blog-lsh(优化后)/static/account/js/account.js
diff --git a/blog-lsh/static/assets/css/bootstrap.min.css b/blog-lsh(优化后)/static/assets/css/bootstrap.min.css
similarity index 100%
rename from blog-lsh/static/assets/css/bootstrap.min.css
rename to blog-lsh(优化后)/static/assets/css/bootstrap.min.css
diff --git a/blog-lsh/static/assets/css/docs.min.css b/blog-lsh(优化后)/static/assets/css/docs.min.css
similarity index 100%
rename from blog-lsh/static/assets/css/docs.min.css
rename to blog-lsh(优化后)/static/assets/css/docs.min.css
diff --git a/blog-lsh/static/assets/css/ie10-viewport-bug-workaround.css b/blog-lsh(优化后)/static/assets/css/ie10-viewport-bug-workaround.css
similarity index 100%
rename from blog-lsh/static/assets/css/ie10-viewport-bug-workaround.css
rename to blog-lsh(优化后)/static/assets/css/ie10-viewport-bug-workaround.css
diff --git a/blog-lsh/static/assets/css/signin.css b/blog-lsh(优化后)/static/assets/css/signin.css
similarity index 100%
rename from blog-lsh/static/assets/css/signin.css
rename to blog-lsh(优化后)/static/assets/css/signin.css
diff --git a/blog-lsh/static/assets/css/todc-bootstrap.min.css b/blog-lsh(优化后)/static/assets/css/todc-bootstrap.min.css
similarity index 100%
rename from blog-lsh/static/assets/css/todc-bootstrap.min.css
rename to blog-lsh(优化后)/static/assets/css/todc-bootstrap.min.css
diff --git a/blog-lsh/static/assets/img/checkmark.png b/blog-lsh(优化后)/static/assets/img/checkmark.png
similarity index 100%
rename from blog-lsh/static/assets/img/checkmark.png
rename to blog-lsh(优化后)/static/assets/img/checkmark.png
diff --git a/blog-lsh/static/assets/js/ie-emulation-modes-warning.js b/blog-lsh(优化后)/static/assets/js/ie-emulation-modes-warning.js
similarity index 100%
rename from blog-lsh/static/assets/js/ie-emulation-modes-warning.js
rename to blog-lsh(优化后)/static/assets/js/ie-emulation-modes-warning.js
diff --git a/blog-lsh/static/assets/js/ie10-viewport-bug-workaround.js b/blog-lsh(优化后)/static/assets/js/ie10-viewport-bug-workaround.js
similarity index 100%
rename from blog-lsh/static/assets/js/ie10-viewport-bug-workaround.js
rename to blog-lsh(优化后)/static/assets/js/ie10-viewport-bug-workaround.js
diff --git a/blog-lsh/static/blog/css/ie.css b/blog-lsh(优化后)/static/blog/css/ie.css
similarity index 100%
rename from blog-lsh/static/blog/css/ie.css
rename to blog-lsh(优化后)/static/blog/css/ie.css
diff --git a/blog-lsh/static/blog/css/nprogress.css b/blog-lsh(优化后)/static/blog/css/nprogress.css
similarity index 100%
rename from blog-lsh/static/blog/css/nprogress.css
rename to blog-lsh(优化后)/static/blog/css/nprogress.css
diff --git a/blog-lsh/static/blog/css/oauth_style.css b/blog-lsh(优化后)/static/blog/css/oauth_style.css
similarity index 100%
rename from blog-lsh/static/blog/css/oauth_style.css
rename to blog-lsh(优化后)/static/blog/css/oauth_style.css
diff --git a/blog-lsh/static/blog/css/style.css b/blog-lsh(优化后)/static/blog/css/style.css
similarity index 100%
rename from blog-lsh/static/blog/css/style.css
rename to blog-lsh(优化后)/static/blog/css/style.css
diff --git a/blog-lsh/static/blog/fonts/fonts.css b/blog-lsh(优化后)/static/blog/fonts/fonts.css
similarity index 100%
rename from blog-lsh/static/blog/fonts/fonts.css
rename to blog-lsh(优化后)/static/blog/fonts/fonts.css
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2
diff --git a/blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2
diff --git a/blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2
diff --git a/blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2
diff --git a/blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2
diff --git a/blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2
diff --git a/blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2
diff --git a/blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2
diff --git a/blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2
diff --git a/blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2
diff --git a/blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2
diff --git a/blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2
diff --git a/blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2
diff --git a/blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2
diff --git a/blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2 b/blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2
rename to blog-lsh(优化后)/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2
diff --git a/blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2 b/blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2
similarity index 100%
rename from blog-lsh/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2
rename to blog-lsh(优化后)/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2
diff --git a/blog-lsh/static/blog/img/avatar.png b/blog-lsh(优化后)/static/blog/img/avatar.png
similarity index 100%
rename from blog-lsh/static/blog/img/avatar.png
rename to blog-lsh(优化后)/static/blog/img/avatar.png
diff --git a/blog-lsh/static/blog/img/icon-sn.svg b/blog-lsh(优化后)/static/blog/img/icon-sn.svg
similarity index 100%
rename from blog-lsh/static/blog/img/icon-sn.svg
rename to blog-lsh(优化后)/static/blog/img/icon-sn.svg
diff --git a/blog-lsh/static/blog/js/blog.js b/blog-lsh(优化后)/static/blog/js/blog.js
similarity index 100%
rename from blog-lsh/static/blog/js/blog.js
rename to blog-lsh(优化后)/static/blog/js/blog.js
diff --git a/blog-lsh/static/blog/js/html5.js b/blog-lsh(优化后)/static/blog/js/html5.js
similarity index 100%
rename from blog-lsh/static/blog/js/html5.js
rename to blog-lsh(优化后)/static/blog/js/html5.js
diff --git a/blog-lsh/static/blog/js/jquery-3.6.0.min.js b/blog-lsh(优化后)/static/blog/js/jquery-3.6.0.min.js
similarity index 100%
rename from blog-lsh/static/blog/js/jquery-3.6.0.min.js
rename to blog-lsh(优化后)/static/blog/js/jquery-3.6.0.min.js
diff --git a/blog-lsh/static/blog/js/navigation.js b/blog-lsh(优化后)/static/blog/js/navigation.js
similarity index 100%
rename from blog-lsh/static/blog/js/navigation.js
rename to blog-lsh(优化后)/static/blog/js/navigation.js
diff --git a/blog-lsh/static/blog/js/nprogress.js b/blog-lsh(优化后)/static/blog/js/nprogress.js
similarity index 100%
rename from blog-lsh/static/blog/js/nprogress.js
rename to blog-lsh(优化后)/static/blog/js/nprogress.js
diff --git a/blog-lsh/static/mathjax/js/mathjax-config.js b/blog-lsh(优化后)/static/mathjax/js/mathjax-config.js
similarity index 100%
rename from blog-lsh/static/mathjax/js/mathjax-config.js
rename to blog-lsh(优化后)/static/mathjax/js/mathjax-config.js
diff --git a/blog-lsh/static/pygments/default.css b/blog-lsh(优化后)/static/pygments/default.css
old mode 100644
new mode 100755
similarity index 100%
rename from blog-lsh/static/pygments/default.css
rename to blog-lsh(优化后)/static/pygments/default.css
diff --git a/blog-lsh/migrations/__init__.py b/blog-lsh(优化后)/templatetags/__init__.py
similarity index 100%
rename from blog-lsh/migrations/__init__.py
rename to blog-lsh(优化后)/templatetags/__init__.py
diff --git a/blog-lsh/templatetags/blog_tags.py b/blog-lsh(优化后)/templatetags/blog_tags.py
similarity index 100%
rename from blog-lsh/templatetags/blog_tags.py
rename to blog-lsh(优化后)/templatetags/blog_tags.py
diff --git a/blog-lsh/tests.py b/blog-lsh(优化后)/tests.py
similarity index 78%
rename from blog-lsh/tests.py
rename to blog-lsh(优化后)/tests.py
index d415fdb..ee13505 100644
--- a/blog-lsh/tests.py
+++ b/blog-lsh(优化后)/tests.py
@@ -1,7 +1,3 @@
-"""
-LJX: 测试模块
-包含blog应用的单元测试和集成测试,确保代码质量和功能正确性
-"""
import os
from django.conf import settings
@@ -24,16 +20,12 @@ from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
class ArticleTest(TestCase):
- """LJX: 文章相关功能测试类"""
def setUp(self):
- """LJX: 测试初始化设置,创建测试客户端和工厂"""
- self.client = Client() # LJX: Django测试客户端
- self.factory = RequestFactory() # LJX: 请求工厂,用于创建请求对象
+ self.client = Client()
+ self.factory = RequestFactory()
def test_validate_article(self):
- """LJX: 测试文章验证和相关功能"""
site = get_current_site().domain
- # LJX: 创建测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@@ -41,16 +33,10 @@ class ArticleTest(TestCase):
user.is_staff = True
user.is_superuser = True
user.save()
-
- # LJX: 测试用户详情页访问
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
-
- # LJX: 测试后台页面访问
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
-
- # LJX: 创建测试侧边栏
s = SideBar()
s.sequence = 1
s.name = 'test'
@@ -58,19 +44,16 @@ class ArticleTest(TestCase):
s.is_enable = True
s.save()
- # LJX: 创建测试分类
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
- # LJX: 创建测试标签
tag = Tag()
tag.name = "nicetag"
tag.save()
- # LJX: 创建测试文章
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
@@ -78,15 +61,13 @@ class ArticleTest(TestCase):
article.category = category
article.type = 'a'
article.status = 'p'
+
article.save()
-
- # LJX: 测试标签关联
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
article.save()
self.assertEqual(1, article.tags.count())
- # LJX: 创建多篇文章用于测试分页和搜索
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
@@ -98,46 +79,32 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
-
- # LJX: 测试Elasticsearch搜索功能
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
- call_command("build_index") # LJX: 构建搜索索引
+ call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
- # LJX: 测试文章详情页访问
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
-
- # LJX: 测试搜索引擎通知
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
-
- # LJX: 测试标签页访问
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
- # LJX: 测试分类页访问
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
- # LJX: 测试搜索功能
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
-
- # LJX: 测试模板标签
s = load_articletags(article)
self.assertIsNotNone(s)
- # LJX: 用户登录测试
self.client.login(username='liangliangyy', password='liangliangyy')
- # LJX: 测试归档页面
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
- # LJX: 测试分页功能
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
@@ -152,20 +119,16 @@ class ArticleTest(TestCase):
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
- # LJX: 测试搜索表单
f = BlogSearchForm()
f.search()
-
- # LJX: 测试百度通知功能
+ # self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
- # LJX: 测试Gravatar相关功能
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
- # LJX: 测试友情链接功能
link = Links(
sequence=1,
name="lylinux",
@@ -174,46 +137,37 @@ class ArticleTest(TestCase):
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
- # LJX: 测试Feed和Sitemap
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
- # LJX: 测试后台删除操作
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
- """LJX: 检查分页功能是否正常工作"""
for page in range(1, p.num_pages + 1):
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
- if s['previous_url']: # LJX: 测试上一页链接
+ if s['previous_url']:
response = self.client.get(s['previous_url'])
self.assertEqual(response.status_code, 200)
- if s['next_url']: # LJX: 测试下一页链接
+ if s['next_url']:
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
- """LJX: 测试图片上传和处理功能"""
import requests
- # LJX: 下载测试图片
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
-
- # LJX: 测试未授权上传
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
-
- # LJX: 测试授权上传
sign = get_sha256(get_sha256(settings.SECRET_KEY))
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
@@ -222,21 +176,17 @@ class ArticleTest(TestCase):
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
- os.remove(imagepath) # LJX: 清理测试文件
-
- # LJX: 测试工具函数
+ os.remove(imagepath)
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
- """LJX: 测试错误页面处理"""
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
- """LJX: 测试自定义管理命令"""
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@@ -245,7 +195,6 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
- # LJX: 测试OAuth配置
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
@@ -273,7 +222,6 @@ class ArticleTest(TestCase):
}'''
u.save()
- # LJX: 测试各种管理命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
@@ -281,4 +229,4 @@ class ArticleTest(TestCase):
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
- call_command("build_search_words")
\ No newline at end of file
+ call_command("build_search_words")
diff --git a/blog-lsh(优化后)/urls.py b/blog-lsh(优化后)/urls.py
new file mode 100644
index 0000000..adf2703
--- /dev/null
+++ b/blog-lsh(优化后)/urls.py
@@ -0,0 +1,62 @@
+from django.urls import path
+from django.views.decorators.cache import cache_page
+
+from . import views
+
+app_name = "blog"
+urlpatterns = [
+ path(
+ r'',
+ views.IndexView.as_view(),
+ name='index'),
+ path(
+ r'page//',
+ views.IndexView.as_view(),
+ name='index_page'),
+ path(
+ r'article////.html',
+ views.ArticleDetailView.as_view(),
+ name='detailbyid'),
+ path(
+ r'category/.html',
+ views.CategoryDetailView.as_view(),
+ name='category_detail'),
+ path(
+ r'category//.html',
+ views.CategoryDetailView.as_view(),
+ name='category_detail_page'),
+ path(
+ r'author/.html',
+ views.AuthorDetailView.as_view(),
+ name='author_detail'),
+ path(
+ r'author//.html',
+ views.AuthorDetailView.as_view(),
+ name='author_detail_page'),
+ path(
+ r'tag/.html',
+ views.TagDetailView.as_view(),
+ name='tag_detail'),
+ path(
+ r'tag//.html',
+ views.TagDetailView.as_view(),
+ name='tag_detail_page'),
+ path(
+ 'archives.html',
+ cache_page(
+ 60 * 60)(
+ views.ArchivesView.as_view()),
+ name='archives'),
+ path(
+ 'links.html',
+ views.LinkListView.as_view(),
+ name='links'),
+ path(
+ r'upload',
+ views.fileupload,
+ name='upload'),
+ path(
+ r'clean',
+ views.clean_cache_view,
+ name='clean'),
+]
diff --git a/blog-lsh/views.py b/blog-lsh(优化后)/views.py
similarity index 52%
rename from blog-lsh/views.py
rename to blog-lsh(优化后)/views.py
index 327cba5..d5dc7ec 100644
--- a/blog-lsh/views.py
+++ b/blog-lsh(优化后)/views.py
@@ -1,8 +1,3 @@
-"""
-LJX: 视图处理模块
-定义blog应用的所有视图函数和类,处理HTTP请求并返回响应
-包含文章列表、详情、分类、标签、搜索等核心功能
-"""
import logging
import os
import uuid
@@ -30,135 +25,136 @@ logger = logging.getLogger(__name__)
class ArticleListView(ListView):
- """LJX: 文章列表基类视图,提供文章列表的通用功能"""
- # LJX: template_name属性用于指定使用哪个模板进行渲染
+ # template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
- # LJX: context_object_name属性用于给上下文变量取名(在模板中使用该名字)
+ # context_object_name属性用于给上下文变量取名(在模板中使用该名字)
context_object_name = 'article_list'
- # LJX: 页面类型,分类目录或标签列表等
+ # 页面类型,分类目录或标签列表等
page_type = ''
- paginate_by = settings.PAGINATE_BY # LJX: 每页显示文章数量
- page_kwarg = 'page' # LJX: URL中页码参数的名称
- link_type = LinkShowType.L # LJX: 友情链接显示类型
+ paginate_by = settings.PAGINATE_BY
+ page_kwarg = 'page'
+ link_type = LinkShowType.L
def get_view_cache_key(self):
- """LJX: 获取视图缓存键"""
return self.request.get['pages']
@property
def page_number(self):
- """LJX: 获取当前页码属性"""
page_kwarg = self.page_kwarg
- page = self.kwargs.get( # LJX: 从URL参数获取页码
- page_kwarg) or self.request.GET.get(page_kwarg) or 1 # LJX: 从GET参数获取或默认第一页
+ page = self.kwargs.get(
+ page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
- """LJX: 子类重写.获得queryset的缓存key"""
+ """
+ 子类重写.获得queryset的缓存key
+ """
raise NotImplementedError()
def get_queryset_data(self):
- """LJX: 子类重写.获取queryset的数据"""
+ """
+ 子类重写.获取queryset的数据
+ """
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
- '''LJX: 缓存页面数据,提高性能'''
+ '''
+ 缓存页面数据
+ :param cache_key: 缓存key
+ :return:
+ '''
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
- return value # LJX: 如果缓存中存在,直接返回
+ return value
else:
- article_list = self.get_queryset_data() # LJX: 从数据库获取数据
- cache.set(cache_key, article_list) # LJX: 设置缓存
+ article_list = self.get_queryset_data()
+ cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
- '''LJX: 重写默认,从缓存获取数据'''
+ '''
+ 重写默认,从缓存获取数据
+ :return:
+ '''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
- """LJX: 添加上下文数据"""
- kwargs['linktype'] = self.link_type # LJX: 添加链接类型到上下文
+ kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
- '''LJX: 首页视图,显示最新的文章列表'''
- # LJX: 友情链接类型 - 在首页显示
+ '''
+ 首页
+ '''
+ # 友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
- """LJX: 获取首页文章数据 - 只获取已发布的普通文章"""
- article_list = Article.objects.filter(type='a', status='p') # LJX: 过滤普通类型且已发布的文章
+ article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
- """LJX: 生成首页缓存键,包含页码信息"""
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
- '''LJX: 文章详情页面视图,显示单篇文章的完整内容'''
- template_name = 'blog/article_detail.html' # LJX: 详情页模板
- model = Article # LJX: 关联的模型
- pk_url_kwarg = 'article_id' # LJX: URL中主键参数的名称
- context_object_name = "article" # LJX: 模板中使用的变量名
+ '''
+ 文章详情页面
+ '''
+ template_name = 'blog/article_detail.html'
+ model = Article
+ pk_url_kwarg = 'article_id'
+ context_object_name = "article"
def get_context_data(self, **kwargs):
- """LJX: 添加上下文数据,包括评论表单、评论列表等"""
- comment_form = CommentForm() # LJX: 评论表单实例
+ comment_form = CommentForm()
- # LJX: 获取文章评论列表
article_comments = self.object.comment_list()
- parent_comments = article_comments.filter(parent_comment=None) # LJX: 顶级评论(无父评论)
- blog_setting = get_blog_setting() # LJX: 获取博客设置
-
- # LJX: 评论分页处理
+ parent_comments = article_comments.filter(parent_comment=None)
+ blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
- page = self.request.GET.get('comment_page', '1') # LJX: 获取评论页码
+ page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
- page = 1 # LJX: 如果不是数字,默认第一页
+ page = 1
else:
page = int(page)
if page < 1:
- page = 1 # LJX: 页码不能小于1
+ page = 1
if page > paginator.num_pages:
- page = paginator.num_pages # LJX: 页码不能大于总页数
+ page = paginator.num_pages
- p_comments = paginator.page(page) # LJX: 获取指定页的评论
- next_page = p_comments.next_page_number() if p_comments.has_next() else None # LJX: 下一页页码
- prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None # LJX: 上一页页码
+ p_comments = paginator.page(page)
+ next_page = p_comments.next_page_number() if p_comments.has_next() else None
+ prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
- # LJX: 添加上下一页评论的URL
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
-
- # LJX: 添加各种上下文数据
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
- kwargs['next_article'] = self.object.next_article # LJX: 下一篇文章
- kwargs['prev_article'] = self.object.prev_article # LJX: 上一篇文章
+ kwargs['next_article'] = self.object.next_article
+ kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
-
- # LJX: Action Hook, 通知插件"文章详情已获取"
+ # Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
- # LJX: Filter Hook, 允许插件修改文章正文
+ # # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
@@ -166,25 +162,24 @@ class ArticleDetailView(DetailView):
class CategoryDetailView(ArticleListView):
- '''LJX: 分类目录列表视图,显示指定分类下的所有文章'''
- page_type = "分类目录归档" # LJX: 页面类型描述
+ '''
+ 分类目录列表
+ '''
+ page_type = "分类目录归档"
def get_queryset_data(self):
- """LJX: 获取分类下的文章数据,包括子分类的文章"""
- slug = self.kwargs['category_name'] # LJX: 从URL获取分类slug
- category = get_object_or_404(Category, slug=slug) # LJX: 获取分类对象,不存在返回404
+ slug = self.kwargs['category_name']
+ category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
- map(lambda c: c.name, category.get_sub_categorys())) # LJX: 获取所有子分类名称
- # LJX: 获取分类及其子分类下的所有已发布文章
+ map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
- """LJX: 生成分类页缓存键"""
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
@@ -194,60 +189,59 @@ class CategoryDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
- """LJX: 添加上下文数据"""
+
categoryname = self.categoryname
try:
- categoryname = categoryname.split('/')[-1] # LJX: 处理分类路径,取最后一部分
+ categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
- kwargs['tag_name'] = categoryname # LJX: 实际上这里是分类名,变量名保持兼容
+ kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
- '''LJX: 作者详情页视图,显示指定作者的所有文章'''
- page_type = '作者文章归档' # LJX: 页面类型描述
+ '''
+ 作者详情页
+ '''
+ page_type = '作者文章归档'
def get_queryset_cache_key(self):
- """LJX: 生成作者页缓存键"""
from uuslug import slugify
- author_name = slugify(self.kwargs['author_name']) # LJX: 对作者名进行slugify处理
+ author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
- """LJX: 获取作者的文章数据"""
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
- author__username=author_name, type='a', status='p') # LJX: 过滤指定作者的已发布普通文章
+ author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
- """LJX: 添加上下文数据"""
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
- kwargs['tag_name'] = author_name # LJX: 变量名保持兼容
+ kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView):
- '''LJX: 标签列表页面视图,显示指定标签的所有文章'''
- page_type = '分类标签归档' # LJX: 页面类型描述
+ '''
+ 标签列表页面
+ '''
+ page_type = '分类标签归档'
def get_queryset_data(self):
- """LJX: 获取标签下的文章数据"""
slug = self.kwargs['tag_name']
- tag = get_object_or_404(Tag, slug=slug) # LJX: 获取标签对象
+ tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
- tags__name=tag_name, type='a', status='p') # LJX: 多对多关系查询
+ tags__name=tag_name, type='a', status='p')
return article_list
def get_queryset_cache_key(self):
- """LJX: 生成标签页缓存键"""
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@@ -257,7 +251,7 @@ class TagDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
- """LJX: 添加上下文数据"""
+ # tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
@@ -265,134 +259,121 @@ class TagDetailView(ArticleListView):
class ArchivesView(ArticleListView):
- '''LJX: 文章归档页面视图,显示所有文章的按时间归档'''
- page_type = '文章归档' # LJX: 页面类型描述
- paginate_by = None # LJX: 不分页,显示所有文章
- page_kwarg = None # LJX: 无页码参数
- template_name = 'blog/article_archives.html' # LJX: 归档页专用模板
+ '''
+ 文章归档页面
+ '''
+ page_type = '文章归档'
+ paginate_by = None
+ page_kwarg = None
+ template_name = 'blog/article_archives.html'
def get_queryset_data(self):
- """LJX: 获取所有已发布文章"""
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
- """LJX: 生成归档页缓存键"""
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
- """LJX: 友情链接列表视图,显示所有启用的友情链接"""
- model = Links # LJX: 关联的模型
- template_name = 'blog/links_list.html' # LJX: 模板名称
+ model = Links
+ template_name = 'blog/links_list.html'
def get_queryset(self):
- """LJX: 获取已启用的友情链接"""
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
- """LJX: Elasticsearch搜索视图,处理全文搜索请求"""
def get_context(self):
- """LJX: 获取搜索上下文数据"""
- paginator, page = self.build_page() # LJX: 构建分页
+ paginator, page = self.build_page()
context = {
- "query": self.query, # LJX: 搜索关键词
- "form": self.form, # LJX: 搜索表单
- "page": page, # LJX: 当前页
- "paginator": paginator, # LJX: 分页器
- "suggestion": None, # LJX: 搜索建议
+ "query": self.query,
+ "form": self.form,
+ "page": page,
+ "paginator": paginator,
+ "suggestion": None,
}
- # LJX: 如果有拼写建议,添加到上下文
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
- context.update(self.extra_context()) # LJX: 添加额外上下文
+ context.update(self.extra_context())
return context
-@csrf_exempt # LJX: 免除CSRF保护,用于文件上传
+@csrf_exempt
def fileupload(request):
"""
- LJX: 文件上传视图函数
该方法需自己写调用端来上传图片,该方法仅提供图床功能
+ :param request:
+ :return:
"""
if request.method == 'POST':
- sign = request.GET.get('sign', None) # LJX: 获取签名参数
+ sign = request.GET.get('sign', None)
if not sign:
- return HttpResponseForbidden() # LJX: 无签名,拒绝访问
+ return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
- return HttpResponseForbidden() # LJX: 签名验证失败,拒绝访问
-
+ return HttpResponseForbidden()
response = []
- for filename in request.FILES: # LJX: 遍历所有上传的文件
- timestr = timezone.now().strftime('%Y/%m/%d') # LJX: 按日期创建目录结构
- imgextensions = ['jpg', 'png', 'jpeg', 'bmp'] # LJX: 图片文件扩展名
+ for filename in request.FILES:
+ timestr = timezone.now().strftime('%Y/%m/%d')
+ imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
- isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0 # LJX: 判断是否为图片文件
- # LJX: 构建保存路径
+ isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
- os.makedirs(base_dir) # LJX: 创建目录
- # LJX: 生成唯一文件名
+ os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
if not savepath.startswith(base_dir):
- return HttpResponse("only for post") # LJX: 路径安全检查
- # LJX: 保存文件
+ return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
- # LJX: 如果是图片,进行压缩优化
if isimage:
from PIL import Image
image = Image.open(savepath)
- image.save(savepath, quality=20, optimize=True) # LJX: 质量压缩到20%,优化存储
- url = static(savepath) # LJX: 生成静态文件URL
+ image.save(savepath, quality=20, optimize=True)
+ url = static(savepath)
response.append(url)
- return HttpResponse(response) # LJX: 返回文件URL列表
+ return HttpResponse(response)
else:
- return HttpResponse("only for post") # LJX: 只支持POST请求
+ return HttpResponse("only for post")
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
- """LJX: 404页面未找到错误处理视图"""
if exception:
- logger.error(exception) # LJX: 记录错误日志
+ logger.error(exception)
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
- 'statuscode': '404'}, # LJX: 错误信息
- status=404) # LJX: 返回404状态码
+ 'statuscode': '404'},
+ status=404)
def server_error_view(request, template_name='blog/error_page.html'):
- """LJX: 500服务器错误处理视图"""
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
- status=500) # LJX: 返回500状态码
+ status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
- """LJX: 403权限拒绝错误处理视图"""
if exception:
logger.error(exception)
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
- 'statuscode': '403'}, status=403) # LJX: 返回403状态码
+ 'statuscode': '403'}, status=403)
def clean_cache_view(request):
- """LJX: 清理缓存视图,用于手动清理系统缓存"""
- cache.clear() # LJX: 清除所有缓存
- return HttpResponse('ok') # LJX: 返回成功响应
\ No newline at end of file
+ cache.clear()
+ return HttpResponse('ok')
diff --git a/blog-lsh/__pycache__/__init__.cpython-312.pyc b/blog-lsh/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index c02cb87..0000000
Binary files a/blog-lsh/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/__pycache__/admin.cpython-312.pyc b/blog-lsh/__pycache__/admin.cpython-312.pyc
deleted file mode 100644
index 38e685e..0000000
Binary files a/blog-lsh/__pycache__/admin.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/__pycache__/apps.cpython-312.pyc b/blog-lsh/__pycache__/apps.cpython-312.pyc
deleted file mode 100644
index 95b88b4..0000000
Binary files a/blog-lsh/__pycache__/apps.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/__pycache__/documents.cpython-312.pyc b/blog-lsh/__pycache__/documents.cpython-312.pyc
deleted file mode 100644
index 5681bee..0000000
Binary files a/blog-lsh/__pycache__/documents.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/__pycache__/middleware.cpython-312.pyc b/blog-lsh/__pycache__/middleware.cpython-312.pyc
deleted file mode 100644
index 36078af..0000000
Binary files a/blog-lsh/__pycache__/middleware.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/__pycache__/models.cpython-312.pyc b/blog-lsh/__pycache__/models.cpython-312.pyc
deleted file mode 100644
index a7c3108..0000000
Binary files a/blog-lsh/__pycache__/models.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/__pycache__/urls.cpython-312.pyc b/blog-lsh/__pycache__/urls.cpython-312.pyc
deleted file mode 100644
index f97d159..0000000
Binary files a/blog-lsh/__pycache__/urls.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/__pycache__/views.cpython-312.pyc b/blog-lsh/__pycache__/views.cpython-312.pyc
deleted file mode 100644
index c95b610..0000000
Binary files a/blog-lsh/__pycache__/views.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/apps.py b/blog-lsh/apps.py
deleted file mode 100644
index b26e48a..0000000
--- a/blog-lsh/apps.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-LJX: Blog应用配置模块
-定义blog应用的配置信息,包括应用名称等基础设置
-"""
-from django.apps import AppConfig
-
-
-class BlogConfig(AppConfig):
- """LJX: Blog应用配置类"""
- name = 'blog' # LJX: 应用名称
\ No newline at end of file
diff --git a/blog-lsh/context_processors.py b/blog-lsh/context_processors.py
deleted file mode 100644
index 0af9401..0000000
--- a/blog-lsh/context_processors.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""
-LJX: 模板上下文处理器模块
-为所有模板提供全局的上下文变量,包括SEO信息、导航数据、网站设置等
-这些变量在所有模板中都可以直接使用
-"""
-import logging
-
-from django.utils import timezone
-
-from djangoblog.utils import cache, get_blog_setting
-from .models import Category, Article
-
-logger = logging.getLogger(__name__)
-
-
-def seo_processor(requests):
- """LJX: SEO上下文处理器,为模板提供SEO相关变量"""
- key = 'seo_processor'
- value = cache.get(key)
- if value:
- return value # LJX: 如果缓存中存在,直接返回
- else:
- logger.info('set processor cache.')
- setting = get_blog_setting() # LJX: 获取博客设置
- value = {
- 'SITE_NAME': setting.site_name, # LJX: 网站名称
- 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # LJX: 是否显示Google广告
- 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # LJX: 广告代码
- 'SITE_SEO_DESCRIPTION': setting.site_seo_description, # LJX: SEO描述
- 'SITE_DESCRIPTION': setting.site_description, # LJX: 网站描述
- 'SITE_KEYWORDS': setting.site_keywords, # LJX: 网站关键词
- 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # LJX: 网站基础URL
- 'ARTICLE_SUB_LENGTH': setting.article_sub_length, # LJX: 文章摘要长度
- 'nav_category_list': Category.objects.all(), # LJX: 导航分类列表
- 'nav_pages': Article.objects.filter( # LJX: 导航页面
- type='p',
- status='p'),
- 'OPEN_SITE_COMMENT': setting.open_site_comment, # LJX: 是否开启评论
- 'BEIAN_CODE': setting.beian_code, # LJX: 备案号
- 'ANALYTICS_CODE': setting.analytics_code, # LJX: 统计代码
- "BEIAN_CODE_GONGAN": setting.gongan_beiancode, # LJX: 公安备案号
- "SHOW_GONGAN_CODE": setting.show_gongan_code, # LJX: 是否显示公安备案
- "CURRENT_YEAR": timezone.now().year, # LJX: 当前年份
- "GLOBAL_HEADER": setting.global_header, # LJX: 全局头部
- "GLOBAL_FOOTER": setting.global_footer, # LJX: 全局尾部
- "COMMENT_NEED_REVIEW": setting.comment_need_review, # LJX: 评论是否需要审核
- }
- cache.set(key, value, 60 * 60 * 10) # LJX: 缓存10小时
- return value
\ No newline at end of file
diff --git a/blog-lsh/forms.py b/blog-lsh/forms.py
deleted file mode 100644
index 82e6929..0000000
--- a/blog-lsh/forms.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""
-LJX: 表单定义模块
-定义博客搜索相关的表单类和验证逻辑
-"""
-import logging
-
-from django import forms
-from haystack.forms import SearchForm
-
-logger = logging.getLogger(__name__)
-
-
-class BlogSearchForm(SearchForm):
- """LJX: 博客搜索表单,继承自Haystack的SearchForm"""
- querydata = forms.CharField(required=True) # LJX: 搜索查询字段,必须填写
-
- def search(self):
- """LJX: 执行搜索操作"""
- datas = super(BlogSearchForm, self).search() # LJX: 调用父类搜索方法
- if not self.is_valid(): # LJX: 表单验证
- return self.no_query_found() # LJX: 如果没有查询条件,返回空结果
-
- if self.cleaned_data['querydata']:
- logger.info(self.cleaned_data['querydata']) # LJX: 记录搜索关键词
- return datas
\ No newline at end of file
diff --git a/blog-lsh/middleware.py b/blog-lsh/middleware.py
deleted file mode 100644
index af8172d..0000000
--- a/blog-lsh/middleware.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""
-LJX: 中间件模块
-定义自定义中间件,用于处理请求和响应的额外逻辑
-包括性能监控、用户访问统计等功能
-"""
-import logging
-import time
-
-from ipware import get_client_ip
-from user_agents import parse
-
-from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
-
-logger = logging.getLogger(__name__)
-
-
-class OnlineMiddleware(object):
- """LJX: 在线中间件,用于监控页面渲染时间和用户访问信息"""
- def __init__(self, get_response=None):
- self.get_response = get_response
- super().__init__()
-
- def __call__(self, request):
- ''' LJX: 页面渲染时间监控 '''
- start_time = time.time() # LJX: 记录开始时间
- response = self.get_response(request) # LJX: 获取响应
- http_user_agent = request.META.get('HTTP_USER_AGENT', '') # LJX: 获取用户代理
- ip, _ = get_client_ip(request) # LJX: 获取客户端IP
- user_agent = parse(http_user_agent) # LJX: 解析用户代理信息
-
- if not response.streaming: # LJX: 如果不是流式响应
- try:
- cast_time = time.time() - start_time # LJX: 计算渲染耗时
- if ELASTICSEARCH_ENABLED:
- time_taken = round((cast_time) * 1000, 2) # LJX: 转换为毫秒
- url = request.path # LJX: 请求路径
- from django.utils import timezone
- # LJX: 创建性能监控记录
- ElaspedTimeDocumentManager.create(
- url=url,
- time_taken=time_taken,
- log_datetime=timezone.now(),
- useragent=user_agent,
- ip=ip)
- # LJX: 在响应内容中替换加载时间占位符
- response.content = response.content.replace(
- b'', str.encode(str(cast_time)[:5]))
- except Exception as e:
- logger.error("Error OnlineMiddleware: %s" % e) # LJX: 记录错误日志
-
- return response
\ No newline at end of file
diff --git a/blog-lsh/migrations/__pycache__/0001_initial.cpython-312.pyc b/blog-lsh/migrations/__pycache__/0001_initial.cpython-312.pyc
deleted file mode 100644
index 8a9660b..0000000
Binary files a/blog-lsh/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-312.pyc b/blog-lsh/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-312.pyc
deleted file mode 100644
index b0f3a9e..0000000
Binary files a/blog-lsh/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-312.pyc b/blog-lsh/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-312.pyc
deleted file mode 100644
index 844b4a3..0000000
Binary files a/blog-lsh/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-312.pyc b/blog-lsh/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-312.pyc
deleted file mode 100644
index 33a5e45..0000000
Binary files a/blog-lsh/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-312.pyc b/blog-lsh/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-312.pyc
deleted file mode 100644
index 28359fc..0000000
Binary files a/blog-lsh/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/migrations/__pycache__/0006_alter_blogsettings_options.cpython-312.pyc b/blog-lsh/migrations/__pycache__/0006_alter_blogsettings_options.cpython-312.pyc
deleted file mode 100644
index 4710bc6..0000000
Binary files a/blog-lsh/migrations/__pycache__/0006_alter_blogsettings_options.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/migrations/__pycache__/__init__.cpython-312.pyc b/blog-lsh/migrations/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 4fe1f59..0000000
Binary files a/blog-lsh/migrations/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/models.py b/blog-lsh/models.py
deleted file mode 100644
index 74c8535..0000000
--- a/blog-lsh/models.py
+++ /dev/null
@@ -1,397 +0,0 @@
-"""
-LJX: 数据模型定义模块
-定义博客系统的核心数据模型,包括文章、分类、标签、友情链接等
-使用Django的ORM进行数据库映射和操作
-"""
-import logging
-import re
-from abc import abstractmethod
-
-from django.conf import settings
-from django.core.exceptions import ValidationError
-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 mdeditor.fields import MDTextField
-from uuslug import slugify
-
-from djangoblog.utils import cache_decorator, cache
-from djangoblog.utils import get_current_site
-
-logger = logging.getLogger(__name__)
-
-
-class LinkShowType(models.TextChoices):
- """LJX: 链接显示类型选择枚举"""
- I = ('i', _('index')) # LJX: 首页显示
- L = ('l', _('list')) # LJX: 列表页显示
- P = ('p', _('post')) # LJX: 文章页显示
- A = ('a', _('all')) # LJX: 所有页面显示
- S = ('s', _('slide')) # LJX: 幻灯片显示
-
-
-class BaseModel(models.Model):
- """LJX: 基础模型类,提供公共字段和方法"""
- id = models.AutoField(primary_key=True) # LJX: 自增主键
- creation_time = models.DateTimeField(_('creation time'), default=now) # LJX: 创建时间
- last_modify_time = models.DateTimeField(_('modify time'), default=now) # LJX: 最后修改时间
-
- def save(self, *args, **kwargs):
- """LJX: 重写保存方法,添加自动处理逻辑"""
- is_update_views = isinstance(
- self,
- Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] # LJX: 检查是否是更新浏览量
- if is_update_views:
- Article.objects.filter(pk=self.pk).update(views=self.views) # LJX: 直接更新浏览量,避免递归
- else:
- if 'slug' in self.__dict__: # LJX: 如果有slug字段,自动生成
- slug = getattr(
- self, 'title') if 'title' in self.__dict__ else getattr(
- self, 'name') # LJX: 根据title或name生成slug
- setattr(self, 'slug', slugify(slug)) # LJX: 使用uuslug生成友好的URL
- super().save(*args, **kwargs) # LJX: 调用父类保存方法
-
- def get_full_url(self):
- """LJX: 获取完整URL"""
- site = get_current_site().domain # LJX: 获取当前站点域名
- url = "https://{site}{path}".format(site=site,
- path=self.get_absolute_url()) # LJX: 拼接完整URL
- return url
-
- class Meta:
- abstract = True # LJX: 抽象基类,不会创建数据库表
-
- @abstractmethod
- def get_absolute_url(self):
- """LJX: 抽象方法,子类必须实现获取绝对URL的方法"""
- pass
-
-
-class Article(BaseModel):
- """LJX: 文章模型,博客系统的核心数据模型"""
- STATUS_CHOICES = ( # LJX: 文章状态选择
- ('d', _('Draft')), # LJX: 草稿
- ('p', _('Published')), # LJX: 已发布
- )
- COMMENT_STATUS = ( # LJX: 评论状态选择
- ('o', _('Open')), # LJX: 开启评论
- ('c', _('Close')), # LJX: 关闭评论
- )
- TYPE = ( # LJX: 文章类型选择
- ('a', _('Article')), # LJX: 普通文章
- ('p', _('Page')), # LJX: 页面
- )
-
- # LJX: 文章核心字段
- title = models.CharField(_('title'), max_length=200, unique=True) # LJX: 标题,唯一
- body = MDTextField(_('body')) # LJX: 内容,使用Markdown编辑器
- pub_time = models.DateTimeField( # LJX: 发布时间
- _('publish time'), blank=False, null=False, default=now)
- status = models.CharField( # LJX: 状态
- _('status'),
- max_length=1,
- choices=STATUS_CHOICES,
- default='p') # LJX: 默认已发布
- comment_status = models.CharField( # LJX: 评论状态
- _('comment status'),
- max_length=1,
- choices=COMMENT_STATUS,
- default='o') # LJX: 默认开启评论
- type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # LJX: 类型,默认普通文章
- views = models.PositiveIntegerField(_('views'), default=0) # LJX: 浏览量
- author = models.ForeignKey( # LJX: 作者,外键关联用户模型
- settings.AUTH_USER_MODEL,
- verbose_name=_('author'),
- blank=False,
- null=False,
- on_delete=models.CASCADE) # LJX: 级联删除
- article_order = models.IntegerField( # LJX: 文章排序
- _('order'), blank=False, null=False, default=0)
- show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # LJX: 是否显示目录
- category = models.ForeignKey( # LJX: 分类,外键关联分类模型
- 'Category',
- verbose_name=_('category'),
- on_delete=models.CASCADE,
- blank=False,
- null=False)
- tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # LJX: 标签,多对多关系
-
- def body_to_string(self):
- """LJX: 将文章内容转换为字符串"""
- return self.body
-
- def __str__(self):
- """LJX: 字符串表示,返回文章标题"""
- return self.title
-
- class Meta:
- ordering = ['-article_order', '-pub_time'] # LJX: 默认按排序和发布时间降序
- verbose_name = _('article') # LJX: 单数名称
- verbose_name_plural = verbose_name # LJX: 复数名称
- get_latest_by = 'id' # LJX: 最新记录按ID
-
- def get_absolute_url(self):
- """LJX: 获取文章绝对URL,用于生成文章详情页链接"""
- return reverse('blog:detailbyid', kwargs={
- 'article_id': self.id,
- 'year': self.creation_time.year, # LJX: 包含年月日用于SEO友好的URL
- 'month': self.creation_time.month,
- 'day': self.creation_time.day
- })
-
- @cache_decorator(60 * 60 * 10) # LJX: 缓存10小时
- def get_category_tree(self):
- """LJX: 获取分类树,返回分类的层级结构"""
- tree = self.category.get_category_tree() # LJX: 调用分类的获取分类树方法
- names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) # LJX: 转换为名称和URL的元组列表
- return names
-
- def save(self, *args, **kwargs):
- """LJX: 重写保存方法"""
- super().save(*args, **kwargs)
-
- def viewed(self):
- """LJX: 增加文章浏览量"""
- self.views += 1
- self.save(update_fields=['views']) # LJX: 只更新views字段
-
- def comment_list(self):
- """LJX: 获取文章评论列表,使用缓存提高性能"""
- cache_key = 'article_comments_{id}'.format(id=self.id)
- value = cache.get(cache_key)
- if value:
- logger.info('get article comments:{id}'.format(id=self.id))
- return value # LJX: 如果缓存中存在,直接返回
- else:
- comments = self.comment_set.filter(is_enable=True).order_by('-id') # LJX: 获取已启用的评论,按ID降序
- cache.set(cache_key, comments, 60 * 100) # LJX: 缓存100分钟
- logger.info('set article comments:{id}'.format(id=self.id))
- return comments
-
- def get_admin_url(self):
- """LJX: 获取文章在Admin后台的URL"""
- info = (self._meta.app_label, self._meta.model_name)
- return reverse('admin:%s_%s_change' % info, args=(self.pk,))
-
- @cache_decorator(expiration=60 * 100) # LJX: 缓存100分钟
- def next_article(self):
- """LJX: 获取下一篇文章"""
- return Article.objects.filter(
- id__gt=self.id, status='p').order_by('id').first() # LJX: ID大于当前文章的第一篇已发布文章
-
- @cache_decorator(expiration=60 * 100) # LJX: 缓存100分钟
- def prev_article(self):
- """LJX: 获取上一篇文章"""
- return Article.objects.filter(id__lt=self.id, status='p').first() # LJX: ID小于当前文章的第一篇已发布文章
-
- def get_first_image_url(self):
- """LJX: 从文章内容中提取第一张图片的URL"""
- match = re.search(r'!\[.*?\]\((.+?)\)', self.body) # LJX: 使用正则匹配Markdown图片语法
- if match:
- return match.group(1) # LJX: 返回图片URL
- return ""
-
-class Category(BaseModel):
- """LJX: 文章分类模型,支持多级分类结构"""
- name = models.CharField(_('category name'), max_length=30, unique=True) # LJX: 分类名称,唯一
- parent_category = models.ForeignKey( # LJX: 父级分类,支持分类层级
- 'self',
- verbose_name=_('parent category'),
- blank=True,
- null=True,
- on_delete=models.CASCADE) # LJX: 自关联外键
- slug = models.SlugField(default='no-slug', max_length=60, blank=True) # LJX: URL友好名称
- index = models.IntegerField(default=0, verbose_name=_('index')) # LJX: 排序索引
-
- class Meta:
- ordering = ['-index'] # LJX: 按索引降序排列
- verbose_name = _('category') # LJX: 单数名称
- verbose_name_plural = verbose_name # LJX: 复数名称
-
- def get_absolute_url(self):
- """LJX: 获取分类绝对URL"""
- return reverse(
- 'blog:category_detail', kwargs={
- 'category_name': self.slug}) # LJX: 使用slug作为URL参数
-
- def __str__(self):
- """LJX: 字符串表示,返回分类名称"""
- return self.name
-
- @cache_decorator(60 * 60 * 10) # LJX: 缓存10小时
- def get_category_tree(self):
- """LJX: 递归获得分类目录的父级,返回从当前分类到根分类的路径"""
- categorys = [] # LJX: 存储分类路径
-
- def parse(category):
- """LJX: 递归解析分类父级"""
- categorys.append(category)
- if category.parent_category: # LJX: 如果存在父分类,继续递归
- parse(category.parent_category)
-
- parse(self) # LJX: 从当前分类开始解析
- return categorys
-
- @cache_decorator(60 * 60 * 10) # LJX: 缓存10小时
- def get_sub_categorys(self):
- """LJX: 获得当前分类目录所有子集,包括所有下级分类"""
- categorys = [] # LJX: 存储所有子分类
- all_categorys = Category.objects.all() # LJX: 获取所有分类
-
- def parse(category):
- """LJX: 递归解析子分类"""
- if category not in categorys:
- categorys.append(category) # LJX: 添加当前分类
- childs = all_categorys.filter(parent_category=category) # LJX: 查找直接子分类
- for child in childs:
- if category not in categorys:
- categorys.append(child) # LJX: 添加子分类
- parse(child) # LJX: 递归解析子分类的子分类
-
- parse(self) # LJX: 从当前分类开始解析
- return categorys
-
-
-class Tag(BaseModel):
- """LJX: 文章标签模型,用于文章分类和检索"""
- name = models.CharField(_('tag name'), max_length=30, unique=True) # LJX: 标签名称,唯一
- slug = models.SlugField(default='no-slug', max_length=60, blank=True) # LJX: URL友好名称
-
- def __str__(self):
- """LJX: 字符串表示,返回标签名称"""
- return self.name
-
- def get_absolute_url(self):
- """LJX: 获取标签绝对URL"""
- return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) # LJX: 使用slug作为URL参数
-
- @cache_decorator(60 * 60 * 10) # LJX: 缓存10小时
- def get_article_count(self):
- """LJX: 获取使用该标签的文章数量"""
- return Article.objects.filter(tags__name=self.name).distinct().count() # LJX: 去重计数
-
- class Meta:
- ordering = ['name'] # LJX: 按名称排序
- verbose_name = _('tag') # LJX: 单数名称
- verbose_name_plural = verbose_name # LJX: 复数名称
-
-
-class Links(models.Model):
- """LJX: 友情链接模型,管理网站的外部链接"""
-
- name = models.CharField(_('link name'), max_length=30, unique=True) # LJX: 链接名称,唯一
- link = models.URLField(_('link')) # LJX: 链接地址
- sequence = models.IntegerField(_('order'), unique=True) # LJX: 显示顺序,唯一
- is_enable = models.BooleanField( # LJX: 是否启用显示
- _('is show'), default=True, blank=False, null=False)
- show_type = models.CharField( # LJX: 显示类型
- _('show type'),
- max_length=1,
- choices=LinkShowType.choices,
- default=LinkShowType.I) # LJX: 默认在首页显示
- creation_time = models.DateTimeField(_('creation time'), default=now) # LJX: 创建时间
- last_mod_time = models.DateTimeField(_('modify time'), default=now) # LJX: 最后修改时间
-
- class Meta:
- ordering = ['sequence'] # LJX: 按顺序排序
- verbose_name = _('link') # LJX: 单数名称
- verbose_name_plural = verbose_name # LJX: 复数名称
-
- def __str__(self):
- """LJX: 字符串表示,返回链接名称"""
- return self.name
-
-
-class SideBar(models.Model):
- """LJX: 侧边栏模型,可以展示一些HTML内容"""
- name = models.CharField(_('title'), max_length=100) # LJX: 侧边栏标题
- content = models.TextField(_('content')) # LJX: 侧边栏内容,支持HTML
- sequence = models.IntegerField(_('order'), unique=True) # LJX: 显示顺序,唯一
- is_enable = models.BooleanField(_('is enable'), default=True) # LJX: 是否启用
- creation_time = models.DateTimeField(_('creation time'), default=now) # LJX: 创建时间
- last_mod_time = models.DateTimeField(_('modify time'), default=now) # LJX: 最后修改时间
-
- class Meta:
- ordering = ['sequence'] # LJX: 按顺序排序
- verbose_name = _('sidebar') # LJX: 单数名称
- verbose_name_plural = verbose_name # LJX: 复数名称
-
- def __str__(self):
- """LJX: 字符串表示,返回侧边栏名称"""
- return self.name
-
-
-class BlogSettings(models.Model):
- """LJX: 博客设置模型,存储博客的全局配置信息"""
- site_name = models.CharField( # LJX: 网站名称
- _('site name'),
- max_length=200,
- null=False,
- blank=False,
- default='')
- site_description = models.TextField( # LJX: 网站描述
- _('site description'),
- max_length=1000,
- null=False,
- blank=False,
- default='')
- site_seo_description = models.TextField( # LJX: 网站SEO描述
- _('site seo description'), max_length=1000, null=False, blank=False, default='')
- site_keywords = models.TextField( # LJX: 网站关键词
- _('site keywords'),
- max_length=1000,
- null=False,
- blank=False,
- default='')
- article_sub_length = models.IntegerField(_('article sub length'), default=300) # LJX: 文章摘要长度
- sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # LJX: 侧边栏文章数量
- sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # LJX: 侧边栏评论数量
- article_comment_count = models.IntegerField(_('article comment count'), default=5) # LJX: 文章评论显示数量
- show_google_adsense = models.BooleanField(_('show adsense'), default=False) # LJX: 是否显示Google广告
- google_adsense_codes = models.TextField( # LJX: Google广告代码
- _('adsense code'), max_length=2000, null=True, blank=True, default='')
- open_site_comment = models.BooleanField(_('open site comment'), default=True) # LJX: 是否开启全站评论
- global_header = models.TextField("公共头部", null=True, blank=True, default='') # LJX: 全局头部HTML
- global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # LJX: 全局尾部HTML
- beian_code = models.CharField( # LJX: 备案号
- '备案号',
- max_length=2000,
- null=True,
- blank=True,
- default='')
- analytics_code = models.TextField( # LJX: 网站统计代码
- "网站统计代码",
- max_length=1000,
- null=False,
- blank=False,
- default='')
- show_gongan_code = models.BooleanField( # LJX: 是否显示公安备案号
- '是否显示公安备案号', default=False, null=False)
- gongan_beiancode = models.TextField( # LJX: 公安备案号
- '公安备案号',
- max_length=2000,
- null=True,
- blank=True,
- default='')
- comment_need_review = models.BooleanField( # LJX: 评论是否需要审核
- '评论是否需要审核', default=False, null=False)
-
- class Meta:
- verbose_name = _('Website configuration') # LJX: 单数名称
- verbose_name_plural = verbose_name # LJX: 复数名称
-
- def __str__(self):
- """LJX: 字符串表示,返回网站名称"""
- return self.site_name
-
- def clean(self):
- """LJX: 数据清洗验证,确保只能有一个配置实例"""
- if BlogSettings.objects.exclude(id=self.id).count():
- raise ValidationError(_('There can only be one configuration')) # LJX: 只能有一个配置
-
- def save(self, *args, **kwargs):
- """LJX: 重写保存方法,保存后清除缓存"""
- super().save(*args, **kwargs)
- from djangoblog.utils import cache
- cache.clear() # LJX: 清除缓存,使配置立即生效
\ No newline at end of file
diff --git a/blog-lsh/search_indexes.py b/blog-lsh/search_indexes.py
deleted file mode 100644
index 09af7eb..0000000
--- a/blog-lsh/search_indexes.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""
-LJX: Haystack搜索索引配置模块
-定义Django Haystack的搜索索引配置,用于全文搜索功能
-"""
-from haystack import indexes
-
-from blog.models import Article
-
-
-class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
- """LJX: 文章搜索索引类,定义文章的搜索字段和索引行为"""
- text = indexes.CharField(document=True, use_template=True) # LJX: 主搜索字段,使用模板定义
-
- def get_model(self):
- """LJX: 返回要索引的模型类"""
- return Article
-
- def index_queryset(self, using=None):
- """LJX: 返回要索引的查询集,只索引已发布的文章"""
- return self.get_model().objects.filter(status='p') # LJX: 只索引已发布状态的文章
\ No newline at end of file
diff --git a/blog-lsh/templatetags/__pycache__/__init__.cpython-312.pyc b/blog-lsh/templatetags/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 8e5af91..0000000
Binary files a/blog-lsh/templatetags/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/templatetags/__pycache__/blog_tags.cpython-312.pyc b/blog-lsh/templatetags/__pycache__/blog_tags.cpython-312.pyc
deleted file mode 100644
index e47902d..0000000
Binary files a/blog-lsh/templatetags/__pycache__/blog_tags.cpython-312.pyc and /dev/null differ
diff --git a/blog-lsh/urls.py b/blog-lsh/urls.py
deleted file mode 100644
index 95aa89d..0000000
--- a/blog-lsh/urls.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""
-LJX: URL路由配置模块
-定义blog应用的所有URL路由规则,将URL映射到相应的视图函数或类
-使用Django的path函数定义RESTful风格的URL
-"""
-from django.urls import path
-from django.views.decorators.cache import cache_page
-
-from . import views
-
-app_name = "blog" # LJX: 应用命名空间,用于URL反向解析
-urlpatterns = [
- # LJX: 首页路由 - 显示文章列表
- path(
- r'',
- views.IndexView.as_view(), # LJX: 使用类视图处理首页
- name='index'), # LJX: URL名称,用于反向解析
- # LJX: 首页分页路由 - 显示指定页码的文章列表
- path(
- r'page//', # LJX: 页码参数,整数类型
- views.IndexView.as_view(),
- name='index_page'),
- # LJX: 文章详情页路由 - 使用年月日和文章ID构建SEO友好的URL
- path(
- r'article////.html', # LJX: 包含年月日的URL结构
- views.ArticleDetailView.as_view(),
- name='detailbyid'), # LJX: 通过ID获取文章详情
- # LJX: 分类详情页路由 - 显示指定分类的文章列表
- path(
- r'category/.html', # LJX: 使用slug作为分类标识
- views.CategoryDetailView.as_view(),
- name='category_detail'),
- # LJX: 分类分页路由 - 显示指定分类的指定页码
- path(
- r'category//.html',
- views.CategoryDetailView.as_view(),
- name='category_detail_page'),
- # LJX: 作者详情页路由 - 显示指定作者的文章列表
- path(
- r'author/.html', # LJX: 作者用户名作为参数
- views.AuthorDetailView.as_view(),
- name='author_detail'),
- # LJX: 作者分页路由 - 显示指定作者的指定页码
- path(
- r'author//.html',
- views.AuthorDetailView.as_view(),
- name='author_detail_page'),
- # LJX: 标签详情页路由 - 显示指定标签的文章列表
- path(
- r'tag/.html', # LJX: 使用slug作为标签标识
- views.TagDetailView.as_view(),
- name='tag_detail'),
- # LJX: 标签分页路由 - 显示指定标签的指定页码
- path(
- r'tag//.html',
- views.TagDetailView.as_view(),
- name='tag_detail_page'),
- # LJX: 文章归档页路由 - 显示所有文章的归档列表,使用缓存提高性能
- path(
- 'archives.html',
- cache_page( # LJX: 页面缓存,60*60秒=1小时
- 60 * 60)(
- views.ArchivesView.as_view()),
- name='archives'),
- # LJX: 友情链接页路由 - 显示所有友情链接
- path(
- 'links.html',
- views.LinkListView.as_view(),
- name='links'),
- # LJX: 文件上传路由 - 处理图片等文件的上传
- path(
- r'upload',
- views.fileupload, # LJX: 使用函数视图处理文件上传
- name='upload'),
- # LJX: 缓存清理路由 - 用于手动清理系统缓存
- path(
- r'clean',
- views.clean_cache_view,
- name='clean'),
-]
\ No newline at end of file
diff --git a/blog-lsh/templatetags/__init__.py b/comments/__init__.py
similarity index 100%
rename from blog-lsh/templatetags/__init__.py
rename to comments/__init__.py
diff --git a/comments/admin.py b/comments/admin.py
new file mode 100644
index 0000000..a814f3f
--- /dev/null
+++ b/comments/admin.py
@@ -0,0 +1,47 @@
+from django.contrib import admin
+from django.urls import reverse
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+
+
+def disable_commentstatus(modeladmin, request, queryset):
+ queryset.update(is_enable=False)
+
+
+def enable_commentstatus(modeladmin, request, queryset):
+ queryset.update(is_enable=True)
+
+
+disable_commentstatus.short_description = _('Disable comments')
+enable_commentstatus.short_description = _('Enable comments')
+
+
+class CommentAdmin(admin.ModelAdmin):
+ list_per_page = 20
+ list_display = (
+ 'id',
+ 'body',
+ 'link_to_userinfo',
+ 'link_to_article',
+ 'is_enable',
+ 'creation_time')
+ list_display_links = ('id', 'body', 'is_enable')
+ list_filter = ('is_enable',)
+ exclude = ('creation_time', 'last_modify_time')
+ actions = [disable_commentstatus, enable_commentstatus]
+
+ def link_to_userinfo(self, obj):
+ info = (obj.author._meta.app_label, obj.author._meta.model_name)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
+ return format_html(
+ u'%s' %
+ (link, obj.author.nickname if obj.author.nickname else obj.author.email))
+
+ def link_to_article(self, obj):
+ info = (obj.article._meta.app_label, obj.article._meta.model_name)
+ link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
+ return format_html(
+ u'%s' % (link, obj.article.title))
+
+ link_to_userinfo.short_description = _('User')
+ link_to_article.short_description = _('Article')
diff --git a/comments/apps.py b/comments/apps.py
new file mode 100644
index 0000000..ff01b77
--- /dev/null
+++ b/comments/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CommentsConfig(AppConfig):
+ name = 'comments'
diff --git a/comments/forms.py b/comments/forms.py
new file mode 100644
index 0000000..e83737d
--- /dev/null
+++ b/comments/forms.py
@@ -0,0 +1,13 @@
+from django import forms
+from django.forms import ModelForm
+
+from .models import Comment
+
+
+class CommentForm(ModelForm):
+ parent_comment_id = forms.IntegerField(
+ widget=forms.HiddenInput, required=False)
+
+ class Meta:
+ model = Comment
+ fields = ['body']
diff --git a/comments/migrations/0001_initial.py b/comments/migrations/0001_initial.py
new file mode 100644
index 0000000..61d1e53
--- /dev/null
+++ b/comments/migrations/0001_initial.py
@@ -0,0 +1,38 @@
+# 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
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('blog', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Comment',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('body', models.TextField(max_length=300, 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='修改时间')),
+ ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
+ ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
+ ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
+ ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
+ ],
+ options={
+ 'verbose_name': '评论',
+ 'verbose_name_plural': '评论',
+ 'ordering': ['-id'],
+ 'get_latest_by': 'id',
+ },
+ ),
+ ]
diff --git a/comments/migrations/0002_alter_comment_is_enable.py b/comments/migrations/0002_alter_comment_is_enable.py
new file mode 100644
index 0000000..17c44db
--- /dev/null
+++ b/comments/migrations/0002_alter_comment_is_enable.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.7 on 2023-04-24 13:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('comments', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='comment',
+ name='is_enable',
+ field=models.BooleanField(default=False, verbose_name='是否显示'),
+ ),
+ ]
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
new file mode 100644
index 0000000..a1ca970
--- /dev/null
+++ b/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py
@@ -0,0 +1,60 @@
+# 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
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('blog', '0005_alter_article_options_alter_category_options_and_more'),
+ ('comments', '0002_alter_comment_is_enable'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='comment',
+ options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
+ ),
+ migrations.RemoveField(
+ model_name='comment',
+ name='created_time',
+ ),
+ migrations.RemoveField(
+ model_name='comment',
+ name='last_mod_time',
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='creation_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='last_modify_time',
+ field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='article',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='author',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='is_enable',
+ field=models.BooleanField(default=False, verbose_name='enable'),
+ ),
+ migrations.AlterField(
+ model_name='comment',
+ name='parent_comment',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
+ ),
+ ]
diff --git a/comments/migrations/__init__.py b/comments/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/comments/models.py b/comments/models.py
new file mode 100644
index 0000000..7c3bbc8
--- /dev/null
+++ b/comments/models.py
@@ -0,0 +1,39 @@
+from django.conf import settings
+from django.db import models
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+
+from blog.models import Article
+
+
+# Create your models here.
+
+class Comment(models.Model):
+ body = models.TextField('正文', max_length=300)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+ author = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ verbose_name=_('author'),
+ on_delete=models.CASCADE)
+ article = models.ForeignKey(
+ Article,
+ verbose_name=_('article'),
+ on_delete=models.CASCADE)
+ parent_comment = models.ForeignKey(
+ 'self',
+ verbose_name=_('parent comment'),
+ blank=True,
+ null=True,
+ on_delete=models.CASCADE)
+ is_enable = models.BooleanField(_('enable'),
+ default=False, blank=False, null=False)
+
+ class Meta:
+ ordering = ['-id']
+ verbose_name = _('comment')
+ verbose_name_plural = verbose_name
+ get_latest_by = 'id'
+
+ def __str__(self):
+ return self.body
diff --git a/comments/templatetags/__init__.py b/comments/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/comments/templatetags/comments_tags.py b/comments/templatetags/comments_tags.py
new file mode 100644
index 0000000..fde02b4
--- /dev/null
+++ b/comments/templatetags/comments_tags.py
@@ -0,0 +1,30 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def parse_commenttree(commentlist, comment):
+ """获得当前评论子评论的列表
+ 用法: {% parse_commenttree article_comments comment as childcomments %}
+ """
+ datas = []
+
+ def parse(c):
+ childs = commentlist.filter(parent_comment=c, is_enable=True)
+ for child in childs:
+ datas.append(child)
+ parse(child)
+
+ parse(comment)
+ return datas
+
+
+@register.inclusion_tag('comments/tags/comment_item.html')
+def show_comment_item(comment, ischild):
+ """评论"""
+ depth = 1 if ischild else 2
+ return {
+ 'comment_item': comment,
+ 'depth': depth
+ }
diff --git a/comments/tests.py b/comments/tests.py
new file mode 100644
index 0000000..2a7f55f
--- /dev/null
+++ b/comments/tests.py
@@ -0,0 +1,109 @@
+from django.test import Client, RequestFactory, TransactionTestCase
+from django.urls import reverse
+
+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
+
+
+# Create your tests here.
+
+class CommentsTest(TransactionTestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+ from blog.models import BlogSettings
+ value = BlogSettings()
+ value.comment_need_review = True
+ value.save()
+
+ self.user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="liangliangyy1")
+
+ def update_article_comment_status(self, article):
+ comments = article.comment_set.all()
+ for comment in comments:
+ comment.is_enable = True
+ comment.save()
+
+ def test_validate_comment(self):
+ self.client.login(username='liangliangyy1', password='liangliangyy1')
+
+ category = Category()
+ category.name = "categoryccc"
+ category.save()
+
+ article = Article()
+ article.title = "nicetitleccc"
+ article.body = "nicecontentccc"
+ article.author = self.user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+
+ comment_url = reverse(
+ 'comments:postcomment', kwargs={
+ 'article_id': article.id})
+
+ response = self.client.post(comment_url,
+ {
+ 'body': '123ffffffffff'
+ })
+
+ self.assertEqual(response.status_code, 302)
+
+ article = Article.objects.get(pk=article.pk)
+ self.assertEqual(len(article.comment_list()), 0)
+ self.update_article_comment_status(article)
+
+ self.assertEqual(len(article.comment_list()), 1)
+
+ response = self.client.post(comment_url,
+ {
+ 'body': '123ffffffffff',
+ })
+
+ self.assertEqual(response.status_code, 302)
+
+ article = Article.objects.get(pk=article.pk)
+ self.update_article_comment_status(article)
+ self.assertEqual(len(article.comment_list()), 2)
+ parent_comment_id = article.comment_list()[0].id
+
+ response = self.client.post(comment_url,
+ {
+ 'body': '''
+ # Title1
+
+ ```python
+ import os
+ ```
+
+ [url](https://www.lylinux.net/)
+
+ [ddd](http://www.baidu.com)
+
+
+ ''',
+ 'parent_comment_id': parent_comment_id
+ })
+
+ self.assertEqual(response.status_code, 302)
+ self.update_article_comment_status(article)
+ article = Article.objects.get(pk=article.pk)
+ self.assertEqual(len(article.comment_list()), 3)
+ comment = Comment.objects.get(id=parent_comment_id)
+ tree = parse_commenttree(article.comment_list(), comment)
+ self.assertEqual(len(tree), 1)
+ data = show_comment_item(comment, True)
+ self.assertIsNotNone(data)
+ s = get_max_articleid_commentid()
+ self.assertIsNotNone(s)
+
+ from comments.utils import send_comment_email
+ send_comment_email(comment)
diff --git a/comments/urls.py b/comments/urls.py
new file mode 100644
index 0000000..7df3fab
--- /dev/null
+++ b/comments/urls.py
@@ -0,0 +1,11 @@
+from django.urls import path
+
+from . import views
+
+app_name = "comments"
+urlpatterns = [
+ path(
+ 'article//postcomment',
+ views.CommentPostView.as_view(),
+ name='postcomment'),
+]
diff --git a/comments/utils.py b/comments/utils.py
new file mode 100644
index 0000000..f01dba7
--- /dev/null
+++ b/comments/utils.py
@@ -0,0 +1,38 @@
+import logging
+
+from django.utils.translation import gettext_lazy as _
+
+from djangoblog.utils import get_current_site
+from djangoblog.utils import send_email
+
+logger = logging.getLogger(__name__)
+
+
+def send_comment_email(comment):
+ site = get_current_site().domain
+ subject = _('Thanks for your comment')
+ article_url = f"https://{site}{comment.article.get_absolute_url()}"
+ html_content = _("""Thank you very much for your comments on this site
+ You can visit %(article_title)s
+ to review your comments,
+ Thank you again!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
+ tomail = comment.author.email
+ send_email([tomail], subject, html_content)
+ try:
+ if comment.parent_comment:
+ html_content = _("""Your comment on %(article_title)s
has
+ received a reply.
%(comment_body)s
+
+ go check it out!
+
+ If the link above cannot be opened, please copy this link to your browser.
+ %(article_url)s
+ """) % {'article_url': article_url, 'article_title': comment.article.title,
+ 'comment_body': comment.parent_comment.body}
+ tomail = comment.parent_comment.author.email
+ send_email([tomail], subject, html_content)
+ except Exception as e:
+ logger.error(e)
diff --git a/comments/views.py b/comments/views.py
new file mode 100644
index 0000000..ad9b2b9
--- /dev/null
+++ b/comments/views.py
@@ -0,0 +1,63 @@
+# Create your views here.
+from django.core.exceptions import ValidationError
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.utils.decorators import method_decorator
+from django.views.decorators.csrf import csrf_protect
+from django.views.generic.edit import FormView
+
+from accounts.models import BlogUser
+from blog.models import Article
+from .forms import CommentForm
+from .models import Comment
+
+
+class CommentPostView(FormView):
+ form_class = CommentForm
+ template_name = 'blog/article_detail.html'
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ return super(CommentPostView, self).dispatch(*args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+ url = article.get_absolute_url()
+ return HttpResponseRedirect(url + "#comments")
+
+ def form_invalid(self, form):
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+
+ return self.render_to_response({
+ 'form': form,
+ 'article': article
+ })
+
+ def form_valid(self, form):
+ """提交的数据验证合法后的逻辑"""
+ user = self.request.user
+ author = BlogUser.objects.get(pk=user.pk)
+ article_id = self.kwargs['article_id']
+ article = get_object_or_404(Article, pk=article_id)
+
+ if article.comment_status == 'c' or article.status == 'c':
+ raise ValidationError("该文章评论已关闭.")
+ comment = form.save(False)
+ comment.article = article
+ from djangoblog.utils import get_blog_setting
+ settings = get_blog_setting()
+ if not settings.comment_need_review:
+ comment.is_enable = True
+ comment.author = author
+
+ if form.cleaned_data['parent_comment_id']:
+ parent_comment = Comment.objects.get(
+ pk=form.cleaned_data['parent_comment_id'])
+ comment.parent_comment = parent_comment
+
+ comment.save(True)
+ return HttpResponseRedirect(
+ "%s#div-comment-%d" %
+ (article.get_absolute_url(), comment.pk))
diff --git a/djangoblog/__init__.py b/djangoblog/__init__.py
new file mode 100644
index 0000000..1e205f4
--- /dev/null
+++ b/djangoblog/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
diff --git a/djangoblog/admin_site.py b/djangoblog/admin_site.py
new file mode 100644
index 0000000..f120405
--- /dev/null
+++ b/djangoblog/admin_site.py
@@ -0,0 +1,64 @@
+from django.contrib.admin import AdminSite
+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 *
+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 *
+
+
+class DjangoBlogAdminSite(AdminSite):
+ site_header = 'djangoblog administration'
+ site_title = 'djangoblog site admin'
+
+ def __init__(self, name='admin'):
+ super().__init__(name)
+
+ def has_permission(self, request):
+ return request.user.is_superuser
+
+ # def get_urls(self):
+ # urls = super().get_urls()
+ # from django.urls import path
+ # from blog.views import refresh_memcache
+ #
+ # my_urls = [
+ # path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
+ # ]
+ # return urls + my_urls
+
+
+admin_site = DjangoBlogAdminSite(name='admin')
+
+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)
+
+admin_site.register(BlogUser, BlogUserAdmin)
+
+admin_site.register(Comment, CommentAdmin)
+
+admin_site.register(OAuthUser, OAuthUserAdmin)
+admin_site.register(OAuthConfig, OAuthConfigAdmin)
+
+admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
+
+admin_site.register(Site, SiteAdmin)
+
+admin_site.register(LogEntry, LogEntryAdmin)
diff --git a/djangoblog/apps.py b/djangoblog/apps.py
new file mode 100644
index 0000000..d29e318
--- /dev/null
+++ b/djangoblog/apps.py
@@ -0,0 +1,11 @@
+from django.apps import AppConfig
+
+class DjangoblogAppConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'djangoblog'
+
+ def ready(self):
+ super().ready()
+ # Import and load plugins here
+ from .plugin_manage.loader import load_plugins
+ load_plugins()
\ No newline at end of file
diff --git a/djangoblog/blog_signals.py b/djangoblog/blog_signals.py
new file mode 100644
index 0000000..393f441
--- /dev/null
+++ b/djangoblog/blog_signals.py
@@ -0,0 +1,122 @@
+import _thread
+import logging
+
+import django.dispatch
+from django.conf import settings
+from django.contrib.admin.models import LogEntry
+from django.contrib.auth.signals import user_logged_in, user_logged_out
+from django.core.mail import EmailMultiAlternatives
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from comments.models import Comment
+from comments.utils import send_comment_email
+from djangoblog.spider_notify import SpiderNotify
+from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
+from djangoblog.utils import get_current_site
+from oauth.models import OAuthUser
+
+logger = logging.getLogger(__name__)
+
+oauth_user_login_signal = django.dispatch.Signal(['id'])
+send_email_signal = django.dispatch.Signal(
+ ['emailto', 'title', 'content'])
+
+
+@receiver(send_email_signal)
+def send_email_signal_handler(sender, **kwargs):
+ emailto = kwargs['emailto']
+ title = kwargs['title']
+ content = kwargs['content']
+
+ msg = EmailMultiAlternatives(
+ title,
+ content,
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ to=emailto)
+ msg.content_subtype = "html"
+
+ from servermanager.models import EmailSendLog
+ log = EmailSendLog()
+ log.title = title
+ log.content = content
+ log.emailto = ','.join(emailto)
+
+ try:
+ result = msg.send()
+ log.send_result = result > 0
+ except Exception as e:
+ logger.error(f"失败邮箱号: {emailto}, {e}")
+ log.send_result = False
+ log.save()
+
+
+@receiver(oauth_user_login_signal)
+def oauth_user_login_signal_handler(sender, **kwargs):
+ id = kwargs['id']
+ oauthuser = OAuthUser.objects.get(id=id)
+ site = get_current_site().domain
+ if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
+ from djangoblog.utils import save_user_avatar
+ oauthuser.picture = save_user_avatar(oauthuser.picture)
+ oauthuser.save()
+
+ delete_sidebar_cache()
+
+
+@receiver(post_save)
+def model_post_save_callback(
+ sender,
+ instance,
+ created,
+ raw,
+ using,
+ update_fields,
+ **kwargs):
+ clearcache = False
+ if isinstance(instance, LogEntry):
+ return
+ if 'get_full_url' in dir(instance):
+ is_update_views = update_fields == {'views'}
+ if not settings.TESTING and not is_update_views:
+ try:
+ notify_url = instance.get_full_url()
+ SpiderNotify.baidu_notify([notify_url])
+ except Exception as ex:
+ logger.error("notify sipder", ex)
+ if not is_update_views:
+ clearcache = True
+
+ if isinstance(instance, Comment):
+ if instance.is_enable:
+ path = instance.article.get_absolute_url()
+ site = get_current_site().domain
+ if site.find(':') > 0:
+ site = site[0:site.find(':')]
+
+ expire_view_cache(
+ path,
+ servername=site,
+ serverport=80,
+ key_prefix='blogdetail')
+ if cache.get('seo_processor'):
+ cache.delete('seo_processor')
+ comment_cache_key = 'article_comments_{id}'.format(
+ id=instance.article.id)
+ cache.delete(comment_cache_key)
+ delete_sidebar_cache()
+ delete_view_cache('article_comments', [str(instance.article.pk)])
+
+ _thread.start_new_thread(send_comment_email, (instance,))
+
+ if clearcache:
+ cache.clear()
+
+
+@receiver(user_logged_in)
+@receiver(user_logged_out)
+def user_auth_callback(sender, request, user, **kwargs):
+ if user and user.username:
+ logger.info(user)
+ delete_sidebar_cache()
+ # cache.clear()
diff --git a/djangoblog/elasticsearch_backend.py b/djangoblog/elasticsearch_backend.py
new file mode 100644
index 0000000..4afe498
--- /dev/null
+++ b/djangoblog/elasticsearch_backend.py
@@ -0,0 +1,183 @@
+from django.utils.encoding import force_str
+from elasticsearch_dsl import Q
+from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
+from haystack.forms import ModelSearchForm
+from haystack.models import SearchResult
+from haystack.utils import log as logging
+
+from blog.documents import ArticleDocument, ArticleDocumentManager
+from blog.models import Article
+
+logger = logging.getLogger(__name__)
+
+
+class ElasticSearchBackend(BaseSearchBackend):
+ def __init__(self, connection_alias, **connection_options):
+ super(
+ ElasticSearchBackend,
+ self).__init__(
+ connection_alias,
+ **connection_options)
+ self.manager = ArticleDocumentManager()
+ self.include_spelling = True
+
+ def _get_models(self, iterable):
+ models = iterable if iterable and iterable[0] else Article.objects.all()
+ docs = self.manager.convert_to_doc(models)
+ return docs
+
+ def _create(self, models):
+ self.manager.create_index()
+ docs = self._get_models(models)
+ self.manager.rebuild(docs)
+
+ def _delete(self, models):
+ for m in models:
+ m.delete()
+ return True
+
+ def _rebuild(self, models):
+ models = models if models else Article.objects.all()
+ docs = self.manager.convert_to_doc(models)
+ self.manager.update_docs(docs)
+
+ def update(self, index, iterable, commit=True):
+
+ models = self._get_models(iterable)
+ self.manager.update_docs(models)
+
+ def remove(self, obj_or_string):
+ models = self._get_models([obj_or_string])
+ self._delete(models)
+
+ def clear(self, models=None, commit=True):
+ self.remove(None)
+
+ @staticmethod
+ def get_suggestion(query: str) -> str:
+ """获取推荐词, 如果没有找到添加原搜索词"""
+
+ search = ArticleDocument.search() \
+ .query("match", body=query) \
+ .suggest('suggest_search', query, term={'field': 'body'}) \
+ .execute()
+
+ keywords = []
+ for suggest in search.suggest.suggest_search:
+ if suggest["options"]:
+ keywords.append(suggest["options"][0]["text"])
+ else:
+ keywords.append(suggest["text"])
+
+ return ' '.join(keywords)
+
+ @log_query
+ def search(self, query_string, **kwargs):
+ logger.info('search query_string:' + query_string)
+
+ start_offset = kwargs.get('start_offset')
+ end_offset = kwargs.get('end_offset')
+
+ # 推荐词搜索
+ if getattr(self, "is_suggest", None):
+ suggestion = self.get_suggestion(query_string)
+ else:
+ suggestion = query_string
+
+ q = Q('bool',
+ should=[Q('match', body=suggestion), Q('match', title=suggestion)],
+ minimum_should_match="70%")
+
+ search = ArticleDocument.search() \
+ .query('bool', filter=[q]) \
+ .filter('term', status='p') \
+ .filter('term', type='a') \
+ .source(False)[start_offset: end_offset]
+
+ results = search.execute()
+ hits = results['hits'].total
+ raw_results = []
+ for raw_result in results['hits']['hits']:
+ app_label = 'blog'
+ model_name = 'Article'
+ additional_fields = {}
+
+ result_class = SearchResult
+
+ result = result_class(
+ app_label,
+ model_name,
+ raw_result['_id'],
+ raw_result['_score'],
+ **additional_fields)
+ raw_results.append(result)
+ facets = {}
+ spelling_suggestion = None if query_string == suggestion else suggestion
+
+ return {
+ 'results': raw_results,
+ 'hits': hits,
+ 'facets': facets,
+ 'spelling_suggestion': spelling_suggestion,
+ }
+
+
+class ElasticSearchQuery(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):
+ return value.query_string
+
+ def get_count(self):
+ results = self.get_results()
+ return len(results) if results else 0
+
+ def get_spelling_suggestion(self, preferred_query=None):
+ return self._spelling_suggestion
+
+ def build_params(self, spelling_query=None):
+ kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
+ return kwargs
+
+
+class ElasticSearchModelSearchForm(ModelSearchForm):
+
+ def search(self):
+ # 是否建议搜索
+ self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
+ sqs = super().search()
+ return sqs
+
+
+class ElasticSearchEngine(BaseEngine):
+ backend = ElasticSearchBackend
+ query = ElasticSearchQuery
diff --git a/djangoblog/feeds.py b/djangoblog/feeds.py
new file mode 100644
index 0000000..8c4e851
--- /dev/null
+++ b/djangoblog/feeds.py
@@ -0,0 +1,40 @@
+from django.contrib.auth import get_user_model
+from django.contrib.syndication.views import Feed
+from django.utils import timezone
+from django.utils.feedgenerator import Rss201rev2Feed
+
+from blog.models import Article
+from djangoblog.utils import CommonMarkdown
+
+
+class DjangoBlogFeed(Feed):
+ feed_type = Rss201rev2Feed
+
+ description = '大巧无工,重剑无锋.'
+ title = "且听风吟 大巧无工,重剑无锋. "
+ link = "/feed/"
+
+ def author_name(self):
+ return get_user_model().objects.first().nickname
+
+ def author_link(self):
+ return get_user_model().objects.first().get_absolute_url()
+
+ def items(self):
+ return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
+
+ def item_title(self, item):
+ return item.title
+
+ def item_description(self, item):
+ return CommonMarkdown.get_markdown(item.body)
+
+ def feed_copyright(self):
+ now = timezone.now()
+ return "Copyright© {year} 且听风吟".format(year=now.year)
+
+ def item_link(self, item):
+ return item.get_absolute_url()
+
+ def item_guid(self, item):
+ return
diff --git a/djangoblog/logentryadmin.py b/djangoblog/logentryadmin.py
new file mode 100644
index 0000000..2f6a535
--- /dev/null
+++ b/djangoblog/logentryadmin.py
@@ -0,0 +1,91 @@
+from django.contrib import admin
+from django.contrib.admin.models import DELETION
+from django.contrib.contenttypes.models import ContentType
+from django.urls import reverse, NoReverseMatch
+from django.utils.encoding import force_str
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
+
+
+class LogEntryAdmin(admin.ModelAdmin):
+ list_filter = [
+ 'content_type'
+ ]
+
+ search_fields = [
+ 'object_repr',
+ 'change_message'
+ ]
+
+ list_display_links = [
+ 'action_time',
+ 'get_change_message',
+ ]
+ list_display = [
+ 'action_time',
+ 'user_link',
+ 'content_type',
+ 'object_link',
+ 'get_change_message',
+ ]
+
+ def has_add_permission(self, request):
+ return False
+
+ def has_change_permission(self, request, obj=None):
+ return (
+ request.user.is_superuser or
+ request.user.has_perm('admin.change_logentry')
+ ) and request.method != 'POST'
+
+ def has_delete_permission(self, request, obj=None):
+ return False
+
+ def object_link(self, obj):
+ object_link = escape(obj.object_repr)
+ content_type = obj.content_type
+
+ if obj.action_flag != DELETION and content_type is not None:
+ # try returning an actual link instead of object repr string
+ try:
+ url = reverse(
+ 'admin:{}_{}_change'.format(content_type.app_label,
+ content_type.model),
+ args=[obj.object_id]
+ )
+ object_link = '{}'.format(url, object_link)
+ except NoReverseMatch:
+ pass
+ return mark_safe(object_link)
+
+ object_link.admin_order_field = 'object_repr'
+ object_link.short_description = _('object')
+
+ def user_link(self, obj):
+ content_type = ContentType.objects.get_for_model(type(obj.user))
+ user_link = escape(force_str(obj.user))
+ try:
+ # try returning an actual link instead of object repr string
+ url = reverse(
+ 'admin:{}_{}_change'.format(content_type.app_label,
+ content_type.model),
+ args=[obj.user.pk]
+ )
+ user_link = '{}'.format(url, user_link)
+ except NoReverseMatch:
+ pass
+ return mark_safe(user_link)
+
+ user_link.admin_order_field = 'user'
+ user_link.short_description = _('user')
+
+ def get_queryset(self, request):
+ queryset = super(LogEntryAdmin, self).get_queryset(request)
+ return queryset.prefetch_related('content_type')
+
+ def get_actions(self, request):
+ actions = super(LogEntryAdmin, self).get_actions(request)
+ if 'delete_selected' in actions:
+ del actions['delete_selected']
+ return actions
diff --git a/djangoblog/plugin_manage/base_plugin.py b/djangoblog/plugin_manage/base_plugin.py
new file mode 100644
index 0000000..2b4be5c
--- /dev/null
+++ b/djangoblog/plugin_manage/base_plugin.py
@@ -0,0 +1,41 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class BasePlugin:
+ # 插件元数据
+ PLUGIN_NAME = None
+ PLUGIN_DESCRIPTION = None
+ PLUGIN_VERSION = None
+
+ def __init__(self):
+ if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
+ raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
+ self.init_plugin()
+ self.register_hooks()
+
+ def init_plugin(self):
+ """
+ 插件初始化逻辑
+ 子类可以重写此方法来实现特定的初始化操作
+ """
+ logger.info(f'{self.PLUGIN_NAME} initialized.')
+
+ def register_hooks(self):
+ """
+ 注册插件钩子
+ 子类可以重写此方法来注册特定的钩子
+ """
+ pass
+
+ def get_plugin_info(self):
+ """
+ 获取插件信息
+ :return: 包含插件元数据的字典
+ """
+ return {
+ 'name': self.PLUGIN_NAME,
+ 'description': self.PLUGIN_DESCRIPTION,
+ 'version': self.PLUGIN_VERSION
+ }
diff --git a/djangoblog/plugin_manage/hook_constants.py b/djangoblog/plugin_manage/hook_constants.py
new file mode 100644
index 0000000..6685b7c
--- /dev/null
+++ b/djangoblog/plugin_manage/hook_constants.py
@@ -0,0 +1,7 @@
+ARTICLE_DETAIL_LOAD = 'article_detail_load'
+ARTICLE_CREATE = 'article_create'
+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
new file mode 100644
index 0000000..d712540
--- /dev/null
+++ b/djangoblog/plugin_manage/hooks.py
@@ -0,0 +1,44 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+_hooks = {}
+
+
+def register(hook_name: str, callback: callable):
+ """
+ 注册一个钩子回调。
+ """
+ if hook_name not in _hooks:
+ _hooks[hook_name] = []
+ _hooks[hook_name].append(callback)
+ logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
+
+
+def run_action(hook_name: str, *args, **kwargs):
+ """
+ 执行一个 Action Hook。
+ 它会按顺序执行所有注册到该钩子上的回调函数。
+ """
+ if hook_name in _hooks:
+ logger.debug(f"Running action hook '{hook_name}'")
+ for callback in _hooks[hook_name]:
+ try:
+ callback(*args, **kwargs)
+ except Exception as e:
+ 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):
+ """
+ 执行一个 Filter Hook。
+ 它会把 value 依次传递给所有注册的回调函数进行处理。
+ """
+ if hook_name in _hooks:
+ logger.debug(f"Applying filter hook '{hook_name}'")
+ for callback in _hooks[hook_name]:
+ 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)
+ return value
diff --git a/djangoblog/plugin_manage/loader.py b/djangoblog/plugin_manage/loader.py
new file mode 100644
index 0000000..12e824b
--- /dev/null
+++ b/djangoblog/plugin_manage/loader.py
@@ -0,0 +1,19 @@
+import os
+import logging
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+def load_plugins():
+ """
+ Dynamically loads and initializes plugins from the 'plugins' directory.
+ This function is intended to be called when the Django app registry is ready.
+ """
+ for plugin_name in settings.ACTIVE_PLUGINS:
+ plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
+ if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
+ try:
+ __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
diff --git a/djangoblog/settings.py b/djangoblog/settings.py
new file mode 100644
index 0000000..d076bb6
--- /dev/null
+++ b/djangoblog/settings.py
@@ -0,0 +1,343 @@
+"""
+Django settings for djangoblog project.
+
+Generated by 'django-admin startproject' using Django 1.10.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.10/ref/settings/
+"""
+import os
+import sys
+from pathlib import Path
+
+from django.utils.translation import gettext_lazy as _
+
+
+def env_to_bool(env, default):
+ str_val = os.environ.get(env)
+ return default if str_val is None else str_val == 'True'
+
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.environ.get(
+ 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = env_to_bool('DJANGO_DEBUG', True)
+# DEBUG = False
+TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
+
+# ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
+# django 4.0新增配置
+CSRF_TRUSTED_ORIGINS = ['http://example.com']
+# Application definition
+
+
+INSTALLED_APPS = [
+ # 'django.contrib.admin',
+ 'django.contrib.admin.apps.SimpleAdminConfig',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django.contrib.sites',
+ 'django.contrib.sitemaps',
+ 'mdeditor',
+ 'haystack',
+ 'blog',
+ 'accounts',
+ 'comments',
+ 'oauth',
+ 'servermanager',
+ 'owntracks',
+ 'compressor',
+ 'djangoblog'
+]
+
+MIDDLEWARE = [
+
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.middleware.gzip.GZipMiddleware',
+ # 'django.middleware.cache.UpdateCacheMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ # 'django.middleware.cache.FetchFromCacheMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'django.middleware.http.ConditionalGetMiddleware',
+ 'blog.middleware.OnlineMiddleware'
+]
+
+ROOT_URLCONF = 'djangoblog.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ 'blog.context_processors.seo_processor'
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'djangoblog.wsgi.application'
+
+# Database
+# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
+
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
+ 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
+ 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
+ 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
+ 'PORT': int(
+ os.environ.get('DJANGO_MYSQL_PORT') or 3306),
+ 'OPTIONS': {
+ 'charset': 'utf8mb4'},
+ }}
+
+# Password validation
+# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+LANGUAGES = (
+ ('en', _('English')),
+ ('zh-hans', _('Simplified Chinese')),
+ ('zh-hant', _('Traditional Chinese')),
+)
+LOCALE_PATHS = (
+ os.path.join(BASE_DIR, 'locale'),
+)
+
+LANGUAGE_CODE = 'zh-hans'
+
+TIME_ZONE = 'Asia/Shanghai'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = False
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.10/howto/static-files/
+
+
+HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
+ 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
+ },
+}
+# Automatically update searching index
+HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
+# Allow user login with username and password
+AUTHENTICATION_BACKENDS = [
+ 'accounts.user_login_backend.EmailOrUsernameModelBackend']
+
+STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
+
+STATIC_URL = '/static/'
+STATICFILES = os.path.join(BASE_DIR, 'static')
+
+AUTH_USER_MODEL = 'accounts.BlogUser'
+LOGIN_URL = '/login/'
+
+TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
+DATE_TIME_FORMAT = '%Y-%m-%d'
+
+# bootstrap color styles
+BOOTSTRAP_COLOR_TYPES = [
+ 'default', 'primary', 'success', 'info', 'warning', 'danger'
+]
+
+# paginate
+PAGINATE_BY = 10
+# http cache timeout
+CACHE_CONTROL_MAX_AGE = 2592000
+# cache setting
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'TIMEOUT': 10800,
+ 'LOCATION': 'unique-snowflake',
+ }
+}
+# 使用redis作为缓存
+if os.environ.get("DJANGO_REDIS_URL"):
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
+ 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
+ }
+ }
+
+SITE_ID = 1
+BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
+ or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
+
+# Email:
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
+EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
+EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
+EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
+EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
+EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
+DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
+SERVER_EMAIL = EMAIL_HOST_USER
+# Setting debug=false did NOT handle except email notifications
+ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
+# WX ADMIN password(Two times md5)
+WXADMIN = os.environ.get(
+ 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
+
+LOG_PATH = os.path.join(BASE_DIR, 'logs')
+if not os.path.exists(LOG_PATH):
+ os.makedirs(LOG_PATH, exist_ok=True)
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'root': {
+ 'level': 'INFO',
+ 'handlers': ['console', 'log_file'],
+ },
+ 'formatters': {
+ 'verbose': {
+ 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
+ }
+ },
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse',
+ },
+ 'require_debug_true': {
+ '()': 'django.utils.log.RequireDebugTrue',
+ },
+ },
+ 'handlers': {
+ 'log_file': {
+ 'level': 'INFO',
+ 'class': 'logging.handlers.TimedRotatingFileHandler',
+ 'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
+ 'when': 'D',
+ 'formatter': 'verbose',
+ 'interval': 1,
+ 'delay': True,
+ 'backupCount': 5,
+ 'encoding': 'utf-8'
+ },
+ 'console': {
+ 'level': 'DEBUG',
+ 'filters': ['require_debug_true'],
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'verbose'
+ },
+ 'null': {
+ 'class': 'logging.NullHandler',
+ },
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'],
+ 'class': 'django.utils.log.AdminEmailHandler'
+ }
+ },
+ 'loggers': {
+ 'djangoblog': {
+ 'handlers': ['log_file', 'console'],
+ 'level': 'INFO',
+ 'propagate': True,
+ },
+ 'django.request': {
+ 'handlers': ['mail_admins'],
+ 'level': 'ERROR',
+ 'propagate': False,
+ }
+ }
+}
+
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ # other
+ 'compressor.finders.CompressorFinder',
+)
+COMPRESS_ENABLED = True
+# COMPRESS_OFFLINE = True
+
+
+COMPRESS_CSS_FILTERS = [
+ # creates absolute urls from relative ones
+ 'compressor.filters.css_default.CssAbsoluteFilter',
+ # css minimizer
+ 'compressor.filters.cssmin.CSSMinFilter'
+]
+COMPRESS_JS_FILTERS = [
+ 'compressor.filters.jsmin.JSMinFilter'
+]
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
+MEDIA_URL = '/media/'
+X_FRAME_OPTIONS = 'SAMEORIGIN'
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
+ ELASTICSEARCH_DSL = {
+ 'default': {
+ 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
+ },
+ }
+ HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
+ },
+ }
+
+# Plugin System
+PLUGINS_DIR = BASE_DIR / 'plugins'
+ACTIVE_PLUGINS = [
+ 'article_copyright',
+ 'reading_time',
+ 'external_links',
+ 'view_count',
+ 'seo_optimizer'
+]
\ No newline at end of file
diff --git a/djangoblog/sitemap.py b/djangoblog/sitemap.py
new file mode 100644
index 0000000..8b7d446
--- /dev/null
+++ b/djangoblog/sitemap.py
@@ -0,0 +1,59 @@
+from django.contrib.sitemaps import Sitemap
+from django.urls import reverse
+
+from blog.models import Article, Category, Tag
+
+
+class StaticViewSitemap(Sitemap):
+ priority = 0.5
+ changefreq = 'daily'
+
+ def items(self):
+ return ['blog:index', ]
+
+ def location(self, item):
+ return reverse(item)
+
+
+class ArticleSiteMap(Sitemap):
+ changefreq = "monthly"
+ priority = "0.6"
+
+ def items(self):
+ return Article.objects.filter(status='p')
+
+ def lastmod(self, obj):
+ return obj.last_modify_time
+
+
+class CategorySiteMap(Sitemap):
+ changefreq = "Weekly"
+ priority = "0.6"
+
+ def items(self):
+ return Category.objects.all()
+
+ def lastmod(self, obj):
+ return obj.last_modify_time
+
+
+class TagSiteMap(Sitemap):
+ changefreq = "Weekly"
+ priority = "0.3"
+
+ def items(self):
+ return Tag.objects.all()
+
+ def lastmod(self, obj):
+ return obj.last_modify_time
+
+
+class UserSiteMap(Sitemap):
+ changefreq = "Weekly"
+ priority = "0.3"
+
+ def items(self):
+ return list(set(map(lambda x: x.author, Article.objects.all())))
+
+ def lastmod(self, obj):
+ return obj.date_joined
diff --git a/djangoblog/spider_notify.py b/djangoblog/spider_notify.py
new file mode 100644
index 0000000..7b909e9
--- /dev/null
+++ b/djangoblog/spider_notify.py
@@ -0,0 +1,21 @@
+import logging
+
+import requests
+from django.conf import settings
+
+logger = logging.getLogger(__name__)
+
+
+class SpiderNotify():
+ @staticmethod
+ def baidu_notify(urls):
+ try:
+ data = '\n'.join(urls)
+ result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
+ logger.info(result.text)
+ except Exception as e:
+ logger.error(e)
+
+ @staticmethod
+ def notify(url):
+ SpiderNotify.baidu_notify(url)
diff --git a/djangoblog/tests.py b/djangoblog/tests.py
new file mode 100644
index 0000000..01237d9
--- /dev/null
+++ b/djangoblog/tests.py
@@ -0,0 +1,32 @@
+from django.test import TestCase
+
+from djangoblog.utils import *
+
+
+class DjangoBlogTest(TestCase):
+ def setUp(self):
+ pass
+
+ def test_utils(self):
+ md5 = get_sha256('test')
+ self.assertIsNotNone(md5)
+ c = CommonMarkdown.get_markdown('''
+ # Title1
+
+ ```python
+ import os
+ ```
+
+ [url](https://www.lylinux.net/)
+
+ [ddd](http://www.baidu.com)
+
+
+ ''')
+ self.assertIsNotNone(c)
+ d = {
+ 'd': 'key1',
+ 'd2': 'key2'
+ }
+ data = parse_dict_to_url(d)
+ self.assertIsNotNone(data)
diff --git a/djangoblog/urls.py b/djangoblog/urls.py
new file mode 100644
index 0000000..4aae58a
--- /dev/null
+++ b/djangoblog/urls.py
@@ -0,0 +1,64 @@
+"""djangoblog URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf import settings
+from django.conf.urls.i18n import i18n_patterns
+from django.conf.urls.static import static
+from django.contrib.sitemaps.views import sitemap
+from django.urls import path, include
+from django.urls import re_path
+from haystack.views import search_view_factory
+
+from blog.views import EsSearchView
+from djangoblog.admin_site import admin_site
+from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
+from djangoblog.feeds import DjangoBlogFeed
+from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
+
+sitemaps = {
+
+ 'blog': ArticleSiteMap,
+ 'Category': CategorySiteMap,
+ 'Tag': TagSiteMap,
+ 'User': UserSiteMap,
+ 'static': StaticViewSitemap
+}
+
+handler404 = 'blog.views.page_not_found_view'
+handler500 = 'blog.views.server_error_view'
+handle403 = 'blog.views.permission_denied_view'
+
+urlpatterns = [
+ path('i18n/', include('django.conf.urls.i18n')),
+]
+urlpatterns += i18n_patterns(
+ re_path(r'^admin/', admin_site.urls),
+ re_path(r'', include('blog.urls', namespace='blog')),
+ re_path(r'mdeditor/', include('mdeditor.urls')),
+ re_path(r'', include('comments.urls', namespace='comment')),
+ re_path(r'', include('accounts.urls', namespace='account')),
+ re_path(r'', include('oauth.urls', namespace='oauth')),
+ re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
+ name='django.contrib.sitemaps.views.sitemap'),
+ re_path(r'^feed/$', DjangoBlogFeed()),
+ re_path(r'^rss/$', DjangoBlogFeed()),
+ re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
+ name='search'),
+ re_path(r'', include('servermanager.urls', namespace='servermanager')),
+ re_path(r'', include('owntracks.urls', namespace='owntracks'))
+ , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL,
+ document_root=settings.MEDIA_ROOT)
diff --git a/djangoblog/utils.py b/djangoblog/utils.py
new file mode 100644
index 0000000..57f63dc
--- /dev/null
+++ b/djangoblog/utils.py
@@ -0,0 +1,232 @@
+#!/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:
+ 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))
+ value = func(*args, **kwargs)
+ if value is None:
+ cache.set(key, '__default_cache_value__', expiration)
+ else:
+ cache.set(key, value, expiration)
+ return value
+
+ return news
+
+ 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)
+
+ 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)
+ 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
new file mode 100644
index 0000000..04e3f7f
--- /dev/null
+++ b/djangoblog/whoosh_cn_backend.py
@@ -0,0 +1,1044 @@
+# 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):
+ """
+ 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.
+ """
+ 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.
+ """
+ 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
+
+ if isinstance(value, six.string_types) and value != ' ':
+ # It's not an ``InputType``. Assume ``Clean``.
+ value = Clean(value)
+ else:
+ value = PythonData(value)
+
+ # Prepare the query using the InputType.
+ prepared_value = value.prepare(self)
+
+ 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
diff --git a/djangoblog/wsgi.py b/djangoblog/wsgi.py
new file mode 100644
index 0000000..2295efd
--- /dev/null
+++ b/djangoblog/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for djangoblog project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
+
+application = get_wsgi_application()
diff --git a/docs/README-en.md b/docs/README-en.md
new file mode 100644
index 0000000..37ea069
--- /dev/null
+++ b/docs/README-en.md
@@ -0,0 +1,158 @@
+# DjangoBlog
+
+
+
+
+
+
+
+
+
+ A powerful, elegant, and modern blog system.
+
+ English • 简体中文
+
+
+---
+
+DjangoBlog is a high-performance blog platform built with Python 3.10 and Django 4.0. It not only provides all the core functionalities of a traditional blog but also features a flexible plugin system, allowing you to easily extend and customize your website. Whether you are a personal blogger, a tech enthusiast, or a content creator, DjangoBlog aims to provide a stable, efficient, and easy-to-maintain environment for writing and publishing.
+
+## ✨ Features
+
+- **Powerful Content Management**: Full support for managing articles, standalone pages, categories, and tags. Comes with a powerful built-in Markdown editor with syntax highlighting.
+- **Full-Text Search**: Integrated search engine for fast and accurate content searching.
+- **Interactive Comment System**: Supports replies, email notifications, and Markdown formatting in comments.
+- **Flexible Sidebar**: Customizable modules for displaying recent articles, most viewed posts, tag cloud, and more.
+- **Social Login**: Built-in OAuth support, with integrations for Google, GitHub, Facebook, Weibo, QQ, and other major platforms.
+- **High-Performance Caching**: Native support for Redis caching with an automatic refresh mechanism to ensure high-speed website responses.
+- **SEO Friendly**: Basic SEO features are included, with automatic notifications to Google and Baidu upon new content publication.
+- **Extensible Plugin System**: Extend blog functionalities by creating standalone plugins, ensuring decoupled and maintainable code. We have already implemented features like view counting and SEO optimization through plugins!
+- **Integrated Image Hosting**: A simple, built-in image hosting feature for easy uploads and management.
+- **Automated Frontend**: Integrated with `django-compressor` to automatically compress and optimize CSS and JavaScript files.
+- **Robust Operations**: Built-in email notifications for website exceptions and management capabilities through a WeChat Official Account.
+
+## 🛠️ Tech Stack
+
+- **Backend**: Python 3.10, Django 4.0
+- **Database**: MySQL, SQLite (configurable)
+- **Cache**: Redis
+- **Frontend**: HTML5, CSS3, JavaScript
+- **Search**: Whoosh, Elasticsearch (configurable)
+- **Editor**: Markdown (mdeditor)
+
+## 🚀 Getting Started
+
+### 1. Prerequisites
+
+Ensure you have Python 3.10+ and MySQL/MariaDB installed on your system.
+
+### 2. Clone & Installation
+
+```bash
+# Clone the project to your local machine
+git clone https://github.com/liangliangyy/DjangoBlog.git
+cd DjangoBlog
+
+# Install dependencies
+pip install -r requirements.txt
+```
+
+### 3. Project Configuration
+
+- **Database**:
+ Open `djangoblog/settings.py`, locate the `DATABASES` section, and update it with your MySQL connection details.
+
+ ```python
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'djangoblog',
+ 'USER': 'root',
+ 'PASSWORD': 'your_password',
+ 'HOST': '127.0.0.1',
+ 'PORT': 3306,
+ }
+ }
+ ```
+ Create the database in MySQL:
+ ```sql
+ CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+ ```
+
+- **More Configurations**:
+ For advanced settings such as email, OAuth, caching, and more, please refer to our [Detailed Configuration Guide](/docs/config-en.md).
+
+### 4. Database Initialization
+
+```bash
+python manage.py makemigrations
+python manage.py migrate
+
+# Create a superuser account
+python manage.py createsuperuser
+```
+
+### 5. Running the Project
+
+```bash
+# (Optional) Generate some test data
+python manage.py create_testdata
+
+# (Optional) Collect and compress static files
+python manage.py collectstatic --noinput
+python manage.py compress --force
+
+# Start the development server
+python manage.py runserver
+```
+
+Now, open your browser and navigate to `http://127.0.0.1:8000/`. You should see the DjangoBlog homepage!
+
+## Deployment
+
+- **Traditional Deployment**: A detailed guide for server deployment is available here: [Deployment Tutorial](https://www.lylinux.net/article/2019/8/5/58.html) (in Chinese).
+- **Docker Deployment**: This project fully supports Docker. If you are familiar with containerization, please refer to the [Docker Deployment Guide](/docs/docker-en.md) for a quick start.
+- **Kubernetes Deployment**: We also provide a complete [Kubernetes Deployment Guide](/docs/k8s-en.md) to help you go cloud-native easily.
+
+## 🧩 Plugin System
+
+The plugin system is a core feature of DjangoBlog. It allows you to add new functionalities to your blog without modifying the core codebase by writing standalone plugins.
+
+- **How it Works**: Plugins operate by registering callback functions to predefined "hooks". For instance, when an article is rendered, the `after_article_body_get` hook is triggered, and all functions registered to this hook are executed.
+- **Existing Plugins**: Features like `view_count` and `seo_optimizer` are implemented through this plugin system.
+- **Develop Your Own Plugin**: Simply create a new folder under the `plugins` directory and write your `plugin.py`. We welcome you to explore and contribute your creative ideas to the DjangoBlog community!
+
+## 🤝 Contributing
+
+We warmly welcome contributions of any kind! If you have great ideas or have found a bug, please feel free to open an issue or submit a pull request.
+
+## 📄 License
+
+This project is open-sourced under the [MIT License](LICENSE).
+
+---
+
+## ❤️ Support & Sponsorship
+
+If you find this project helpful and wish to support its continued maintenance and development, please consider buying me a coffee! Your support is my greatest motivation.
+
+
+
+
+
+
+ (Left) Alipay / (Right) WeChat
+
+
+## 🙏 Acknowledgements
+
+A special thanks to **JetBrains** for providing a free open-source license for this project.
+
+
+
+
+
+
+
+---
+> If this project has helped you, please leave your website URL [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it. Your feedback is the driving force for my continued updates and maintenance.
diff --git a/docs/config-en.md b/docs/config-en.md
new file mode 100644
index 0000000..b877efb
--- /dev/null
+++ b/docs/config-en.md
@@ -0,0 +1,64 @@
+# Introduction to main features settings
+
+## Cache:
+Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`.
+```python
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+ 'LOCATION': '127.0.0.1:11211',
+ 'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog',
+ 'TIMEOUT': 60 * 60 * 10
+ },
+ 'locmemcache': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'TIMEOUT': 10800,
+ 'LOCATION': 'unique-snowflake',
+ }
+}
+```
+
+## OAuth Login:
+QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration.
+
+### Callback address examples:
+QQ: http://your-domain-name/oauth/authorize?type=qq
+Weibo: http://your-domain-name/oauth/authorize?type=weibo
+type is in the type field of `oauthmanager`.
+
+## owntracks:
+owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap.
+
+## Email feature:
+Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify:
+```python
+EMAIL_HOST = 'smtp.zoho.com'
+EMAIL_PORT = 587
+EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
+EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
+DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
+SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER')
+```
+with your email account information.
+
+## WeChat Official Account
+Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account.
+
+## Introduction to website configuration
+You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc.
+OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default.
+
+## Source code highlighting
+If the code block in your article didn't show hightlight, please write the code blocks as following:
+
+
+
+That is, you should add the corresponding language name before the code block.
+
+## Update
+If you get errors as following while executing database migrations:
+```python
+django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))
+```
+This problem may cause by the mysql version under 5.6, a new version( >= 5.6 ) mysql is needed.
+
diff --git a/docs/config.md b/docs/config.md
new file mode 100644
index 0000000..24673a3
--- /dev/null
+++ b/docs/config.md
@@ -0,0 +1,58 @@
+# 主要功能配置介绍:
+
+## 缓存:
+缓存默认使用`localmem`缓存,如果你有`redis`环境,可以设置`DJANGO_REDIS_URL`环境变量,则会自动使用该redis来作为缓存,或者你也可以直接修改如下代码来使用。
+https://github.com/liangliangyy/DjangoBlog/blob/ffcb2c3711de805f2067dd3c1c57449cd24d84ee/djangoblog/settings.py#L185-L199
+
+
+## oauth登录:
+
+现在已经支持QQ,微博,Google,GitHub,Facebook登录,需要在其对应的开放平台申请oauth登录权限,然后在
+**后台->Oauth** 配置中新增配置,填写对应的`appkey`和`appsecret`以及回调地址。
+### 回调地址示例:
+qq:http://你的域名/oauth/authorize?type=qq
+微博:http://你的域名/oauth/authorize?type=weibo
+type对应在`oauthmanager`中的type字段。
+
+## owntracks:
+owntracks是一个位置追踪软件,可以定时的将你的坐标提交到你的服务器上,现在简单的支持owntracks功能,需要安装owntracks的app,然后将api地址设置为:
+`你的域名/owntracks/logtracks`就可以了。然后访问`你的域名/owntracks/show_dates`就可以看到有经纬度记录的日期,点击之后就可以看到运动轨迹了。地图是使用高德地图绘制。
+
+## 邮件功能:
+同样,将`settings.py`中的`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]`配置为你自己的错误接收邮箱,另外修改:
+```python
+EMAIL_HOST = 'smtp.zoho.com'
+EMAIL_PORT = 587
+EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
+EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
+DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
+SERVER_EMAIL = os.environ.get('DJANGO_EMAIL_USER')
+```
+为你自己的邮箱配置。
+
+## 微信公众号
+集成了简单的微信公众号功能,在微信后台将token地址设置为:`你的域名/robot` 即可,默认token为`lylinux`,当然你可以修改为你自己的,在`servermanager/robot.py`中。
+然后在**后台->Servermanager->命令**中新增命令,这样就可以使用微信公众号来管理了。
+## 网站配置介绍
+在**后台->BLOG->网站配置**中,可以新增网站配置,比如关键字,描述等,以及谷歌广告,网站统计代码及备案号等等。
+其中的*静态文件保存地址*是保存oauth用户登录的头像路径,填写绝对路径,默认是代码目录。
+## 代码高亮
+如果你发现你文章的代码没有高亮,请这样书写代码块:
+
+
+
+
+也就是说,需要在代码块开始位置加入这段代码对应的语言。
+
+## update
+如果你发现执行数据库迁移的时候出现如下报错:
+```python
+django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))
+```
+可能是因为你的mysql版本低于5.6,需要升级mysql版本>=5.6即可。
+
+
+django 4.0登录可能会报错CSRF,需要配置下`settings.py`中的`CSRF_TRUSTED_ORIGINS`
+
+https://github.com/liangliangyy/DjangoBlog/blob/master/djangoblog/settings.py#L39
+
diff --git a/docs/docker-en.md b/docs/docker-en.md
new file mode 100644
index 0000000..8d5d59e
--- /dev/null
+++ b/docs/docker-en.md
@@ -0,0 +1,114 @@
+# Deploying DjangoBlog with Docker
+
+
+
+
+
+This project fully supports containerized deployment using Docker, providing you with a fast, consistent, and isolated runtime environment. We recommend using `docker-compose` to launch the entire blog service stack with a single command.
+
+## 1. Prerequisites
+
+Before you begin, please ensure you have the following software installed on your system:
+- [Docker Engine](https://docs.docker.com/engine/install/)
+- [Docker Compose](https://docs.docker.com/compose/install/) (Included with Docker Desktop for Mac and Windows)
+
+## 2. Recommended Method: Using `docker-compose` (One-Click Deployment)
+
+This is the simplest and most recommended way to deploy. It automatically creates and manages the Django application, a MySQL database, and an optional Elasticsearch service for you.
+
+### Step 1: Start the Basic Services
+
+From the project's root directory, run the following command:
+
+```bash
+# Build and start the containers in detached mode (includes Django app and MySQL)
+docker-compose up -d --build
+```
+
+`docker-compose` will read the `docker-compose.yml` file, pull the necessary images, build the project image, and start all services.
+
+- **Access Your Blog**: Once the services are up, you can access the blog by navigating to `http://127.0.0.1` in your browser.
+- **Data Persistence**: MySQL data files will be stored in the `data/mysql` directory within the project root, ensuring that your data persists across container restarts.
+
+### Step 2: (Optional) Enable Elasticsearch for Full-Text Search
+
+If you want to use Elasticsearch for more powerful full-text search capabilities, you can include the `docker-compose.es.yml` configuration file:
+
+```bash
+# Build and start all services in detached mode (Django, MySQL, Elasticsearch)
+docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
+```
+- **Data Persistence**: Elasticsearch data will be stored in the `data/elasticsearch` directory.
+
+### Step 3: First-Time Initialization
+
+After the containers start for the first time, you'll need to execute some initialization commands inside the application container.
+
+```bash
+# Get a shell inside the djangoblog application container (named 'web')
+docker-compose exec web bash
+
+# Inside the container, run the following commands:
+# Create a superuser account (follow the prompts to set username, email, and password)
+python manage.py createsuperuser
+
+# (Optional) Create some test data
+python manage.py create_testdata
+
+# (Optional, if ES is enabled) Create the search index
+python manage.py rebuild_index
+
+# Exit the container
+exit
+```
+
+## 3. Alternative Method: Using the Standalone Docker Image
+
+If you already have an external MySQL database running, you can run the DjangoBlog application image by itself.
+
+```bash
+# Pull the latest image from Docker Hub
+docker pull liangliangyy/djangoblog:latest
+
+# Run the container and connect it to your external database
+docker run -d \
+ -p 8000:8000 \
+ -e DJANGO_SECRET_KEY='your-strong-secret-key' \
+ -e DJANGO_MYSQL_HOST='your-mysql-host' \
+ -e DJANGO_MYSQL_USER='your-mysql-user' \
+ -e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
+ -e DJANGO_MYSQL_DATABASE='djangoblog' \
+ --name djangoblog \
+ liangliangyy/djangoblog:latest
+```
+
+- **Access Your Blog**: After startup, visit `http://127.0.0.1:8000`.
+- **Create Superuser**: `docker exec -it djangoblog python manage.py createsuperuser`
+
+## 4. Configuration (Environment Variables)
+
+Most of the project's configuration is managed through environment variables. You can modify them in the `docker-compose.yml` file or pass them using the `-e` flag with the `docker run` command.
+
+| Environment Variable | Default/Example Value | Notes |
+|---------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
+| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **Must be changed to a random, complex string!** |
+| `DJANGO_DEBUG` | `False` | Toggles Django's debug mode. |
+| `DJANGO_MYSQL_HOST` | `mysql` | Database hostname. |
+| `DJANGO_MYSQL_PORT` | `3306` | Database port. |
+| `DJANGO_MYSQL_DATABASE` | `djangoblog` | Database name. |
+| `DJANGO_MYSQL_USER` | `root` | Database username. |
+| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | Database password. |
+| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis connection URL (for caching). |
+| `DJANGO_ELASTICSEARCH_HOST`| `elasticsearch:9200` | Elasticsearch host address. |
+| `DJANGO_EMAIL_HOST` | `smtp.example.org` | Email server address. |
+| `DJANGO_EMAIL_PORT` | `465` | Email server port. |
+| `DJANGO_EMAIL_USER` | `user@example.org` | Email account username. |
+| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | Email account password. |
+| `DJANGO_EMAIL_USE_SSL` | `True` | Whether to use SSL. |
+| `DJANGO_EMAIL_USE_TLS` | `False` | Whether to use TLS. |
+| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | Admin email for receiving error reports. |
+| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | Push API from [Baidu Webmaster Tools](https://ziyuan.baidu.com/linksubmit/index). |
+
+---
+
+After deployment, please review and adjust these environment variables according to your needs, especially `DJANGO_SECRET_KEY` and the database and email settings.
\ No newline at end of file
diff --git a/docs/docker.md b/docs/docker.md
new file mode 100644
index 0000000..e7c255a
--- /dev/null
+++ b/docs/docker.md
@@ -0,0 +1,114 @@
+# 使用 Docker 部署 DjangoBlog
+
+
+
+
+
+本项目全面支持使用 Docker 进行容器化部署,为您提供了快速、一致且隔离的运行环境。我们推荐使用 `docker-compose` 来一键启动整个博客服务栈。
+
+## 1. 环境准备
+
+在开始之前,请确保您的系统中已经安装了以下软件:
+- [Docker Engine](https://docs.docker.com/engine/install/)
+- [Docker Compose](https://docs.docker.com/compose/install/) (对于 Docker Desktop 用户,它已内置)
+
+## 2. 推荐方式:使用 `docker-compose` (一键部署)
+
+这是最简单、最推荐的部署方式。它会自动为您创建并管理 Django 应用、MySQL 数据库,以及可选的 Elasticsearch 服务。
+
+### 步骤 1: 启动基础服务
+
+在项目根目录下,执行以下命令:
+
+```bash
+# 构建并以后台模式启动容器 (包含 Django 应用和 MySQL)
+docker-compose up -d --build
+```
+
+`docker-compose` 会读取 `docker-compose.yml` 文件,自动拉取所需镜像、构建项目镜像,并启动所有服务。
+
+- **访问您的博客**: 服务启动后,在浏览器中访问 `http://127.0.0.1` 即可看到博客首页。
+- **数据持久化**: MySQL 的数据文件将存储在项目根目录下的 `data/mysql` 文件夹中,确保数据在容器重启后不丢失。
+
+### 步骤 2: (可选) 启用 Elasticsearch 全文搜索
+
+如果您希望使用 Elasticsearch 提供更强大的全文搜索功能,可以额外加载 `docker-compose.es.yml` 配置文件:
+
+```bash
+# 构建并以后台模式启动所有服务 (Django, MySQL, Elasticsearch)
+docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
+```
+- **数据持久化**: Elasticsearch 的数据将存储在 `data/elasticsearch` 文件夹中。
+
+### 步骤 3: 首次运行的初始化操作
+
+当容器首次启动后,您需要进入容器来执行一些初始化命令。
+
+```bash
+# 进入 djangoblog 应用容器
+docker-compose exec web bash
+
+# 在容器内执行以下命令:
+# 创建超级管理员账户 (请按照提示设置用户名、邮箱和密码)
+python manage.py createsuperuser
+
+# (可选) 创建一些测试数据
+python manage.py create_testdata
+
+# (可选,如果启用了 ES) 创建索引
+python manage.py rebuild_index
+
+# 退出容器
+exit
+```
+
+## 3. 备选方式:使用独立的 Docker 镜像
+
+如果您已经拥有一个正在运行的外部 MySQL 数据库,您也可以只运行 DjangoBlog 的应用镜像。
+
+```bash
+# 从 Docker Hub 拉取最新镜像
+docker pull liangliangyy/djangoblog:latest
+
+# 运行容器,并链接到您的外部数据库
+docker run -d \
+ -p 8000:8000 \
+ -e DJANGO_SECRET_KEY='your-strong-secret-key' \
+ -e DJANGO_MYSQL_HOST='your-mysql-host' \
+ -e DJANGO_MYSQL_USER='your-mysql-user' \
+ -e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
+ -e DJANGO_MYSQL_DATABASE='djangoblog' \
+ --name djangoblog \
+ liangliangyy/djangoblog:latest
+```
+
+- **访问您的博客**: 启动完成后,访问 `http://127.0.0.1:8000`。
+- **创建管理员**: `docker exec -it djangoblog python manage.py createsuperuser`
+
+## 4. 配置说明 (环境变量)
+
+本项目的大部分配置都通过环境变量来管理。您可以在 `docker-compose.yml` 文件中修改它们,或者在使用 `docker run` 命令时通过 `-e` 参数传入。
+
+| 环境变量名称 | 默认值/示例 | 备注 |
+|-------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
+| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **请务必修改为一个随机且复杂的字符串!** |
+| `DJANGO_DEBUG` | `False` | 是否开启 Django 的调试模式 |
+| `DJANGO_MYSQL_HOST` | `mysql` | 数据库主机名 |
+| `DJANGO_MYSQL_PORT` | `3306` | 数据库端口 |
+| `DJANGO_MYSQL_DATABASE` | `djangoblog` | 数据库名称 |
+| `DJANGO_MYSQL_USER` | `root` | 数据库用户名 |
+| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | 数据库密码 |
+| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis 连接地址 (用于缓存) |
+| `DJANGO_ELASTICSEARCH_HOST` | `elasticsearch:9200` | Elasticsearch 主机地址 |
+| `DJANGO_EMAIL_HOST` | `smtp.example.org` | 邮件服务器地址 |
+| `DJANGO_EMAIL_PORT` | `465` | 邮件服务器端口 |
+| `DJANGO_EMAIL_USER` | `user@example.org` | 邮件账户 |
+| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | 邮件密码 |
+| `DJANGO_EMAIL_USE_SSL` | `True` | 是否使用 SSL |
+| `DJANGO_EMAIL_USE_TLS` | `False` | 是否使用 TLS |
+| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | 接收异常报告的管理员邮箱 |
+| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | [百度站长平台](https://ziyuan.baidu.com/linksubmit/index) 的推送接口 |
+
+---
+
+部署完成后,请务必检查并根据您的实际需求调整这些环境变量,特别是 `DJANGO_SECRET_KEY` 和数据库、邮件相关的配置。
diff --git a/docs/es.md b/docs/es.md
new file mode 100644
index 0000000..97226c5
--- /dev/null
+++ b/docs/es.md
@@ -0,0 +1,28 @@
+# 集成Elasticsearch
+如果你已经有了`Elasticsearch`环境,那么可以将搜索从`Whoosh`换成`Elasticsearch`,集成方式也很简单,
+首先需要注意如下几点:
+1. 你的`Elasticsearch`支持`ik`中文分词
+2. 你的`Elasticsearch`版本>=7.3.0
+
+接下来在`settings.py`做如下改动即可:
+- 增加es链接,如下所示:
+```python
+ELASTICSEARCH_DSL = {
+ 'default': {
+ 'hosts': '127.0.0.1:9200'
+ },
+}
+```
+- 修改`HAYSTACK`配置:
+```python
+HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
+ },
+}
+```
+然后终端执行:
+```shell script
+./manage.py build_index
+```
+这将会在你的es中创建两个索引,分别是`blog`和`performance`,其中`blog`索引就是搜索所使用的,而`performance`会记录每个请求的响应时间,以供将来优化使用。
\ No newline at end of file
diff --git a/docs/imgs/alipay.jpg b/docs/imgs/alipay.jpg
new file mode 100644
index 0000000..424d70a
Binary files /dev/null and b/docs/imgs/alipay.jpg differ
diff --git a/docs/imgs/pycharm_logo.png b/docs/imgs/pycharm_logo.png
new file mode 100644
index 0000000..7f2a4b0
Binary files /dev/null and b/docs/imgs/pycharm_logo.png differ
diff --git a/docs/imgs/wechat.jpg b/docs/imgs/wechat.jpg
new file mode 100644
index 0000000..7edf525
Binary files /dev/null and b/docs/imgs/wechat.jpg differ
diff --git a/docs/k8s-en.md b/docs/k8s-en.md
new file mode 100644
index 0000000..20e9527
--- /dev/null
+++ b/docs/k8s-en.md
@@ -0,0 +1,141 @@
+# Deploying DjangoBlog with Kubernetes
+
+This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch.
+
+## Architecture Overview
+
+This deployment utilizes a microservices-based, cloud-native architecture:
+
+- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`.
+- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.**
+- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names.
+- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application.
+- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC).
+
+## 1. Prerequisites
+
+Before you begin, please ensure you have the following:
+
+- A running Kubernetes cluster.
+- The `kubectl` command-line tool configured to connect to your cluster.
+- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster.
+- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories.
+
+## 2. Deployment Steps
+
+### Step 1: Create a Namespace
+
+We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management.
+
+```bash
+# Create a namespace named 'djangoblog'
+kubectl create namespace djangoblog
+```
+
+### Step 2: Configure Persistent Storage
+
+This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`).
+
+```bash
+# Log in to your master node
+ssh user@master-node
+
+# Create the required storage directories
+sudo mkdir -p /mnt/local-storage-db
+sudo mkdir -p /mnt/local-storage-djangoblog
+sudo mkdir -p /mnt/resource/
+sudo mkdir -p /mnt/local-storage-elasticsearch
+
+# Log out from the node
+exit
+```
+**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file.
+
+After creating the directories, apply the storage-related configurations:
+
+```bash
+# Apply the StorageClass
+kubectl apply -f deploy/k8s/storageclass.yaml
+
+# Apply the PersistentVolumes (PVs)
+kubectl apply -f deploy/k8s/pv.yaml
+
+# Apply the PersistentVolumeClaims (PVCs)
+kubectl apply -f deploy/k8s/pvc.yaml
+```
+
+### Step 3: Configure the Application
+
+Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings.
+
+**It is strongly recommended to change the following fields:**
+- `DJANGO_SECRET_KEY`: Change to a random, complex string.
+- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password.
+
+```bash
+# Edit the ConfigMap file
+vim deploy/k8s/configmap.yaml
+
+# Apply the configuration
+kubectl apply -f deploy/k8s/configmap.yaml
+```
+
+### Step 4: Deploy the Application Stack
+
+Now, we can deploy all the core services.
+
+```bash
+# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
+kubectl apply -f deploy/k8s/deployment.yaml
+
+# Deploy the Services (to create internal endpoints for the Deployments)
+kubectl apply -f deploy/k8s/service.yaml
+```
+
+The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`):
+
+```bash
+kubectl get pods -n djangoblog -w
+```
+
+### Step 5: Expose the Application Externally
+
+Finally, expose the Nginx service to external traffic by applying the `Ingress` rule.
+
+```bash
+# Apply the Ingress rule
+kubectl apply -f deploy/k8s/gateway.yaml
+```
+
+Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address:
+
+```bash
+kubectl get ingress -n djangoblog
+```
+
+### Step 6: First-Time Initialization
+
+Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run.
+
+```bash
+# First, get the name of a djangoblog pod
+kubectl get pods -n djangoblog | grep djangoblog
+
+# Exec into one of the Pods (replace [pod-name] with the name from the previous step)
+kubectl exec -it [pod-name] -n djangoblog -- bash
+
+# Inside the Pod, run the following commands:
+# Create a superuser account (follow the prompts)
+python manage.py createsuperuser
+
+# (Optional) Create some test data
+python manage.py create_testdata
+
+# (Optional, if ES is enabled) Create the search index
+python manage.py rebuild_index
+
+# Exit the Pod
+exit
+```
+
+Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster.
\ No newline at end of file
diff --git a/docs/k8s.md b/docs/k8s.md
new file mode 100644
index 0000000..9da3c28
--- /dev/null
+++ b/docs/k8s.md
@@ -0,0 +1,141 @@
+# 使用 Kubernetes 部署 DjangoBlog
+
+本文档将指导您如何在 Kubernetes (K8s) 集群上部署 DjangoBlog 应用。我们提供了一套完整的 `.yaml` 配置文件,位于 `deploy/k8s` 目录下,用于部署一个包含 DjangoBlog 应用、Nginx、MySQL、Redis 和 Elasticsearch 的完整服务栈。
+
+## 架构概览
+
+本次部署采用的是微服务化的云原生架构:
+
+- **核心组件**: 每个核心服务 (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) 都将作为独立的 `Deployment` 运行。
+- **配置管理**: Nginx 的配置文件和 Django 应用的环境变量通过 `ConfigMap` 进行管理。**注意:敏感信息(如密码)建议使用 `Secret` 进行管理。**
+- **服务发现**: 所有服务都通过 `ClusterIP` 类型的 `Service` 在集群内部暴露,并通过服务名相互通信。
+- **外部访问**: 使用 `Ingress` 资源将外部的 HTTP 流量路由到 Nginx 服务,作为整个博客应用的统一入口。
+- **数据持久化**: 采用基于节点本地路径的 `local-storage` 方案。这需要您在指定的 K8s 节点上手动创建存储目录,并通过 `PersistentVolume` (PV) 和 `PersistentVolumeClaim` (PVC) 进行静态绑定。
+
+## 1. 环境准备
+
+在开始之前,请确保您已具备以下环境:
+
+- 一个正在运行的 Kubernetes 集群。
+- `kubectl` 命令行工具已配置并能够连接到您的集群。
+- 集群中已安装并配置好 [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/)。
+- 对集群中的一个节点(默认为 `master`)拥有文件系统访问权限,用于创建本地存储目录。
+
+## 2. 部署步骤
+
+### 步骤 1: 创建命名空间
+
+我们建议将 DjangoBlog 相关的所有资源都部署在一个独立的命名空间中,便于管理。
+
+```bash
+# 创建一个名为 djangoblog 的命名空间
+kubectl create namespace djangoblog
+```
+
+### 步骤 2: 配置持久化存储
+
+此方案使用本地持久卷 (Local Persistent Volume)。您需要在集群的一个节点上(在 `pv.yaml` 文件中默认为 `master` 节点)创建用于数据存储的目录。
+
+```bash
+# 登录到您的 master 节点
+ssh user@master-node
+
+# 创建所需的存储目录
+sudo mkdir -p /mnt/local-storage-db
+sudo mkdir -p /mnt/local-storage-djangoblog
+sudo mkdir -p /mnt/resource/
+sudo mkdir -p /mnt/local-storage-elasticsearch
+
+# 退出节点
+exit
+```
+**注意**: 如果您希望将数据存储在其他节点或使用不同的路径,请务必修改 `deploy/k8s/pv.yaml` 文件中 `nodeAffinity` 和 `local.path` 的配置。
+
+创建目录后,应用存储相关的配置文件:
+
+```bash
+# 应用 StorageClass
+kubectl apply -f deploy/k8s/storageclass.yaml
+
+# 应用 PersistentVolume (PV)
+kubectl apply -f deploy/k8s/pv.yaml
+
+# 应用 PersistentVolumeClaim (PVC)
+kubectl apply -f deploy/k8s/pvc.yaml
+```
+
+### 步骤 3: 配置应用
+
+在部署应用之前,您需要编辑 `deploy/k8s/configmap.yaml` 文件,修改其中的敏感信息和个性化配置。
+
+**强烈建议修改以下字段:**
+- `DJANGO_SECRET_KEY`: 修改为一个随机且复杂的字符串。
+- `DJANGO_MYSQL_PASSWORD` 和 `MYSQL_ROOT_PASSWORD`: 修改为您自己的数据库密码。
+
+```bash
+# 编辑 ConfigMap 文件
+vim deploy/k8s/configmap.yaml
+
+# 应用配置
+kubectl apply -f deploy/k8s/configmap.yaml
+```
+
+### 步骤 4: 部署应用服务栈
+
+现在,我们可以部署所有的核心服务了。
+
+```bash
+# 部署 Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
+kubectl apply -f deploy/k8s/deployment.yaml
+
+# 部署 Services (为 Deployments 创建内部访问端点)
+kubectl apply -f deploy/k8s/service.yaml
+```
+
+部署需要一些时间,您可以运行以下命令检查所有 Pod 是否都已成功运行 (STATUS 为 `Running`):
+
+```bash
+kubectl get pods -n djangoblog -w
+```
+
+### 步骤 5: 暴露应用到外部
+
+最后,通过应用 `Ingress` 规则来将外部流量引导至我们的 Nginx 服务。
+
+```bash
+# 应用 Ingress 规则
+kubectl apply -f deploy/k8s/gateway.yaml
+```
+
+部署完成后,您可以通过 Ingress Controller 的外部 IP 地址来访问您的博客。执行以下命令获取地址:
+
+```bash
+kubectl get ingress -n djangoblog
+```
+
+### 步骤 6: 首次运行的初始化操作
+
+与 Docker 部署类似,首次运行时,您需要进入 DjangoBlog 应用的 Pod 来执行数据库初始化和创建管理员账户。
+
+```bash
+# 首先,获取 djangoblog pod 的名称
+kubectl get pods -n djangoblog | grep djangoblog
+
+# 进入其中一个 Pod (将 [pod-name] 替换为上一步获取到的名称)
+kubectl exec -it [pod-name] -n djangoblog -- bash
+
+# 在 Pod 内部执行以下命令:
+# 创建超级管理员账户 (请按照提示操作)
+python manage.py createsuperuser
+
+# (可选) 创建测试数据
+python manage.py create_testdata
+
+# (可选,如果启用了 ES) 创建索引
+python manage.py rebuild_index
+
+# 退出 Pod
+exit
+```
+
+至此,您已成功在 Kubernetes 集群上完成了 DjangoBlog 的部署!
\ No newline at end of file