Compare commits

...

8 Commits

@ -15,8 +15,9 @@
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (pythonProject1)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.12 (pythonProject)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="db" uuid="d458bf6f-0313-4a3c-8984-4405349d463d">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:D:\软件方法学\Djangoblog\src\db.sqlite3</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.12 (pythonProject1)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (pythonProject1)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (pythonProject)" project-jdk-type="Python SDK" />
</project>

Binary file not shown.

Binary file not shown.

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

@ -0,0 +1,176 @@
name: 自动部署到生产环境
on:
workflow_run:
workflows: ["Django CI"]
types:
- completed
branches:
- master
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'production'
type: choice
options:
- production
- staging
image_tag:
description: '镜像标签 (默认: latest)'
required: false
default: 'latest'
type: string
skip_tests:
description: '跳过测试直接部署'
required: false
default: false
type: boolean
env:
REGISTRY: registry.cn-shenzhen.aliyuncs.com
IMAGE_NAME: liangliangyy/djangoblog
NAMESPACE: djangoblog
jobs:
deploy:
name: 构建镜像并部署到生产环境
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置部署参数
id: deploy-params
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "trigger_type=手动触发" >> $GITHUB_OUTPUT
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
echo "image_tag=${{ github.event.inputs.image_tag }}" >> $GITHUB_OUTPUT
echo "skip_tests=${{ github.event.inputs.skip_tests }}" >> $GITHUB_OUTPUT
else
echo "trigger_type=CI自动触发" >> $GITHUB_OUTPUT
echo "environment=production" >> $GITHUB_OUTPUT
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "skip_tests=false" >> $GITHUB_OUTPUT
fi
- name: 显示部署信息
run: |
echo "🚀 部署信息:"
echo " 触发方式: ${{ steps.deploy-params.outputs.trigger_type }}"
echo " 部署环境: ${{ steps.deploy-params.outputs.environment }}"
echo " 镜像标签: ${{ steps.deploy-params.outputs.image_tag }}"
echo " 跳过测试: ${{ steps.deploy-params.outputs.skip_tests }}"
- name: 设置Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 登录私有镜像仓库
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: 提取镜像元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=${{ steps.deploy-params.outputs.image_tag }}
- name: 构建并推送Docker镜像
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
- name: 部署到生产服务器
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_PORT || 22 }}
script: |
echo "🚀 开始部署 DjangoBlog..."
# 检查kubectl是否可用
if ! command -v kubectl &> /dev/null; then
echo "❌ 错误: kubectl 未安装或不在PATH中"
exit 1
fi
# 检查命名空间是否存在
if ! kubectl get namespace ${{ env.NAMESPACE }} &> /dev/null; then
echo "❌ 错误: 命名空间 ${{ env.NAMESPACE }} 不存在"
exit 1
fi
# 更新deployment镜像
echo "📦 更新deployment镜像为: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }}"
kubectl set image deployment/djangoblog \
djangoblog=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }} \
-n ${{ env.NAMESPACE }}
# 重启deployment
echo "🔄 重启deployment..."
kubectl -n ${{ env.NAMESPACE }} rollout restart deployment djangoblog
# 等待deployment完成
echo "⏳ 等待deployment完成..."
kubectl rollout status deployment/djangoblog -n ${{ env.NAMESPACE }} --timeout=300s
# 检查deployment状态
echo "✅ 检查deployment状态..."
kubectl get deployment djangoblog -n ${{ env.NAMESPACE }}
kubectl get pods -l app=djangoblog -n ${{ env.NAMESPACE }}
echo "🎉 部署完成!"
- name: 发送部署通知
if: always()
run: |
# 设置通知内容
if [ "${{ job.status }}" = "success" ]; then
TITLE="✅ DjangoBlog部署成功"
STATUS="成功"
else
TITLE="❌ DjangoBlog部署失败"
STATUS="失败"
fi
MESSAGE="部署状态: ${STATUS}
触发方式: ${{ steps.deploy-params.outputs.trigger_type }}
部署环境: ${{ steps.deploy-params.outputs.environment }}
镜像标签: ${{ steps.deploy-params.outputs.image_tag }}
提交者: ${{ github.actor }}
时间: $(date '+%Y-%m-%d %H:%M:%S')
查看详情: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# 发送Server酱通知
if [ -n "${{ secrets.SERVERCHAN_KEY }}" ]; then
echo "{\"title\": \"${TITLE}\", \"desp\": \"${MESSAGE}\"}" > /tmp/serverchan.json
curl --location "https://sctapi.ftqq.com/${{ secrets.SERVERCHAN_KEY }}.send" \
--header "Content-Type: application/json" \
--data @/tmp/serverchan.json \
--silent > /dev/null
rm -f /tmp/serverchan.json
echo "📱 部署通知已发送"
fi

2
src/.gitignore vendored

@ -78,5 +78,3 @@ uploads/
settings_production.py
werobot_session.db
bin/datas/
.idea/

@ -146,7 +146,7 @@ python manage.py runserver
## 🙏 鸣谢
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。gs ls syj zyd164
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
<p align="center">
<a href="https://www.jetbrains.com/?from=DjangoBlog">

@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
# 吴铠涛自定义用户创建表单继承自ModelForm
class BlogUserCreationForm(forms.ModelForm):
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
@ -16,24 +16,24 @@ class BlogUserCreationForm(forms.ModelForm):
model = BlogUser
fields = ('email',)
# 吴铠涛:验证两次输入的密码是否一致
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
# 吴铠涛:保存用户,对密码进行哈希处理
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite'
user.source = 'adminsite' # 吴铠涛:标记用户来源为管理员站点
user.save()
return user
# 吴铠涛自定义用户修改表单继承自UserChangeForm
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
@ -43,10 +43,11 @@ class BlogUserChangeForm(UserChangeForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 吴铠涛自定义用户管理类继承自UserAdmin
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
# 吴铠涛:定义列表页显示的字段
list_display = (
'id',
'nickname',
@ -55,5 +56,5 @@ class BlogUserAdmin(UserAdmin):
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)
list_display_links = ('id', 'username') # 吴铠涛:定义可点击链接的字段
ordering = ('-id',) # 吴铠涛按ID倒序排列

@ -1,5 +1,5 @@
from django.apps import AppConfig
# 吴铠涛:应用配置类
class AccountsConfig(AppConfig):
name = 'accounts'
name = 'accounts' # 吴铠涛:指定应用名称

@ -7,20 +7,21 @@ from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
# 吴铠涛登录表单继承自AuthenticationForm
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
# 吴铠涛:为表单字段添加样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# 吴铠涛注册表单继承自UserCreationForm
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
# 吴铠涛:为所有字段添加样式和占位符
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
@ -30,6 +31,7 @@ class RegisterForm(UserCreationForm):
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
# 吴铠涛:验证邮箱是否已存在
def clean_email(self):
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
@ -40,7 +42,7 @@ class RegisterForm(UserCreationForm):
model = get_user_model()
fields = ("username", "email")
# 吴铠涛:忘记密码表单
class ForgetPasswordForm(forms.Form):
new_password1 = forms.CharField(
label=_("New password"),
@ -82,24 +84,23 @@ class ForgetPasswordForm(forms.Form):
),
)
# 吴铠涛:验证两次输入的新密码是否一致
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2)
password_validation.validate_password(password2) # 吴铠涛使用Django内置密码验证
return password2
# 吴铠涛:验证邮箱是否存在
def clean_email(self):
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
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(
@ -110,8 +111,8 @@ class ForgetPasswordForm(forms.Form):
raise ValidationError(error)
return code
# 吴铠涛:获取忘记密码验证码的表单
class ForgetPasswordCodeForm(forms.Form):
email = forms.EmailField(
label=_('Email'),
)
)

@ -5,23 +5,25 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
# 吴铠涛自定义用户模型继承自AbstractUser
class BlogUser(AbstractUser):
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
nickname = models.CharField(_('nick name'), max_length=100, blank=True) # 吴铠涛:昵称字段
creation_time = models.DateTimeField(_('creation time'), default=now) # 吴铠涛:创建时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 吴铠涛:最后修改时间
source = models.CharField(_('create source'), max_length=100, blank=True) # 吴铠涛:用户来源
# 吴铠涛获取用户绝对URL
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
return self.email # 吴铠涛:以邮箱作为字符串表示
# 吴铠涛获取完整URL
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
@ -29,7 +31,7 @@ class BlogUser(AbstractUser):
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'
ordering = ['-id'] # 吴铠涛按ID倒序排列
verbose_name = _('user') # 吴铠涛:单数名称
verbose_name_plural = verbose_name # 吴铠涛:复数名称
get_latest_by = 'id' # 吴铠涛指定最新记录按ID排序

