Compare commits

..

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

8
.idea/.gitignore vendored

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/src" />
<option name="settingsModule" value="settings.py" />
<option name="manageScript" value="$MODULE_DIR$/src/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (DjangoBlog)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/src/templates" />
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="py.test" />
</component>
</module>

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (DjangoBlog)" project-jdk-type="Python SDK" />
</project>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/DjangoBlog.iml" filepath="$PROJECT_DIR$/.idea/DjangoBlog.iml" />
</modules>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

@ -0,0 +1,2 @@
# software_DjangoBlog

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1 +0,0 @@
Subproject commit ef67f8db4fafce7e84c0e7bae23d5c5bf8869fac

@ -0,0 +1,10 @@
[run]
source = .
include = *.py
omit =
*migrations*
*tests*
*.html
*whoosh_cn_backend*
*settings.py*
*venv*

@ -8,5 +8,4 @@ settings_production.py
*.md
docs/
logs/
static/
.github/
static/

@ -0,0 +1,18 @@
<!--
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
-->
**我确定我已经查看了** (标注`[ ]`为`[x]`)
- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
----
**我要申请** (标注`[ ]`为`[x]`)
- [ ] BUG 反馈
- [ ] 添加新的特性或者功能
- [ ] 请求技术支持

@ -0,0 +1,47 @@
name: "CodeQL"
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
schedule:
- cron: '30 1 * * 0'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

@ -0,0 +1,136 @@
name: Django CI
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
jobs:
build-normal:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
run: |
python manage.py makemigrations
python manage.py migrate
python manage.py test
build-with-es:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: ["3.10","3.11" ]
steps:
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
- name: Configure sysctl limits
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- uses: miyataka/elasticsearch-github-actions@1
with:
stack-version: '7.12.1'
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
run: |
python manage.py makemigrations
python manage.py migrate
coverage run manage.py test
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: false
tags: djangoblog/djangoblog:dev

@ -0,0 +1,43 @@
name: docker
on:
push:
paths-ignore:
- '**/*.md'
- '**/*.yml'
branches:
- 'master'
- 'dev'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set env to docker dev tag
if: endsWith(github.ref, '/dev')
run: |
echo "DOCKER_TAG=test" >> $GITHUB_ENV
- name: Set env to docker latest tag
if: endsWith(github.ref, '/master')
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}

@ -0,0 +1,39 @@
name: publish release
on:
release:
types: [ published ]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: name/app
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
linux/arm/v6
linux/386
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}

@ -62,6 +62,7 @@ target/
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
static/
# virtualenv
venv/
@ -77,4 +78,3 @@ uploads/
settings_production.py
werobot_session.db
bin/datas/
myenv/

@ -0,0 +1,59 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',)
def clean_password2(self):
# 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.save()
return user
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)

@ -0,0 +1,35 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'

