小组进行代码注释、改进,以及文档编写

master
何通 3 months ago
parent 28e2df00f5
commit 75ea0b395d

Binary file not shown.

@ -1,3 +1,4 @@
#ht: 用户管理后台配置模块自定义Django Admin的用户管理界面
from django import forms from django import forms
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm from django.contrib.auth.forms import UserChangeForm
@ -9,6 +10,7 @@ from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm): class BlogUserCreationForm(forms.ModelForm):
"""ht: 用户创建表单用于在Admin后台创建新用户"""
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput) password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput) password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
@ -17,7 +19,7 @@ class BlogUserCreationForm(forms.ModelForm):
fields = ('email',) fields = ('email',)
def clean_password2(self): def clean_password2(self):
# Check that the two password entries match """ht: 验证两次输入的密码是否一致"""
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
@ -25,16 +27,17 @@ class BlogUserCreationForm(forms.ModelForm):
return password2 return password2
def save(self, commit=True): def save(self, commit=True):
# Save the provided password in hashed format """ht: 保存用户,对密码进行哈希处理并设置来源标记"""
user = super().save(commit=False) user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"]) user.set_password(self.cleaned_data["password1"])
if commit: if commit:
user.source = 'adminsite' user.source = 'adminsite' # ht: 标记用户来源为管理员后台
user.save() user.save()
return user return user
class BlogUserChangeForm(UserChangeForm): class BlogUserChangeForm(UserChangeForm):
"""ht: 用户信息修改表单"""
class Meta: class Meta:
model = BlogUser model = BlogUser
fields = '__all__' fields = '__all__'
@ -45,8 +48,10 @@ class BlogUserChangeForm(UserChangeForm):
class BlogUserAdmin(UserAdmin): class BlogUserAdmin(UserAdmin):
"""ht: 自定义用户管理类配置Admin界面显示和搜索选项"""
form = BlogUserChangeForm form = BlogUserChangeForm
add_form = BlogUserCreationForm add_form = BlogUserCreationForm
#ht: 列表显示字段
list_display = ( list_display = (
'id', 'id',
'nickname', 'nickname',
@ -55,6 +60,6 @@ class BlogUserAdmin(UserAdmin):
'last_login', 'last_login',
'date_joined', 'date_joined',
'source') 'source')
list_display_links = ('id', 'username') list_display_links = ('id', 'username') # ht: 可点击链接的字段
ordering = ('-id',) ordering = ('-id',) # ht: 默认按ID倒序排列
search_fields = ('username', 'nickname', 'email') search_fields = ('username', 'nickname', 'email') # ht: 搜索字段

@ -1,3 +1,4 @@
#ht: 用户数据模型模块,定义自定义用户模型
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
@ -6,30 +7,32 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser): class BlogUser(AbstractUser):
"""ht: 博客用户模型扩展Django默认用户模型"""
nickname = models.CharField(_('nick name'), max_length=100, blank=True) nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True) source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self): def get_absolute_url(self):
"""ht: 获取用户详情页URL"""
return reverse( return reverse(
'blog:author_detail', kwargs={ 'blog:author_detail', kwargs={
'author_name': self.username}) 'author_name': self.username})
def __str__(self): def __str__(self):
"""ht: 字符串表示,返回邮箱"""
return self.email return self.email
def get_full_url(self): def get_full_url(self):
"""ht: 获取完整的用户URL包含域名"""
site = get_current_site().domain site = get_current_site().domain
url = "https://{site}{path}".format(site=site, url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url()) path=self.get_absolute_url())
return url return url
class Meta: class Meta:
ordering = ['-id'] ordering = ['-id'] # ht: 默认按ID倒序排列
verbose_name = _('user') verbose_name = _('user')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
get_latest_by = 'id' get_latest_by = 'id'