@ -8,9 +8,9 @@ from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
# 吴铠涛:账户相关测试类
class AccountTest(TestCase):
def setUp(self):
self.client = Client()
@ -22,6 +22,7 @@ class AccountTest(TestCase):
)
self.new_test = "xxx123--="
# 吴铠涛:测试账户验证功能
def test_validate_account(self):
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
@ -55,6 +56,7 @@ class AccountTest(TestCase):
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
# 吴铠涛:测试注册功能
def test_validate_register(self):
self.assertEquals(
0, len(
@ -118,6 +120,7 @@ class AccountTest(TestCase):
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()
@ -130,6 +133,7 @@ class AccountTest(TestCase):
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
# 吴铠涛:测试成功发送忘记密码邮箱验证码
def test_forget_password_email_code_success(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
@ -139,6 +143,7 @@ class AccountTest(TestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
# 吴铠涛:测试发送忘记密码邮箱验证码失败情况
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
@ -152,6 +157,7 @@ class AccountTest(TestCase):
)
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)
@ -167,13 +173,14 @@ class AccountTest(TestCase):
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
# 吴铠涛:验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
# 吴铠涛:测试用户不存在时的忘记密码功能
def test_forget_password_email_not_user(self):
data = dict(
new_password1=self.new_test,
@ -188,7 +195,7 @@ class AccountTest(TestCase):
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)
@ -203,5 +210,4 @@ class AccountTest(TestCase):
data=data
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.status_code, 200)

@ -4,25 +4,27 @@ from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
app_name = "accounts" # 吴铠涛:定义应用命名空间
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]
# 吴铠涛URL配置
urlpatterns = [
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}), # 吴铠涛:登录页面
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'), # 吴铠涛:注册页面
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'), # 吴铠涛:登出页面
path(r'account/result.html',
views.account_result,
name='result'), # 吴铠涛:账户操作结果页面
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'), # 吴铠涛:忘记密码页面
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'), # 吴铠涛:获取忘记密码验证码
]

@ -1,20 +1,21 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
# 吴铠涛:自定义认证后端,允许使用用户名或邮箱登录
class EmailOrUsernameModelBackend(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):
if user.check_password(password): # 吴铠涛:验证密码
return user
except get_user_model().DoesNotExist:
return None
@ -23,4 +24,4 @@ class EmailOrUsernameModelBackend(ModelBackend):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None
return None

@ -7,9 +7,9 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5)
_code_ttl = timedelta(minutes=5) # 吴铠涛验证码有效期为5分钟
# 吴铠涛:发送验证邮件
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
@ -22,7 +22,7 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
"properly") % {'code': code}
send_email([to_mail], subject, html_content)
# 吴铠涛:验证验证码是否正确
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
@ -38,12 +38,12 @@ def verify(email: str, code: str) -> typing.Optional[str]:
if cache_code != code:
return gettext("Verification code error")
# 吴铠涛:将验证码存入缓存
def set_code(email: str, code: str):
"""设置code"""
cache.set(email, code, _code_ttl.seconds)
# 吴铠涛:从缓存获取验证码
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
return cache.get(email)
return cache.get(email)