@ -0,0 +1,26 @@
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):
return user
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -1,7 +1,3 @@
# 模块说明:
# 该模块定义了与用户账户相关的视图,包括注册、登录、注销、忘记密码等功能。
# 使用 Django 的类视图和表单视图来处理用户请求。
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
@ -32,24 +28,23 @@ from .models import BlogUser
logger = logging.getLogger(__name__)
# 注册视图
# Create your views here.
class RegisterView(FormView):
# 使用注册表单和模板
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
# 确保请求受 CSRF 保护
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
# 表单验证通过后处理注册逻辑
if form.is_valid():
user = form.save(False) # 创建用户但不保存到数据库
user.is_active = False # 设置用户为未激活状态
user.source = 'Register' # 标记用户来源
user.save(True) # 保存用户到数据库
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)))
@ -75,74 +70,82 @@ class RegisterView(FormView):
],
title='验证您的电子邮箱',
content=content)
# 重定向到结果页面
url = reverse('accounts:result') + '?type=register&id=' + str(user.id)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# 如果表单无效,重新渲染表单
return self.render_to_response({'form': form})
return self.render_to_response({
'form': form
})
# 注销视图
class LogoutView(RedirectView):
url = '/login/' # 注销后重定向到登录页面
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
# 确保注销后页面不被缓存
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request) # 执行注销操作
delete_sidebar_cache() # 清除侧边栏缓存
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
# 登录视图
class LoginView(FormView):
# 使用登录表单和模板
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/' # 登录成功后重定向的默认地址
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):
# 确保密码字段敏感、请求受 CSRF 保护且页面不被缓存
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
# 获取上下文数据,添加重定向地址
redirect_to = self.request.GET.get(self.redirect_field_name, '/')
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
# 表单验证通过后处理登录逻辑
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache() # 清除侧边栏缓存
auth.login(self.request, form.get_user()) # 执行登录操作
delete_sidebar_cache()
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl) # 设置会话过期时间
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})
return self.render_to_response({
'form': form
})
def get_success_url(self):
# 获取登录成功后的重定向地址
redirect_to = self.request.POST.get(self.redirect_field_name, self.success_url)
if not url_has_allowed_host_and_scheme(url=redirect_to, allowed_hosts=[self.request.get_host()]):
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
# 账户结果视图
def account_result(request):
# 处理注册或验证结果
type = request.GET.get('type')
id = request.GET.get('id')
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active:
@ -157,8 +160,8 @@ def account_result(request):
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden() # 验证失败返回 403
user.is_active = True # 激活用户
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
@ -171,30 +174,31 @@ def account_result(request):
else:
return HttpResponseRedirect('/')
# 忘记密码视图
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
# 表单验证通过后处理密码重置逻辑
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"]) # 设置新密码
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/') # 重定向到登录页面
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
# 忘记密码验证码视图
class ForgetPasswordEmailCode(View):
def post(self, request: HttpRequest):
# 处理验证码发送请求
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
code = generate_code() # 生成验证码
utils.send_verify_email(to_email, code) # 发送验证码邮件
utils.set_code(to_email, code) # 缓存验证码
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok")

@ -5,55 +5,44 @@ from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# 导入相关模型
from .models import Article, Category, Tag, Links, SideBar, BlogSettings
# Register your models here.
from .models import Article
# 自定义表单,用于文章模型的管理界面
class ArticleForm(forms.ModelForm):
# 可以在此处自定义字段的表单小部件
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
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')
# 使用自定义表单
form = ArticleForm
# 列表显示的字段
list_display = (
'id',
'title',
@ -64,28 +53,17 @@ class ArticlelAdmin(admin.ModelAdmin):
'status',
'type',
'article_order')
# 可点击进入详情的字段
list_display_links = ('id', 'title')
# 列表过滤器
list_filter = ('status', 'type', 'category')
# 按日期层级显示
date_hierarchy = 'creation_time'
# 多对多字段的水平过滤器
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]
# 使用原始 ID 字段显示外键
raw_id_fields = ('author', 'category',)
# 自定义显示分类链接
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,))
@ -93,59 +71,42 @@ 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(
).objects.filter(is_superuser=True)
return form
# 保存模型时的自定义逻辑
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
url = obj.get_full_url()
return url
else:
# 否则返回当前站点的域名
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
# 自定义标签管理界面
class TagAdmin(admin.ModelAdmin):
# 排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# 自定义分类管理界面
class CategoryAdmin(admin.ModelAdmin):
# 列表显示的字段
list_display = ('name', 'parent_category', 'index')
# 排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# 自定义链接管理界面
class LinksAdmin(admin.ModelAdmin):
# 排除的字段
exclude = ('last_mod_time', 'creation_time')
# 自定义侧边栏管理界面
class SideBarAdmin(admin.ModelAdmin):
# 列表显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# 排除的字段
exclude = ('last_mod_time', 'creation_time')
# 自定义博客设置管理界面
class BlogSettingsAdmin(admin.ModelAdmin):
pass