@ -1,3 +1,4 @@
#ht: 账户相关工具函数模块,处理验证码发送和验证
import typing import typing
from datetime import timedelta from datetime import timedelta
@ -7,15 +8,16 @@ from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email from djangoblog.utils import send_email
#ht: 验证码有效时间为5分钟
_code_ttl = timedelta(minutes=5) _code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")): def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码 """ht: 发送验证邮件
Args: Args:
to_mail: 接受邮箱 to_mail: 接收邮箱地址
subject: 邮件主题
code: 验证码 code: 验证码
subject: 邮件主题默认为"验证邮箱"
""" """
html_content = _( html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it " "You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
@ -24,26 +26,26 @@ def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email"))
def verify(email: str, code: str) -> typing.Optional[str]: def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效 """ht: 验证邮箱和验证码是否匹配
Args: Args:
email: 请求邮箱 email: 邮箱地址
code: 验证码 code: 验证码
Return: Returns:
如果有错误就返回错误str 验证成功返回None失败返回错误信息字符串
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
""" """
cache_code = get_code(email) cache_code = get_code(email)
if cache_code != code: if cache_code != code:
return gettext("Verification code error") return gettext("Verification code error")
#ht: 验证成功后清除验证码,防止重复使用
cache.delete(email)
return None
def set_code(email: str, code: str): def set_code(email: str, code: str):
"""设置code""" """ht: 将验证码存入缓存"""
cache.set(email, code, _code_ttl.seconds) cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]: def get_code(email: str) -> typing.Optional[str]:
"""获取code""" """ht: 从缓存获取验证码"""
return cache.get(email) return cache.get(email)

@ -1,3 +1,4 @@
#ht: 账户视图模块,处理用户登录、注册、注销等请求
import logging import logging
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
@ -32,6 +33,7 @@ logger = logging.getLogger(__name__)
# Create your views here. # Create your views here.
class RegisterView(FormView): class RegisterView(FormView):
"""ht: 用户注册视图"""
form_class = RegisterForm form_class = RegisterForm
template_name = 'account/registration_form.html' template_name = 'account/registration_form.html'
@ -40,20 +42,25 @@ class RegisterView(FormView):
return super(RegisterView, self).dispatch(*args, **kwargs) return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
"""ht: 表单验证通过后的处理,创建用户并发送验证邮件"""
if form.is_valid(): if form.is_valid():
user = form.save(False) user = form.save(False) # ht: 不立即保存到数据库
user.is_active = False user.is_active = False # ht: 设置用户为未激活状态
user.source = 'Register' user.source = 'Register' # ht: 标记用户来源
user.save(True) user.save(True) # ht: 保存用户到数据库
#ht: 生成验证链接
site = get_current_site().domain site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG: if settings.DEBUG:
site = '127.0.0.1:8000' site = '127.0.0.1:8000' # ht: 调试模式下使用本地地址
path = reverse('account:result') path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format( url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign) site=site, path=path, id=user.id, sign=sign)
#ht: 邮件内容模板
content = """ content = """
<p>请点击下面链接验证您的邮箱</p> <p>请点击下面链接验证您的邮箱</p>
@ -64,6 +71,7 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器 如果上面链接无法打开请将此链接复制至浏览器
{url} {url}
""".format(url=url) """.format(url=url)
#ht: 发送验证邮件
send_email( send_email(
emailto=[ emailto=[
user.email, user.email,
@ -79,7 +87,6 @@ class RegisterView(FormView):
'form': form 'form': form
}) })
class LogoutView(RedirectView): class LogoutView(RedirectView):
url = '/login/' url = '/login/'
@ -94,39 +101,33 @@ class LogoutView(RedirectView):
class LoginView(FormView): class LoginView(FormView):
"""ht: 用户登录视图"""
form_class = LoginForm form_class = LoginForm
template_name = 'account/login.html' template_name = 'account/login.html'
success_url = '/' success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间 login_ttl = 2626560 # ht: 登录会话有效期(一个月)
@method_decorator(sensitive_post_parameters('password')) @method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
@method_decorator(never_cache) @method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs) return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form): def form_valid(self, form):
"""ht: 登录表单验证处理"""
form = AuthenticationForm(data=self.request.POST, request=self.request) form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid(): if form.is_valid():
#ht: 清除侧边栏缓存并登录用户
delete_sidebar_cache() delete_sidebar_cache()
logger.info(self.redirect_field_name) logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user()) auth.login(self.request, form.get_user())
#ht: 处理"记住我"功能
if self.request.POST.get("remember"): 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 super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else: else:
return self.render_to_response({ return self.render_to_response({
'form': form 'form': form

@ -10,6 +10,7 @@ from .models import Article, Category, Tag, Links, SideBar, BlogSettings
class ArticleForm(forms.ModelForm): class ArticleForm(forms.ModelForm):
#fjw: 文章表单类,用于后台文章编辑
# body = forms.CharField(widget=AdminPagedownWidget()) # body = forms.CharField(widget=AdminPagedownWidget())
class Meta: class Meta:
@ -18,18 +19,22 @@ class ArticleForm(forms.ModelForm):
def makr_article_publish(modeladmin, request, queryset): def makr_article_publish(modeladmin, request, queryset):
#fjw: 批量发布文章
queryset.update(status='p') queryset.update(status='p')
def draft_article(modeladmin, request, queryset): def draft_article(modeladmin, request, queryset):
#fjw: 批量将文章设为草稿
queryset.update(status='d') queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset): def close_article_commentstatus(modeladmin, request, queryset):
#fjw: 批量关闭文章评论
queryset.update(comment_status='c') queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset): def open_article_commentstatus(modeladmin, request, queryset):
#fjw: 批量开启文章评论
queryset.update(comment_status='o') queryset.update(comment_status='o')
@ -40,6 +45,7 @@ open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin): class ArticlelAdmin(admin.ModelAdmin):
#fjw: 文章管理后台配置
list_per_page = 20 list_per_page = 20
search_fields = ('body', 'title') search_fields = ('body', 'title')
form = ArticleForm form = ArticleForm
@ -67,6 +73,7 @@ class ArticlelAdmin(admin.ModelAdmin):
raw_id_fields = ('author', 'category',) raw_id_fields = ('author', 'category',)
def link_to_category(self, obj): def link_to_category(self, obj):
#fjw: 生成分类链接
info = (obj.category._meta.app_label, obj.category._meta.model_name) info = (obj.category._meta.app_label, obj.category._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name)) return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
@ -74,15 +81,18 @@ class ArticlelAdmin(admin.ModelAdmin):
link_to_category.short_description = _('category') link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
#fjw: 限制作者只能选择超级用户
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model( form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True) ).objects.filter(is_superuser=True)
return form return form
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
#fjw: 保存模型时的额外处理
super(ArticlelAdmin, self).save_model(request, obj, form, change) super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None): def get_view_on_site_url(self, obj=None):
#fjw: 获取文章在前台的URL
if obj: if obj:
url = obj.get_full_url() url = obj.get_full_url()
return url return url
@ -93,22 +103,27 @@ class ArticlelAdmin(admin.ModelAdmin):
class TagAdmin(admin.ModelAdmin): class TagAdmin(admin.ModelAdmin):
#fjw: 标签管理后台配置
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin):
#fjw: 分类管理后台配置
list_display = ('name', 'parent_category', 'index') list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time') exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin): class LinksAdmin(admin.ModelAdmin):
#fjw: 友情链接管理后台配置
exclude = ('last_mod_time', 'creation_time') exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin): class SideBarAdmin(admin.ModelAdmin):
#fjw: 侧边栏管理后台配置
list_display = ('name', 'content', 'is_enable', 'sequence') list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time') exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin): class BlogSettingsAdmin(admin.ModelAdmin):
pass #fjw: 博客设置管理后台配置
pass

@ -2,4 +2,5 @@ from django.apps import AppConfig
class BlogConfig(AppConfig): class BlogConfig(AppConfig):
name = 'blog' #fjw: 博客应用配置
name = 'blog'

@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
def seo_processor(requests): def seo_processor(requests):
#fjw: SEO上下文处理器为所有模板提供SEO相关变量
key = 'seo_processor' key = 'seo_processor'
value = cache.get(key) value = cache.get(key)
if value: if value:
@ -40,4 +41,4 @@ def seo_processor(requests):
"COMMENT_NEED_REVIEW": setting.comment_need_review, "COMMENT_NEED_REVIEW": setting.comment_need_review,
} }
cache.set(key, value, 60 * 60 * 10) cache.set(key, value, 60 * 60 * 10)
return value return value

@ -18,6 +18,7 @@ logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices): class LinkShowType(models.TextChoices):
#fjw: 链接显示类型枚举
I = ('i', _('index')) I = ('i', _('index'))
L = ('l', _('list')) L = ('l', _('list'))
P = ('p', _('post')) P = ('p', _('post'))
@ -26,11 +27,13 @@ class LinkShowType(models.TextChoices):
class BaseModel(models.Model): class BaseModel(models.Model):
#fjw: 基础模型类,提供通用字段和方法
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now) last_modify_time = models.DateTimeField(_('modify time'), default=now)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
#fjw: 保存时的通用处理逻辑
is_update_views = isinstance( is_update_views = isinstance(
self, self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
@ -45,6 +48,7 @@ class BaseModel(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_full_url(self): def get_full_url(self):
#fjw: 获取完整URL
site = get_current_site().domain site = get_current_site().domain
url = "https://{site}{path}".format(site=site, url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url()) path=self.get_absolute_url())
@ -59,15 +63,18 @@ class BaseModel(models.Model):
class Article(BaseModel): class Article(BaseModel):
"""文章""" """文章模型"""
#fjw: 文章状态选择
STATUS_CHOICES = ( STATUS_CHOICES = (
('d', _('Draft')), ('d', _('Draft')),
('p', _('Published')), ('p', _('Published')),
) )
#fjw: 评论状态选择
COMMENT_STATUS = ( COMMENT_STATUS = (
('o', _('Open')), ('o', _('Open')),
('c', _('Close')), ('c', _('Close')),
) )
#fjw: 文章类型选择
TYPE = ( TYPE = (
('a', _('Article')), ('a', _('Article')),
('p', _('Page')), ('p', _('Page')),
@ -118,6 +125,7 @@ class Article(BaseModel):
get_latest_by = 'id' get_latest_by = 'id'
def get_absolute_url(self): def get_absolute_url(self):
#fjw: 获取文章详情页URL
return reverse('blog:detailbyid', kwargs={ return reverse('blog:detailbyid', kwargs={
'article_id': self.id, 'article_id': self.id,
'year': self.creation_time.year, 'year': self.creation_time.year,
@ -127,6 +135,7 @@ class Article(BaseModel):
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_category_tree(self): def get_category_tree(self):
#fjw: 获取分类树
tree = self.category.get_category_tree() tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
@ -136,10 +145,12 @@ class Article(BaseModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def viewed(self): def viewed(self):
#fjw: 增加文章浏览量
self.views += 1 self.views += 1
self.save(update_fields=['views']) self.save(update_fields=['views'])
def comment_list(self): def comment_list(self):
#fjw: 获取文章评论列表(带缓存)
cache_key = 'article_comments_{id}'.format(id=self.id) cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key) value = cache.get(cache_key)
if value: if value:
@ -152,24 +163,25 @@ class Article(BaseModel):
return comments return comments
def get_admin_url(self): def get_admin_url(self):
#fjw: 获取文章在后台的URL
info = (self._meta.app_label, self._meta.model_name) info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,)) return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100) @cache_decorator(expiration=60 * 100)
def next_article(self): def next_article(self):
# 下一篇 #fjw: 获取下一篇文章
return Article.objects.filter( return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first() id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100) @cache_decorator(expiration=60 * 100)
def prev_article(self): def prev_article(self):
# 前一篇 #fjw: 获取上一篇文章
return Article.objects.filter(id__lt=self.id, status='p').first() return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self): def get_first_image_url(self):
""" """
Get the first image url from article.body. Get the first image url from article body.
:return: :return: 文章正文中的第一张图片URL
""" """
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match: if match:
@ -178,7 +190,7 @@ class Article(BaseModel):
class Category(BaseModel): 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( parent_category = models.ForeignKey(
'self', 'self',
@ -206,7 +218,7 @@ class Category(BaseModel):
def get_category_tree(self): def get_category_tree(self):
""" """
递归获得分类目录的父级 递归获得分类目录的父级
:return: :return: 分类树列表
""" """
categorys = [] categorys = []
@ -222,7 +234,7 @@ class Category(BaseModel):
def get_sub_categorys(self): def get_sub_categorys(self):
""" """
获得当前分类目录所有子集 获得当前分类目录所有子集
:return: :return: 子分类列表
""" """
categorys = [] categorys = []
all_categorys = Category.objects.all() all_categorys = Category.objects.all()
@ -241,7 +253,7 @@ class Category(BaseModel):
class Tag(BaseModel): class Tag(BaseModel):
"""文章标签""" """文章标签模型"""
name = models.CharField(_('tag name'), max_length=30, unique=True) name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True)
@ -253,6 +265,7 @@ class Tag(BaseModel):
@cache_decorator(60 * 60 * 10) @cache_decorator(60 * 60 * 10)
def get_article_count(self): def get_article_count(self):
#fjw: 获取标签下的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count() return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta: class Meta:
@ -262,7 +275,7 @@ class Tag(BaseModel):
class Links(models.Model): class Links(models.Model):
"""友情链接""" """友情链接模型"""
name = models.CharField(_('link name'), max_length=30, unique=True) name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link')) link = models.URLField(_('link'))
@ -287,7 +300,7 @@ class Links(models.Model):
class SideBar(models.Model): class SideBar(models.Model):
"""侧边栏,可以展示一些html内容""" """侧边栏模型,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100) name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content')) content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True) sequence = models.IntegerField(_('order'), unique=True)
@ -305,7 +318,7 @@ class SideBar(models.Model):
class BlogSettings(models.Model): class BlogSettings(models.Model):
"""blog的配置""" """博客配置模型"""
site_name = models.CharField( site_name = models.CharField(
_('site name'), _('site name'),
max_length=200, max_length=200,
@ -367,10 +380,11 @@ class BlogSettings(models.Model):
return self.site_name return self.site_name
def clean(self): def clean(self):
#fjw: 确保只有一个配置实例
if BlogSettings.objects.exclude(id=self.id).count(): if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration')) raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
from djangoblog.utils import cache from djangoblog.utils import cache
cache.clear() cache.clear() #fjw: 保存配置后清除缓存

@ -32,6 +32,7 @@ def head_meta(context):
@register.simple_tag @register.simple_tag
def timeformat(data): def timeformat(data):
#fjw: 时间格式化标签
try: try:
return data.strftime(settings.TIME_FORMAT) return data.strftime(settings.TIME_FORMAT)
except Exception as e: except Exception as e:
@ -56,13 +57,7 @@ def custom_markdown(content):
主要用于文章内容处理 主要用于文章内容处理
""" """
html_content = CommonMarkdown.get_markdown(content) html_content = CommonMarkdown.get_markdown(content)
return mark_safe(html_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() @register.filter()
@ -136,13 +131,13 @@ def comment_markdown(content):
return mark_safe(sanitize_html(content)) return mark_safe(sanitize_html(content))
@register.filter(is_safe=True) @register.filter()
@stringfilter @stringfilter
def truncatechars_content(content): def truncatechars_content(content):
""" """
获得文章内容的摘要 获得文章内容的摘要
:param content: :param content: 文章内容
:return: :return: 截取后的摘要
""" """
from django.template.defaultfilters import truncatechars_html from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
@ -162,8 +157,8 @@ def truncate(content):
def load_breadcrumb(article): def load_breadcrumb(article):
""" """
获得文章面包屑 获得文章面包屑
:param article: :param article: 文章对象
:return: :return: 面包屑数据
""" """
names = article.get_category_tree() names = article.get_category_tree()
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
@ -183,8 +178,8 @@ def load_breadcrumb(article):
def load_articletags(article): def load_articletags(article):
""" """
文章标签 文章标签
:param article: :param article: 文章对象
:return: :return: 标签数据
""" """
tags = article.tags.all() tags = article.tags.all()
tags_list = [] tags_list = []
@ -199,11 +194,14 @@ def load_articletags(article):
} }
@register.inclusion_tag('blog/tags/sidebar.html') @register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype): def load_sidebar(user, linktype):
""" """
加载侧边栏 加载侧边栏
:return: :param user: 当前用户
:param linktype: 链接类型
:return: 侧边栏数据
""" """
value = cache.get("sidebar" + linktype) value = cache.get("sidebar" + linktype)
if value: if value:
@ -225,8 +223,8 @@ def load_sidebar(user, linktype):
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A)) Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by( commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count] '-id')[:blogsetting.sidebar_comment_count]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长 # 标签云计算字体大小
increment = 5 increment = 5
tags = Tag.objects.all() tags = Tag.objects.all()
sidebar_tags = None sidebar_tags = None
@ -364,34 +362,34 @@ def load_article_detail(article, isindex, user):
# 模板使用方法: {{ email|gravatar_url:150 }} # 模板使用方法: {{ email|gravatar_url:150 }}
@register.filter @register.filter
def gravatar_url(email, size=40): def gravatar_url(email, size=40):
"""获得用户头像 - 优先使用OAuth头像否则使用默认头像""" """获得用户头像URL"""
cachekey = 'avatar/' + email cachekey = 'avatar/' + email
url = cache.get(cachekey) url = cache.get(cachekey)
if url: if url:
return url return url
# 改进:添加默认头像路径常量
DEFAULT_AVATAR_PATH = static('blog/img/avatar.png')
# 检查OAuth用户是否有自定义头像 # 检查OAuth用户是否有自定义头像
usermodels = OAuthUser.objects.filter(email=email) usermodels = OAuthUser.objects.filter(email=email)
if usermodels: if usermodels:
# 过滤出有头像的用户 # 过滤出有头像的用户
users_with_picture = list(filter(lambda x: x.picture is not None, usermodels)) users_with_picture = list(filter(lambda x: x.picture is not None, usermodels))
if users_with_picture: 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')] 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] selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
url = selected_user.picture url = selected_user.picture
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时 cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
avatar_type = 'non-default' if non_default_users else 'default' avatar_type = 'non-default' if non_default_users else 'default'
logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type)) logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type))
return url return url
# 使用默认头像 # 使用默认头像
url = static('blog/img/avatar.png') url = DEFAULT_AVATAR_PATH
cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时 cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
logger.info('Using default avatar for {}'.format(email)) logger.info('Using default avatar for {}'.format(email))
return url return url