@ -28,9 +28,9 @@ from .models import BlogUser
logger = logging.getLogger(__name__)
# Create your views here.
# 吴铠涛:注册视图
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
@ -41,15 +41,15 @@ class RegisterView(FormView):
def form_valid(self, form):
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
user = form.save(False) # 吴铠涛:不立即保存到数据库
user.is_active = False # 吴铠涛:设置用户为未激活状态
user.source = 'Register' # 吴铠涛:标记用户来源
user.save(True) # 吴铠涛:保存到数据库
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
site = '127.0.0.1:8000'
site = '127.0.0.1:8000' # 吴铠涛:调试模式下使用本地地址
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
@ -79,7 +79,7 @@ class RegisterView(FormView):
'form': form
})
# 吴铠涛:登出视图
class LogoutView(RedirectView):
url = '/login/'
@ -89,22 +89,21 @@ class LogoutView(RedirectView):
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
delete_sidebar_cache() # 吴铠涛:清除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs)
# 吴铠涛:登录视图
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
login_ttl = 2626560 # 吴铠涛:一个月的时间(秒数)
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@ -119,21 +118,19 @@ class LoginView(FormView):
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
delete_sidebar_cache() # 吴铠涛:清除侧边栏缓存
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
auth.login(self.request, form.get_user()) # 吴铠涛:登录用户
if self.request.POST.get("remember"): # 吴铠涛:处理"记住我"功能
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
@ -141,7 +138,7 @@ class LoginView(FormView):
redirect_to = self.success_url
return redirect_to
# 吴铠涛:账户操作结果页面
def account_result(request):
type = request.GET.get('type')
id = request.GET.get('id')
@ -159,9 +156,9 @@ def account_result(request):
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
if sign != c_sign: # 吴铠涛:验证签名
return HttpResponseForbidden()
user.is_active = True
user.is_active = True # 吴铠涛:激活用户
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
@ -174,7 +171,7 @@ def account_result(request):
else:
return HttpResponseRedirect('/')
# 吴铠涛:忘记密码视图
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
@ -182,13 +179,13 @@ class ForgetPasswordView(FormView):
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.password = make_password(form.cleaned_data["new_password2"]) # 吴铠涛:哈希密码
blog_user.save()
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
# 吴铠涛:忘记密码邮箱验证码视图
class ForgetPasswordEmailCode(View):
def post(self, request: HttpRequest):
@ -198,7 +195,7 @@ class ForgetPasswordEmailCode(View):
to_email = form.cleaned_data["email"]
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code) # 吴铠涛:发送验证邮件
utils.set_code(to_email, code) # 吴铠涛:保存验证码到缓存
return HttpResponse("ok")
return HttpResponse("ok")

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article
# 吴铠涛:文章表单类
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
@ -16,54 +16,56 @@ class ArticleForm(forms.ModelForm):
model = Article
fields = '__all__'
# 吴铠涛:管理员动作 - 发布选中的文章
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
# 吴铠涛:管理员动作 - 将选中的文章设为草稿
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
# 吴铠涛:管理员动作 - 关闭文章评论
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
# 吴铠涛:管理员动作 - 开启文章评论
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
# 吴铠涛:设置管理员动作的描述
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
# 吴铠涛:文章管理类
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20
search_fields = ('body', 'title')
list_per_page = 20 # 吴铠涛每页显示20条记录
search_fields = ('body', 'title') # 吴铠涛:搜索字段
form = ArticleForm
list_display = (
'id',
'title',
'author',
'link_to_category',
'link_to_category', # 吴铠涛:自定义字段显示分类链接
'creation_time',
'views',
'likes',
'status',
'type',
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
actions = [
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):
info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
@ -71,6 +73,7 @@ class ArticlelAdmin(admin.ModelAdmin):
link_to_category.short_description = _('category')
# 吴铠涛:重写表单获取方法,限制作者只能选择超级用户
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
@ -80,6 +83,7 @@ class ArticlelAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
# 吴铠涛获取站点查看URL
def get_view_on_site_url(self, obj=None):
if obj:
url = obj.get_full_url()
@ -89,24 +93,24 @@ class ArticlelAdmin(admin.ModelAdmin):
site = get_current_site().domain
return site
# 吴铠涛:标签管理类
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
exclude = ('slug', 'last_mod_time', 'creation_time') # 吴铠涛:排除的字段
# 吴铠涛:分类管理类
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
list_display = ('name', 'parent_category', 'index') # 吴铠涛:列表显示字段
exclude = ('slug', 'last_mod_time', 'creation_time') # 吴铠涛:排除的字段
# 吴铠涛:链接管理类
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
exclude = ('last_mod_time', 'creation_time') # 吴铠涛:排除的字段
# 吴铠涛:侧边栏管理类
class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
list_display = ('name', 'content', 'is_enable', 'sequence') # 吴铠涛:列表显示字段
exclude = ('last_mod_time', 'creation_time') # 吴铠涛:排除的字段
# 吴铠涛:博客设置管理类
class BlogSettingsAdmin(admin.ModelAdmin):
pass
pass

@ -1,5 +1,5 @@
from django.apps import AppConfig
# 吴铠涛:博客应用配置类
class BlogConfig(AppConfig):
name = 'blog'
name = 'blog' # 吴铠涛:应用名称

@ -7,7 +7,7 @@ from .models import Category, Article
logger = logging.getLogger(__name__)
# 吴铠涛SEO处理器为模板提供全局上下文变量
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
@ -17,27 +17,27 @@ def seo_processor(requests):
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(),
'SITE_NAME': setting.site_name, # 吴铠涛:站点名称
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, # 吴铠涛是否显示Google广告
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, # 吴铠涛Google广告代码
'SITE_SEO_DESCRIPTION': setting.site_seo_description, # 吴铠涛SEO描述
'SITE_DESCRIPTION': setting.site_description, # 吴铠涛:站点描述
'SITE_KEYWORDS': setting.site_keywords, # 吴铠涛:站点关键词
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', # 吴铠涛站点基础URL
'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,
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
cache.set(key, value, 60 * 60 * 10) # 吴铠涛缓存10小时
return value

@ -7,6 +7,7 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
# 吴铠涛检查是否启用Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
@ -32,29 +33,29 @@ if ELASTICSEARCH_ENABLED:
]
}''')
# 吴铠涛GeoIP内部文档类
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
# 吴铠涛:用户代理浏览器信息类
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
# 吴铠涛:用户代理操作系统信息类
class UserAgentOS(UserAgentBrowser):
pass
# 吴铠涛:用户代理设备信息类
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
# 吴铠涛:用户代理信息类
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
@ -62,7 +63,7 @@ class UserAgent(InnerDoc):
string = Text()
is_bot = Boolean()
# 吴铠涛:响应时间文档类
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
@ -81,7 +82,7 @@ class ElapsedTimeDocument(Document):
class Meta:
doc_type = 'ElapsedTime'
# 吴铠涛:响应时间文档管理器
class ElaspedTimeDocumentManager:
@staticmethod
def build_index():
@ -129,7 +130,7 @@ class ElaspedTimeDocumentManager:
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
# 吴铠涛:文章文档类
class ArticleDocument(Document):
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
@ -163,7 +164,7 @@ class ArticleDocument(Document):
class Meta:
doc_type = 'Article'
# 吴铠涛:文章文档管理器
class ArticleDocumentManager():
def __init__(self):
@ -210,4 +211,4 @@ class ArticleDocumentManager():
def update_docs(self, docs):
for doc in docs:
doc.save()
doc.save()

@ -5,9 +5,9 @@ from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
# 吴铠涛:博客搜索表单
class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True)
querydata = forms.CharField(required=True) # 吴铠涛:搜索查询字段
def search(self):
datas = super(BlogSearchForm, self).search()
@ -16,4 +16,4 @@ class BlogSearchForm(SearchForm):
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas
return datas

@ -8,7 +8,7 @@ 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
@ -39,4 +39,4 @@ class OnlineMiddleware(object):
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response
return response

@ -0,0 +1,30 @@
# Generated by Django 5.2.4 on 2025-11-16 23:10
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("blog", "0006_alter_blogsettings_options"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="article",
name="liked_users",
field=models.ManyToManyField(
blank=True,
related_name="liked_articles",
to=settings.AUTH_USER_MODEL,
verbose_name="liked users",
),
),
migrations.AddField(
model_name="article",
name="likes",
field=models.PositiveIntegerField(default=0, verbose_name="likes"),
),
]

@ -16,15 +16,15 @@ 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'))
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)
@ -57,53 +57,53 @@ class BaseModel(models.Model):
def get_absolute_url(self):
pass
# 吴铠涛:文章模型
class Article(BaseModel):
"""文章"""
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
('d', _('Draft')), # 吴铠涛:草稿状态
('p', _('Published')), # 吴铠涛:发布状态
)
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
('o', _('Open')), # 吴铠涛:开放评论
('c', _('Close')), # 吴铠涛:关闭评论
)
TYPE = (
('a', _('Article')),
('p', _('Page')),
('a', _('Article')), # 吴铠涛:文章类型
('p', _('Page')), # 吴铠涛:页面类型
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
title = models.CharField(_('title'), max_length=200, unique=True) # 吴铠涛:文章标题
body = MDTextField(_('body')) # 吴铠涛文章正文使用Markdown编辑器
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
_('publish time'), blank=False, null=False, default=now) # 吴铠涛:发布时间
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
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)
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)
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)
_('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)
null=False) # 吴铠涛:分类外键
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 吴铠涛:标签多对多关系
def body_to_string(self):
return self.body
@ -112,7 +112,7 @@ class Article(BaseModel):
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
ordering = ['-article_order', '-pub_time'] # 吴铠涛:按文章排序和发布时间倒序排列
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
@ -168,7 +168,7 @@ class Article(BaseModel):
def get_first_image_url(self):
"""
Get the first image url from article.body.
Get the first image url from article body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
@ -176,21 +176,29 @@ class Article(BaseModel):
return match.group(1)
return ""
likes = models.PositiveIntegerField(_('likes'), default=0) # 吴铠涛:点赞数
liked_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
verbose_name=_('liked users'),
related_name='liked_articles',
blank=True
) # 吴铠涛:记录点赞用户,防止重复点赞
# 吴铠涛:分类模型
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
name = models.CharField(_('category name'), max_length=30, unique=True) # 吴铠涛:分类名称
parent_category = models.ForeignKey(
'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'))
on_delete=models.CASCADE) # 吴铠涛:父级分类
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 吴铠涛URL别名
index = models.IntegerField(default=0, verbose_name=_('index')) # 吴铠涛:排序索引
class Meta:
ordering = ['-index']
ordering = ['-index'] # 吴铠涛:按索引倒序排列
verbose_name = _('category')
verbose_name_plural = verbose_name
@ -239,11 +247,11 @@ class Category(BaseModel):
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)
name = models.CharField(_('tag name'), max_length=30, unique=True) # 吴铠涛:标签名称
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 吴铠涛URL别名
def __str__(self):
return self.name
@ -256,54 +264,54 @@ class Tag(BaseModel):
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
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)
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)
_('is show'), default=True, blank=False, null=False) # 吴铠涛:是否启用
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
default=LinkShowType.I) # 吴铠涛:显示类型
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence']
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)
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']
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(
@ -311,53 +319,53 @@ class BlogSettings(models.Model):
max_length=200,
null=False,
blank=False,
default='')
default='') # 吴铠涛:站点名称
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
default='') # 吴铠涛:站点描述
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
_('site seo description'), max_length=1000, null=False, blank=False, default='') # 吴铠涛SEO描述
site_keywords = models.TextField(
_('site keywords'),
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)
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广告
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='')
_('adsense code'), max_length=2000, null=True, blank=True, default='') # 吴铠涛Google广告代码
open_site_comment = models.BooleanField(_('open site comment'), default=True) # 吴铠涛:是否开启站点评论
global_header = models.TextField("公共头部", null=True, blank=True, default='') # 吴铠涛全局头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 吴铠涛全局尾部HTML
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
default='') # 吴铠涛:备案号
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
default='') # 吴铠涛:网站统计代码
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
'是否显示公安备案号', default=False, null=False) # 吴铠涛:是否显示公安备案
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
default='') # 吴铠涛:公安备案号
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
'评论是否需要审核', default=False, null=False) # 吴铠涛:评论是否需要审核
class Meta:
verbose_name = _('Website configuration')
@ -373,4 +381,4 @@ class BlogSettings(models.Model):
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
cache.clear() # 吴铠涛:保存设置时清除缓存

@ -16,14 +16,15 @@ from blog.templatetags.blog_tags import load_pagination_info, load_articletags
from djangoblog.utils import get_current_site, get_sha256
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
# 吴铠涛:文章测试类
class ArticleTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
# 吴铠涛:测试文章验证
def test_validate_article(self):
site = get_current_site().domain
user = BlogUser.objects.get_or_create(
@ -148,6 +149,7 @@ class ArticleTest(TestCase):
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
# 吴铠涛:检查分页功能
def check_pagination(self, p, type, value):
for page in range(1, p.num_pages + 1):
s = load_pagination_info(p.page(page), type, value)
@ -159,6 +161,7 @@ class ArticleTest(TestCase):
response = self.client.get(s['next_url'])
self.assertEqual(response.status_code, 200)
# 吴铠涛:测试图片上传功能
def test_image(self):
import requests
rsp = requests.get(
@ -182,10 +185,12 @@ class ArticleTest(TestCase):
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
# 吴铠涛:测试错误页面
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
# 吴铠涛:测试管理命令
def test_commands(self):
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
@ -229,4 +234,4 @@ class ArticleTest(TestCase):
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
call_command("build_search_words")

@ -3,60 +3,71 @@ from django.views.decorators.cache import cache_page
from . import views
app_name = "blog"
app_name = "blog" # 吴铠涛:定义应用命名空间
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
name='index'), # 吴铠涛:首页
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
name='index_page'), # 吴铠涛:首页分页
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
name='detailbyid'), # 吴铠涛:文章详情页
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
name='category_detail'), # 吴铠涛:分类详情页
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
name='category_detail_page'), # 吴铠涛:分类详情分页
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
name='author_detail'), # 吴铠涛:作者详情页
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
name='author_detail_page'), # 吴铠涛:作者详情分页
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
name='tag_detail'), # 吴铠涛:标签详情页
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
name='tag_detail_page'), # 吴铠涛:标签详情分页
path(
'archives.html',
cache_page(
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
name='archives'), # 吴铠涛文章归档页缓存1小时
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
name='links'), # 吴铠涛:友情链接页
path(
r'upload',
views.fileupload,
name='upload'),
name='upload'), # 吴铠涛:文件上传
path(
r'clean',
views.clean_cache_view,
name='clean'),
]
name='clean'), # 吴铠涛:清理缓存
path(
r'article/<int:article_id>/like/',
views.like_article,
name='like_article'
), # 吴铠涛:文章点赞
path(
r'comment/<int:comment_id>/like/',
views.like_comment,
name='like_comment'
), # 吴铠涛:评论点赞
]

@ -21,9 +21,14 @@ from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from comments.models import Comment
logger = logging.getLogger(__name__)
# 吴铠涛:文章列表视图基类
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
@ -33,9 +38,9 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
paginate_by = settings.PAGINATE_BY # 吴铠涛:分页大小
page_kwarg = 'page' # 吴铠涛:页码参数名
link_type = LinkShowType.L # 吴铠涛:链接显示类型
def get_view_cache_key(self):
return self.request.get['pages']
@ -88,7 +93,7 @@ class ArticleListView(ListView):
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
# 吴铠涛:首页视图
class IndexView(ArticleListView):
'''
首页
@ -97,14 +102,14 @@ class IndexView(ArticleListView):
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
article_list = Article.objects.filter(type='a', status='p') # 吴铠涛:获取已发布的文章
return article_list
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
# 吴铠涛:文章详情视图
class ArticleDetailView(DetailView):
'''
文章详情页面
@ -160,7 +165,7 @@ class ArticleDetailView(DetailView):
return context
# 吴铠涛:分类详情视图
class CategoryDetailView(ArticleListView):
'''
分类目录列表
@ -199,7 +204,7 @@ class CategoryDetailView(ArticleListView):
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
# 吴铠涛:作者详情视图
class AuthorDetailView(ArticleListView):
'''
作者详情页
@ -225,7 +230,7 @@ class AuthorDetailView(ArticleListView):
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
# 吴铠涛:标签详情视图
class TagDetailView(ArticleListView):
'''
标签列表页面
@ -257,14 +262,14 @@ class TagDetailView(ArticleListView):
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
# 吴铠涛:归档视图
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
paginate_by = None # 吴铠涛:不分页
page_kwarg = None # 吴铠涛:无页码参数
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
@ -274,15 +279,15 @@ class ArchivesView(ArticleListView):
cache_key = 'archives'
return cache_key
# 吴铠涛:链接列表视图
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
def get_queryset(self):
return Links.objects.filter(is_enable=True)
return Links.objects.filter(is_enable=True) # 吴铠涛:只获取启用的链接
# 吴铠涛Elasticsearch搜索视图
class EsSearchView(SearchView):
def get_context(self):
paginator, page = self.build_page()
@ -299,7 +304,7 @@ class EsSearchView(SearchView):
return context
# 吴铠涛:文件上传视图
@csrf_exempt
def fileupload(request):
"""
@ -339,7 +344,7 @@ def fileupload(request):
else:
return HttpResponse("only for post")
# 吴铠涛404错误页面视图
def page_not_found_view(
request,
exception,
@ -353,7 +358,7 @@ def page_not_found_view(
'statuscode': '404'},
status=404)
# 吴铠涛500错误页面视图
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
@ -361,7 +366,7 @@ def server_error_view(request, template_name='blog/error_page.html'):
'statuscode': '500'},
status=500)
# 吴铠涛403错误页面视图
def permission_denied_view(
request,
exception,
@ -373,7 +378,63 @@ def permission_denied_view(
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
# 吴铠涛:清理缓存视图
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
# 吴铠涛:文章点赞视图
@login_required
@require_POST
@csrf_exempt
def like_article(request, article_id):
article = get_object_or_404(Article, pk=article_id)
user = request.user
if article.liked_users.filter(pk=user.pk).exists():
# 如果已经点赞,取消点赞
article.liked_users.remove(user)
article.likes = max(0, article.likes - 1)
liked = False
else:
# 如果未点赞,添加点赞
article.liked_users.add(user)
article.likes += 1
liked = True
article.save(update_fields=['likes'])
return JsonResponse({
'success': True,
'likes': article.likes,
'liked': liked
})
# 吴铠涛:评论点赞视图
@login_required
@require_POST
@csrf_exempt
def like_comment(request, comment_id):
comment = get_object_or_404(Comment, pk=comment_id)
user = request.user
if comment.liked_users.filter(pk=user.pk).exists():
# 如果已经点赞,取消点赞
comment.liked_users.remove(user)
comment.likes = max(0, comment.likes - 1)
liked = False
else:
# 如果未点赞,添加点赞
comment.liked_users.add(user)
comment.likes += 1
liked = True
comment.save(update_fields=['likes'])
return JsonResponse({
'success': True,
'likes': comment.likes,
'liked': liked
})

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

@ -1,21 +1,23 @@
# admin.py
#吴铠涛导入Django管理模块和相关工具
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 = (
@ -24,12 +26,14 @@ class CommentAdmin(admin.ModelAdmin):
'link_to_userinfo',
'link_to_article',
'is_enable',
'likes',
'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,))
@ -37,6 +41,7 @@ class CommentAdmin(admin.ModelAdmin):
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
#吴铠涛:生成文章链接的方法
def link_to_article(self, obj):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
@ -44,4 +49,4 @@ class CommentAdmin(admin.ModelAdmin):
u'<a href="%s">%s</a>' % (link, obj.article.title))
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')
link_to_article.short_description = _('Article')

@ -1,5 +1,7 @@
# apps.py
#吴铠涛导入Django应用配置模块
from django.apps import AppConfig
#吴铠涛:评论应用配置类
class CommentsConfig(AppConfig):
name = 'comments'
name = 'comments'

@ -1,13 +1,15 @@
# forms.py
# 吴铠涛导入Django表单模块
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']
fields = ['body']

@ -0,0 +1,30 @@
# Generated by Django 5.2.4 on 2025-11-16 23:10
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("comments", "0003_alter_comment_options_remove_comment_created_time_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="comment",
name="liked_users",
field=models.ManyToManyField(
blank=True,
related_name="liked_comments",
to=settings.AUTH_USER_MODEL,
verbose_name="liked users",
),
),
migrations.AddField(
model_name="comment",
name="likes",
field=models.PositiveIntegerField(default=0, verbose_name="likes"),
),
]

@ -1,3 +1,5 @@
# models.py
# 吴铠涛导入Django模型相关模块
from django.conf import settings
from django.db import models
from django.utils.timezone import now
@ -5,9 +7,7 @@ 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)
@ -29,6 +29,14 @@ class Comment(models.Model):
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
likes = models.PositiveIntegerField(_('likes'), default=0) # 吴铠涛:评论点赞数
liked_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
verbose_name=_('liked users'),
related_name='liked_comments',
blank=True
) # 吴铠涛:记录点赞用户
class Meta:
ordering = ['-id']
verbose_name = _('comment')
@ -36,4 +44,4 @@ class Comment(models.Model):
get_latest_by = 'id'
def __str__(self):
return self.body
return self.body

@ -1,3 +1,5 @@
# tests.py
# 吴铠涛:导入测试相关模块
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse
@ -7,9 +9,7 @@ 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()
@ -24,12 +24,14 @@ class CommentsTest(TransactionTestCase):
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')
@ -106,4 +108,4 @@ class CommentsTest(TransactionTestCase):
self.assertIsNotNone(s)
from comments.utils import send_comment_email
send_comment_email(comment)
send_comment_email(comment)

@ -1,11 +1,14 @@
# urls.py
# 吴铠涛导入URL路由模块
from django.urls import path
from . import views
# 吴铠涛定义评论应用的URL模式
app_name = "comments"
urlpatterns = [
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]
]

@ -1,3 +1,5 @@
# utils.py
# 吴铠涛:导入工具模块
import logging
from django.utils.translation import gettext_lazy as _
@ -7,7 +9,7 @@ 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')
@ -35,4 +37,4 @@ def send_comment_email(comment):
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e)
logger.error(e)

@ -1,4 +1,6 @@
# Create your views here.
# views.py
# 吴铠涛:导入视图相关模块
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
@ -11,7 +13,7 @@ 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'
@ -60,4 +62,4 @@ class CommentPostView(FormView):
comment.save(True)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))
(article.get_absolute_url(), comment.pk))

@ -16,14 +16,15 @@ from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
# 吴铠涛自定义DjangoBlog管理站点
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
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
@ -37,9 +38,10 @@ class DjangoBlogAdminSite(AdminSite):
# ]
# return urls + my_urls
# 吴铠涛:创建自定义管理站点实例
admin_site = DjangoBlogAdminSite(name='admin')
# 吴铠涛:注册所有模型到管理站点
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
@ -61,4 +63,4 @@ admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,11 +1,12 @@
from django.apps import AppConfig
# 吴铠涛DjangoBlog应用配置类
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
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()
load_plugins()

@ -18,11 +18,12 @@ 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']
@ -50,7 +51,7 @@ def send_email_signal_handler(sender, **kwargs):
log.send_result = False
log.save()
# 吴铠涛OAuth用户登录信号处理器
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
@ -63,7 +64,7 @@ def oauth_user_login_signal_handler(sender, **kwargs):
delete_sidebar_cache()
# 吴铠涛:模型保存后回调信号处理器
@receiver(post_save)
def model_post_save_callback(
sender,
@ -112,11 +113,11 @@ def model_post_save_callback(
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()
# cache.clear()

@ -10,7 +10,7 @@ from blog.models import Article
logger = logging.getLogger(__name__)
# 吴铠涛Elasticsearch搜索后端
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
@ -78,7 +78,7 @@ class ElasticSearchBackend(BaseSearchBackend):
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:
@ -121,7 +121,7 @@ class ElasticSearchBackend(BaseSearchBackend):
'spelling_suggestion': spelling_suggestion,
}
# 吴铠涛Elasticsearch搜索查询类
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
@ -131,12 +131,7 @@ class ElasticSearchQuery(BaseSearchQuery):
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 = []
@ -168,16 +163,16 @@ class ElasticSearchQuery(BaseSearchQuery):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
# 吴铠涛Elasticsearch模型搜索表单
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
# 吴铠涛:是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
# 吴铠涛Elasticsearch搜索引擎
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery
query = ElasticSearchQuery

@ -6,35 +6,35 @@ from django.utils.feedgenerator import Rss201rev2Feed
from blog.models import Article
from djangoblog.utils import CommonMarkdown
# 吴铠涛DjangoBlog RSS订阅类
class DjangoBlogFeed(Feed):
feed_type = Rss201rev2Feed
feed_type = Rss201rev2Feed # 吴铠涛使用RSS 2.0格式
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
description = '大巧无工,重剑无锋.' # 吴铠涛:订阅描述
title = "且听风吟 大巧无工,重剑无锋. " # 吴铠涛:订阅标题
link = "/feed/" # 吴铠涛:订阅链接
def author_name(self):
return get_user_model().objects.first().nickname
return get_user_model().objects.first().nickname # 吴铠涛:作者名称
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
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]
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] # 吴铠涛获取最新的5篇已发布文章
def item_title(self, item):
return item.title
return item.title # 吴铠涛:文章标题
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
return CommonMarkdown.get_markdown(item.body) # 吴铠涛文章内容Markdown转换
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
return "Copyright© {year} 且听风吟".format(year=now.year) # 吴铠涛:版权信息
def item_link(self, item):
return item.get_absolute_url()
return item.get_absolute_url() # 吴铠涛:文章链接
def item_guid(self, item):
return
return # 吴铠涛:文章唯一标识(可选)

@ -7,47 +7,48 @@ 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'
'content_type' # 吴铠涛:按内容类型筛选
]
search_fields = [
'object_repr',
'change_message'
'change_message' # 吴铠涛:搜索字段
]
list_display_links = [
'action_time',
'get_change_message',
'get_change_message', # 吴铠涛:可点击链接的字段
]
list_display = [
'action_time',
'user_link',
'user_link', # 吴铠涛:用户链接
'content_type',
'object_link',
'get_change_message',
'object_link', # 吴铠涛:对象链接
'get_change_message', # 吴铠涛:变更消息
]
def has_add_permission(self, request):
return False
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'
) and request.method != 'POST' # 吴铠涛:只有超级用户或有权限的用户可以查看
def has_delete_permission(self, request, obj=None):
return False
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,
@ -62,11 +63,12 @@ class LogEntryAdmin(admin.ModelAdmin):
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),
@ -82,10 +84,10 @@ class LogEntryAdmin(admin.ModelAdmin):
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
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
del actions['delete_selected'] # 吴铠涛:删除批量删除动作
return actions

@ -15,36 +15,31 @@ 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'
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
CSRF_TRUSTED_ORIGINS = ['http://example.com'] # 吴铠涛CSRF信任源
# 吴铠涛:已安装应用
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.admin.apps.SimpleAdminConfig', # 吴铠涛:简单管理员配置
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
@ -52,24 +47,25 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'mdeditor', # 吴铠涛Markdown编辑器
'haystack', # 吴铠涛:搜索框架
'blog',
'accounts',
'comments',
'oauth',
'servermanager',
'owntracks',
'compressor',
'compressor', # 吴铠涛:静态文件压缩
'djangoblog'
]
# 吴铠涛:中间件配置
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
'django.middleware.locale.LocaleMiddleware', # 吴铠涛:国际化中间件
'django.middleware.gzip.GZipMiddleware', # 吴铠涛GZip压缩中间件
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
@ -78,15 +74,16 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
'blog.middleware.OnlineMiddleware' # 吴铠涛:自定义在线中间件
]
ROOT_URLCONF = 'djangoblog.urls'
ROOT_URLCONF = 'djangoblog.urls' # 吴铠涛根URL配置
# 吴铠涛:模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 吴铠涛:模板目录
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -94,34 +91,27 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
'blog.context_processors.seo_processor' # 吴铠涛SEO处理器
],
},
},
]
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
WSGI_APPLICATION = 'djangoblog.wsgi.application' # 吴铠涛WSGI应用
# 吴铠涛:数据库配置
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 '123456',
'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
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD': '123456',
'HOST': '127.0.0.1',
'PORT': 3306,
}
}
# 吴铠涛:密码验证器
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@ -137,70 +127,68 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# 吴铠涛:多语言配置
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
os.path.join(BASE_DIR, 'locale'), # 吴铠涛:本地化文件路径
)
LANGUAGE_CODE = 'zh-hans'
LANGUAGE_CODE = 'zh-hans' # 吴铠涛:默认语言
TIME_ZONE = 'Asia/Shanghai'
TIME_ZONE = 'Asia/Shanghai' # 吴铠涛:时区
USE_I18N = True
USE_I18N = True # 吴铠涛:启用国际化
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
USE_L10N = True # 吴铠涛:启用本地化
USE_TZ = False # 吴铠涛:不使用时区
# 吴铠涛Haystack搜索配置
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', # 吴铠涛使用中文Whoosh引擎
'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_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 吴铠涛:静态文件收集目录
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
STATICFILES = os.path.join(BASE_DIR, 'static') # 吴铠涛:静态文件目录
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
AUTH_USER_MODEL = 'accounts.BlogUser' # 吴铠涛:自定义用户模型
LOGIN_URL = '/login/' # 吴铠涛登录URL
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # 吴铠涛:时间格式
DATE_TIME_FORMAT = '%Y-%m-%d' # 吴铠涛:日期格式
# bootstrap color styles
# 吴铠涛bootstrap颜色样式
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# paginate
# 吴铠涛:分页设置
PAGINATE_BY = 10
# http cache timeout
# 吴铠涛HTTP缓存超时
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
# 吴铠涛:缓存设置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 吴铠涛:本地内存缓存
'TIMEOUT': 10800, # 吴铠涛缓存超时时间3小时
'LOCATION': 'unique-snowflake',
}
}
# 使用redis作为缓存
# 吴铠涛:使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
@ -209,11 +197,12 @@ if os.environ.get("DJANGO_REDIS_URL"):
}
}
SITE_ID = 1
SITE_ID = 1 # 吴铠涛站点ID
# 吴铠涛百度推送URL
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)
@ -223,12 +212,13 @@ 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)
# 吴铠涛微信管理员密码两次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)
@ -256,13 +246,13 @@ LOGGING = {
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'class': 'logging.handlers.TimedRotatingFileHandler', # 吴铠涛:按时间轮转的日志处理器
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'when': 'D', # 吴铠涛:按天轮转
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'backupCount': 5, # 吴铠涛保留5个备份文件
'encoding': 'utf-8'
},
'console': {
@ -277,7 +267,7 @@ LOGGING = {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
'class': 'django.utils.log.AdminEmailHandler' # 吴铠涛:管理员邮件处理器
}
},
'loggers': {
@ -294,32 +284,35 @@ LOGGING = {
}
}
# 吴铠涛:静态文件查找器
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
'compressor.finders.CompressorFinder', # 吴铠涛:压缩文件查找器
)
COMPRESS_ENABLED = True
COMPRESS_ENABLED = True # 吴铠涛:启用压缩
# COMPRESS_OFFLINE = True
# 吴铠涛CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
]
# 吴铠涛JS压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') # 吴铠涛:媒体文件根目录
MEDIA_URL = '/media/' # 吴铠涛媒体文件URL
X_FRAME_OPTIONS = 'SAMEORIGIN' # 吴铠涛X-Frame-Options设置
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 吴铠涛:默认自增字段
# 吴铠涛Elasticsearch配置
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
@ -328,13 +321,13 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', # 吴铠涛使用Elasticsearch引擎
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
# 吴铠涛:插件系统配置
PLUGINS_DIR = BASE_DIR / 'plugins' # 吴铠涛:插件目录
ACTIVE_PLUGINS = [ # 吴铠涛:激活的插件列表
'article_copyright',
'reading_time',
'external_links',

@ -3,57 +3,57 @@ from django.urls import reverse
from blog.models import Article, Category, Tag
# 吴铠涛:静态视图站点地图
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
priority = 0.5 # 吴铠涛:优先级
changefreq = 'daily' # 吴铠涛:更新频率
def items(self):
return ['blog:index', ]
return ['blog:index', ] # 吴铠涛:首页
def location(self, item):
return reverse(item)
return reverse(item) # 吴铠涛生成URL
# 吴铠涛:文章站点地图
class ArticleSiteMap(Sitemap):
changefreq = "monthly"
priority = "0.6"
changefreq = "monthly" # 吴铠涛:每月更新
priority = "0.6" # 吴铠涛:优先级
def items(self):
return Article.objects.filter(status='p')
return Article.objects.filter(status='p') # 吴铠涛:已发布的文章
def lastmod(self, obj):
return obj.last_modify_time
return obj.last_modify_time # 吴铠涛:最后修改时间
# 吴铠涛:分类站点地图
class CategorySiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.6"
changefreq = "Weekly" # 吴铠涛:每周更新
priority = "0.6" # 吴铠涛:优先级
def items(self):
return Category.objects.all()
return Category.objects.all() # 吴铠涛:所有分类
def lastmod(self, obj):
return obj.last_modify_time
return obj.last_modify_time # 吴铠涛:最后修改时间
# 吴铠涛:标签站点地图
class TagSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
changefreq = "Weekly" # 吴铠涛:每周更新
priority = "0.3" # 吴铠涛:优先级
def items(self):
return Tag.objects.all()
return Tag.objects.all() # 吴铠涛:所有标签
def lastmod(self, obj):
return obj.last_modify_time
return obj.last_modify_time # 吴铠涛:最后修改时间
# 吴铠涛:用户站点地图
class UserSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
changefreq = "Weekly" # 吴铠涛:每周更新
priority = "0.3" # 吴铠涛:优先级
def items(self):
return list(set(map(lambda x: x.author, Article.objects.all())))
return list(set(map(lambda x: x.author, Article.objects.all()))) # 吴铠涛:所有文章作者
def lastmod(self, obj):
return obj.date_joined
return obj.date_joined # 吴铠涛:注册时间

@ -5,17 +5,17 @@ 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)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) # 吴铠涛向百度推送URL
logger.info(result.text)
except Exception as e:
logger.error(e)
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)
SpiderNotify.baidu_notify(url) # 吴铠涛:通知百度(兼容方法)

@ -2,13 +2,14 @@ from django.test import TestCase
from djangoblog.utils import *
# 吴铠涛DjangoBlog测试类
class DjangoBlogTest(TestCase):
def setUp(self):
pass
# 吴铠涛:测试工具函数
def test_utils(self):
md5 = get_sha256('test')
md5 = get_sha256('test') # 吴铠涛测试SHA256加密
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''
# Title1
@ -22,11 +23,11 @@ class DjangoBlogTest(TestCase):
[ddd](http://www.baidu.com)
''')
''') # 吴铠涛测试Markdown转换
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)
data = parse_dict_to_url(d) # 吴铠涛测试字典转URL参数
self.assertIsNotNone(data)

@ -27,6 +27,7 @@ from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# 吴铠涛:站点地图配置
sitemaps = {
'blog': ArticleSiteMap,
@ -36,29 +37,33 @@ sitemaps = {
'static': StaticViewSitemap
}
# 吴铠涛:错误处理视图
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
# 吴铠涛URL模式配置
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
path('i18n/', include('django.conf.urls.i18n')), # 吴铠涛国际化URL
]
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'^admin/', admin_site.urls), # 吴铠涛管理后台URL
re_path(r'', include('blog.urls', namespace='blog')), # 吴铠涛博客URL
re_path(r'mdeditor/', include('mdeditor.urls')), # 吴铠涛Markdown编辑器URL
re_path(r'', include('comments.urls', namespace='comment')), # 吴铠涛评论URL
re_path(r'', include('accounts.urls', namespace='account')), # 吴铠涛账户URL
re_path(r'', include('oauth.urls', namespace='oauth')), # 吴铠涛OAuth URL
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
name='django.contrib.sitemaps.views.sitemap'), # 吴铠涛站点地图URL
re_path(r'^feed/$', DjangoBlogFeed()), # 吴铠涛RSS订阅URL
re_path(r'^rss/$', DjangoBlogFeed()), # 吴铠涛RSS订阅URL别名
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)
name='search'), # 吴铠涛搜索URL
re_path(r'', include('servermanager.urls', namespace='servermanager')), # 吴铠涛服务器管理URL
re_path(r'', include('owntracks.urls', namespace='owntracks')) # 吴铠涛位置跟踪URL
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # 吴铠涛静态文件URL
# 吴铠涛调试模式下的媒体文件URL
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
document_root=settings.MEDIA_ROOT)

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
@ -19,18 +18,18 @@ from django.templatetags.static import static
logger = logging.getLogger(__name__)
# 吴铠涛获取最大文章ID和评论ID
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
# 吴铠涛SHA256加密函数
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):
@ -66,7 +65,7 @@ def cache_decorator(expiration=3 * 60):
return wrapper
# 吴铠涛:刷新视图缓存
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
@ -91,13 +90,13 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
return True
return False
# 吴铠涛:获取当前站点(带缓存)
@cache_decorator()
def get_current_site():
site = Site.objects.get_current()
return site
# 吴铠涛通用Markdown处理类
class CommonMarkdown:
@staticmethod
def _convert_markdown(value):
@ -123,7 +122,7 @@ class CommonMarkdown:
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(
@ -132,19 +131,19 @@ def send_email(emailto, title, content):
title=title,
content=content)
# 吴铠涛:生成随机验证码
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
# 吴铠涛字典转URL参数
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:
@ -172,7 +171,7 @@ def get_blog_setting():
cache.set('get_blog_setting', value)
return value
# 吴铠涛:保存用户头像
def save_user_avatar(url):
'''
保存用户头像
@ -200,7 +199,7 @@ def save_user_avatar(url):
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]
@ -208,13 +207,13 @@ def delete_sidebar_cache():
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)
# 吴铠涛获取资源URL
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
@ -222,11 +221,11 @@ def get_resource_url():
site = get_current_site()
return 'http://' + site.domain + '/static/'
# 吴铠涛允许的HTML标签和属性
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']}
# 吴铠涛HTML清理函数
def sanitize_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

@ -13,4 +13,4 @@ from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
application = get_wsgi_application()
application = get_wsgi_application() # 吴铠涛WSGI应用入口点

@ -0,0 +1,205 @@
import logging
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
from blog.models import Article
logger = logging.getLogger(__name__)
class ArticleRecommendationPlugin(BasePlugin):
PLUGIN_NAME = '文章推荐'
PLUGIN_DESCRIPTION = '智能文章推荐系统,支持多位置展示'
PLUGIN_VERSION = '1.0.0'
PLUGIN_AUTHOR = 'liangliangyy'
# 支持的位置
SUPPORTED_POSITIONS = ['article_bottom']
# 各位置优先级
POSITION_PRIORITIES = {
'article_bottom': 80, # 文章底部优先级
}
# 插件配置
CONFIG = {
'article_bottom_count': 8, # 文章底部推荐数量
'sidebar_count': 5, # 侧边栏推荐数量
'enable_category_fallback': True, # 启用分类回退
'enable_popular_fallback': True, # 启用热门文章回退
}
def register_hooks(self):
"""注册钩子"""
hooks.register(ARTICLE_DETAIL_LOAD, self.on_article_detail_load)
def on_article_detail_load(self, article, context, request, *args, **kwargs):
"""文章详情页加载时的处理"""
# 可以在这里预加载推荐数据到context中
recommendations = self.get_recommendations(article)
context['article_recommendations'] = recommendations
def should_display(self, position, context, **kwargs):
"""条件显示逻辑"""
# 只在文章详情页底部显示
if position == 'article_bottom':
article = kwargs.get('article') or context.get('article')
# 检查是否有文章对象,以及是否不是索引页面
is_index = context.get('isindex', False) if hasattr(context, 'get') else False
return article is not None and not is_index
return False
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部推荐"""
article = kwargs.get('article') or context.get('article')
if not article:
return None
# 使用配置的数量也可以通过kwargs覆盖
count = kwargs.get('count', self.CONFIG['article_bottom_count'])
recommendations = self.get_recommendations(article, count=count)
if not recommendations:
return None
# 将RequestContext转换为普通字典
context_dict = {}
if hasattr(context, 'flatten'):
context_dict = context.flatten()
elif hasattr(context, 'dicts'):
# 合并所有上下文字典
for d in context.dicts:
context_dict.update(d)
template_context = {
'recommendations': recommendations,
'article': article,
'title': '相关推荐',
**context_dict
}
return self.render_template('bottom_widget.html', template_context)
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏推荐"""
article = context.get('article')
# 使用配置的数量也可以通过kwargs覆盖
count = kwargs.get('count', self.CONFIG['sidebar_count'])
if article:
# 文章页面,显示相关文章
recommendations = self.get_recommendations(article, count=count)
title = '相关文章'
else:
# 其他页面,显示热门文章
recommendations = self.get_popular_articles(count=count)
title = '热门推荐'
if not recommendations:
return None
# 将RequestContext转换为普通字典
context_dict = {}
if hasattr(context, 'flatten'):
context_dict = context.flatten()
elif hasattr(context, 'dicts'):
# 合并所有上下文字典
for d in context.dicts:
context_dict.update(d)
template_context = {
'recommendations': recommendations,
'title': title,
**context_dict
}
return self.render_template('sidebar_widget.html', template_context)
def get_css_files(self):
"""返回CSS文件"""
return ['css/recommendation.css']
def get_js_files(self):
"""返回JS文件"""
return ['js/recommendation.js']
def get_recommendations(self, article, count=5):
"""获取推荐文章"""
if not article:
return []
recommendations = []
# 1. 基于标签的推荐
if article.tags.exists():
tag_ids = list(article.tags.values_list('id', flat=True))
tag_based = list(Article.objects.filter(
status='p',
tags__id__in=tag_ids
).exclude(
id=article.id
).exclude(
title__isnull=True
).exclude(
title__exact=''
).distinct().order_by('-views')[:count])
recommendations.extend(tag_based)
# 2. 如果数量不够,基于分类推荐
if len(recommendations) < count and self.CONFIG['enable_category_fallback']:
needed = count - len(recommendations)
existing_ids = [r.id for r in recommendations] + [article.id]
category_based = list(Article.objects.filter(
status='p',
category=article.category
).exclude(
id__in=existing_ids
).exclude(
title__isnull=True
).exclude(
title__exact=''
).order_by('-views')[:needed])
recommendations.extend(category_based)
# 3. 如果还是不够,推荐热门文章
if len(recommendations) < count and self.CONFIG['enable_popular_fallback']:
needed = count - len(recommendations)
existing_ids = [r.id for r in recommendations] + [article.id]
popular_articles = list(Article.objects.filter(
status='p'
).exclude(
id__in=existing_ids
).exclude(
title__isnull=True
).exclude(
title__exact=''
).order_by('-views')[:needed])
recommendations.extend(popular_articles)
# 过滤掉无效的推荐
valid_recommendations = []
for rec in recommendations:
if rec.title and len(rec.title.strip()) > 0:
valid_recommendations.append(rec)
else:
logger.warning(f"过滤掉空标题文章: ID={rec.id}, 标题='{rec.title}'")
# 调试:记录推荐结果
logger.info(f"原始推荐数量: {len(recommendations)}, 有效推荐数量: {len(valid_recommendations)}")
for i, rec in enumerate(valid_recommendations):
logger.info(f"推荐 {i+1}: ID={rec.id}, 标题='{rec.title}', 长度={len(rec.title)}")
return valid_recommendations[:count]
def get_popular_articles(self, count=3):
"""获取热门文章"""
return list(Article.objects.filter(
status='p'
).order_by('-views')[:count])
# 实例化插件
plugin = ArticleRecommendationPlugin()

@ -0,0 +1 @@
# Image Lazy Loading Plugin

@ -0,0 +1,182 @@
import re
import hashlib
from urllib.parse import urlparse
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
class ImageOptimizationPlugin(BasePlugin):
PLUGIN_NAME = '图片性能优化插件'
PLUGIN_DESCRIPTION = '自动为文章中的图片添加懒加载、异步解码等性能优化属性,显著提升页面加载速度。'
PLUGIN_VERSION = '1.0.0'
PLUGIN_AUTHOR = 'liangliangyy'
def __init__(self):
# 插件配置
self.config = {
'enable_lazy_loading': True, # 启用懒加载
'enable_async_decoding': True, # 启用异步解码
'add_loading_placeholder': True, # 添加加载占位符
'optimize_external_images': True, # 优化外部图片
'add_responsive_attributes': True, # 添加响应式属性
'skip_first_image': True, # 跳过第一张图片LCP优化
}
super().__init__()
def register_hooks(self):
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.optimize_images)
def optimize_images(self, content, *args, **kwargs):
"""
优化文章中的图片标签
"""
if not content:
return content
# 正则表达式匹配 img 标签
img_pattern = re.compile(
r'<img\s+([^>]*?)(?:\s*/)?>',
re.IGNORECASE | re.DOTALL
)
image_count = 0
def replace_img_tag(match):
nonlocal image_count
image_count += 1
# 获取原始属性
original_attrs = match.group(1)
# 解析现有属性
attrs = self._parse_img_attributes(original_attrs)
# 应用优化
optimized_attrs = self._apply_optimizations(attrs, image_count)
# 重构 img 标签
return self._build_img_tag(optimized_attrs)
# 替换所有 img 标签
optimized_content = img_pattern.sub(replace_img_tag, content)
return optimized_content
def _parse_img_attributes(self, attr_string):
"""
解析 img 标签的属性
"""
attrs = {}
# 正则表达式匹配属性
attr_pattern = re.compile(r'(\w+)=(["\'])(.*?)\2')
for match in attr_pattern.finditer(attr_string):
attr_name = match.group(1).lower()
attr_value = match.group(3)
attrs[attr_name] = attr_value
return attrs
def _apply_optimizations(self, attrs, image_index):
"""
应用各种图片优化
"""
# 1. 懒加载优化跳过第一张图片以优化LCP
if self.config['enable_lazy_loading']:
if not (self.config['skip_first_image'] and image_index == 1):
if 'loading' not in attrs:
attrs['loading'] = 'lazy'
# 2. 异步解码
if self.config['enable_async_decoding']:
if 'decoding' not in attrs:
attrs['decoding'] = 'async'
# 3. 添加样式优化
current_style = attrs.get('style', '')
# 确保图片不会超出容器
if 'max-width' not in current_style:
if current_style and not current_style.endswith(';'):
current_style += ';'
current_style += 'max-width:100%;height:auto;'
attrs['style'] = current_style
# 4. 添加 alt 属性SEO和可访问性
if 'alt' not in attrs:
# 尝试从图片URL生成有意义的alt文本
src = attrs.get('src', '')
if src:
# 从文件名生成alt文本
filename = src.split('/')[-1].split('.')[0]
# 移除常见的无意义字符
clean_name = re.sub(r'[0-9a-f]{8,}', '', filename) # 移除长hash
clean_name = re.sub(r'[_-]+', ' ', clean_name).strip()
attrs['alt'] = clean_name if clean_name else '文章图片'
else:
attrs['alt'] = '文章图片'
# 5. 外部图片优化
if self.config['optimize_external_images'] and 'src' in attrs:
src = attrs['src']
parsed_url = urlparse(src)
# 如果是外部图片,添加 referrerpolicy
if parsed_url.netloc and parsed_url.netloc != self._get_current_domain():
attrs['referrerpolicy'] = 'no-referrer-when-downgrade'
# 为外部图片添加crossorigin属性以支持性能监控
if 'crossorigin' not in attrs:
attrs['crossorigin'] = 'anonymous'
# 6. 响应式图片属性(如果配置启用)
if self.config['add_responsive_attributes']:
# 添加 sizes 属性(如果没有的话)
if 'sizes' not in attrs and 'srcset' not in attrs:
attrs['sizes'] = '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
# 7. 添加图片唯一标识符用于性能追踪
if 'data-img-id' not in attrs and 'src' in attrs:
img_hash = hashlib.md5(attrs['src'].encode()).hexdigest()[:8]
attrs['data-img-id'] = f'img-{img_hash}'
# 8. 为第一张图片添加高优先级提示LCP优化
if image_index == 1 and self.config['skip_first_image']:
attrs['fetchpriority'] = 'high'
# 移除懒加载以确保快速加载
if 'loading' in attrs:
del attrs['loading']
return attrs
def _build_img_tag(self, attrs):
"""
重新构建 img 标签
"""
attr_strings = []
# 确保 src 属性在最前面
if 'src' in attrs:
attr_strings.append(f'src="{attrs["src"]}"')
# 添加其他属性
for key, value in attrs.items():
if key != 'src': # src 已经添加过了
attr_strings.append(f'{key}="{value}"')
return f'<img {" ".join(attr_strings)}>'
def _get_current_domain(self):
"""
获取当前网站域名
"""
try:
from djangoblog.utils import get_current_site
return get_current_site().domain
except:
return ''
# 实例化插件
plugin = ImageOptimizationPlugin()

@ -1,18 +1,41 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load static %}
{% block header %}
{% endblock %}
{% block content %}
<div id="primary" class="site-content">
<div id="content" role="main">
<!-- 吴铠涛:文章点赞按钮 -->
<div class="article-likes" style="text-align: center; margin: 20px 0;">
{% if user.is_authenticated %}
<button class="like-btn {% if user in article.liked_users.all %}liked{% endif %}"
data-article-id="{{ article.id }}"
style="background: none; border: 2px solid #e74c3c; color: #e74c3c; padding: 10px 25px; border-radius: 25px; cursor: pointer; font-size: 16px; transition: all 0.3s ease;">
<i class="fa fa-heart" style="margin-right: 8px;"></i>
<span class="like-count">{{ article.likes }}</span>
<span>点赞</span>
</button>
{% else %}
<div class="login-required" style="background: #f8f9fa; border: 1px dashed #95a5a6; padding: 10px 25px; border-radius: 25px; color: #95a5a6; display: inline-block;">
<i class="fa fa-heart" style="margin-right: 8px;"></i>
<span class="like-count">{{ article.likes }}</span>
<span>点赞</span>
<div style="font-size: 12px; margin-top: 5px;">
<a href="{% url 'account:login' %}?next={{ request.get_full_path }}" style="color: #e74c3c;">登录后即可点赞</a>
</div>
</div>
{% endif %}
</div>
{% load_article_detail article False user %}
{% if article.type == 'a' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
{% if next_article %}
<span class="nav-previous"><a href="{{ next_article.get_absolute_url }}" rel="prev"><span
class="meta-nav">&larr;</span> {{ next_article.title }}</a></span>
{% endif %}
@ -26,8 +49,6 @@
</div><!-- #content -->
{% if article.comment_status == "o" and OPEN_SITE_COMMENT %}
{% include 'comments/tags/comment_list.html' %}
{% if user.is_authenticated %}
{% include 'comments/tags/post_comment.html' %}
@ -36,17 +57,142 @@
<h3 class="comment-meta">您还没有登录,请您<a
href="{% url "account:login" %}?next={{ request.get_full_path }}" rel="nofollow">登录</a>后发表评论。
</h3>
{% load oauth_tags %}
{% load_oauth_applications request %}
</div>
{% endif %}
{% endif %}
</div><!-- #primary -->
{% endblock %}
{% block sidebar %}
{% load_sidebar user "p" %}
{% endblock %}
{% block extra_js %}
<script>
// 文章点赞处理
document.querySelectorAll('.like-btn').forEach(btn => {
btn.addEventListener('click', function() {
const articleId = this.dataset.articleId;
likeArticle(articleId, this);
});
});
// 评论点赞处理 (使用事件委托)
document.addEventListener('click', function(e) {
if (e.target.closest('.like-comment-btn')) {
const btn = e.target.closest('.like-comment-btn');
const commentId = btn.dataset.commentId;
likeComment(commentId, btn);
}
});
function likeArticle(articleId, element) {
fetch(`/article/${articleId}/like/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
const likeCount = element.querySelector('.like-count');
likeCount.textContent = data.likes;
if (data.liked) {
element.classList.add('liked');
element.style.background = '#e74c3c';
element.style.color = 'white';
} else {
element.classList.remove('liked');
element.style.background = 'none';
element.style.color = '#e74c3c';
}
}
})
.catch(error => console.error('Error:', error));
}
function likeComment(commentId, element) {
fetch(`/comment/${commentId}/like/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
const likeCount = element.querySelector('.like-count');
likeCount.textContent = data.likes;
if (data.liked) {
element.classList.add('liked');
element.style.background = '#e74c3c';
element.style.color = 'white';
} else {
element.classList.remove('liked');
element.style.background = 'none';
element.style.color = '#e74c3c';
}
}
})
.catch(error => console.error('Error:', error));
}
function getCsrfToken() {
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]');
return csrfToken ? csrfToken.value : '';
}
</script>
<style>
.like-btn, .like-comment-btn {
background: none;
border: 1px solid #e74c3c;
color: #e74c3c;
padding: 4px 12px;
border-radius: 15px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
margin-right: 10px;
}
.like-btn:hover, .like-comment-btn:hover {
background: #e74c3c;
color: white;
}
.like-btn.liked, .like-comment-btn.liked {
background: #e74c3c;
color: white;
}
/* 未登录状态样式 */
.login-required, .login-required-comment {
opacity: 0.8;
}
.login-required a, .login-required-comment a {
text-decoration: none;
}
.login-required a:hover, .login-required-comment a:hover {
text-decoration: underline;
}
.like-count {
margin-left: 5px;
font-weight: bold;
}
.comment-actions {
margin-top: 8px;
display: flex;
align-items: center;
}
</style>
{% endblock %}

@ -32,6 +32,13 @@
</span>
</a>
{% endif %}
<!-- 吴铠涛:添加点赞显示 -->
<span class="likes-count" style="margin-left: 15px;">
<i class="fa fa-heart" style="color: #e74c3c; margin-right: 5px;"></i>
<span class="like-count">{{ article.likes }}</span>
</span>
<div style="float:right">
{{ article.views }} views
</div>

@ -1,12 +1,10 @@
{% load i18n %}
{% load blog_tags %}
<footer class="entry-meta">
{% trans 'posted in' %}
<a href="{{ article.category.get_absolute_url }}" rel="category tag">{{ article.category.name }}</a>
</a>
{% if article.type == 'a' %}
{% if article.tags.all %}
@ -19,7 +17,6 @@
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
.{% trans 'by ' %}
@ -39,9 +36,8 @@
</span>
</a>
</span>
{% trans 'on' %}
{% trans 'on' %}
<a href="{{ article.get_absolute_url }}"
title="{% datetimeformat article.pub_time %}"
itemprop="datePublished" content="{% datetimeformat article.pub_time %}"
@ -54,6 +50,28 @@
<a href="{{ article.get_admin_url }}">{% trans 'edit' %}</a>
{% endif %}
</span>
</footer><!-- .entry-meta -->
<!-- 吴铠涛:添加点赞按钮(仅在文章详情页显示) -->
{% if not isindex %}
<div class="article-likes" style="margin-top: 15px; text-align: center;">
{% if user.is_authenticated %}
<button class="like-btn {% if user in article.liked_users.all %}liked{% endif %}"
data-article-id="{{ article.id }}"
style="background: none; border: 2px solid #e74c3c; color: #e74c3c; padding: 8px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.3s ease;">
<i class="fa fa-heart" style="margin-right: 5px;"></i>
<span class="like-count">{{ article.likes }}</span>
<span>{% trans 'Like' %}</span>
</button>
{% else %}
<div class="login-required" style="background: #f8f9fa; border: 1px dashed #95a5a6; padding: 8px 20px; border-radius: 20px; color: #95a5a6; display: inline-block;">
<i class="fa fa-heart" style="margin-right: 5px;"></i>
<span class="like-count">{{ article.likes }}</span>
<span>{% trans 'Like' %}</span>
<div style="font-size: 11px; margin-top: 3px;">
<a href="{% url 'account:login' %}?next={{ request.get_full_path }}" style="color: #e74c3c;">{% trans 'Login to like' %}</a>
</div>
</div>
{% endif %}
</div>
{% endif %}
</footer><!-- .entry-meta -->

@ -17,7 +17,6 @@
class="url">{{ comment_item.author.username }}
</a>
</cite>
</div>
<div class="comment-meta commentmetadata">
@ -25,10 +24,32 @@
<div>回复给:@{{ comment_item.author.parent_comment.username }}</div>
</div>
<p>{{ comment_item.body|escape|comment_markdown }}</p>
<div class="reply"><a rel="nofollow" class="comment-reply-link"
href="javascript:void(0)"
onclick="do_reply({{ comment_item.pk }})"
aria-label="回复给{{ comment_item.author.username }}">回复</a></div>
</div>
<!-- 吴铠涛:评论点赞按钮 -->
<div class="comment-actions">
{% if user.is_authenticated %}
<button class="like-comment-btn {% if user in comment_item.liked_users.all %}liked{% endif %}"
data-comment-id="{{ comment_item.id }}"
style="background: none; border: 1px solid #e74c3c; color: #e74c3c; padding: 4px 12px; border-radius: 15px; cursor: pointer; font-size: 14px; transition: all 0.3s ease; margin-right: 10px;">
<i class="fa fa-heart"></i>
<span class="like-count">{{ comment_item.likes }}</span>
</button>
{% else %}
<div class="login-required-comment" style="background: #f8f9fa; border: 1px dashed #95a5a6; padding: 4px 12px; border-radius: 15px; color: #95a5a6; display: inline-block; margin-right: 10px;">
<i class="fa fa-heart"></i>
<span class="like-count">{{ comment_item.likes }}</span>
<div style="font-size: 10px; margin-top: 2px;">
<a href="{% url 'account:login' %}?next={{ request.get_full_path }}" style="color: #e74c3c;">登录</a>
</div>
</div>
{% endif %}
<div class="reply">
<a rel="nofollow" class="comment-reply-link"
href="javascript:void(0)"
onclick="do_reply({{ comment_item.pk }})"
aria-label="回复给{{ comment_item.author.username }}">回复</a>
</div>
</div>
</div>
</li><!-- #comment-## -->

@ -18,7 +18,6 @@
class="url">{{ comment_item.author.username }}
</a>
</cite>
</div>
<div class="comment-meta commentmetadata">
@ -34,9 +33,31 @@
<p>{{ comment_item.body|escape|comment_markdown }}</p>
<div class="reply"><a rel="nofollow" class="comment-reply-link"
href="javascript:void(0)" data-pk="{{ comment_item.pk }}"
aria-label="回复给{{ comment_item.author.username }}">回复</a></div>
<!-- 吴铠涛:评论点赞按钮 -->
<div class="comment-actions">
{% if user.is_authenticated %}
<button class="like-comment-btn {% if user in comment_item.liked_users.all %}liked{% endif %}"
data-comment-id="{{ comment_item.id }}"
style="background: none; border: 1px solid #e74c3c; color: #e74c3c; padding: 4px 12px; border-radius: 15px; cursor: pointer; font-size: 14px; transition: all 0.3s ease; margin-right: 10px;">
<i class="fa fa-heart"></i>
<span class="like-count">{{ comment_item.likes }}</span>
</button>
{% else %}
<div class="login-required-comment" style="background: #f8f9fa; border: 1px dashed #95a5a6; padding: 4px 12px; border-radius: 15px; color: #95a5a6; display: inline-block; margin-right: 10px;">
<i class="fa fa-heart"></i>
<span class="like-count">{{ comment_item.likes }}</span>
<div style="font-size: 10px; margin-top: 2px;">
<a href="{% url 'account:login' %}?next={{ request.get_full_path }}" style="color: #e74c3c;">登录</a>
</div>
</div>
{% endif %}
<div class="reply">
<a rel="nofollow" class="comment-reply-link"
href="javascript:void(0)" data-pk="{{ comment_item.pk }}"
aria-label="回复给{{ comment_item.author.username }}">回复</a>
</div>
</div>
</div>
</li><!-- #comment-## -->

@ -0,0 +1,23 @@
{% load i18n %}
<div class="article-recommendations">
<h3 class="recommendations-title">
<span class="recommendations-icon">📖</span>{{ title }}
</h3>
<div class="recommendations-grid">
{% for article in recommendations %}
{% if article.title and article.title|length > 0 %}
<div class="recommendation-card">
<a href="{{ article.get_absolute_url }}" class="recommendation-link" title="{{ article.title }}">
<div class="recommendation-title">{{ article.title|truncatechars:45 }}</div>
<div class="recommendation-meta">
{% if article.category %}
<span class="recommendation-category">{{ article.category.name }}</span>
{% endif %}
<span class="recommendation-date">{{ article.pub_time|date:"m-d" }}</span>
</div>
</a>
</div>
{% endif %}
{% endfor %}
</div>
</div>

@ -0,0 +1,17 @@
{% load i18n %}
<aside class="widget widget_recommendations">
<p class="widget-title">{{ title }}</p>
<ul class="recommendations-list">
{% for article in recommendations %}
<li class="recommendation-item">
<a href="{{ article.get_absolute_url }}" title="{{ article.title }}">
{{ article.title|truncatechars:35 }}
</a>
<div class="recommendation-meta">
<span class="recommendation-views">{{ article.views }} {% trans 'views' %}</span>
<span class="recommendation-date">{{ article.pub_time|date:"m-d" }}</span>
</div>
</li>
{% endfor %}
</ul>
</aside>

@ -0,0 +1,4 @@
{% comment %}插件CSS文件包含模板 - 用于压缩{% endcomment %}
{% for css_file in css_files %}
<link rel="stylesheet" href="{{ css_file }}" type="text/css">
{% endfor %}

@ -0,0 +1,4 @@
{% comment %}插件JS文件包含模板 - 用于压缩{% endcomment %}
{% for js_file in js_files %}
<script src="{{ js_file }}"></script>
{% endfor %}

@ -105,6 +105,83 @@
{% include 'share_layout/footer.html' %}
</div><!-- #page -->
<script>
// 文章点赞处理
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.like-btn').forEach(btn => {
btn.addEventListener('click', function() {
const articleId = this.dataset.articleId;
likeArticle(articleId, this);
});
});
});
// 评论点赞处理 (使用事件委托)
document.addEventListener('click', function(e) {
if (e.target.closest('.like-comment-btn')) {
const btn = e.target.closest('.like-comment-btn');
const commentId = btn.dataset.commentId;
likeComment(commentId, btn);
}
});
function likeArticle(articleId, element) {
fetch(`/article/${articleId}/like/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
const likeCount = element.querySelector('.like-count');
likeCount.textContent = data.likes;
// 更新所有相同文章的点赞计数
document.querySelectorAll(`[data-article-id="${articleId}"] .like-count`).forEach(count => {
count.textContent = data.likes;
});
if (data.liked) {
element.classList.add('liked');
} else {
element.classList.remove('liked');
}
}
})
.catch(error => console.error('Error:', error));
}
function likeComment(commentId, element) {
fetch(`/comment/${commentId}/like/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
const likeCount = element.querySelector('.like-count');
likeCount.textContent = data.likes;
if (data.liked) {
element.classList.add('liked');
} else {
element.classList.remove('liked');
}
}
})
.catch(error => console.error('Error:', error));
}
function getCsrfToken() {
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]');
return csrfToken ? csrfToken.value : '';
}
</script>
</body>
<footer>
<script src="//cdn.bootcss.com/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>

Loading…
Cancel
Save