@ -7,11 +7,9 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
# 检查是否启用了 Elasticsearch
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
# 创建 Elasticsearch 连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
@ -21,10 +19,8 @@ if ELASTICSEARCH_ENABLED:
c = IngestClient(es)
try:
# 检查是否存在名为 'geoip' 的 pipeline
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
# 如果不存在,则创建 'geoip' pipeline
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
@ -37,7 +33,6 @@ if ELASTICSEARCH_ENABLED:
}''')
# 定义 GeoIP 信息的内部文档
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
@ -45,25 +40,21 @@ class GeoIp(InnerDoc):
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)
@ -72,7 +63,6 @@ class UserAgent(InnerDoc):
is_bot = Boolean()
# 定义性能日志的 Elasticsearch 文档
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
@ -82,7 +72,6 @@ class ElapsedTimeDocument(Document):
useragent = Object(UserAgent, required=False)
class Index:
# 定义索引名称和设置
name = 'performance'
settings = {
"number_of_shards": 1,
@ -93,11 +82,9 @@ class ElapsedTimeDocument(Document):
doc_type = 'ElapsedTime'
# 定义性能日志文档的管理器
class ElaspedTimeDocumentManager:
@staticmethod
def build_index():
# 创建 Elasticsearch 索引
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
@ -106,14 +93,12 @@ class ElaspedTimeDocumentManager:
@staticmethod
def delete_index():
# 删除 Elasticsearch 索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
# 创建性能日志文档
ElaspedTimeDocumentManager.build_index()
ua = UserAgent()
ua.browser = UserAgentBrowser()
@ -145,7 +130,6 @@ class ElaspedTimeDocumentManager:
doc.save(pipeline="geoip")
# 定义文章的 Elasticsearch 文档
class ArticleDocument(Document):
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
@ -170,7 +154,6 @@ class ArticleDocument(Document):
article_order = Integer()
class Index:
# 定义索引名称和设置
name = 'blog'
settings = {
"number_of_shards": 1,
@ -181,25 +164,20 @@ class ArticleDocument(Document):
doc_type = 'Article'
# 定义文章文档的管理器
class ArticleDocumentManager():
def __init__(self):
# 初始化时创建索引
self.create_index()
def create_index(self):
# 创建 Elasticsearch 索引
ArticleDocument.init()
def delete_index(self):
# 删除 Elasticsearch 索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
# 将文章对象转换为 Elasticsearch 文档
return [
ArticleDocument(
meta={
@ -224,7 +202,6 @@ class ArticleDocumentManager():
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
# 重建索引并重新保存文档
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
@ -232,6 +209,5 @@ class ArticleDocumentManager():
doc.save()
def update_docs(self, docs):
# 更新文档
for doc in docs:
doc.save()
doc.save()

@ -0,0 +1,42 @@
import logging
import time
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
''' page render time '''
start_time = time.time()
response = self.get_response(request)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request)
user_agent = parse(http_user_agent)
if not response.streaming:
try:
cast_time = time.time() - start_time
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response

@ -51,75 +51,7 @@ def datetimeformat(data):
@register.filter()
@stringfilter
def custom_markdown(content):
"""
通用markdown过滤器应用文章内容插件
主要用于文章内容处理
"""
html_content = CommonMarkdown.get_markdown(content)
# 然后应用插件过滤器优化HTML
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content)
return mark_safe(optimized_html)
@register.filter()
@stringfilter
def sidebar_markdown(content):
html_content = CommonMarkdown.get_markdown(content)
return mark_safe(html_content)
@register.simple_tag(takes_context=True)
def render_article_content(context, article, is_summary=False):
"""
渲染文章内容包含完整的上下文信息供插件使用
Args:
context: 模板上下文
article: 文章对象
is_summary: 是否为摘要模式首页使用
"""
if not article or not hasattr(article, 'body'):
return ''
# 先转换Markdown为HTML
html_content = CommonMarkdown.get_markdown(article.body)
# 如果是摘要模式,先截断内容再应用插件
if is_summary:
# 截断HTML内容到合适的长度约300字符
from django.utils.html import strip_tags
from django.template.defaultfilters import truncatechars
# 先去除HTML标签截断纯文本然后重新转换为HTML
plain_text = strip_tags(html_content)
truncated_text = truncatechars(plain_text, 300)
# 重新转换截断后的文本为HTML简化版避免复杂的插件处理
html_content = CommonMarkdown.get_markdown(truncated_text)
# 然后应用插件过滤器,传递完整的上下文
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
# 获取request对象
request = context.get('request')
# 应用所有文章内容相关的插件
# 注意:摘要模式下某些插件(如版权声明)可能不适用
optimized_html = hooks.apply_filters(
ARTICLE_CONTENT_HOOK_NAME,
html_content,
article=article,
request=request,
context=context,
is_summary=is_summary # 传递摘要标志,插件可以据此调整行为
)
return mark_safe(optimized_html)
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
@ -360,49 +292,38 @@ def load_article_detail(article, isindex, user):
}
# 返回用户头像URL
# 模板使用方法: {{ email|gravatar_url:150 }}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得用户头像 - 优先使用OAuth头像否则使用默认头像"""
cachekey = 'avatar/' + email
"""获得gravatar头像"""
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
# 检查OAuth用户是否有自定义头像
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
# 过滤出有头像的用户
users_with_picture = list(filter(lambda x: x.picture is not None, usermodels))
if users_with_picture:
# 获取默认头像路径用于比较
default_avatar_path = static('blog/img/avatar.png')
# 优先选择非默认头像的用户,否则选择第一个
non_default_users = [u for u in users_with_picture if u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')]
selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
url = selected_user.picture
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
avatar_type = 'non-default' if non_default_users else 'default'
logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type))
return url
# 使用默认头像
url = static('blog/img/avatar.png')
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
logger.info('Using default avatar for {}'.format(email))
return url
else:
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
email = email.encode('utf-8')
default = static('blog/img/avatar.png')
url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
cache.set(cachekey, url, 60 * 60 * 10)
logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
"""获得用户头像HTML标签"""
"""获得gravatar头像"""
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d" class="avatar" alt="用户头像">' %
'<img src="%s" height="%d" width="%d">' %
(url, size, size))
@ -421,134 +342,3 @@ def query(qs, **kwargs):
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
# === 插件系统模板标签 ===
@register.simple_tag(takes_context=True)
def render_plugin_widgets(context, position, **kwargs):
"""
渲染指定位置的所有插件组件
Args:
context: 模板上下文
position: 位置标识
**kwargs: 传递给插件的额外参数
Returns:
按优先级排序的所有插件HTML内容
"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
widgets = []
for plugin in get_loaded_plugins():
try:
widget_data = plugin.render_position_widget(
position=position,
context=context,
**kwargs
)
if widget_data:
widgets.append(widget_data)
except Exception as e:
logger.error(f"Error rendering widget from plugin {plugin.PLUGIN_NAME}: {e}")
# 按优先级排序(数字越小优先级越高)
widgets.sort(key=lambda x: x['priority'])
# 合并HTML内容
html_parts = [widget['html'] for widget in widgets]
return mark_safe(''.join(html_parts))
@register.simple_tag(takes_context=True)
def plugin_head_resources(context):
"""渲染所有插件的head资源仅自定义HTMLCSS已集成到压缩系统"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
resources = []
for plugin in get_loaded_plugins():
try:
# 只处理自定义head HTMLCSS文件已通过压缩系统处理
head_html = plugin.get_head_html(context)
if head_html:
resources.append(head_html)
except Exception as e:
logger.error(f"Error loading head resources from plugin {plugin.PLUGIN_NAME}: {e}")
return mark_safe('\n'.join(resources))
@register.simple_tag(takes_context=True)
def plugin_body_resources(context):
"""渲染所有插件的body资源仅自定义HTMLJS已集成到压缩系统"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
resources = []
for plugin in get_loaded_plugins():
try:
# 只处理自定义body HTMLJS文件已通过压缩系统处理
body_html = plugin.get_body_html(context)
if body_html:
resources.append(body_html)
except Exception as e:
logger.error(f"Error loading body resources from plugin {plugin.PLUGIN_NAME}: {e}")
return mark_safe('\n'.join(resources))
@register.inclusion_tag('plugins/css_includes.html')
def plugin_compressed_css():
"""插件CSS压缩包含模板"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
css_files = []
for plugin in get_loaded_plugins():
for css_file in plugin.get_css_files():
css_url = plugin.get_static_url(css_file)
css_files.append(css_url)
return {'css_files': css_files}
@register.inclusion_tag('plugins/js_includes.html')
def plugin_compressed_js():
"""插件JS压缩包含模板"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
js_files = []
for plugin in get_loaded_plugins():
for js_file in plugin.get_js_files():
js_url = plugin.get_static_url(js_file)
js_files.append(js_url)
return {'js_files': js_files}
@register.simple_tag(takes_context=True)
def plugin_widget(context, plugin_name, widget_type='default', **kwargs):
"""
渲染指定插件的组件
使用方式
{% plugin_widget 'article_recommendation' 'bottom' article=article count=5 %}
"""
from djangoblog.plugin_manage.loader import get_plugin_by_slug
plugin = get_plugin_by_slug(plugin_name)
if plugin and hasattr(plugin, 'render_template'):
try:
widget_context = {**context.flatten(), **kwargs}
template_name = f"{widget_type}.html"
return mark_safe(plugin.render_template(template_name, widget_context))
except Exception as e:
logger.error(f"Error rendering plugin widget {plugin_name}.{widget_type}: {e}")
return ""

@ -4,83 +4,57 @@ from django.views.decorators.cache import cache_page
from . import views
app_name = "blog"
urlpatterns = [
# 首页,显示博客文章列表
path(
r'',
views.IndexView.as_view(),
name='index'),
# 分页的首页,显示指定页码的博客文章列表
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
# 文章详情页通过年份、月份、日期和文章ID访问
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
# 分类详情页,通过分类名称访问
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
# 分类详情页的分页,通过分类名称和页码访问
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
# 作者详情页,通过作者名称访问
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
# 作者详情页的分页,通过作者名称和页码访问
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
# 标签详情页,通过标签名称访问
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
# 标签详情页的分页,通过标签名称和页码访问
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
# 归档页面显示所有文章的归档信息缓存时间为1小时
path(
'archives.html',
cache_page(
60 * 60)( # 缓存时间60分钟 * 60秒
60 * 60)(
views.ArchivesView.as_view()),
name='archives'),
# 友情链接页面
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
# 文件上传接口
path(
r'upload',
views.fileupload,
name='upload'),
# 清理缓存接口
path(
r'clean',
views.clean_cache_view,

@ -23,7 +23,7 @@ from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
# 文章列表视图基类
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
@ -38,7 +38,6 @@ class ArticleListView(ListView):
link_type = LinkShowType.L
def get_view_cache_key(self):
# 获取视图缓存的键
return self.request.get['pages']
@property
@ -62,7 +61,7 @@ class ArticleListView(ListView):
def get_queryset_from_cache(self, cache_key):
'''
缓存中获取页面数据如果不存在则设置缓存
缓存页面数据
:param cache_key: 缓存key
:return:
'''
@ -78,7 +77,7 @@ class ArticleListView(ListView):
def get_queryset(self):
'''
重写默认方法从缓存获取数据
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
@ -90,10 +89,9 @@ class ArticleListView(ListView):
return super(ArticleListView, self).get_context_data(**kwargs)
# 首页视图
class IndexView(ArticleListView):
'''
首页文章列表
首页
'''
# 友情链接类型
link_type = LinkShowType.I
@ -107,7 +105,6 @@ class IndexView(ArticleListView):
return cache_key
# 文章详情视图
class ArticleDetailView(DetailView):
'''
文章详情页面
@ -155,20 +152,18 @@ class ArticleDetailView(DetailView):
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# 触发文章详情加载钩子,让插件可以添加额外的上下文数据
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request)
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context
# 分类目录视图
class CategoryDetailView(ArticleListView):
'''
分类目录文章列表
分类目录列表
'''
page_type = "分类目录归档"
@ -205,10 +200,9 @@ class CategoryDetailView(ArticleListView):
return super(CategoryDetailView, self).get_context_data(**kwargs)
# 作者详情视图
class AuthorDetailView(ArticleListView):
'''
作者文章归档页面
作者详情页
'''
page_type = '作者文章归档'
@ -232,10 +226,9 @@ class AuthorDetailView(ArticleListView):
return super(AuthorDetailView, self).get_context_data(**kwargs)
# 标签详情视图
class TagDetailView(ArticleListView):
'''
标签文章列表页面
标签列表页面
'''
page_type = '分类标签归档'
@ -265,7 +258,6 @@ class TagDetailView(ArticleListView):
return super(TagDetailView, self).get_context_data(**kwargs)
# 文章归档视图
class ArchivesView(ArticleListView):
'''
文章归档页面
@ -283,11 +275,7 @@ class ArchivesView(ArticleListView):
return cache_key
# 友情链接视图
class LinkListView(ListView):
'''
友情链接页面
'''
model = Links
template_name = 'blog/links_list.html'
@ -295,11 +283,7 @@ class LinkListView(ListView):
return Links.objects.filter(is_enable=True)
# 搜索视图
class EsSearchView(SearchView):
'''
自定义搜索视图
'''
def get_context(self):
paginator, page = self.build_page()
context = {
@ -316,11 +300,12 @@ class EsSearchView(SearchView):
return context
# 文件上传视图
@csrf_exempt
def fileupload(request):
"""
提供图床功能的文件上传接口
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
@ -355,11 +340,10 @@ def fileupload(request):
return HttpResponse("only for post")
# 自定义错误页面视图
def page_not_found_view(request, exception, template_name='blog/error_page.html'):
'''
404 页面未找到错误处理
'''
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
url = request.get_full_path()
@ -371,9 +355,6 @@ def page_not_found_view(request, exception, template_name='blog/error_page.html'
def server_error_view(request, template_name='blog/error_page.html'):
'''
500 服务器错误处理
'''
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -381,10 +362,10 @@ def server_error_view(request, template_name='blog/error_page.html'):
status=500)
def permission_denied_view(request, exception, template_name='blog/error_page.html'):
'''
403 权限不足错误处理
'''
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
return render(
@ -393,10 +374,6 @@ def permission_denied_view(request, exception, template_name='blog/error_page.ht
'statuscode': '403'}, status=403)
# 清理缓存视图
def clean_cache_view(request):
'''
清理缓存
'''
cache.clear()
return HttpResponse('ok')

@ -0,0 +1,47 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
list_display_links = ('id', 'body', 'is_enable')
list_filter = ('is_enable',)
exclude = ('creation_time', 'last_modify_time')
actions = [disable_commentstatus, enable_commentstatus]
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
return format_html(
u'<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,))
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')

@ -1,27 +1,17 @@
import logging
from django.utils.translation import gettext_lazy as _ # 用于支持多语言翻译
from djangoblog.utils import get_current_site # 获取当前站点的工具函数
from djangoblog.utils import send_email # 发送邮件的工具函数
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__) # 初始化日志记录器,用于记录错误或调试信息
def send_comment_email(comment):
"""
功能发送评论相关的通知邮件
参数
comment: 当前的评论对象包含评论内容作者信息文章信息等
"""
# 获取当前站点的域名
site = get_current_site().domain
# 定义邮件主题
subject = _('Thanks for your comment') # 邮件主题,支持多语言
# 构造文章的绝对 URL
subject = _('Thanks for your comment')
article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 构造邮件内容HTML 格式),感谢用户的评论
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
@ -29,17 +19,10 @@ def send_comment_email(comment):
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
# 获取评论作者的邮箱
tomail = comment.author.email
# 发送邮件给评论作者
send_email([tomail], subject, html_content)
try:
# 如果当前评论是对其他评论的回复
if comment.parent_comment:
# 构造回复通知邮件内容
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
@ -49,12 +32,7 @@ def send_comment_email(comment):
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
# 获取被回复评论作者的邮箱
tomail = comment.parent_comment.author.email
# 发送邮件给被回复的评论作者
send_email([tomail], subject, html_content)
except Exception as e:
# 如果发送邮件时发生异常,记录错误日志
logger.error(e)
logger.error(e)

@ -0,0 +1,63 @@
# Create your views here.
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView
from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm
from .models import Comment
class CommentPostView(FormView):
form_class = CommentForm
template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
return self.render_to_response({
'form': form,
'article': article
})
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
comment = form.save(False)
comment.article = article
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
comment.save(True)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

@ -26,13 +26,13 @@ spec:
name: djangoblog-env
readinessProbe:
httpGet:
path: /health/
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
livenessProbe:
httpGet:
path: /health/
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30

@ -0,0 +1 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -0,0 +1,64 @@
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
super().__init__(name)
def has_permission(self, request):
return request.user.is_superuser
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
# from blog.views import refresh_memcache
#
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# return urls + my_urls
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
admin_site.register(BlogUser, BlogUserAdmin)
admin_site.register(Comment, CommentAdmin)
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)

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

Loading…
Cancel
Save