@ -25,14 +25,10 @@ logger = logging.getLogger(__name__)
class ArticleListView(ListView): class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染 #fjw: 文章列表基类视图
template_name = 'blog/article_index.html' template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list' context_object_name = 'article_list'
page_type = '' #fjw: 页面类型,分类目录或标签列表等
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY paginate_by = settings.PAGINATE_BY
page_kwarg = 'page' page_kwarg = 'page'
link_type = LinkShowType.L link_type = LinkShowType.L
@ -42,6 +38,7 @@ class ArticleListView(ListView):
@property @property
def page_number(self): def page_number(self):
#fjw: 获取当前页码
page_kwarg = self.page_kwarg page_kwarg = self.page_kwarg
page = self.kwargs.get( page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1 page_kwarg) or self.request.GET.get(page_kwarg) or 1
@ -63,7 +60,7 @@ class ArticleListView(ListView):
''' '''
缓存页面数据 缓存页面数据
:param cache_key: 缓存key :param cache_key: 缓存key
:return: :return: 文章列表数据
''' '''
value = cache.get(cache_key) value = cache.get(cache_key)
if value: if value:
@ -78,7 +75,7 @@ class ArticleListView(ListView):
def get_queryset(self): def get_queryset(self):
''' '''
重写默认从缓存获取数据 重写默认从缓存获取数据
:return: :return: 文章列表
''' '''
key = self.get_queryset_cache_key() key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key) value = self.get_queryset_from_cache(key)
@ -91,12 +88,12 @@ class ArticleListView(ListView):
class IndexView(ArticleListView): class IndexView(ArticleListView):
''' '''
首页 首页视图
''' '''
# 友情链接类型 link_type = LinkShowType.I #fjw: 友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self): def get_queryset_data(self):
#fjw: 获取首页文章列表
article_list = Article.objects.filter(type='a', status='p') article_list = Article.objects.filter(type='a', status='p')
return article_list return article_list
@ -107,7 +104,7 @@ class IndexView(ArticleListView):
class ArticleDetailView(DetailView): class ArticleDetailView(DetailView):
''' '''
文章详情页面 文章详情页面视图
''' '''
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html'
model = Article model = Article
@ -115,6 +112,7 @@ class ArticleDetailView(DetailView):
context_object_name = "article" context_object_name = "article"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#fjw: 添加上下文数据:评论表单、评论列表等
comment_form = CommentForm() comment_form = CommentForm()
article_comments = self.object.comment_list() article_comments = self.object.comment_list()
@ -152,23 +150,22 @@ class ArticleDetailView(DetailView):
context = super(ArticleDetailView, self).get_context_data(**kwargs) context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object article = self.object
# 触发文章详情加载钩子,让插件可以添加额外的上下文数据 # 触发文章详情加载钩子,让插件可以添加额外的上下文数据
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request) 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)
return context return context
class CategoryDetailView(ArticleListView): class CategoryDetailView(ArticleListView):
''' '''
分类目录列表 分类目录列表视图
''' '''
page_type = "分类目录归档" page_type = "分类目录归档"
def get_queryset_data(self): def get_queryset_data(self):
#fjw: 获取分类下的文章列表
slug = self.kwargs['category_name'] slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug) category = get_object_or_404(Category, slug=slug)
@ -190,7 +187,6 @@ class CategoryDetailView(ArticleListView):
return cache_key return cache_key
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
categoryname = self.categoryname categoryname = self.categoryname
try: try:
categoryname = categoryname.split('/')[-1] categoryname = categoryname.split('/')[-1]
@ -203,7 +199,7 @@ class CategoryDetailView(ArticleListView):
class AuthorDetailView(ArticleListView): class AuthorDetailView(ArticleListView):
''' '''
作者详情页 作者详情页视图
''' '''
page_type = '作者文章归档' page_type = '作者文章归档'
@ -229,7 +225,7 @@ class AuthorDetailView(ArticleListView):
class TagDetailView(ArticleListView): class TagDetailView(ArticleListView):
''' '''
标签列表页面 标签列表页面视图
''' '''
page_type = '分类标签归档' page_type = '分类标签归档'
@ -252,7 +248,6 @@ class TagDetailView(ArticleListView):
return cache_key return cache_key
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name kwargs['tag_name'] = tag_name
@ -261,7 +256,7 @@ class TagDetailView(ArticleListView):
class ArchivesView(ArticleListView): class ArchivesView(ArticleListView):
''' '''
文章归档页面 文章归档页面视图
''' '''
page_type = '文章归档' page_type = '文章归档'
paginate_by = None paginate_by = None
@ -277,6 +272,7 @@ class ArchivesView(ArticleListView):
class LinkListView(ListView): class LinkListView(ListView):
#fjw: 友情链接页面视图
model = Links model = Links
template_name = 'blog/links_list.html' template_name = 'blog/links_list.html'
@ -284,29 +280,12 @@ class LinkListView(ListView):
return Links.objects.filter(is_enable=True) return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
def get_context(self):
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context
@csrf_exempt @csrf_exempt
def fileupload(request): def fileupload(request):
""" """
该方法需自己写调用端来上传图片该方法仅提供图床功能 文件上传方法需自己写调用端来上传图片该方法仅提供图床功能
:param request: :param request:
:return: :return: 上传文件的URL列表
""" """
if request.method == 'POST': if request.method == 'POST':
sign = request.GET.get('sign', None) sign = request.GET.get('sign', None)
@ -345,6 +324,7 @@ def page_not_found_view(
request, request,
exception, exception,
template_name='blog/error_page.html'): template_name='blog/error_page.html'):
#fjw: 404错误页面视图
if exception: if exception:
logger.error(exception) logger.error(exception)
url = request.get_full_path() url = request.get_full_path()
@ -356,6 +336,7 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'): def server_error_view(request, template_name='blog/error_page.html'):
#fjw: 500错误页面视图
return render(request, return render(request,
template_name, template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'), {'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -367,6 +348,7 @@ def permission_denied_view(
request, request,
exception, exception,
template_name='blog/error_page.html'): template_name='blog/error_page.html'):
#fjw: 403错误页面视图
if exception: if exception:
logger.error(exception) logger.error(exception)
return render( return render(
@ -376,5 +358,6 @@ def permission_denied_view(
def clean_cache_view(request): def clean_cache_view(request):
#fjw: 清理缓存视图
cache.clear() cache.clear()
return HttpResponse('ok') return HttpResponse('ok')

@ -1,3 +1,4 @@
#wzc: 评论模块的后台管理配置
from django.contrib import admin from django.contrib import admin
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
@ -5,10 +6,12 @@ from django.utils.translation import gettext_lazy as _
def disable_commentstatus(modeladmin, request, queryset): def disable_commentstatus(modeladmin, request, queryset):
"""wzc: 批量禁用评论状态"""
queryset.update(is_enable=False) queryset.update(is_enable=False)
def enable_commentstatus(modeladmin, request, queryset): def enable_commentstatus(modeladmin, request, queryset):
"""wzc: 批量启用评论状态"""
queryset.update(is_enable=True) queryset.update(is_enable=True)
@ -17,6 +20,7 @@ enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin): class CommentAdmin(admin.ModelAdmin):
"""wzc: 评论模型的后台管理配置"""
list_per_page = 20 list_per_page = 20
list_display = ( list_display = (
'id', 'id',
@ -33,17 +37,18 @@ class CommentAdmin(admin.ModelAdmin):
search_fields = ('body',) search_fields = ('body',)
def link_to_userinfo(self, obj): def link_to_userinfo(self, obj):
"""wzc: 生成用户信息链接"""
info = (obj.author._meta.app_label, obj.author._meta.model_name) info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
return format_html( #wzc: 使用昵称或邮箱作为显示文本
u'<a href="%s">%s</a>' % display_text = obj.author.nickname if obj.author.nickname else obj.author.email
(link, obj.author.nickname if obj.author.nickname else obj.author.email)) return format_html(u'<a href="{}">{}</a>', link, display_text)
def link_to_article(self, obj): def link_to_article(self, obj):
"""wzc: 生成文章链接"""
info = (obj.article._meta.app_label, obj.article._meta.model_name) info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html( return format_html(u'<a href="{}">{}</a>', link, obj.article.title)
u'<a href="%s">%s</a>' % (link, obj.article.title))
link_to_userinfo.short_description = _('User') link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article') link_to_article.short_description = _('Article')

@ -1,5 +1,7 @@
#wzc: 评论应用的配置类
from django.apps import AppConfig from django.apps import AppConfig
class CommentsConfig(AppConfig): class CommentsConfig(AppConfig):
name = 'comments' """wzc: 评论应用配置"""
name = 'comments'

@ -1,3 +1,4 @@
#wzc: 评论相关的表单定义
from django import forms from django import forms
from django.forms import ModelForm from django.forms import ModelForm
@ -5,9 +6,13 @@ from .models import Comment
class CommentForm(ModelForm): class CommentForm(ModelForm):
"""wzc: 评论表单支持父评论ID"""
parent_comment_id = forms.IntegerField( parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False) widget=forms.HiddenInput,
required=False,
help_text="wzc: 父评论ID用于回复功能"
)
class Meta: class Meta:
model = Comment model = Comment
fields = ['body'] fields = ['body']

@ -1,3 +1,4 @@
#wzc: 评论数据模型定义
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
@ -6,9 +7,8 @@ from django.utils.translation import gettext_lazy as _
from blog.models import Article from blog.models import Article
# Create your models here.
class Comment(models.Model): class Comment(models.Model):
"""wzc: 评论模型,存储用户对文章的评论信息"""
body = models.TextField('正文', max_length=300) body = models.TextField('正文', max_length=300)
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
@ -25,9 +25,14 @@ class Comment(models.Model):
verbose_name=_('parent comment'), verbose_name=_('parent comment'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE,
is_enable = models.BooleanField(_('enable'), help_text="wzc: 父评论,用于实现评论回复功能")
default=False, blank=False, null=False) is_enable = models.BooleanField(
_('enable'),
default=False,
blank=False,
null=False,
help_text="wzc: 评论是否启用(审核通过)")
class Meta: class Meta:
ordering = ['-id'] ordering = ['-id']
@ -36,4 +41,5 @@ class Comment(models.Model):
get_latest_by = 'id' get_latest_by = 'id'
def __str__(self): def __str__(self):
return self.body """wzc: 返回评论正文作为字符串表示"""
return self.body[:50] #wzc: 只返回前50个字符避免过长

@ -1,3 +1,4 @@
#wzc: 评论模块的测试用例
from django.test import Client, RequestFactory, TransactionTestCase from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse from django.urls import reverse
@ -8,102 +9,73 @@ from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
class CommentsTest(TransactionTestCase): class CommentsTest(TransactionTestCase):
"""wzc: 评论功能测试类"""
def setUp(self): def setUp(self):
"""wzc: 测试初始化设置"""
self.client = Client() self.client = Client()
self.factory = RequestFactory() self.factory = RequestFactory()
#wzc: 设置博客配置,评论需要审核
from blog.models import BlogSettings from blog.models import BlogSettings
value = BlogSettings() value = BlogSettings()
value.comment_need_review = True value.comment_need_review = True
value.save() value.save()
#wzc: 创建测试用户
self.user = BlogUser.objects.create_superuser( self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
password="liangliangyy1") password="liangliangyy1")
def update_article_comment_status(self, article): def update_article_comment_status(self, article):
"""wzc: 更新文章所有评论为启用状态"""
comments = article.comment_set.all() comments = article.comment_set.all()
for comment in comments: for comment in comments:
comment.is_enable = True comment.is_enable = True
comment.save() comment.save()
def test_validate_comment(self): def test_validate_comment(self):
"""wzc: 测试评论验证流程"""
self.client.login(username='liangliangyy1', password='liangliangyy1') self.client.login(username='liangliangyy1', password='liangliangyy1')
category = Category() #wzc: 创建测试分类和文章
category.name = "categoryccc" category = Category.objects.create(name="categoryccc")
category.save() article = Article.objects.create(
title="nicetitleccc",
article = Article() body="nicecontentccc",
article.title = "nicetitleccc" author=self.user,
article.body = "nicecontentccc" category=category,
article.author = self.user type='a',
article.category = category status='p'
article.type = 'a' )
article.status = 'p'
article.save() comment_url = reverse('comments:postcomment', kwargs={'article_id': article.id})
comment_url = reverse( #wzc: 测试发表评论
'comments:postcomment', kwargs={ response = self.client.post(comment_url, {'body': '123ffffffffff'})
'article_id': article.id})
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
#wzc: 验证评论状态
article = Article.objects.get(pk=article.pk) article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0) self.assertEqual(len(article.comment_list()), 0)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk) #wzc: 启用评论后验证
self.update_article_comment_status(article) self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2) self.assertEqual(len(article.comment_list()), 1)
parent_comment_id = article.comment_list()[0].id
response = self.client.post(comment_url,
{
'body': '''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
})
self.assertEqual(response.status_code, 302) #wzc: 测试评论模板标签功能
self.update_article_comment_status(article) comment = Comment.objects.first()
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment) tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1) self.assertEqual(len(tree), 1)
data = show_comment_item(comment, True) data = show_comment_item(comment, True)
self.assertIsNotNone(data) self.assertIsNotNone(data)
#wzc: 测试工具函数
s = get_max_articleid_commentid() s = get_max_articleid_commentid()
self.assertIsNotNone(s) self.assertIsNotNone(s)
from comments.utils import send_comment_email from comments.utils import send_comment_email
send_comment_email(comment) send_comment_email(comment)

@ -1,3 +1,4 @@
#wzc: 评论模块URL路由配置
from django.urls import path from django.urls import path
from . import views from . import views
@ -8,4 +9,4 @@ urlpatterns = [
'article/<int:article_id>/postcomment', 'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(), views.CommentPostView.as_view(),
name='postcomment'), name='postcomment'),
] ]

@ -1,3 +1,4 @@
#wzc: 评论工具函数模块
import logging import logging
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -9,9 +10,12 @@ logger = logging.getLogger(__name__)
def send_comment_email(comment): def send_comment_email(comment):
"""wzc: 发送评论通知邮件"""
site = get_current_site().domain site = get_current_site().domain
subject = _('Thanks for your comment') subject = _('Thanks for your comment')
article_url = f"https://{site}{comment.article.get_absolute_url()}" article_url = f"https://{site}{comment.article.get_absolute_url()}"
#wzc: 主评论邮件内容
html_content = _("""<p>Thank you very much for your comments on this site</p> 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> You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments, to review your comments,
@ -19,8 +23,11 @@ def send_comment_email(comment):
<br /> <br />
If the link above cannot be opened, please copy this link to your browser. 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} %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
tomail = comment.author.email tomail = comment.author.email
send_email([tomail], subject, html_content) send_email([tomail], subject, html_content)
#wzc: 如果是回复评论,发送通知给父评论作者
try: try:
if comment.parent_comment: if comment.parent_comment:
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
@ -30,9 +37,12 @@ def send_comment_email(comment):
<br/> <br/>
If the link above cannot be opened, please copy this link to your browser. If the link above cannot be opened, please copy this link to your browser.
%(article_url)s %(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title, """) % {
'comment_body': comment.parent_comment.body} 'article_url': article_url,
'article_title': comment.article.title,
'comment_body': comment.parent_comment.body
}
tomail = comment.parent_comment.author.email tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content) send_email([tomail], subject, html_content)
except Exception as e: except Exception as e:
logger.error(e) logger.error("wzc: 发送评论回复邮件失败: %s", e)

@ -1,4 +1,4 @@
# Create your views here. #wzc: 评论视图处理模块
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@ -13,51 +13,66 @@ from .models import Comment
class CommentPostView(FormView): class CommentPostView(FormView):
"""wzc: 评论发表视图"""
form_class = CommentForm form_class = CommentForm
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect) @method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
"""wzc: CSRF保护装饰器"""
return super(CommentPostView, self).dispatch(*args, **kwargs) return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""wzc: GET请求重定向到文章详情页"""
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url() url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments") return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form): def form_invalid(self, form):
"""wzc: 表单验证失败处理"""
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
return self.render_to_response({ return self.render_to_response({
'form': form, 'form': form,
'article': article 'article': article
}) })
def form_valid(self, form): def form_valid(self, form):
"""提交的数据验证合法后的逻辑""" """wzc: 表单验证成功后的评论保存逻辑"""
user = self.request.user user = self.request.user
#wzc: 获取评论作者信息
author = BlogUser.objects.get(pk=user.pk) author = BlogUser.objects.get(pk=user.pk)
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id)
#wzc: 检查文章是否允许评论
if article.comment_status == 'c' or article.status == 'c': if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") raise ValidationError("该文章评论已关闭.")
comment = form.save(False)
#wzc: 创建评论对象
comment = form.save(commit=False)
comment.article = article comment.article = article
comment.author = author
#wzc: 根据博客设置决定是否自动启用评论
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
settings = get_blog_setting() settings = get_blog_setting()
if not settings.comment_need_review: if not settings.comment_need_review:
comment.is_enable = True comment.is_enable = True
comment.author = author
if form.cleaned_data['parent_comment_id']: #wzc: 处理父评论
parent_comment = Comment.objects.get( parent_comment_id = form.cleaned_data.get('parent_comment_id')
pk=form.cleaned_data['parent_comment_id']) if parent_comment_id:
comment.parent_comment = parent_comment try:
parent_comment = Comment.objects.get(pk=parent_comment_id)
comment.parent_comment = parent_comment
except Comment.DoesNotExist:
#wzc: 父评论不存在时忽略,不中断流程
pass
comment.save()
comment.save(True) #wzc: 重定向到文章详情页的评论区域
return HttpResponseRedirect( return HttpResponseRedirect(
"%s#div-comment-%d" % "%s#div-comment-%d" % (article.get_absolute_url(), comment.pk))
(article.get_absolute_url(), comment.pk))

@ -1,3 +1,4 @@
#xy: Django博客管理站点配置
from django.contrib.admin import AdminSite from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin from django.contrib.sites.admin import SiteAdmin
@ -18,13 +19,16 @@ from servermanager.models import *
class DjangoBlogAdminSite(AdminSite): class DjangoBlogAdminSite(AdminSite):
"""#xy: 自定义Django博客管理站点"""
site_header = 'djangoblog administration' site_header = 'djangoblog administration'
site_title = 'djangoblog site admin' site_title = 'djangoblog site admin'
def __init__(self, name='admin'): def __init__(self, name='admin'):
#xy: 初始化管理站点
super().__init__(name) super().__init__(name)
def has_permission(self, request): def has_permission(self, request):
#xy: 检查用户权限,只允许超级用户访问
return request.user.is_superuser return request.user.is_superuser
# def get_urls(self): # def get_urls(self):
@ -38,8 +42,10 @@ class DjangoBlogAdminSite(AdminSite):
# return urls + my_urls # return urls + my_urls
#xy: 创建管理站点实例
admin_site = DjangoBlogAdminSite(name='admin') admin_site = DjangoBlogAdminSite(name='admin')
#xy: 注册所有模型到管理站点
admin_site.register(Article, ArticlelAdmin) admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin) admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin) admin_site.register(Tag, TagAdmin)
@ -61,4 +67,4 @@ admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
admin_site.register(Site, SiteAdmin) admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin) admin_site.register(LogEntry, LogEntryAdmin)

@ -1,3 +1,4 @@
#xy: Django博客信号处理器模块
import _thread import _thread
import logging import logging
@ -18,6 +19,7 @@ from oauth.models import OAuthUser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#xy: 自定义信号定义
oauth_user_login_signal = django.dispatch.Signal(['id']) oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal( send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content']) ['emailto', 'title', 'content'])
@ -25,10 +27,12 @@ send_email_signal = django.dispatch.Signal(
@receiver(send_email_signal) @receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs): def send_email_signal_handler(sender, **kwargs):
"""#xy: 邮件发送信号处理器"""
emailto = kwargs['emailto'] emailto = kwargs['emailto']
title = kwargs['title'] title = kwargs['title']
content = kwargs['content'] content = kwargs['content']
#xy: 创建邮件消息
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
title, title,
content, content,
@ -36,6 +40,7 @@ def send_email_signal_handler(sender, **kwargs):
to=emailto) to=emailto)
msg.content_subtype = "html" msg.content_subtype = "html"
#xy: 记录邮件发送日志
from servermanager.models import EmailSendLog from servermanager.models import EmailSendLog
log = EmailSendLog() log = EmailSendLog()
log.title = title log.title = title
@ -53,15 +58,20 @@ def send_email_signal_handler(sender, **kwargs):
@receiver(oauth_user_login_signal) @receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs): def oauth_user_login_signal_handler(sender, **kwargs):
"""#xy: OAuth用户登录信号处理器"""
id = kwargs['id'] id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id) try:
site = get_current_site().domain oauthuser = OAuthUser.objects.get(id=id)
if oauthuser.picture and not oauthuser.picture.find(site) >= 0: site = get_current_site().domain
from djangoblog.utils import save_user_avatar #xy: 处理用户头像
oauthuser.picture = save_user_avatar(oauthuser.picture) if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
oauthuser.save() from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
delete_sidebar_cache() delete_sidebar_cache()
except OAuthUser.DoesNotExist:
logger.warning(f"OAuth用户不存在: {id}")
@receiver(post_save) @receiver(post_save)
@ -73,9 +83,14 @@ def model_post_save_callback(
using, using,
update_fields, update_fields,
**kwargs): **kwargs):
"""#xy: 模型保存后的回调函数"""
clearcache = False clearcache = False
#xy: 跳过LogEntry模型的保存处理
if isinstance(instance, LogEntry): if isinstance(instance, LogEntry):
return return
#xy: 处理具有完整URL的模型
if 'get_full_url' in dir(instance): if 'get_full_url' in dir(instance):
is_update_views = update_fields == {'views'} is_update_views = update_fields == {'views'}
if not settings.TESTING and not is_update_views: if not settings.TESTING and not is_update_views:
@ -87,6 +102,7 @@ def model_post_save_callback(
if not is_update_views: if not is_update_views:
clearcache = True clearcache = True
#xy: 处理评论保存
if isinstance(instance, Comment): if isinstance(instance, Comment):
if instance.is_enable: if instance.is_enable:
path = instance.article.get_absolute_url() path = instance.article.get_absolute_url()
@ -94,6 +110,7 @@ def model_post_save_callback(
if site.find(':') > 0: if site.find(':') > 0:
site = site[0:site.find(':')] site = site[0:site.find(':')]
#xy: 清除相关缓存
expire_view_cache( expire_view_cache(
path, path,
servername=site, servername=site,
@ -107,8 +124,10 @@ def model_post_save_callback(
delete_sidebar_cache() delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)]) delete_view_cache('article_comments', [str(instance.article.pk)])
#xy: 异步发送评论邮件
_thread.start_new_thread(send_comment_email, (instance,)) _thread.start_new_thread(send_comment_email, (instance,))
#xy: 清除缓存
if clearcache: if clearcache:
cache.clear() cache.clear()
@ -116,7 +135,8 @@ def model_post_save_callback(
@receiver(user_logged_in) @receiver(user_logged_in)
@receiver(user_logged_out) @receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs): def user_auth_callback(sender, request, user, **kwargs):
"""#xy: 用户登录/登出回调函数"""
if user and user.username: if user and user.username:
logger.info(user) logger.info(f"用户认证操作: {user.username}")
delete_sidebar_cache() delete_sidebar_cache()
# cache.clear() # cache.clear()

@ -23,7 +23,7 @@ from haystack.views import search_view_factory
from django.http import JsonResponse from django.http import JsonResponse
import time import time
from blog.views import EsSearchView from blog.views import SearchView
from djangoblog.admin_site import admin_site from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed from djangoblog.feeds import DjangoBlogFeed
@ -68,7 +68,7 @@ urlpatterns += i18n_patterns(
name='django.contrib.sitemaps.views.sitemap'), name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()), re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()), re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), re_path('^search', search_view_factory(view_class=SearchView, form_class=ElasticSearchModelSearchForm),
name='search'), name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')), re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks')) re_path(r'', include('owntracks.urls', namespace='owntracks'))

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# encoding: utf-8 # encoding: utf-8
#xy: Django博客工具函数模块
import logging import logging
import os import os
@ -21,17 +21,20 @@ logger = logging.getLogger(__name__)
def get_max_articleid_commentid(): def get_max_articleid_commentid():
"""#xy: 获取最大文章ID和评论ID"""
from blog.models import Article from blog.models import Article
from comments.models import Comment from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk) return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str): def get_sha256(str):
"""#xy: 计算字符串的SHA256哈希值"""
m = sha256(str.encode('utf-8')) m = sha256(str.encode('utf-8'))
return m.hexdigest() return m.hexdigest()
def cache_decorator(expiration=3 * 60): def cache_decorator(expiration=3 * 60):
"""#xy: 缓存装饰器"""
def wrapper(func): def wrapper(func):
def news(*args, **kwargs): def news(*args, **kwargs):
try: try:
@ -41,12 +44,11 @@ def cache_decorator(expiration=3 * 60):
key = None key = None
if not key: if not key:
unique_str = repr((func, args, kwargs)) unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8')) m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest() key = m.hexdigest()
value = cache.get(key) value = cache.get(key)
if value is not None: if value is not None:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) #xy: 缓存命中
if str(value) == '__default_cache_value__': if str(value) == '__default_cache_value__':
return None return None
else: else:
@ -68,14 +70,7 @@ def cache_decorator(expiration=3 * 60):
def expire_view_cache(path, servername, serverport, key_prefix=None): def expire_view_cache(path, servername, serverport, key_prefix=None):
''' '''#xy: 刷新视图缓存'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.cache import get_cache_key from django.utils.cache import get_cache_key
@ -94,13 +89,16 @@ def expire_view_cache(path, servername, serverport, key_prefix=None):
@cache_decorator() @cache_decorator()
def get_current_site(): def get_current_site():
"""#xy: 获取当前站点"""
site = Site.objects.get_current() site = Site.objects.get_current()
return site return site
class CommonMarkdown: class CommonMarkdown:
"""#xy: Markdown处理工具类"""
@staticmethod @staticmethod
def _convert_markdown(value): def _convert_markdown(value):
"""#xy: 转换Markdown文本"""
md = markdown.Markdown( md = markdown.Markdown(
extensions=[ extensions=[
'extra', 'extra',
@ -115,16 +113,19 @@ class CommonMarkdown:
@staticmethod @staticmethod
def get_markdown_with_toc(value): def get_markdown_with_toc(value):
"""#xy: 获取带目录的Markdown"""
body, toc = CommonMarkdown._convert_markdown(value) body, toc = CommonMarkdown._convert_markdown(value)
return body, toc return body, toc
@staticmethod @staticmethod
def get_markdown(value): def get_markdown(value):
"""#xy: 获取Markdown内容"""
body, toc = CommonMarkdown._convert_markdown(value) body, toc = CommonMarkdown._convert_markdown(value)
return body return body
def send_email(emailto, title, content): def send_email(emailto, title, content):
"""#xy: 发送邮件"""
from djangoblog.blog_signals import send_email_signal from djangoblog.blog_signals import send_email_signal
send_email_signal.send( send_email_signal.send(
send_email.__class__, send_email.__class__,
@ -134,11 +135,12 @@ def send_email(emailto, title, content):
def generate_code() -> str: def generate_code() -> str:
"""生成随机数验证码""" """#xy: 生成随机数验证码"""
return ''.join(random.sample(string.digits, 6)) return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict): def parse_dict_to_url(dict):
"""#xy: 解析字典为URL参数"""
from urllib.parse import quote from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()]) for k, v in dict.items()])
@ -146,12 +148,14 @@ def parse_dict_to_url(dict):
def get_blog_setting(): def get_blog_setting():
"""#xy: 获取博客设置"""
value = cache.get('get_blog_setting') value = cache.get('get_blog_setting')
if value: if value:
return value return value
else: else:
from blog.models import BlogSettings from blog.models import BlogSettings
if not BlogSettings.objects.count(): if not BlogSettings.objects.count():
#xy: 创建默认博客设置
setting = BlogSettings() setting = BlogSettings()
setting.site_name = 'djangoblog' setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统' setting.site_description = '基于Django的博客系统'
@ -174,15 +178,12 @@ def get_blog_setting():
def save_user_avatar(url): def save_user_avatar(url):
''' '''#xy: 保存用户头像'''
保存用户头像 logger.info(f"保存用户头像: {url}")
:param url:头像url
:return: 本地路径
'''
logger.info(url)
try: try:
basedir = os.path.join(settings.STATICFILES, 'avatar') basedir = os.path.join(settings.STATICFILES, 'avatar')
#xy: 下载头像
rsp = requests.get(url, timeout=2) rsp = requests.get(url, timeout=2)
if rsp.status_code == 200: if rsp.status_code == 200:
if not os.path.exists(basedir): if not os.path.exists(basedir):
@ -197,11 +198,12 @@ def save_user_avatar(url):
file.write(rsp.content) file.write(rsp.content)
return static('avatar/' + save_filename) return static('avatar/' + save_filename)
except Exception as e: except Exception as e:
logger.error(e) logger.error(f"保存用户头像失败: {e}")
return static('blog/img/avatar.png') return static('blog/img/avatar.png')
def delete_sidebar_cache(): def delete_sidebar_cache():
"""#xy: 删除侧边栏缓存"""
from blog.models import LinkShowType from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values] keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys: for k in keys:
@ -210,12 +212,14 @@ def delete_sidebar_cache():
def delete_view_cache(prefix, keys): def delete_view_cache(prefix, keys):
"""#xy: 删除视图缓存"""
from django.core.cache.utils import make_template_fragment_key from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys) key = make_template_fragment_key(prefix, keys)
cache.delete(key) cache.delete(key)
def get_resource_url(): def get_resource_url():
"""#xy: 获取资源URL"""
if settings.STATIC_URL: if settings.STATIC_URL:
return settings.STATIC_URL return settings.STATIC_URL
else: else:
@ -223,10 +227,11 @@ def get_resource_url():
return 'http://' + site.domain + '/static/' return 'http://' + site.domain + '/static/'
#xy: HTML清理配置
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p', 'span', 'div'] 'h2', 'p', 'span', 'div']
# 安全的class值白名单 - 只允许代码高亮相关的class #xy: 安全的class值白名单 - 只允许代码高亮相关的class
ALLOWED_CLASSES = [ ALLOWED_CLASSES = [
'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs', 'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt', 'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
@ -236,17 +241,17 @@ ALLOWED_CLASSES = [
] ]
def class_filter(tag, name, value): def class_filter(tag, name, value):
"""自定义class属性过滤器""" """#xy: 自定义class属性过滤器"""
if name == 'class': if name == 'class':
# 只允许预定义的安全class值 #xy: 只允许预定义的安全class值
allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES] allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
return ' '.join(allowed_classes) if allowed_classes else False return ' '.join(allowed_classes) if allowed_classes else False
return value return value
# 安全的属性白名单 #xy: 安全的属性白名单
ALLOWED_ATTRIBUTES = { ALLOWED_ATTRIBUTES = {
'a': ['href', 'title'], 'a': ['href', 'title'],
'abbr': ['title'], 'abbr': ['title'],
'acronym': ['title'], 'acronym': ['title'],
'span': class_filter, 'span': class_filter,
'div': class_filter, 'div': class_filter,
@ -254,19 +259,19 @@ ALLOWED_ATTRIBUTES = {
'code': class_filter 'code': class_filter
} }
# 安全的协议白名单 - 防止javascript:等危险协议 #xy: 安全的协议白名单 - 防止javascript:等危险协议
ALLOWED_PROTOCOLS = ['http', 'https', 'mailto'] ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
def sanitize_html(html): def sanitize_html(html):
""" """
安全的HTML清理函数 #xy: 安全的HTML清理函数
使用bleach库进行白名单过滤防止XSS攻击 #xy: 使用bleach库进行白名单过滤防止XSS攻击
""" """
return bleach.clean( return bleach.clean(
html, html,
tags=ALLOWED_TAGS, tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRIBUTES, attributes=ALLOWED_ATTRIBUTES,
protocols=ALLOWED_PROTOCOLS, # 限制允许的协议 protocols=ALLOWED_PROTOCOLS, #xy: 限制允许的协议
strip=True, # 移除不允许的标签而不是转义 strip=True, #xy: 移除不允许的标签而不是转义
strip_comments=True # 移除HTML注释 strip_comments=True #xy: 移除HTML注释
) )

@ -1,7 +1,7 @@
#zzj: OAuth用户和配置的Django后台管理模块
import logging import logging
from django.contrib import admin from django.contrib import admin
# Register your models here.
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
class OAuthUserAdmin(admin.ModelAdmin): class OAuthUserAdmin(admin.ModelAdmin):
"""OAuth用户后台管理类"""
search_fields = ('nickname', 'email') search_fields = ('nickname', 'email')
list_per_page = 20 list_per_page = 20
list_display = ( list_display = (
@ -24,31 +25,41 @@ class OAuthUserAdmin(admin.ModelAdmin):
readonly_fields = [] readonly_fields = []
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
"""获取只读字段"""
if obj is None:
return self.readonly_fields
return list(self.readonly_fields) + \ return list(self.readonly_fields) + \
[field.name for field in obj._meta.fields] + \ [field.name for field in obj._meta.fields] + \
[field.name for field in obj._meta.many_to_many] [field.name for field in obj._meta.many_to_many]
def has_add_permission(self, request): def has_add_permission(self, request):
"""禁用添加权限"""
return False return False
def link_to_usermodel(self, obj): def link_to_usermodel(self, obj):
"""生成关联用户模型的链接"""
if obj.author: if obj.author:
info = (obj.author._meta.app_label, obj.author._meta.model_name) info = (obj.author._meta.app_label, obj.author._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
return format_html( display_name = obj.author.nickname if obj.author.nickname else obj.author.email
u'<a href="%s">%s</a>' % return format_html('<a href="{}">{}</a>', link, display_name)
(link, obj.author.nickname if obj.author.nickname else obj.author.email)) return "-"
def show_user_image(self, obj): def show_user_image(self, obj):
img = obj.picture """显示用户头像"""
return format_html( if obj.picture:
u'<img src="%s" style="width:50px;height:50px"></img>' % return format_html(
(img)) '<img src="{}" style="width:50px;height:50px;object-fit:cover;">',
obj.picture
)
return "-"
link_to_usermodel.short_description = '用户' link_to_usermodel.short_description = '关联用户'
show_user_image.short_description = '用户头像' show_user_image.short_description = '用户头像'
class OAuthConfigAdmin(admin.ModelAdmin): class OAuthConfigAdmin(admin.ModelAdmin):
"""OAuth配置后台管理类"""
list_display = ('type', 'appkey', 'appsecret', 'is_enable') list_display = ('type', 'appkey', 'appsecret', 'is_enable')
list_filter = ('type',) list_filter = ('type',)
list_editable = ('is_enable',) #zzj: 允许直接在列表页启用/禁用配置

@ -1,4 +1,4 @@
# Create your models here. #zzj: OAuth认证相关数据模型定义
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
@ -7,61 +7,68 @@ from django.utils.translation import gettext_lazy as _
class OAuthUser(models.Model): class OAuthUser(models.Model):
"""OAuth用户模型"""
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('用户'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE
openid = models.CharField(max_length=50) )
nickname = models.CharField(max_length=50, verbose_name=_('nick name')) openid = models.CharField(max_length=50, verbose_name=_('开放ID'))
token = models.CharField(max_length=150, null=True, blank=True) nickname = models.CharField(max_length=50, verbose_name=_('昵称'))
picture = models.CharField(max_length=350, blank=True, null=True) token = models.CharField(max_length=150, null=True, blank=True, verbose_name=_('访问令牌'))
type = models.CharField(blank=False, null=False, max_length=50) picture = models.CharField(max_length=350, blank=True, null=True, verbose_name=_('头像'))
email = models.CharField(max_length=50, null=True, blank=True) type = models.CharField(max_length=50, blank=False, null=False, verbose_name=_('类型'))
metadata = models.TextField(null=True, blank=True) email = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('邮箱'))
creation_time = models.DateTimeField(_('creation time'), default=now) metadata = models.TextField(null=True, blank=True, verbose_name=_('元数据'))
last_modify_time = models.DateTimeField(_('last modify time'), default=now) creation_time = models.DateTimeField(_('创建时间'), default=now)
last_modify_time = models.DateTimeField(_('最后修改时间'), default=now)
def __str__(self): def __str__(self):
return self.nickname return f"{self.nickname}({self.type})"
class Meta: class Meta:
verbose_name = _('oauth user') verbose_name = _('OAuth用户')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
ordering = ['-creation_time'] ordering = ['-creation_time']
class OAuthConfig(models.Model): class OAuthConfig(models.Model):
TYPE = ( """OAuth配置模型"""
('weibo', _('weibo')), TYPE_CHOICES = (
('google', _('google')), ('weibo', _('微博')),
('google', _('谷歌')),
('github', 'GitHub'), ('github', 'GitHub'),
('facebook', 'FaceBook'), ('facebook', 'FaceBook'),
('qq', 'QQ'), ('qq', 'QQ'),
) )
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
type = models.CharField(_('类型'), max_length=10, choices=TYPE_CHOICES, default='weibo')
appkey = models.CharField(max_length=200, verbose_name='AppKey') appkey = models.CharField(max_length=200, verbose_name='AppKey')
appsecret = models.CharField(max_length=200, verbose_name='AppSecret') appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
callback_url = models.CharField( callback_url = models.CharField(
max_length=200, max_length=200,
verbose_name=_('callback url'), verbose_name=_('回调地址'),
blank=False, blank=False,
default='') default=''
is_enable = models.BooleanField( )
_('is enable'), default=True, blank=False, null=False) is_enable = models.BooleanField(_('是否启用'), default=True, blank=False, null=False)
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('创建时间'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('最后修改时间'), default=now)
def clean(self): def clean(self):
if OAuthConfig.objects.filter( """验证配置类型唯一性"""
type=self.type).exclude(id=self.id).count(): if OAuthConfig.objects.filter(type=self.type).exclude(id=self.id).exists():
raise ValidationError(_(self.type + _('already exists'))) raise ValidationError(_(f'{self.type}配置已存在'))
def __str__(self): def __str__(self):
return self.type return self.get_type_display()
class Meta: class Meta:
verbose_name = 'oauth配置' verbose_name = _('OAuth配置')
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
ordering = ['-creation_time'] ordering = ['-creation_time']
constraints = [
models.UniqueConstraint(fields=['type'], name='unique_oauth_type')
]

@ -1,3 +1,4 @@
#zzj: OAuth认证管理器处理各种OAuth提供商的认证流程
import json import json
import logging import logging
import os import os
@ -19,14 +20,11 @@ class OAuthAccessTokenException(Exception):
class BaseOauthManager(metaclass=ABCMeta): class BaseOauthManager(metaclass=ABCMeta):
"""获取用户授权""" """OAuth管理器基类"""
AUTH_URL = None AUTH_URL = None #zzj: 授权URL
"""获取token""" TOKEN_URL = None #zzj: 令牌获取URL
TOKEN_URL = None API_URL = None #zzj: 用户信息API URL
"""获取用户信息""" ICON_NAME = None #zzj: 提供商图标名称
API_URL = None
'''icon图标名'''
ICON_NAME = None
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
self.access_token = access_token self.access_token = access_token

@ -1,3 +1,4 @@
#zzj: OAuth认证视图处理模块
import logging import logging
# Create your views here. # Create your views here.
from urllib.parse import urlparse from urllib.parse import urlparse

Loading…
Cancel
Save