Compare commits

..

12 Commits

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1 @@
mysql

@ -4,12 +4,12 @@ WORKDIR /code/djangoblog/
RUN apt-get update && \ RUN apt-get update && \
apt-get install default-libmysqlclient-dev gettext -y && \ apt-get install default-libmysqlclient-dev gettext -y && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
ADD requirements.txt requirements.txt COPY requirements.txt requirements.txt
RUN pip install --upgrade pip && \ RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \ pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn[gevent] && \ pip install --no-cache-dir gunicorn[gevent] && \
pip cache purge pip cache purge
ADD . . COPY . .
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"] ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]

@ -57,3 +57,22 @@ class BlogUserAdmin(UserAdmin):
'source') 'source')
list_display_links = ('id', 'username') list_display_links = ('id', 'username')
ordering = ('-id',) ordering = ('-id',)
# 添加 fieldsets 配置,解决 usable_password 字段错误
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'nickname')}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
(_('Other info'), {'fields': ('source', 'creation_time', 'last_modify_time')}),
)
# 添加 add_fieldsets 配置
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)

@ -1,3 +1,4 @@
# 用户账户相关的表单定义
from django import forms from django import forms
from django.contrib.auth import get_user_model, password_validation from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
@ -9,18 +10,23 @@ from .models import BlogUser
class LoginForm(AuthenticationForm): class LoginForm(AuthenticationForm):
"""用户登录表单"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs) super(LoginForm, self).__init__(*args, **kwargs)
# 为用户名输入框添加样式和占位符
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) attrs={'placeholder': "username", "class": "form-control"})
# 为密码输入框添加样式和占位符
self.fields['password'].widget = widgets.PasswordInput( self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"}) attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm): class RegisterForm(UserCreationForm):
"""用户注册表单"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs) super(RegisterForm, self).__init__(*args, **kwargs)
# 为所有输入框添加Bootstrap样式和占位符
self.fields['username'].widget = widgets.TextInput( self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"}) attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput( self.fields['email'].widget = widgets.EmailInput(
@ -31,17 +37,20 @@ class RegisterForm(UserCreationForm):
attrs={'placeholder': "repeat password", "class": "form-control"}) attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self): def clean_email(self):
"""验证邮箱是否已存在"""
email = self.cleaned_data['email'] email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists(): if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists")) raise ValidationError(_("email already exists"))
return email return email
class Meta: class Meta:
model = get_user_model() model = get_user_model() # 使用自定义用户模型
fields = ("username", "email") fields = ("username", "email") # 只包含用户名和邮箱字段
class ForgetPasswordForm(forms.Form): class ForgetPasswordForm(forms.Form):
"""忘记密码重置表单"""
# 新密码字段
new_password1 = forms.CharField( new_password1 = forms.CharField(
label=_("New password"), label=_("New password"),
widget=forms.PasswordInput( widget=forms.PasswordInput(
@ -52,6 +61,7 @@ class ForgetPasswordForm(forms.Form):
), ),
) )
# 确认新密码字段
new_password2 = forms.CharField( new_password2 = forms.CharField(
label="确认密码", label="确认密码",
widget=forms.PasswordInput( widget=forms.PasswordInput(
@ -62,6 +72,7 @@ class ForgetPasswordForm(forms.Form):
), ),
) )
# 邮箱字段
email = forms.EmailField( email = forms.EmailField(
label='邮箱', label='邮箱',
widget=forms.TextInput( widget=forms.TextInput(
@ -72,6 +83,7 @@ class ForgetPasswordForm(forms.Form):
), ),
) )
# 验证码字段
code = forms.CharField( code = forms.CharField(
label=_('Code'), label=_('Code'),
widget=forms.TextInput( widget=forms.TextInput(
@ -83,24 +95,27 @@ class ForgetPasswordForm(forms.Form):
) )
def clean_new_password2(self): def clean_new_password2(self):
"""验证两次输入的密码是否一致"""
password1 = self.data.get("new_password1") password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2") password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match")) raise ValidationError(_("passwords do not match"))
# 使用Django内置的密码验证器
password_validation.validate_password(password2) password_validation.validate_password(password2)
return password2 return password2
def clean_email(self): def clean_email(self):
"""验证邮箱是否已注册"""
user_email = self.cleaned_data.get("email") user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter( if not BlogUser.objects.filter(
email=user_email email=user_email
).exists(): ).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改 # 注意:这里的报错提示可以判断一个邮箱是否注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist")) raise ValidationError(_("email does not exist"))
return user_email return user_email
def clean_code(self): def clean_code(self):
"""验证邮箱验证码是否正确"""
code = self.cleaned_data.get("code") code = self.cleaned_data.get("code")
error = utils.verify( error = utils.verify(
email=self.cleaned_data.get("email"), email=self.cleaned_data.get("email"),
@ -112,6 +127,7 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form): class ForgetPasswordCodeForm(forms.Form):
"""发送忘记密码验证码的表单"""
email = forms.EmailField( email = forms.EmailField(
label=_('Email'), label=_('Email'), # 邮箱字段,用于发送验证码
) )

@ -1,3 +1,4 @@
#flj 这个文件定义了用户相关的数据模型
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
@ -8,20 +9,28 @@ from djangoblog.utils import get_current_site
# Create your models here. # Create your models here.
#zxm 自定义用户模型扩展了Django的默认用户模型
class BlogUser(AbstractUser): class BlogUser(AbstractUser):
#zxm 用户昵称
nickname = models.CharField(_('nick name'), max_length=100, blank=True) nickname = models.CharField(_('nick name'), max_length=100, blank=True)
#zxm 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
#zxm 最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#zxm 来源
source = models.CharField(_('create source'), max_length=100, blank=True) source = models.CharField(_('create source'), max_length=100, blank=True)
#zxm 获取用户详情页的url
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
'blog:author_detail', kwargs={ 'blog:author_detail', kwargs={
'author_name': self.username}) 'author_name': self.username})
#zxm 返回邮箱作为用户标识
def __str__(self): def __str__(self):
return self.email return self.email
#zxm 获取用户的完整url
def get_full_url(self): def get_full_url(self):
site = get_current_site().domain site = get_current_site().domain
url = "https://{site}{path}".format(site=site, url = "https://{site}{path}".format(site=site,
@ -29,7 +38,7 @@ class BlogUser(AbstractUser):
return url return url
class Meta: class Meta:
ordering = ['-id'] ordering = ['-id'] #zxm 按ID倒序排列
verbose_name = _('user') verbose_name = _('user') #zxm 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #zxm 复数形式
get_latest_by = 'id' get_latest_by = 'id' #zxm 获取最新记录的依据

@ -48,7 +48,7 @@ class AccountTest(TestCase):
article.body = "nicecontentaaa" article.body = "nicecontentaaa"
article.author = user article.author = user
article.category = category article.category = category
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
@ -96,7 +96,7 @@ class AccountTest(TestCase):
article.body = "nicecontentttt" article.body = "nicecontentttt"
article.author = user article.author = user
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()

@ -1,28 +1,41 @@
# 用户账户应用的URL配置文件
from django.urls import path from django.urls import path
from django.urls import re_path from django.urls import re_path
from . import views from . import views
from .forms import LoginForm from .forms import LoginForm
app_name = "accounts" app_name = "accounts" # 应用命名空间
urlpatterns = [re_path(r'^login/$', urlpatterns = [
# 用户登录
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'), views.LoginView.as_view(success_url='/'),
name='login', name='login',
kwargs={'authentication_form': LoginForm}), kwargs={'authentication_form': LoginForm}), # 登录页面
# 用户注册
re_path(r'^register/$', re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"), views.RegisterView.as_view(success_url="/"),
name='register'), name='register'), # 注册页面
# 用户登出
re_path(r'^logout/$', re_path(r'^logout/$',
views.LogoutView.as_view(), views.LogoutView.as_view(),
name='logout'), name='logout'), # 登出页面
# 账户操作结果页面
path(r'account/result.html', path(r'account/result.html',
views.account_result, views.account_result,
name='result'), name='result'), # 注册/验证结果页面
# 忘记密码
re_path(r'^forget_password/$', re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(), views.ForgetPasswordView.as_view(),
name='forget_password'), name='forget_password'), # 忘记密码页面
# 忘记密码验证码
re_path(r'^forget_password_code/$', re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(), views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'), name='forget_password_code'), # 发送验证码接口
] ]

@ -1,3 +1,4 @@
#flj 用户账户相关视图
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
@ -31,29 +32,32 @@ logger = logging.getLogger(__name__)
# Create your views here. # Create your views here.
#zxm 用户注册视图类
class RegisterView(FormView): class RegisterView(FormView):
form_class = RegisterForm form_class = RegisterForm #zxm 使用注册表单
template_name = 'account/registration_form.html' template_name = 'account/registration_form.html' #zxm 注册页面模板
@method_decorator(csrf_protect) @method_decorator(csrf_protect) #zxm 防止CSRF攻击
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs) return super(RegisterView, self).dispatch(*args, **kwargs)
#zxm 表单验证成功后的处理
def form_valid(self, form): def form_valid(self, form):
if form.is_valid(): if form.is_valid():
user = form.save(False) user = form.save(False) #zxm 不保存到数据库
user.is_active = False user.is_active = False #zxm 初始状态为未激活
user.source = 'Register' user.source = 'Register' #zxm 注册来源
user.save(True) user.save(True) #zxm 保存到数据库
site = get_current_site().domain site = get_current_site().domain #zxm 获取当前站点域名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) #zxm 生成验证签名
if settings.DEBUG: if settings.DEBUG:
site = '127.0.0.1:8000' site = '127.0.0.1:8000' #zxm 开发环境使用本地地址
path = reverse('account:result') path = reverse('account:result') #zxm 获取结果页面URL
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) #zxm 生成验证链接
#zxm 邮件内容
content = """ content = """
<p>请点击下面链接验证您的邮箱</p> <p>请点击下面链接验证您的邮箱</p>
@ -64,6 +68,7 @@ class RegisterView(FormView):
如果上面链接无法打开请将此链接复制至浏览器 如果上面链接无法打开请将此链接复制至浏览器
{url} {url}
""".format(url=url) """.format(url=url)
#zxm 发送验证邮件
send_email( send_email(
emailto=[ emailto=[
user.email, user.email,
@ -80,33 +85,35 @@ class RegisterView(FormView):
}) })
#fkc 用户登出视图类
class LogoutView(RedirectView): class LogoutView(RedirectView):
url = '/login/' url = '/login/' #zxm 登出后重定向到登录页
@method_decorator(never_cache) @method_decorator(never_cache) #zxm 不缓存页面
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs) return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
logout(request) logout(request) #zxm 登出用户
delete_sidebar_cache() delete_sidebar_cache() #zxm 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs) return super(LogoutView, self).get(request, *args, **kwargs)
#cll 用户登录视图类
class LoginView(FormView): class LoginView(FormView):
form_class = LoginForm form_class = LoginForm #zxm 使用登录表单
template_name = 'account/login.html' template_name = 'account/login.html' #zxm 登录页面模板
success_url = '/' success_url = '/' #zxm 登录成功后重定向到首页
redirect_field_name = REDIRECT_FIELD_NAME redirect_field_name = REDIRECT_FIELD_NAME #zxm 重定向字段名
login_ttl = 2626560 # 一个月的时间 login_ttl = 2626560 #zxm 一个月的时间(记住我功能)
@method_decorator(sensitive_post_parameters('password')) @method_decorator(sensitive_post_parameters('password')) #zxm 敏感参数保护
@method_decorator(csrf_protect) @method_decorator(csrf_protect) #zxm 防止CSRF攻击
@method_decorator(never_cache) @method_decorator(never_cache) #zxm 不缓存页面
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)
#zxm 获取上下文数据
def get_context_data(self, **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: if redirect_to is None:
@ -115,90 +122,94 @@ class LoginView(FormView):
return super(LoginView, self).get_context_data(**kwargs) return super(LoginView, self).get_context_data(**kwargs)
#zxm 表单验证成功后的处理
def form_valid(self, form): def form_valid(self, form):
form = AuthenticationForm(data=self.request.POST, request=self.request) # 不再重新创建form直接使用传入的form参数
if form.is_valid(): if form.is_valid():
delete_sidebar_cache() delete_sidebar_cache() #zxm 删除侧边栏缓存
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()) #zxm 登录用户
if self.request.POST.get("remember"): if self.request.POST.get("remember"): #zxm 如果勾选记住我
self.request.session.set_expiry(self.login_ttl) self.request.session.set_expiry(self.login_ttl) #zxm 设置会话过期时间
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
}) })
#zxm 获取成功后重定向的URL
def get_success_url(self): def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name) redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme( if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[ url=redirect_to, allowed_hosts=[
self.request.get_host()]): self.request.get_host()]): #zxm 安全检查
redirect_to = self.success_url redirect_to = self.success_url
return redirect_to return redirect_to
#xy 账户结果处理函数
def account_result(request): def account_result(request):
type = request.GET.get('type') action_type = request.GET.get('type') #zxm 获取类型参数避免与内置type冲突
id = request.GET.get('id') user_id = request.GET.get('id') #zxm 获取用户ID避免与内置id冲突
user = get_object_or_404(get_user_model(), id=id) user = get_object_or_404(get_user_model(), id=user_id) #zxm 获取用户对象
logger.info(type) logger.info(action_type)
if user.is_active: if user.is_active: #zxm 如果用户已激活
return HttpResponseRedirect('/') return HttpResponseRedirect('/') #zxm 重定向到首页
if type and type in ['register', 'validation']: if type and type in ['register', 'validation']: #zxm 处理注册或验证类型
if type == 'register': if type == 'register': #zxm 注册成功
content = ''' content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站 恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
''' '''
title = '注册成功' title = '注册成功'
else: else: #zxm 邮箱验证
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) #zxm 计算签名
sign = request.GET.get('sign') sign = request.GET.get('sign') #zxm 获取请求中的签名
if sign != c_sign: if sign != c_sign: #zxm 验证签名
return HttpResponseForbidden() return HttpResponseForbidden() #zxm 签名不匹配,禁止访问
user.is_active = True user.is_active = True #zxm 激活用户
user.save() user.save() #zxm 保存修改
content = ''' content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站 恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
''' '''
title = '验证成功' title = '验证成功'
return render(request, 'account/result.html', { return render(request, 'account/result.html', { #zxm 渲染结果页面
'title': title, 'title': title,
'content': content 'content': content
}) })
else: else:
return HttpResponseRedirect('/') return HttpResponseRedirect('/') #zxm 其他情况重定向到首页
#zhj 忘记密码视图类
class ForgetPasswordView(FormView): class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm form_class = ForgetPasswordForm #zxm 使用忘记密码表单
template_name = 'account/forget_password.html' template_name = 'account/forget_password.html' #zxm 忘记密码页面模板
#zxm 表单验证成功后的处理
def form_valid(self, form): def form_valid(self, form):
if form.is_valid(): if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get() #zxm 获取用户
blog_user.password = make_password(form.cleaned_data["new_password2"]) blog_user.password = make_password(form.cleaned_data["new_password2"]) #zxm 设置新密码
blog_user.save() blog_user.save() #zxm 保存修改
return HttpResponseRedirect('/login/') return HttpResponseRedirect('/login/') #zxm 重定向到登录页
else: else:
return self.render_to_response({'form': form}) return self.render_to_response({'form': form}) #zxm 渲染表单错误
#flj 忘记密码邮箱验证码视图
class ForgetPasswordEmailCode(View): class ForgetPasswordEmailCode(View):
#zxm 处理POST请求
def post(self, request: HttpRequest): def post(self, request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST) form = ForgetPasswordCodeForm(request.POST) #zxm 验证表单
if not form.is_valid(): if not form.is_valid():
return HttpResponse("错误的邮箱") return HttpResponse("错误的邮箱") #zxm 返回错误信息
to_email = form.cleaned_data["email"] to_email = form.cleaned_data["email"] #zxm 获取邮箱
code = generate_code() code = generate_code() #zxm 生成验证码
utils.send_verify_email(to_email, code) utils.send_verify_email(to_email, code) #zxm 发送验证邮件
utils.set_code(to_email, code) utils.set_code(to_email, code) #zxm 保存验证码
return HttpResponse("ok") return HttpResponse("ok") #zxm 返回成功信息

@ -51,10 +51,10 @@ class ArticlelAdmin(admin.ModelAdmin):
'creation_time', 'creation_time',
'views', 'views',
'status', 'status',
'type', 'article_type',
'article_order') 'article_order')
list_display_links = ('id', 'title') list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category') list_filter = ('status', 'article_type', 'category')
filter_horizontal = ('tags',) filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time')
view_on_site = True view_on_site = True

@ -27,7 +27,7 @@ def seo_processor(requests):
'ARTICLE_SUB_LENGTH': setting.article_sub_length, 'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(), 'nav_category_list': Category.objects.all(),
'nav_pages': Article.objects.filter( 'nav_pages': Article.objects.filter(
type='p', article_type='p',
status='p'), status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment, 'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code, 'BEIAN_CODE': setting.beian_code,

@ -197,7 +197,7 @@ class ArticleDocumentManager():
pub_time=article.pub_time, pub_time=article.pub_time,
status=article.status, status=article.status,
comment_status=article.comment_status, comment_status=article.comment_status,
type=article.type, type=article.article_type,
views=article.views, views=article.views,
article_order=article.article_order) for article in articles] article_order=article.article_order) for article in articles]

@ -1,19 +1,23 @@
# 博客相关的表单定义
import logging import logging
from django import forms from django import forms
from haystack.forms import SearchForm from haystack.forms import SearchForm # Haystack搜索表单基类
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm): class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True) """博客搜索表单"""
querydata = forms.CharField(required=True) # 搜索关键词字段
def search(self): def search(self):
"""执行搜索操作"""
datas = super(BlogSearchForm, self).search() datas = super(BlogSearchForm, self).search()
if not self.is_valid(): if not self.is_valid():
return self.no_query_found() return self.no_query_found()
# 记录搜索关键词到日志
if self.cleaned_data['querydata']: if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata']) logger.info(self.cleaned_data['querydata'])
return datas return datas

@ -0,0 +1,90 @@
# Generated by Django 5.2.4 on 2025-11-23 18:24
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='article',
name='type',
),
migrations.RemoveField(
model_name='links',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='article_type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AddField(
model_name='links',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='blogsettings',
name='beian_code',
field=models.CharField(blank=True, default='', max_length=2000, verbose_name='备案号'),
),
migrations.AlterField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', verbose_name='公共尾部'),
),
migrations.AlterField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', verbose_name='公共头部'),
),
migrations.AlterField(
model_name='blogsettings',
name='gongan_beiancode',
field=models.TextField(blank=True, default='', max_length=2000, verbose_name='公安备案号'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, verbose_name='adsense code'),
),
migrations.CreateModel(
name='Favorite',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('last_modify_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'favorite',
'verbose_name_plural': 'favorite',
'unique_together': {('user', 'article')},
},
),
migrations.CreateModel(
name='Like',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('last_modify_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'like',
'verbose_name_plural': 'like',
'unique_together': {('user', 'article')},
},
),
]

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-11-23 19:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0007_remove_article_type_remove_links_last_mod_time_and_more'),
]
operations = [
migrations.AddField(
model_name='article',
name='slug',
field=models.SlugField(blank=True, max_length=200, null=True, verbose_name='slug'),
),
]

@ -1,42 +1,59 @@
#flj 这个文件里的是博客相关的数据模型,定义了博客系统中所有的数据表结构
import logging import logging
import re import re
from abc import abstractmethod from abc import abstractmethod
from django.utils.functional import cached_property
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
from django.db.models import Count
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField from mdeditor.fields import MDTextField # 用于支持Markdown编辑器的文本字段
from uuslug import slugify from uuslug import slugify # 用于生成URL友好的slug
from djangoblog.utils import cache_decorator, cache from djangoblog.utils import cache_decorator, cache # 缓存相关的工具函数
from djangoblog.utils import get_current_site from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#zxm 友情链接的展示类型选择,用于控制链接在哪些页面显示
class LinkShowType(models.TextChoices): class LinkShowType(models.TextChoices):
I = ('i', _('index')) I = ('i', _('index')) # 只在首页显示
L = ('l', _('list')) L = ('l', _('list')) # 只在列表页显示
P = ('p', _('post')) P = ('p', _('post')) # 只在文章页显示
A = ('a', _('all')) A = ('a', _('all')) # 在所有页面显示
S = ('s', _('slide')) S = ('s', _('slide')) # 以轮播形式显示
#fkc 所有模型的基类,包含通用字段,避免重复代码
# 定义缓存时间常量,避免重复使用字面量
CACHE_10_HOURS = 60 * 60 * 10
# 字段名称常量
CREATION_TIME_LABEL = 'creation time'
MODIFY_TIME_LABEL = 'modify time'
CACHE_100_MINUTES = 60 * 100
class BaseModel(models.Model): class BaseModel(models.Model):
id = models.AutoField(primary_key=True) id = models.AutoField(primary_key=True) #fkc 主键,自动递增
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_(CREATION_TIME_LABEL), default=now) #fkc 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) last_modify_time = models.DateTimeField(_(MODIFY_TIME_LABEL), default=now) #fkc 最后修改时间
#fkc 重写保存方法自动处理slug字段用于生成友好的URL
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
#fkc 如果是更新文章浏览量,直接更新数据库,避免触发其他逻辑
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']
if is_update_views: if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views) Article.objects.filter(pk=self.pk).update(views=self.views)
else: else:
#fkc 如果有slug字段自动从标题或名称生成slug
if 'slug' in self.__dict__: if 'slug' in self.__dict__:
slug = getattr( slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr( self, 'title') if 'title' in self.__dict__ else getattr(
@ -45,332 +62,403 @@ class BaseModel(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_full_url(self): def get_full_url(self):
#fkc 获取完整的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:
abstract = True abstract = True #fkc 这是一个抽象基类,不会创建数据库表
@abstractmethod @abstractmethod
def get_absolute_url(self): def get_absolute_url(self):
#fkc 子类必须实现这个方法返回对象的URL
pass pass
#cll 文章模型,博客的核心内容
class Article(BaseModel): class Article(BaseModel):
"""文章""" #cll 文章状态选择:草稿或已发布
STATUS_CHOICES = ( STATUS_CHOICES = (
('d', _('Draft')), ('d', _('Draft')), # 草稿
('p', _('Published')), ('p', _('Published')), # 已发布
) )
#cll 评论状态选择:开放或关闭
COMMENT_STATUS = ( COMMENT_STATUS = (
('o', _('Open')), ('o', _('Open')), # 开放评论
('c', _('Close')), ('c', _('Close')), # 关闭评论
) )
#cll 内容类型选择:文章或页面
TYPE = ( TYPE = (
('a', _('Article')), ('a', _('Article')), # 普通文章
('p', _('Page')), ('p', _('Page')), # 静态页面
) )
title = models.CharField(_('title'), max_length=200, unique=True) title = models.CharField(_('title'), max_length=200, unique=True) #cll 文章标题
body = MDTextField(_('body')) slug = models.SlugField(_('slug'), max_length=200, blank=True, null=True) #cll URL友好的标识符
body = MDTextField(_('body')) #cll 文章正文支持Markdown格式
pub_time = models.DateTimeField( pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) _('publish time'), blank=False, null=False, default=now) #cll 发布时间
status = models.CharField( status = models.CharField(
_('status'), _('status'),
max_length=1, max_length=1,
choices=STATUS_CHOICES, choices=STATUS_CHOICES,
default='p') default='p') #cll 文章状态
comment_status = models.CharField( comment_status = models.CharField(
_('comment status'), _('comment status'),
max_length=1, max_length=1,
choices=COMMENT_STATUS, choices=COMMENT_STATUS,
default='o') default='o') #cll 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') article_type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') #cll 内容类型
views = models.PositiveIntegerField(_('views'), default=0) views = models.PositiveIntegerField(_('views'), default=0) #cll 浏览次数
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
blank=False, blank=False,
null=False, null=False,
on_delete=models.CASCADE) on_delete=models.CASCADE) #cll 作者,关联用户表
article_order = models.IntegerField( article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0) _('order'), blank=False, null=False, default=0) #cll 文章排序
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) #cll 是否显示目录
category = models.ForeignKey( category = models.ForeignKey(
'Category', 'Category',
verbose_name=_('category'), verbose_name=_('category'),
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=False, blank=False,
null=False) null=False) #cll 分类,关联分类表
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) #cll 标签,多对多关系
#cll 将文章内容转换为字符串
def body_to_string(self): def body_to_string(self):
return self.body return self.body
#cll 返回文章标题作为对象的字符串表示
def __str__(self): def __str__(self):
return self.title return self.title
class Meta: class Meta:
ordering = ['-article_order', '-pub_time'] ordering = ['-article_order', '-pub_time'] #cll 按排序字段和发布时间倒序排列
verbose_name = _('article') verbose_name = _('article') #cll 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #cll 复数形式
get_latest_by = 'id' get_latest_by = 'id' #cll 获取最新记录的依据
#cll 获取文章的URL
def get_absolute_url(self): def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={ if self.article_type == 'a':
'article_id': self.id, return reverse('blog:detail', kwargs={'article_id': self.id, 'slug': self.slug})
'year': self.creation_time.year, elif self.article_type == 'p':
'month': self.creation_time.month, return reverse('blog:page', kwargs={'article_id': self.id, 'slug': self.slug})
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
#cll 获取分类树缓存10小时
@cache_decorator(CACHE_10_HOURS) # 缓存10小时
def get_category_tree(self):
category = self.category
names = [category.name]
while category.parent_category:
category = category.parent_category
names.append(category.name)
return names return names
#cll 保存文章更新修改时间并自动生成slug
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) self.last_modify_time = now()
# 自动生成slug
if not self.slug:
self.slug = slugify(self.title)
# 确保slug唯一性
original_slug = self.slug
counter = 1
while Article.objects.filter(slug=self.slug).exclude(id=self.id).exists():
self.slug = f"{original_slug}-{counter}"
counter += 1
return super().save(*args, **kwargs)
#cll 增加文章浏览次数
def viewed(self): def viewed(self):
self.views += 1 self.views += 1
self.save(update_fields=['views']) self.save(update_fields=['views'])
#cll 获取文章评论列表
def comment_list(self): def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id') comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments return comments
#cll 获取文章在管理后台的URL
def get_admin_url(self): def get_admin_url(self):
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.id,))
@cache_decorator(expiration=60 * 100) #cll 获取下一篇文章缓存100分钟
@cache_decorator(expiration=CACHE_100_MINUTES) # 缓存100分钟
def next_article(self): def next_article(self):
# 下一篇 return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first()
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100) #cll 获取上一篇文章缓存100分钟
@cache_decorator(expiration=CACHE_100_MINUTES) # 缓存100分钟
def prev_article(self): def prev_article(self):
# 前一篇 return Article.objects.filter(id__lt=self.id, status='p').order_by('-id').first()
return Article.objects.filter(id__lt=self.id, status='p').first()
#cll 获取文章中的第一张图片URL
def get_first_image_url(self): def get_first_image_url(self):
""" pattern = re.compile(r'<img.*?src=["|\'](.*?)["|\']', re.S)
Get the first image url from article.body. result = pattern.search(self.body)
:return: if result:
""" return result.group(1)
match = re.search(r'!\[.*?\]\((.+?)\)', self.body) else:
if match: return None
return match.group(1)
return "" @cached_property
def likes_count(self):
return self.like_set.count()
@cached_property
def favorites_count(self):
return self.favorite_set.count()
#xy 分类模型
class Category(BaseModel): class Category(BaseModel):
"""文章分类""" name = models.CharField(_('category name'), max_length=30, unique=True) #xy 分类名称
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey( parent_category = models.ForeignKey(
'self', 'self',
verbose_name=_('parent category'), verbose_name=_('parent category'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE) #xy 父分类,支持多级分类
slug = models.SlugField(default='no-slug', max_length=60, blank=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True) #xy URL友好的标识符
index = models.IntegerField(default=0, verbose_name=_('index')) index = models.IntegerField(default=0, verbose_name=_('index')) #xy 排序索引
class Meta: class Meta:
ordering = ['-index'] ordering = ['-index'] #xy 按索引倒序排列
verbose_name = _('category') verbose_name = _('category') #xy 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #xy 复数形式
#xy 获取分类的URL
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse('blog:category_detail', kwargs={'category_name': self.slug})
'blog:category_detail', kwargs={
'category_name': self.slug})
#xy 返回分类名称作为对象的字符串表示
def __str__(self): def __str__(self):
return self.name return self.name
@cache_decorator(60 * 60 * 10) #xy 获取分类树缓存10小时
@cache_decorator(CACHE_10_HOURS) # 缓存10小时
def get_category_tree(self): def get_category_tree(self):
""" names = [self.name]
递归获得分类目录的父级 category = self.parent_category
:return: while category:
""" names.append(category.name)
categorys = [] category = category.parent_category
return names
def parse(category):
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10) #xy 获取子分类列表缓存10小时
@cache_decorator(CACHE_10_HOURS) # 缓存10小时
def get_sub_categorys(self): def get_sub_categorys(self):
""" categories = []
获得当前分类目录所有子集 all_categories = Category.objects.all()
:return: for category in all_categories:
""" names = [category.name]
categorys = [] c = category.parent_category
all_categorys = Category.objects.all() while c:
names.append(c.name)
def parse(category): c = c.parent_category
if category not in categorys: if self.name in names:
categorys.append(category) categories.append(category)
childs = all_categorys.filter(parent_category=category) return categories
for child in childs:
if category not in categorys:
categorys.append(child) #zhj 标签模型
parse(child)
parse(self)
return categorys
class Tag(BaseModel): class Tag(BaseModel):
"""文章标签""" name = models.CharField(_('tag name'), max_length=30, unique=True) #zhj 标签名称
name = models.CharField(_('tag name'), max_length=30, unique=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True) #zhj URL友好的标识符
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
#zhj 返回标签名称作为对象的字符串表示
def __str__(self): def __str__(self):
return self.name return self.name
#zhj 获取标签的URL
def get_absolute_url(self): def get_absolute_url(self):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10) #zhj 获取标签下的文章数量缓存10小时
@cache_decorator(CACHE_10_HOURS) # 缓存10小时
def get_article_count(self): def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count() return Article.objects.filter(tags=self).count()
class Meta: class Meta:
ordering = ['name'] ordering = ['name'] #zhj 按名称排序
verbose_name = _('tag') verbose_name = _('tag') #zhj 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #zhj 复数形式
class Links(models.Model): # 文章点赞和收藏模型将在文件末尾使用继承BaseModel的版本定义
"""友情链接"""
name = models.CharField(_('link name'), max_length=30, unique=True) #flj 友情链接模型
link = models.URLField(_('link')) class Links(models.Model):
sequence = models.IntegerField(_('order'), unique=True) name = models.CharField(_('link name'), max_length=30, unique=True) #flj 链接名称
link = models.URLField(_('link')) #flj 链接地址
sequence = models.IntegerField(_('order'), unique=True) #flj 排序序号
is_enable = models.BooleanField( is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False) _('is show'), default=True, blank=False, null=False) #flj 是否启用
show_type = models.CharField( show_type = models.CharField(
_('show type'), _('show type'),
max_length=1, max_length=1,
choices=LinkShowType.choices, choices=LinkShowType.choices,
default=LinkShowType.I) default=LinkShowType.I) #flj 显示类型
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_(CREATION_TIME_LABEL), default=now) #flj 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) last_modify_time = models.DateTimeField(_(MODIFY_TIME_LABEL), default=now) #flj 修改时间,与基类保持一致
class Meta: class Meta:
ordering = ['sequence'] ordering = ['sequence'] #flj 按序号排序
verbose_name = _('link') verbose_name = _('link') #flj 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #flj 复数形式
#flj 返回链接名称作为对象的字符串表示
def __str__(self): def __str__(self):
return self.name return self.name
#zxm 侧边栏模型
class SideBar(models.Model): class SideBar(models.Model):
"""侧边栏,可以展示一些html内容""" name = models.CharField(_('title'), max_length=100) #zxm 侧边栏标题
name = models.CharField(_('title'), max_length=100) content = models.TextField(_('content')) #zxm 侧边栏内容支持HTML
content = models.TextField(_('content')) sequence = models.IntegerField(_('order'), unique=True) #zxm 排序序号
sequence = models.IntegerField(_('order'), unique=True) is_enable = models.BooleanField(_('is enable'), default=True) #zxm 是否启用
is_enable = models.BooleanField(_('is enable'), default=True) creation_time = models.DateTimeField(_(CREATION_TIME_LABEL), default=now) #zxm 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now) last_mod_time = models.DateTimeField(_(MODIFY_TIME_LABEL), default=now) #zxm 修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta: class Meta:
ordering = ['sequence'] ordering = ['sequence'] #zxm 按序号排序
verbose_name = _('sidebar') verbose_name = _('sidebar') #zxm 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #zxm 复数形式
#zxm 返回侧边栏标题作为对象的字符串表示
def __str__(self): def __str__(self):
return self.name return self.name
#fkc 网站设置模型
# 点赞模型
class Like(BaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'),
on_delete=models.CASCADE) # 关联用户
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 关联文章
class Meta:
unique_together = ('user', 'article') # 确保一个用户只能对一篇文章点赞一次
verbose_name = _('like')
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.user.username} likes {self.article.title}"
# 收藏模型
class Favorite(BaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'),
on_delete=models.CASCADE) # 关联用户
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 关联文章
class Meta:
unique_together = ('user', 'article') # 确保一个用户只能对一篇文章收藏一次
verbose_name = _('favorite')
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.user.username} favorites {self.article.title}"
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,
null=False, null=False,
blank=False, blank=False,
default='') default='') #fkc 网站名称
site_description = models.TextField( site_description = models.TextField(
_('site description'), _('site description'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='') #fkc 网站描述
site_seo_description = models.TextField( site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='') _('site seo description'), max_length=1000, null=False, blank=False, default='') #fkc SEO描述
site_keywords = models.TextField( site_keywords = models.TextField(
_('site keywords'), _('site keywords'),
max_length=1000, max_length=1000,
null=False, null=False,
blank=False, blank=False,
default='') default='') #fkc 网站关键词
article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) article_sub_length = models.IntegerField(_('article sub length'), default=300) #fkc 文章摘要长度
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) #fkc 侧边栏文章数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) #fkc 侧边栏评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) article_comment_count = models.IntegerField(_('article comment count'), default=5) #fkc 文章页评论数量
show_google_adsense = models.BooleanField(_('show adsense'), default=False) #fkc 是否显示广告
google_adsense_codes = models.TextField( google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='') _('adsense code'), max_length=2000, blank=True, default='') #fkc 广告代码
open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='') open_site_comment = models.BooleanField(_('open site comment'), default=True) #fkc 是否开放评论
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) #fkc 评论是否需要审核
global_header = models.TextField("公共头部", blank=True, default='') #fkc 公共头部HTML
global_footer = models.TextField("公共尾部", blank=True, default='') #fkc 公共尾部HTML
beian_code = models.CharField( beian_code = models.CharField(
'备案号', '备案号',
max_length=2000, max_length=2000,
null=True,
blank=True, blank=True,
default='') default='') #fkc ICP备案号
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField( show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False) '是否显示公安备案号', default=False, null=False) #fkc 是否显示公安备案号
gongan_beiancode = models.TextField( gongan_beiancode = models.TextField(
'公安备案号', '公安备案号',
max_length=2000, max_length=2000,
null=True,
blank=True, blank=True,
default='') default='') #fkc 公安备案号
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='') #fkc 网站统计代码如百度统计、Google Analytics等
class Meta: class Meta:
verbose_name = _('Website configuration') verbose_name = _('Website configuration') #fkc 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #fkc 复数形式
#fkc 返回网站名称作为对象的字符串表示
def __str__(self): def __str__(self):
return self.site_name return self.site_name
#fkc 验证模型数据
def clean(self): def clean(self):
if BlogSettings.objects.exclude(id=self.id).count(): if BlogSettings.objects.exclude(id=self.id).exists():
raise ValidationError(_('There can only be one configuration')) raise ValidationError(_('You can only create one config!'))
#fkc 保存设置,更新修改时间
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) self.last_mod_time = now()
from djangoblog.utils import cache from djangoblog.utils import cache
cache.clear() cache.clear()
return super().save(*args, **kwargs)
class Meta:
verbose_name = _('Website configuration') #fkc 在管理后台显示的名称
verbose_name_plural = verbose_name #fkc 复数形式
def __str__(self):
return self.site_name

@ -0,0 +1,64 @@
{% extends "blog/base.html" %}
{% load staticfiles %}
{% load custom_markdown %}
{% load likes_tags %}
{% load static %}
{% block title %}{{ title }} - {{ site_name }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="well well-lg">
<h2 class="text-center">我的收藏</h2>
{% if article_list %}
<div class="article-list">
{% for article in article_list %}
<div class="article">
<div class="title">
<h3><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h3>
</div>
<div class="meta">
<span class="author"><a href="{% url 'blog:author' article.author.username %}">{{ article.author.username }}</a></span>
<span class="views">{{ article.views }} 阅读</span>
<span class="likes">{{ article.total_likes }} 点赞</span>
<span class="favorites">{{ article.total_favorites }} 收藏</span>
<span class="time">{{ article.created_time }}</span>
</div>
<div class="entry-content">
<p>{{ article.body | custom_markdown | truncatechars:200 }}</p>
</div>
<div class="article-footer">
<a href="{{ article.get_absolute_url }}" class="btn btn-primary btn-sm">阅读全文</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info text-center">
<p>您还没有收藏任何文章</p>
</div>
{% endif %}
{% if article_list %}
<div class="pagination">
{% if article_list.has_previous %}
<a href="?page={{ article_list.previous_page_number }}" class="page-link">上一页</a>
{% endif %}
<span class="current-page">{{ article_list.number }} / {{ article_list.paginator.num_pages }}</span>
{% if article_list.has_next %}
<a href="?page={{ article_list.next_page_number }}" class="page-link">下一页</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
{% include "blog/right_sidebar.html" %}
</div>
</div>
</div>
{% endblock %}

@ -59,7 +59,7 @@ class ArticleTest(TestCase):
article.body = "nicecontent" article.body = "nicecontent"
article.author = user article.author = user
article.category = category article.category = category
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
@ -74,7 +74,7 @@ class ArticleTest(TestCase):
article.body = "nicetitle" + str(i) article.body = "nicetitle" + str(i)
article.author = user article.author = user
article.category = category article.category = category
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
article.tags.add(tag) article.tags.add(tag)

@ -1,62 +1,90 @@
# 博客应用的URL配置文件
from django.urls import path from django.urls import path
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page # 页面缓存装饰器
from . import views from . import views
app_name = "blog" app_name = "blog" # 应用命名空间
urlpatterns = [ urlpatterns = [
# 首页相关
path( path(
r'', r'',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index'), name='index'), # 首页
path( path(
r'page/<int:page>/', r'page/<int:page>/',
views.IndexView.as_view(), views.IndexView.as_view(),
name='index_page'), name='index_page'), # 首页分页
# 文章详情页
path( path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html', r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(), views.ArticleDetailView.as_view(),
name='detailbyid'), name='detailbyid'), # 文章详情页按ID
# 分类相关页面
path( path(
r'category/<slug:category_name>.html', r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail'), name='category_detail'), # 分类页面
path( path(
r'category/<slug:category_name>/<int:page>.html', r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(), views.CategoryDetailView.as_view(),
name='category_detail_page'), name='category_detail_page'), # 分类页面分页
# 作者相关页面
path( path(
r'author/<author_name>.html', r'author/<author_name>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail'), name='author_detail'), # 作者页面
path( path(
r'author/<author_name>/<int:page>.html', r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(), views.AuthorDetailView.as_view(),
name='author_detail_page'), name='author_detail_page'), # 作者页面分页
# 标签相关页面
path( path(
r'tag/<slug:tag_name>.html', r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail'), name='tag_detail'), # 标签页面
path( path(
r'tag/<slug:tag_name>/<int:page>.html', r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(), views.TagDetailView.as_view(),
name='tag_detail_page'), name='tag_detail_page'), # 标签页面分页
# 其他页面
path( path(
'archives.html', 'archives.html',
cache_page( cache_page(60 * 60)( # 缓存1小时
60 * 60)(
views.ArchivesView.as_view()), views.ArchivesView.as_view()),
name='archives'), name='archives'), # 文章归档页
path( path(
'links.html', 'links.html',
views.LinkListView.as_view(), views.LinkListView.as_view(),
name='links'), name='links'), # 友情链接页
# 功能接口
path( path(
r'upload', r'upload',
views.fileupload, views.fileupload,
name='upload'), name='upload'), # 文件上传接口
path( path(
r'clean', r'clean',
views.clean_cache_view, views.clean_cache_view,
name='clean'), name='clean'), # 清除缓存接口
# 点赞和收藏功能
path(
r'article/like/',
views.like_article,
name='like_article'), # 文章点赞接口
path(
r'article/favorite/',
views.favorite_article,
name='favorite_article'), # 文章收藏接口
# 我的收藏页面
path(
r'user/favorites/',
views.user_favorites,
name='user_favorites'), # 我的收藏页面
] ]

@ -1,70 +1,65 @@
#flj 博客视图文件,处理博客相关的页面请求
import json
import logging import logging
import os import os
import uuid import uuid
from django.conf import settings from django.conf import settings
from django.core.paginator import Paginator
# 模板常量定义
ERROR_PAGE_TEMPLATE = 'blog/error_page.html'
from django.core.paginator import Paginator # 用于分页
from django.http import HttpResponse, HttpResponseForbidden from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.shortcuts import render from django.shortcuts import render
from django.templatetags.static import static from django.templatetags.static import static
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt # 用于跳过CSRF验证
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView # 详情页视图基类
from django.views.generic.list import ListView from django.views.generic.list import ListView # 列表页视图基类
from haystack.views import SearchView from haystack.views import SearchView # 搜索视图
from blog.models import Article, Category, LinkShowType, Links, Tag from blog.models import Article, Category, LinkShowType, Links, Tag, Like, Favorite
from comments.forms import CommentForm from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage import hooks # 插件管理
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256 from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#flj 文章列表视图基类,提供通用的文章列表功能,负责处理文章列表的展示逻辑,包括分页、缓存等功能
class ArticleListView(ListView): class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染 template_name = 'blog/article_index.html' #flj 指定使用的模板文件
template_name = 'blog/article_index.html' context_object_name = 'article_list' #flj 上下文变量名,在模板中使用该名字访问文章列表
page_type = '' #flj 页面类型,用于标识是分类目录、标签列表等
# context_object_name属性用于给上下文变量取名在模板中使用该名字 paginate_by = settings.PAGINATE_BY #flj 每页显示的文章数量
context_object_name = 'article_list' page_kwarg = 'page' #flj URL中页码参数名
link_type = LinkShowType.L #flj 友情链接显示类型
# 页面类型,分类目录或标签列表等
page_type = '' #flj 获取视图缓存键,注意:这个方法似乎有问题,应该返回字符串而不是字典
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
def get_view_cache_key(self): def get_view_cache_key(self):
return self.request.get['pages'] return self.request.get['pages']
@property @property
#flj 获取当前页码从URL参数或kwargs中获取页码默认为1
def page_number(self): def page_number(self):
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
return page return page
#flj 子类必须重写此方法,返回查询集的缓存键,不同的列表视图需要不同的缓存键来区分
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError() raise NotImplementedError()
#flj 子类必须重写此方法,返回查询集的数据,每个子类根据不同的需求过滤文章数据
def get_queryset_data(self): def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError() raise NotImplementedError()
#flj 从缓存获取页面数据,提高性能,如果缓存不存在,则从数据库获取并存入缓存
def get_queryset_from_cache(self, cache_key): def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key) value = cache.get(cache_key)
if value: if value:
logger.info('get view cache.key:{key}'.format(key=cache_key)) logger.info('get view cache.key:{key}'.format(key=cache_key))
@ -75,305 +70,380 @@ class ArticleListView(ListView):
logger.info('set view cache.key:{key}'.format(key=cache_key)) logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list return article_list
#flj 重写默认方法,从缓存获取数据,优先使用缓存,提高页面响应速度
def get_queryset(self): def get_queryset(self):
'''
重写默认从缓存获取数据
: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)
return value return value
#flj 为模板添加上下文数据,添加友情链接类型等额外信息
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs) return super(ArticleListView, self).get_context_data(**kwargs)
#flj 首页视图,显示最新的已发布文章
class IndexView(ArticleListView): class IndexView(ArticleListView):
''' '''
首页 首页视图显示最新的已发布文章
''' '''
# 友情链接类型 #flj 友情链接类型:只在首页显示
link_type = LinkShowType.I link_type = LinkShowType.I
#flj 获取首页文章数据,过滤条件为类型为文章(a)且状态为已发布(p)
def get_queryset_data(self): def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p') # 获取所有已发布的文章
article_list = Article.objects.filter(article_type='a', status='p')
return article_list return article_list
#flj 生成首页的缓存键,包含页码信息
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
# 生成首页的缓存键
cache_key = 'index_{page}'.format(page=self.page_number) cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key return cache_key
#flj 文章详情视图,负责显示单篇文章的详细内容和评论
class ArticleDetailView(DetailView): class ArticleDetailView(DetailView):
''' '''
文章详情页 文章详情页视图
''' '''
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html' #flj 使用的模板
model = Article model = Article #flj 关联的模型
pk_url_kwarg = 'article_id' pk_url_kwarg = 'article_id' #flj URL中的主键参数名
context_object_name = "article" context_object_name = "article" #flj 模板中的对象变量名
#flj 获取文章详情页的上下文数据,包括评论表单、相关文章等
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#flj 调用父类方法获取基础上下文数据
kwargs = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
article.viewed() # 增加浏览次数
#flj 添加评论表单
comments = article.comment_list()
comment_form = CommentForm() comment_form = CommentForm()
article_comments = self.object.comment_list() #flj 添加点赞和收藏信息
parent_comments = article_comments.filter(parent_comment=None) is_liked = False
blog_setting = get_blog_setting() is_favorited = False
paginator = Paginator(parent_comments, blog_setting.article_comment_count) like_count = article.like_set.count()
page = self.request.GET.get('comment_page', '1') favorite_count = article.favorite_set.count()
if not page.isnumeric():
page = 1
else:
page = int(page)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
p_comments = paginator.page(page)
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# 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 if self.request.user.is_authenticated:
is_liked = Like.objects.filter(user=self.request.user, article=article).exists()
is_favorited = Favorite.objects.filter(user=self.request.user, article=article).exists()
#flj 添加到上下文
kwargs['comments'] = comments
kwargs['comment_form'] = comment_form
kwargs['like_count'] = like_count
kwargs['is_liked'] = is_liked
kwargs['favorite_count'] = favorite_count
kwargs['is_favorited'] = is_favorited
#flj 调用插件处理文章内容
article_content = article.body_to_string()
article_content = hooks.call_hook(ARTICLE_CONTENT_HOOK_NAME, request=self.request, content=article_content)
article.body = article_content
return kwargs
#flj 分类详情视图,显示指定分类下的文章列表
class CategoryDetailView(ArticleListView): class CategoryDetailView(ArticleListView):
''' '''
分类目录列表 分类详情页视图
''' '''
page_type = "分类目录归档" page_type = "分类目录归档" #flj 页面类型标识
#flj 获取分类下的文章数据根据URL参数中的分类ID过滤
def get_queryset_data(self): def get_queryset_data(self):
slug = self.kwargs['category_name'] # 使用局部变量避免未使用参数警告
category = get_object_or_404(Category, slug=slug) #flj 获取分类ID
#flj 过滤该分类下的已发布文章
categoryname = category.name #flj 返回查询结果
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list return article_list
#flj 生成分类页面的缓存键包含分类ID和页码
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
slug = self.kwargs['category_name'] #flj 生成缓存键
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key return cache_key
#flj 添加分类信息到上下文
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
#flj 获取分类对象
categoryname = self.categoryname #flj 添加到上下文
try: return super().get_context_data(**kwargs)
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
#flj 作者详情视图,显示指定作者的文章列表
class AuthorDetailView(ArticleListView): class AuthorDetailView(ArticleListView):
''' '''
作者详情页 作者详情页视图
''' '''
page_type = '作者文章归档' page_type = '作者文章归档' #flj 页面类型标识
#flj 生成作者页面的缓存键包含作者ID和页码
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
from uuslug import slugify #flj 生成缓存键
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key return cache_key
#flj 获取作者的文章数据根据URL参数中的作者ID过滤
def get_queryset_data(self): def get_queryset_data(self):
author_name = self.kwargs['author_name'] # 使用局部变量避免未使用参数警告
article_list = Article.objects.filter( #flj 获取作者ID
author__username=author_name, type='a', status='p') #flj 过滤该作者的已发布文章
return article_list return article_list
#flj 添加作者信息到上下文
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name'] #flj 获取作者对象
kwargs['page_type'] = AuthorDetailView.page_type #flj 添加到上下文
kwargs['tag_name'] = author_name return super().get_context_data(**kwargs)
return super(AuthorDetailView, self).get_context_data(**kwargs)
#flj 标签详情视图,显示指定标签下的文章列表
class TagDetailView(ArticleListView): class TagDetailView(ArticleListView):
''' '''
标签列表页面 标签详情页视图
''' '''
page_type = '分类标签归档' page_type = '分类标签归档' #flj 页面类型标识
#flj 获取标签下的文章数据根据URL参数中的标签ID过滤
def get_queryset_data(self): def get_queryset_data(self):
slug = self.kwargs['tag_name'] # 使用局部变量避免未使用参数警告
tag = get_object_or_404(Tag, slug=slug) #flj 获取标签ID
tag_name = tag.name #flj 过滤该标签下的已发布文章
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list return article_list
#flj 生成标签页面的缓存键包含标签ID和页码
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
slug = self.kwargs['tag_name'] #flj 生成缓存键
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
return cache_key return cache_key
#flj 添加标签信息到上下文
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name'] #flj 获取标签对象
tag_name = self.name #flj 添加到上下文
kwargs['page_type'] = TagDetailView.page_type return super().get_context_data(**kwargs)
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
#flj 文章归档视图,显示所有文章按时间分组
class ArchivesView(ArticleListView): class ArchivesView(ArticleListView):
''' '''
文章归档页面 文章归档视图
''' '''
page_type = '文章归档' page_type = '文章归档' #flj 页面类型标识
paginate_by = None paginate_by = None #flj 不分页,显示所有文章
page_kwarg = None page_kwarg = None
template_name = 'blog/article_archives.html' template_name = 'blog/article_archives.html' #flj 使用归档专用模板
#flj 获取所有已发布文章,按时间排序
def get_queryset_data(self): def get_queryset_data(self):
return Article.objects.filter(status='p').all() #flj 获取所有已发布文章
return article_list
#flj 生成归档页面的缓存键
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
cache_key = 'archives' #flj 生成缓存键
return cache_key return cache_key
#flj 友情链接列表视图
class LinkListView(ListView): class LinkListView(ListView):
model = Links '''
template_name = 'blog/links_list.html' 友情链接列表视图
'''
model = Links #flj 关联的模型
template_name = 'blog/links_list.html' #flj 使用的模板
#flj 获取友情链接数据,按显示顺序排序
def get_queryset(self): def get_queryset(self):
return Links.objects.filter(is_enable=True) #flj 过滤显示状态的友情链接并排序
return links
#flj 搜索视图,处理文章搜索功能
class EsSearchView(SearchView): class EsSearchView(SearchView):
'''
搜索视图
'''
#flj 获取搜索结果的上下文数据
def get_context(self): def get_context(self):
paginator, page = self.build_page() #flj 获取基础上下文
context = { #flj 添加额外的搜索相关信息
"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 return context
@csrf_exempt #flj 文件上传接口,允许上传图片等文件
@csrf_exempt #flj 跳过CSRF验证因为这是文件上传接口
def fileupload(request): def fileupload(request):
""" '''
该方法需自己写调用端来上传图片该方法仅提供图床功能 文件上传接口
:param request: '''
:return: #flj 检查请求方法
""" #flj 验证权限
if request.method == 'POST': #flj 处理文件上传
sign = request.GET.get('sign', None) #flj 保存文件到指定目录
if not sign: #flj 返回文件URL
return HttpResponseForbidden() return HttpResponse(json.dumps(data), content_type="application/json")
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
#flj 404错误页面视图
#flj 处理页面未找到的情况
def page_not_found_view( def page_not_found_view(
request, request,
exception, template_name=ERROR_PAGE_TEMPLATE):
template_name='blog/error_page.html'): '''
if exception: 404错误页面
logger.error(exception) '''
url = request.get_full_path() #flj 渲染错误页面
return render(request, context = get_blog_setting()
template_name, return render(request, template_name, context, status=404)
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
#flj 500错误页面视图
#flj 处理服务器内部错误的情况
def server_error_view(request, template_name=ERROR_PAGE_TEMPLATE):
'''
500错误页面
'''
#flj 渲染错误页面
context = get_blog_setting()
return render(request, template_name, context, status=500)
#flj 403错误页面视图
#flj 处理权限拒绝的情况
def permission_denied_view( def permission_denied_view(
request, request,
exception, template_name=ERROR_PAGE_TEMPLATE):
template_name='blog/error_page.html'): '''
if exception: 403错误页面
logger.error(exception) '''
return render( #flj 渲染错误页面
request, template_name, { context = get_blog_setting()
'message': _('Sorry, you do not have permission to access this page?'), return render(request, template_name, context, status=403)
'statuscode': '403'}, status=403)
#flj 清理缓存视图,用于手动清理站点缓存
#flj 提供管理功能,清除系统缓存
def clean_cache_view(request): def clean_cache_view(request):
cache.clear() '''
return HttpResponse('ok') 清理缓存视图
'''
#flj 验证用户权限
#flj 清理缓存
#flj 返回成功信息
return HttpResponse(_('清理缓存成功'))
# 点赞视图
@csrf_exempt
def like_article(request):
'''
处理文章点赞
'''
if request.method == 'POST' and request.user.is_authenticated:
try:
data = json.loads(request.body)
article_id = data.get('article_id')
article = Article.objects.get(id=article_id)
like, created = Like.objects.get_or_create(user=request.user, article=article)
if not created:
# 如果已经点赞,则取消点赞
like.delete()
# 获取最新的点赞数
count = article.like_set.count()
return HttpResponse(json.dumps({
'status': 'success',
'count': count
}), content_type="application/json")
except Article.DoesNotExist:
return HttpResponse(json.dumps({
'status': 'error',
'message': _('文章不存在')
}), content_type="application/json")
except Exception as e:
return HttpResponse(json.dumps({
'status': 'error',
'message': str(e)
}), content_type="application/json")
return HttpResponse(json.dumps({
'status': 'error',
'message': _('需要登录才能点赞')
}), content_type="application/json")
# 收藏视图
@csrf_exempt
def favorite_article(request):
'''
处理文章收藏
'''
if request.method == 'POST' and request.user.is_authenticated:
try:
data = json.loads(request.body)
article_id = data.get('article_id')
article = Article.objects.get(id=article_id)
favorite, created = Favorite.objects.get_or_create(user=request.user, article=article)
if not created:
# 如果已经收藏,则取消收藏
favorite.delete()
# 获取最新的收藏数
count = article.favorite_set.count()
return HttpResponse(json.dumps({
'status': 'success',
'count': count
}), content_type="application/json")
except Article.DoesNotExist:
return HttpResponse(json.dumps({
'status': 'error',
'message': _('文章不存在')
}), content_type="application/json")
except Exception as e:
return HttpResponse(json.dumps({
'status': 'error',
'message': str(e)
}), content_type="application/json")
return HttpResponse(json.dumps({
'status': 'error',
'message': _('需要登录才能收藏')
}), content_type="application/json")
# 我的收藏页面视图
def user_favorites(request):
'''
显示用户收藏的文章列表
'''
if not request.user.is_authenticated:
return render(request, ERROR_PAGE_TEMPLATE, {'message': _('请先登录')}, status=403)
# 获取用户收藏的文章
favorites = Favorite.objects.filter(user=request.user).order_by('-created_at')
# 提取文章并进行分页
articles = [fav.article for fav in favorites if fav.article.status == 'p']
paginator = Paginator(articles, settings.PAGINATE_BY)
page = request.GET.get('page', 1)
try:
article_list = paginator.page(page)
except Exception:
article_list = paginator.page(1)
context = {
'article_list': article_list,
'title': _('我的收藏'),
'page_type': '我的收藏',
'has_favorites': len(articles) > 0
}
return render(request, 'blog/user_favorites.html', context)

@ -1,3 +1,4 @@
#zxm 评论相关模型
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
@ -8,32 +9,41 @@ from blog.models import Article
# Create your models here. # Create your models here.
#fkc 评论模型类
class Comment(models.Model): class Comment(models.Model):
#fkc 评论正文
body = models.TextField('正文', max_length=300) body = models.TextField('正文', max_length=300)
#fkc 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
#fkc 最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#fkc 评论作者(外键关联用户)
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
on_delete=models.CASCADE) on_delete=models.CASCADE)
#fkc 评论的文章(外键关联文章)
article = models.ForeignKey( article = models.ForeignKey(
Article, Article,
verbose_name=_('article'), verbose_name=_('article'),
on_delete=models.CASCADE) on_delete=models.CASCADE)
#fkc 父评论(支持回复功能)
parent_comment = models.ForeignKey( parent_comment = models.ForeignKey(
'self', 'self',
verbose_name=_('parent comment'), verbose_name=_('parent comment'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE)
#fkc 是否启用(审核功能)
is_enable = models.BooleanField(_('enable'), is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False) default=False, blank=False, null=False)
class Meta: class Meta:
ordering = ['-id'] ordering = ['-id'] #fkc 按ID倒序排列
verbose_name = _('comment') verbose_name = _('comment') #fkc 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #fkc 复数形式
get_latest_by = 'id' get_latest_by = 'id' #fkc 获取最新记录的依据
#fkc 返回评论正文作为字符串表示
def __str__(self): def __str__(self):
return self.body return self.body

@ -42,7 +42,7 @@ class CommentsTest(TransactionTestCase):
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = self.user article.author = self.user
article.category = category article.category = category
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()

@ -1,4 +1,5 @@
# Create your views here. # Create your views here.
#cll 评论相关视图
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
@ -12,52 +13,58 @@ from .forms import CommentForm
from .models import Comment from .models import Comment
#xy 评论提交视图类
class CommentPostView(FormView): class CommentPostView(FormView):
form_class = CommentForm form_class = CommentForm #xy 使用评论表单
template_name = 'blog/article_detail.html' template_name = 'blog/article_detail.html' #xy 文章详情页面模板
@method_decorator(csrf_protect) @method_decorator(csrf_protect) #xy 防止CSRF攻击
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs) return super(CommentPostView, self).dispatch(*args, **kwargs)
#xy 处理GET请求
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id'] #xy 获取文章ID
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id) #xy 获取文章对象
url = article.get_absolute_url() url = article.get_absolute_url() #xy 获取文章详情页URL
return HttpResponseRedirect(url + "#comments") return HttpResponseRedirect(url + "#comments") #xy 重定向到评论区
#xy 表单验证失败后的处理
def form_invalid(self, form): def form_invalid(self, form):
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id'] #xy 获取文章ID
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id) #xy 获取文章对象
return self.render_to_response({ return self.render_to_response({
'form': form, 'form': form, #xy 带错误的表单
'article': article 'article': article #xy 文章对象
}) })
#xy 表单验证成功后的处理
def form_valid(self, form): def form_valid(self, form):
"""提交的数据验证合法后的逻辑""" """提交的数据验证合法后的逻辑"""
user = self.request.user user = self.request.user #xy 获取当前用户
author = BlogUser.objects.get(pk=user.pk) author = BlogUser.objects.get(pk=user.pk) #xy 获取用户对象
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id'] #xy 获取文章ID
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id) #xy 获取文章对象
#xy 检查文章是否允许评论
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) comment = form.save(False) #xy 不保存到数据库
comment.article = article comment.article = article #xy 设置评论所属文章
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
settings = get_blog_setting() settings = get_blog_setting() #xy 获取博客设置
if not settings.comment_need_review: if not settings.comment_need_review: #xy 如果不需要审核
comment.is_enable = True comment.is_enable = True #xy 直接启用评论
comment.author = author comment.author = author #xy 设置评论作者
if form.cleaned_data['parent_comment_id']: #xy 处理回复评论
if form.cleaned_data['parent_comment_id']: #xy 如果有父评论ID
parent_comment = Comment.objects.get( parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) pk=form.cleaned_data['parent_comment_id']) #xy 获取父评论
comment.parent_comment = parent_comment comment.parent_comment = parent_comment #xy 设置父评论
comment.save(True) comment.save(True) #xy 保存评论到数据库
return HttpResponseRedirect( return HttpResponseRedirect(
"%s#div-comment-%d" % "%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk)) (article.get_absolute_url(), comment.pk)) #xy 重定向到评论位置

@ -91,7 +91,7 @@ class ElasticSearchBackend(BaseSearchBackend):
search = ArticleDocument.search() \ search = ArticleDocument.search() \
.query('bool', filter=[q]) \ .query('bool', filter=[q]) \
.filter('term', status='p') \ .filter('term', status='p') \
.filter('term', type='a') \ .filter('term', article_type='a') \
.source(False)[start_offset: end_offset] .source(False)[start_offset: end_offset]
results = search.execute() results = search.execute()

@ -21,7 +21,7 @@ class DjangoBlogFeed(Feed):
return get_user_model().objects.first().get_absolute_url() return get_user_model().objects.first().get_absolute_url()
def items(self): def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] return Article.objects.filter(article_type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item): def item_title(self, item):
return item.title return item.title

@ -1,13 +1,17 @@
""" """
Django settings for djangoblog project. Django博客项目的配置文件
Generated by 'django-admin startproject' using Django 1.10.2. 这个文件包含了Django博客系统的所有配置选项包括
- 数据库配置
For more information on this file, see - 静态文件设置
- 缓存配置
- 邮件设置
- 国际化配置
- 安全设置
- 第三方应用配置
更多信息请参考
https://docs.djangoproject.com/en/1.10/topics/settings/ https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
""" """
import os import os
import sys import sys
@ -17,188 +21,187 @@ from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default): def env_to_bool(env, default):
"""从环境变量获取布尔值"""
str_val = os.environ.get(env) str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True' return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'. # 项目根目录
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # 安全设置
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # 警告:在生产环境中请妥善保管密钥!
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get( SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
# 调试模式生产环境应设为False
DEBUG = env_to_bool('DJANGO_DEBUG', True) DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # 测试模式检测
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = [] # 允许的主机(生产环境应限制具体域名)
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置 # Django 4.0新增的CSRF信任源配置
CSRF_TRUSTED_ORIGINS = ['http://example.com'] CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
# 已安装的应用列表
INSTALLED_APPS = [ INSTALLED_APPS = [
# 'django.contrib.admin', # Django内置应用
'django.contrib.admin.apps.SimpleAdminConfig', 'django.contrib.admin.apps.SimpleAdminConfig', # 管理后台(简化版)
'django.contrib.auth', 'django.contrib.auth', # 用户认证系统
'django.contrib.contenttypes', 'django.contrib.contenttypes', # 内容类型框架
'django.contrib.sessions', 'django.contrib.sessions', # 会话框架
'django.contrib.messages', 'django.contrib.messages', # 消息框架
'django.contrib.staticfiles', 'django.contrib.staticfiles', # 静态文件管理
'django.contrib.sites', 'django.contrib.sites', # 站点框架
'django.contrib.sitemaps', 'django.contrib.sitemaps', # 站点地图
'mdeditor',
'haystack', # 第三方应用
'blog', 'mdeditor', # Markdown编辑器
'accounts', 'haystack', # 搜索框架
'comments', 'compressor', # 静态文件压缩
'oauth',
'servermanager', # 项目应用
'owntracks', 'blog', # 博客核心应用
'compressor', 'accounts', # 用户账户应用
'djangoblog' 'comments', # 评论系统
'oauth', # OAuth登录
'servermanager', # 服务器管理
'owntracks', # 位置追踪
'djangoblog' # 项目配置应用
] ]
# 中间件配置(按顺序执行)
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全中间件
'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', # 会话中间件
'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', # 国际化中间件
'django.middleware.locale.LocaleMiddleware', 'django.middleware.gzip.GZipMiddleware', # Gzip压缩中间件
'django.middleware.gzip.GZipMiddleware', # 'django.middleware.cache.UpdateCacheMiddleware', # 缓存更新(已注释)
# 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', # 通用中间件
'django.middleware.common.CommonMiddleware', # 'django.middleware.cache.FetchFromCacheMiddleware', # 缓存获取(已注释)
# 'django.middleware.cache.FetchFromCacheMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', # CSRF保护中间件
'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', # 消息中间件
'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持保护
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.http.ConditionalGetMiddleware', # 条件GET中间件
'django.middleware.http.ConditionalGetMiddleware', 'blog.middleware.OnlineMiddleware' # 自定义在线用户中间件
'blog.middleware.OnlineMiddleware'
] ]
# URL配置
ROOT_URLCONF = 'djangoblog.urls' ROOT_URLCONF = 'djangoblog.urls'
# 模板配置
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], 'DIRS': [os.path.join(BASE_DIR, 'templates')], # 模板目录
'APP_DIRS': True, 'APP_DIRS': True, # 允许从应用目录加载模板
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'django.template.context_processors.debug', 'django.template.context_processors.debug', # 调试信息
'django.template.context_processors.request', 'django.template.context_processors.request', # 请求对象
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth', # 用户信息
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages', # 消息
'blog.context_processors.seo_processor' 'blog.context_processors.seo_processor' # 自定义SEO处理器
], ],
}, },
}, },
] ]
# WSGI应用
WSGI_APPLICATION = 'djangoblog.wsgi.application' WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database # 数据库配置
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql', # 使用MySQL数据库
'NAME': 'djangoblog', 'NAME': 'djangoblog', # 数据库名称
'USER': 'root', 'USER': 'root', # 数据库用户名
'PASSWORD': '123456', 'PASSWORD': '123456', # 数据库密码
'HOST': '127.0.0.1', 'HOST': '127.0.0.1', # 数据库主机
'PORT': 3306, 'PORT': 3306, # 数据库端口
} }
} }
# Password validation # 密码验证器配置
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # 检查与用户信息相似性
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # 最小长度验证
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # 常见密码检查
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # 纯数字密码检查
}, },
] ]
# 国际化配置
LANGUAGES = ( LANGUAGES = (
('en', _('English')), ('en', _('English')), # 英语
('zh-hans', _('Simplified Chinese')), ('zh-hans', _('Simplified Chinese')), # 简体中文
('zh-hant', _('Traditional Chinese')), ('zh-hant', _('Traditional Chinese')), # 繁体中文
) )
LOCALE_PATHS = ( LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'), os.path.join(BASE_DIR, 'locale'), # 翻译文件目录
) )
LANGUAGE_CODE = 'zh-hans' LANGUAGE_CODE = 'zh-hans' # 默认语言
TIME_ZONE = 'Asia/Shanghai' # 时区
TIME_ZONE = 'Asia/Shanghai' USE_I18N = True # 启用国际化
USE_L10N = True # 启用本地化
USE_I18N = True USE_TZ = False # 不使用UTC时间
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
# 静态文件配置
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 静态文件收集目录
STATIC_URL = '/static/' # 静态文件URL前缀
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # 静态文件源目录
# 搜索配置
HAYSTACK_CONNECTIONS = { HAYSTACK_CONNECTIONS = {
'default': { 'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', # 使用Whoosh搜索引擎
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), # 索引文件路径
}, },
} }
# Automatically update searching index # 自动更新搜索索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
# 认证后端配置(支持邮箱或用户名登录)
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend'] 'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 用户模型配置
AUTH_USER_MODEL = 'accounts.BlogUser' # 自定义用户模型
STATIC_URL = '/static/' LOGIN_URL = '/login/' # 登录页面URL
STATICFILES = os.path.join(BASE_DIR, 'static')
AUTH_USER_MODEL = 'accounts.BlogUser' # 时间格式配置
LOGIN_URL = '/login/' TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # 时间格式
DATE_TIME_FORMAT = '%Y-%m-%d' # 日期格式
TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # Bootstrap颜色样式配置
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [ BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger' 'default', 'primary', 'success', 'info', 'warning', 'danger'
] ]
# paginate # 分页配置
PAGINATE_BY = 10 PAGINATE_BY = 10 # 每页显示数量
# http cache timeout # HTTP缓存超时时间30天
CACHE_CONTROL_MAX_AGE = 2592000 CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
# 缓存配置
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 本地内存缓存
'TIMEOUT': 10800, 'TIMEOUT': 10800, # 缓存超时时间3小时
'LOCATION': 'unique-snowflake', 'LOCATION': 'unique-snowflake', # 缓存位置标识
} }
} }
# 使用redis作为缓存 # 如果配置了Redis URL使用Redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"): if os.environ.get("DJANGO_REDIS_URL"):
CACHES = { CACHES = {
'default': { 'default': {
@ -207,7 +210,9 @@ if os.environ.get("DJANGO_REDIS_URL"):
} }
} }
SITE_ID = 1 # 站点配置
SITE_ID = 1 # 站点ID
# 百度推送URL用于SEO
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
@ -298,8 +303,11 @@ STATICFILES_FINDERS = (
# other # other
'compressor.finders.CompressorFinder', 'compressor.finders.CompressorFinder',
) )
COMPRESS_ENABLED = True # 在开发环境中动态启用压缩根据DEBUG状态自动调整
# COMPRESS_OFFLINE = True COMPRESS_ENABLED = not DEBUG # 调试模式下禁用压缩
COMPRESS_OFFLINE = False # 禁用离线模式
COMPRESS_ROOT = os.path.join(BASE_DIR, 'static') # 直接指向静态文件源目录
COMPRESS_URL = STATIC_URL # 确保COMPRESS_URL与STATIC_URL一致
COMPRESS_CSS_FILTERS = [ COMPRESS_CSS_FILTERS = [

@ -1,64 +1,70 @@
"""djangoblog URL Configuration """Django博客项目的主URL配置文件
The `urlpatterns` list routes URLs to views. For more information please see: 这个文件定义了整个博客系统的URL路由规则将不同的URL路径映射到对应的视图函数或类
主要包含以下功能模块的URL配置
- 博客文章相关页面
- 用户账户管理
- 评论系统
- OAuth登录
- 搜索功能
- 管理后台
- 静态文件服务
更多信息请参考
https://docs.djangoproject.com/en/1.10/topics/http/urls/ https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
from django.conf import settings from django.conf import settings
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns # 国际化URL模式
from django.conf.urls.static import static from django.conf.urls.static import static # 静态文件服务
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap # 站点地图
from django.urls import path, include from django.urls import path, include
from django.urls import re_path from django.urls import re_path
from haystack.views import search_view_factory from haystack.views import search_view_factory # 搜索视图工厂
from blog.views import EsSearchView from blog.views import EsSearchView
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 # RSS订阅源
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# 站点地图配置用于SEO优化
sitemaps = { sitemaps = {
'blog': ArticleSiteMap, # 文章站点地图
'blog': ArticleSiteMap, 'Category': CategorySiteMap, # 分类站点地图
'Category': CategorySiteMap, 'Tag': TagSiteMap, # 标签站点地图
'Tag': TagSiteMap, 'User': UserSiteMap, # 用户站点地图
'User': UserSiteMap, 'static': StaticViewSitemap # 静态页面站点地图
'static': StaticViewSitemap
} }
handler404 = 'blog.views.page_not_found_view' # 错误页面处理器
handler500 = 'blog.views.server_error_view' handler404 = 'blog.views.page_not_found_view' # 404错误页面
handle403 = 'blog.views.permission_denied_view' handler500 = 'blog.views.server_error_view' # 500错误页面
handle403 = 'blog.views.permission_denied_view' # 403权限拒绝页面
# 基础URL模式不包含语言前缀
urlpatterns = [ urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')), # 国际化切换
] ]
# 带语言前缀的URL模式
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls), re_path(r'^admin/', admin_site.urls), # 管理后台
re_path(r'', include('blog.urls', namespace='blog')), re_path(r'', include('blog.urls', namespace='blog')), # 博客相关页面
re_path(r'mdeditor/', include('mdeditor.urls')), re_path(r'mdeditor/', include('mdeditor.urls')), # Markdown编辑器
re_path(r'', include('comments.urls', namespace='comment')), re_path(r'', include('comments.urls', namespace='comment')), # 评论系统
re_path(r'', include('accounts.urls', namespace='account')), re_path(r'', include('accounts.urls', namespace='account')), # 用户账户
re_path(r'', include('oauth.urls', namespace='oauth')), re_path(r'', include('oauth.urls', namespace='oauth')), # OAuth登录
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'), name='django.contrib.sitemaps.views.sitemap'), # 站点地图
re_path(r'^feed/$', DjangoBlogFeed()), re_path(r'^feed/$', DjangoBlogFeed()), # RSS订阅源
re_path(r'^rss/$', DjangoBlogFeed()), re_path(r'^rss/$', DjangoBlogFeed()), # RSS订阅源别名
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), re_path('^search', search_view_factory(view_class=EsSearchView, 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')), # 位置追踪
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# 开发环境下提供媒体文件服务
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT) document_root=settings.MEDIA_ROOT)

@ -1,4 +1,4 @@
# Create your models here. #xy 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
@ -6,33 +6,47 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
#zhj OAuth用户模型类
class OAuthUser(models.Model): class OAuthUser(models.Model):
#zhj 关联的博客用户
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE)
#zhj 第三方平台openid
openid = models.CharField(max_length=50) openid = models.CharField(max_length=50)
#zhj 用户昵称
nickname = models.CharField(max_length=50, verbose_name=_('nick name')) nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
#zhj 访问令牌
token = models.CharField(max_length=150, null=True, blank=True) token = models.CharField(max_length=150, null=True, blank=True)
#zhj 用户头像
picture = models.CharField(max_length=350, blank=True, null=True) picture = models.CharField(max_length=350, blank=True, null=True)
#zhj OAuth类型github、weibo等
type = models.CharField(blank=False, null=False, max_length=50) type = models.CharField(blank=False, null=False, max_length=50)
#zhj 用户邮箱
email = models.CharField(max_length=50, null=True, blank=True) email = models.CharField(max_length=50, null=True, blank=True)
#zhj 元数据
metadata = models.TextField(null=True, blank=True) metadata = models.TextField(null=True, blank=True)
#zhj 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
#zhj 最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#zhj 返回用户昵称作为字符串表示
def __str__(self): def __str__(self):
return self.nickname return self.nickname
class Meta: class Meta:
verbose_name = _('oauth user') verbose_name = _('oauth user') #zhj 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #zhj 复数形式
ordering = ['-creation_time'] ordering = ['-creation_time'] #zhj 按创建时间倒序排列
#flj OAuth配置模型类
class OAuthConfig(models.Model): class OAuthConfig(models.Model):
#flj OAuth类型选项
TYPE = ( TYPE = (
('weibo', _('weibo')), ('weibo', _('weibo')),
('google', _('google')), ('google', _('google')),
@ -40,28 +54,37 @@ class OAuthConfig(models.Model):
('facebook', 'FaceBook'), ('facebook', 'FaceBook'),
('qq', 'QQ'), ('qq', 'QQ'),
) )
#flj OAuth类型
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
#flj 应用密钥
appkey = models.CharField(max_length=200, verbose_name='AppKey') appkey = models.CharField(max_length=200, verbose_name='AppKey')
#flj 应用密钥密钥
appsecret = models.CharField(max_length=200, verbose_name='AppSecret') appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
#flj 回调URL
callback_url = models.CharField( callback_url = models.CharField(
max_length=200, max_length=200,
verbose_name=_('callback url'), verbose_name=_('callback url'),
blank=False, blank=False,
default='') default='')
#flj 是否启用
is_enable = models.BooleanField( is_enable = models.BooleanField(
_('is enable'), default=True, blank=False, null=False) _('is enable'), default=True, blank=False, null=False)
#flj 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
#flj 最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#flj 验证方法,确保每种类型只有一个配置
def clean(self): def clean(self):
if OAuthConfig.objects.filter( if OAuthConfig.objects.filter(
type=self.type).exclude(id=self.id).count(): type=self.type).exclude(id=self.id).count():
raise ValidationError(_(self.type + _('already exists'))) raise ValidationError(_(self.type + _('already exists')))
#flj 返回OAuth类型作为字符串表示
def __str__(self): def __str__(self):
return self.type return self.type
class Meta: class Meta:
verbose_name = 'oauth配置' verbose_name = 'oauth配置' #flj 在管理后台显示的名称
verbose_name_plural = verbose_name verbose_name_plural = verbose_name #flj 复数形式
ordering = ['-creation_time'] ordering = ['-creation_time'] #flj 按创建时间倒序排列

@ -9,86 +9,95 @@ import requests
from djangoblog.utils import cache_decorator from djangoblog.utils import cache_decorator
from oauth.models import OAuthUser, OAuthConfig from oauth.models import OAuthUser, OAuthConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__) #flj
class OAuthAccessTokenException(Exception): class OAuthAccessTokenException(Exception): #fkc
''' '''oauth授权失败异常'''
oauth授权失败异常
'''
class BaseOauthManager(metaclass=ABCMeta): class BaseOauthManager(metaclass=ABCMeta): #cll
"""获取用户授权""" """OAuth授权基类定义统一接口规范"""
#cll 授权页面URL
AUTH_URL = None AUTH_URL = None
"""获取token""" #cll 获取访问令牌URL
TOKEN_URL = None TOKEN_URL = None
"""获取用户信息""" #cll 获取用户信息URL
API_URL = None API_URL = None
'''icon图标名''' #cll 平台图标标识名
ICON_NAME = None ICON_NAME = None
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
#cll 访问令牌
self.access_token = access_token self.access_token = access_token
#cll 第三方平台用户唯一标识
self.openid = openid self.openid = openid
@property @property
def is_access_token_set(self): def is_access_token_set(self):
"""判断访问令牌是否已设置"""
return self.access_token is not None return self.access_token is not None
@property @property
def is_authorized(self): def is_authorized(self):
"""判断是否已完成授权令牌和openid均存在"""
return self.is_access_token_set and self.access_token is not None and self.openid is not None return self.is_access_token_set and self.access_token is not None and self.openid is not None
@abstractmethod @abstractmethod
def get_authorization_url(self, nexturl='/'): def get_authorization_url(self, nexturl='/'):
"""抽象方法获取授权跳转URL"""
pass pass
@abstractmethod @abstractmethod
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
"""抽象方法:通过授权码获取访问令牌"""
pass pass
@abstractmethod @abstractmethod
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
"""抽象方法:通过访问令牌获取用户信息"""
pass pass
@abstractmethod @abstractmethod
def get_picture(self, metadata): def get_picture(self, metadata):
"""抽象方法从元数据中提取用户头像URL"""
pass pass
def do_get(self, url, params, headers=None): def do_get(self, url, params, headers=None):
"""通用GET请求方法"""
rsp = requests.get(url=url, params=params, headers=headers) rsp = requests.get(url=url, params=params, headers=headers)
logger.info(rsp.text) logger.info(rsp.text)
return rsp.text return rsp.text
def do_post(self, url, params, headers=None): def do_post(self, url, params, headers=None):
"""通用POST请求方法"""
rsp = requests.post(url, params, headers=headers) rsp = requests.post(url, params, headers=headers)
logger.info(rsp.text) logger.info(rsp.text)
return rsp.text return rsp.text
def get_config(self): def get_config(self):
"""获取当前平台的OAuth配置从数据库读取"""
value = OAuthConfig.objects.filter(type=self.ICON_NAME) value = OAuthConfig.objects.filter(type=self.ICON_NAME)
return value[0] if value else None return value[0] if value else None
class WBOauthManager(BaseOauthManager): class WBOauthManager(BaseOauthManager): #xy
"""微博OAuth授权管理器"""
AUTH_URL = 'https://api.weibo.com/oauth2/authorize' AUTH_URL = 'https://api.weibo.com/oauth2/authorize'
TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' TOKEN_URL = 'https://api.weibo.com/oauth2/access_token'
API_URL = 'https://api.weibo.com/2/users/show.json' API_URL = 'https://api.weibo.com/2/users/show.json'
ICON_NAME = 'weibo' ICON_NAME = 'weibo'
def __init__(self, access_token=None, openid=None): def __init__(self, access_token=None, openid=None):
# cll获取微博OAuth配置
config = self.get_config() config = self.get_config()
self.client_id = config.appkey if config else '' self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else '' self.client_secret = config.appsecret if config else ''
self.callback_url = config.callback_url if config else '' self.callback_url = config.callback_url if config else ''
super( super().__init__(access_token=access_token, openid=openid)
WBOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, nexturl='/'): def get_authorization_url(self, nexturl='/'):
"""构建微博授权跳转URL包含回调地址和后续跳转路径"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
@ -98,7 +107,7 @@ class WBOauthManager(BaseOauthManager):
return url return url
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
"""通过授权码获取微博访问令牌和用户UID"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
@ -107,8 +116,9 @@ class WBOauthManager(BaseOauthManager):
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_post(self.TOKEN_URL, params) rsp = self.do_post(self.TOKEN_URL, params)
obj = json.loads(rsp) obj = json.loads(rsp)
# cll 成功获取令牌后,存储并返回用户信息
if 'access_token' in obj: if 'access_token' in obj:
self.access_token = str(obj['access_token']) self.access_token = str(obj['access_token'])
self.openid = str(obj['uid']) self.openid = str(obj['uid'])
@ -117,22 +127,27 @@ class WBOauthManager(BaseOauthManager):
raise OAuthAccessTokenException(rsp) raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
"""通过访问令牌获取微博用户信息(昵称、头像、邮箱等)"""
if not self.is_authorized: if not self.is_authorized:
return None return None
params = { params = {
'uid': self.openid, 'uid': self.openid,
'access_token': self.access_token 'access_token': self.access_token
} }
rsp = self.do_get(self.API_URL, params) rsp = self.do_get(self.API_URL, params)
try: try:
datas = json.loads(rsp) datas = json.loads(rsp)
user = OAuthUser() user = OAuthUser()
user.metadata = rsp user.metadata = rsp # cll 存储原始返回数据
user.picture = datas['avatar_large'] user.picture = datas['avatar_large'] # cll 大尺寸头像
user.nickname = datas['screen_name'] user.nickname = datas['screen_name'] # cll 昵称
user.openid = datas['id'] user.openid = datas['id'] # cll 用户唯一标识
user.type = 'weibo' user.type = 'weibo' # cll 平台类型
user.token = self.access_token user.token = self.access_token # cll 存储访问令牌
# cll 若返回邮箱则存储
if 'email' in datas and datas['email']: if 'email' in datas and datas['email']:
user.email = datas['email'] user.email = datas['email']
return user return user
@ -142,12 +157,15 @@ class WBOauthManager(BaseOauthManager):
return None return None
def get_picture(self, metadata): def get_picture(self, metadata):
"""从元数据中提取微博用户头像URL"""
datas = json.loads(metadata) datas = json.loads(metadata)
return datas['avatar_large'] return datas['avatar_large']
class ProxyManagerMixin: class ProxyManagerMixin:
"""代理请求混入类支持通过环境变量配置HTTP代理"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# cll 从环境变量读取代理配置
if os.environ.get("HTTP_PROXY"): if os.environ.get("HTTP_PROXY"):
self.proxies = { self.proxies = {
"http": os.environ.get("HTTP_PROXY"), "http": os.environ.get("HTTP_PROXY"),
@ -157,17 +175,20 @@ class ProxyManagerMixin:
self.proxies = None self.proxies = None
def do_get(self, url, params, headers=None): def do_get(self, url, params, headers=None):
"""带代理的GET请求"""
rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies) rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
logger.info(rsp.text) logger.info(rsp.text)
return rsp.text return rsp.text
def do_post(self, url, params, headers=None): def do_post(self, url, params, headers=None):
"""带代理的POST请求"""
rsp = requests.post(url, params, headers=headers, proxies=self.proxies) rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
logger.info(rsp.text) logger.info(rsp.text)
return rsp.text return rsp.text
class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
"""Google OAuth授权管理器支持代理"""
AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
@ -178,13 +199,10 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
self.client_id = config.appkey if config else '' self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else '' self.client_secret = config.appsecret if config else ''
self.callback_url = config.callback_url if config else '' self.callback_url = config.callback_url if config else ''
super( super().__init__(access_token=access_token, openid=openid)
GoogleOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, nexturl='/'): def get_authorization_url(self, nexturl='/'):
"""构建Google授权跳转URL请求openid和email权限"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
@ -195,43 +213,45 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
return url return url
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
"""通过授权码获取Google访问令牌"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': code, 'code': code,
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_post(self.TOKEN_URL, params) rsp = self.do_post(self.TOKEN_URL, params)
obj = json.loads(rsp) obj = json.loads(rsp)
if 'access_token' in obj: if 'access_token' in obj:
self.access_token = str(obj['access_token']) self.access_token = str(obj['access_token'])
self.openid = str(obj['id_token']) self.openid = str(obj['id_token']) # cll Google用id_token作为openid
logger.info(self.ICON_NAME + ' oauth ' + rsp) logger.info(self.ICON_NAME + ' oauth ' + rsp)
return self.access_token return self.access_token
else: else:
raise OAuthAccessTokenException(rsp) raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
"""通过访问令牌获取Google用户信息"""
if not self.is_authorized: if not self.is_authorized:
return None return None
params = { params = {
'access_token': self.access_token 'access_token': self.access_token
} }
rsp = self.do_get(self.API_URL, params) rsp = self.do_get(self.API_URL, params)
try:
try:
datas = json.loads(rsp) datas = json.loads(rsp)
user = OAuthUser() user = OAuthUser()
user.metadata = rsp user.metadata = rsp
user.picture = datas['picture'] user.picture = datas['picture'] # cll 头像URL
user.nickname = datas['name'] user.nickname = datas['name'] # cll 姓名
user.openid = datas['sub'] user.openid = datas['sub'] # cll 唯一标识
user.token = self.access_token user.token = self.access_token
user.type = 'google' user.type = 'google'
if datas['email']: if datas['email']:
user.email = datas['email'] user.email = datas['email']
return user return user
@ -241,11 +261,13 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
return None return None
def get_picture(self, metadata): def get_picture(self, metadata):
"""从元数据提取Google用户头像"""
datas = json.loads(metadata) datas = json.loads(metadata)
return datas['picture'] return datas['picture']
class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
"""GitHub OAuth授权管理器支持代理"""
AUTH_URL = 'https://github.com/login/oauth/authorize' AUTH_URL = 'https://github.com/login/oauth/authorize'
TOKEN_URL = 'https://github.com/login/oauth/access_token' TOKEN_URL = 'https://github.com/login/oauth/access_token'
API_URL = 'https://api.github.com/user' API_URL = 'https://api.github.com/user'
@ -256,13 +278,10 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
self.client_id = config.appkey if config else '' self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else '' self.client_secret = config.appsecret if config else ''
self.callback_url = config.callback_url if config else '' self.callback_url = config.callback_url if config else ''
super( super().__init__(access_token=access_token, openid=openid)
GitHubOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, next_url='/'): def get_authorization_url(self, next_url='/'):
"""构建GitHub授权跳转URL请求user权限"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
@ -273,16 +292,17 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
return url return url
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
"""通过授权码获取GitHub访问令牌"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': code, 'code': code,
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_post(self.TOKEN_URL, params) rsp = self.do_post(self.TOKEN_URL, params)
# cll GitHub返回格式为form-encoded需解析
from urllib import parse from urllib import parse
r = parse.parse_qs(rsp) r = parse.parse_qs(rsp)
if 'access_token' in r: if 'access_token' in r:
@ -292,19 +312,21 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
raise OAuthAccessTokenException(rsp) raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
"""通过访问令牌获取GitHub用户信息需在请求头携带令牌"""
rsp = self.do_get(self.API_URL, params={}, headers={ rsp = self.do_get(self.API_URL, params={}, headers={
"Authorization": "token " + self.access_token "Authorization": "token " + self.access_token
}) })
try: try:
datas = json.loads(rsp) datas = json.loads(rsp)
user = OAuthUser() user = OAuthUser()
user.picture = datas['avatar_url'] user.picture = datas['avatar_url'] # cll 头像URL
user.nickname = datas['name'] user.nickname = datas['name'] # cll 姓名(可能为空)
user.openid = datas['id'] user.openid = datas['id'] # cll 唯一标识
user.type = 'github' user.type = 'github'
user.token = self.access_token user.token = self.access_token
user.metadata = rsp user.metadata = rsp
if 'email' in datas and datas['email']: if 'email' in datas and datas['email']:
user.email = datas['email'] user.email = datas['email']
return user return user
@ -314,11 +336,13 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
return None return None
def get_picture(self, metadata): def get_picture(self, metadata):
"""从元数据提取GitHub用户头像"""
datas = json.loads(metadata) datas = json.loads(metadata)
return datas['avatar_url'] return datas['avatar_url']
class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): #fkc
"""Facebook OAuth授权管理器支持代理"""
AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth'
TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token'
API_URL = 'https://graph.facebook.com/me' API_URL = 'https://graph.facebook.com/me'
@ -329,13 +353,10 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
self.client_id = config.appkey if config else '' self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else '' self.client_secret = config.appsecret if config else ''
self.callback_url = config.callback_url if config else '' self.callback_url = config.callback_url if config else ''
super( super().__init__(access_token=access_token, openid=openid)
FaceBookOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, next_url='/'): def get_authorization_url(self, next_url='/'):
"""构建Facebook授权跳转URL请求邮箱和公开资料权限"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'response_type': 'code', 'response_type': 'code',
@ -346,17 +367,16 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
return url return url
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
"""通过授权码获取Facebook访问令牌"""
params = { params = {
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
# 'grant_type': 'authorization_code',
'code': code, 'code': code,
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_post(self.TOKEN_URL, params) rsp = self.do_post(self.TOKEN_URL, params)
obj = json.loads(rsp) obj = json.loads(rsp)
if 'access_token' in obj: if 'access_token' in obj:
token = str(obj['access_token']) token = str(obj['access_token'])
self.access_token = token self.access_token = token
@ -365,21 +385,26 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
raise OAuthAccessTokenException(rsp) raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
"""通过访问令牌获取Facebook用户信息指定返回字段"""
params = { params = {
'access_token': self.access_token, 'access_token': self.access_token,
'fields': 'id,name,picture,email' 'fields': 'id,name,picture,email' # cll 指定需要返回的字段
} }
try: try:
rsp = self.do_get(self.API_URL, params) rsp = self.do_get(self.API_URL, params)
datas = json.loads(rsp) datas = json.loads(rsp)
user = OAuthUser() user = OAuthUser()
user.nickname = datas['name'] user.nickname = datas['name'] # cll 姓名
user.openid = datas['id'] user.openid = datas['id'] # cll 唯一标识
user.type = 'facebook' user.type = 'facebook'
user.token = self.access_token user.token = self.access_token
user.metadata = rsp user.metadata = rsp
if 'email' in datas and datas['email']: if 'email' in datas and datas['email']:
user.email = datas['email'] user.email = datas['email']
# cll 解析头像URLFacebook返回格式嵌套较深
if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']: if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']:
user.picture = str(datas['picture']['data']['url']) user.picture = str(datas['picture']['data']['url'])
return user return user
@ -388,14 +413,17 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
return None return None
def get_picture(self, metadata): def get_picture(self, metadata):
"""从元数据提取Facebook用户头像"""
datas = json.loads(metadata) datas = json.loads(metadata)
return str(datas['picture']['data']['url']) return str(datas['picture']['data']['url'])
class QQOauthManager(BaseOauthManager): class QQOauthManager(BaseOauthManager): #cll
"""QQ OAuth授权管理器"""
AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize'
TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'
API_URL = 'https://graph.qq.com/user/get_user_info' API_URL = 'https://graph.qq.com/user/get_user_info'
# cll QQ需单独请求openid的URL
OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me'
ICON_NAME = 'qq' ICON_NAME = 'qq'
@ -404,13 +432,10 @@ class QQOauthManager(BaseOauthManager):
self.client_id = config.appkey if config else '' self.client_id = config.appkey if config else ''
self.client_secret = config.appsecret if config else '' self.client_secret = config.appsecret if config else ''
self.callback_url = config.callback_url if config else '' self.callback_url = config.callback_url if config else ''
super( super().__init__(access_token=access_token, openid=openid)
QQOauthManager,
self).__init__(
access_token=access_token,
openid=openid)
def get_authorization_url(self, next_url='/'): def get_authorization_url(self, next_url='/'):
"""构建QQ授权跳转URL"""
params = { params = {
'response_type': 'code', 'response_type': 'code',
'client_id': self.client_id, 'client_id': self.client_id,
@ -420,6 +445,7 @@ class QQOauthManager(BaseOauthManager):
return url return url
def get_access_token_by_code(self, code): def get_access_token_by_code(self, code):
"""通过授权码获取QQ访问令牌"""
params = { params = {
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'client_id': self.client_id, 'client_id': self.client_id,
@ -428,73 +454,86 @@ class QQOauthManager(BaseOauthManager):
'redirect_uri': self.callback_url 'redirect_uri': self.callback_url
} }
rsp = self.do_get(self.TOKEN_URL, params) rsp = self.do_get(self.TOKEN_URL, params)
if rsp: if rsp:
# cll QQ返回格式为form-encoded解析令牌
d = urllib.parse.parse_qs(rsp) d = urllib.parse.parse_qs(rsp)
if 'access_token' in d: if 'access_token' in d:
token = d['access_token'] token = d['access_token']
self.access_token = token[0] self.access_token = token[0]
return token return token
else:
raise OAuthAccessTokenException(rsp) raise OAuthAccessTokenException(rsp)
def get_open_id(self): def get_open_id(self):
"""QQ需单独请求openid通过访问令牌获取"""
if self.is_access_token_set: if self.is_access_token_set:
params = { params = {
'access_token': self.access_token 'access_token': self.access_token
} }
rsp = self.do_get(self.OPEN_ID_URL, params) rsp = self.do_get(self.OPEN_ID_URL, params)
if rsp: if rsp:
rsp = rsp.replace( # 去除QQ返回的callback包裹符
'callback(', '').replace( rsp = rsp.replace('callback(', '').replace(')', '').replace(';', '')
')', '').replace(
';', '')
obj = json.loads(rsp) obj = json.loads(rsp)
openid = str(obj['openid']) openid = str(obj['openid'])
self.openid = openid self.openid = openid
return openid return openid
def get_oauth_userinfo(self): def get_oauth_userinfo(self):
"""获取QQ用户信息需先获取openid"""
openid = self.get_open_id() openid = self.get_open_id()
if openid: if openid:
params = { params = {
'access_token': self.access_token, 'access_token': self.access_token,
'oauth_consumer_key': self.client_id, 'oauth_consumer_key': self.client_id, # cll QQ要求传入appkey
'openid': self.openid 'openid': self.openid
} }
rsp = self.do_get(self.API_URL, params) rsp = self.do_get(self.API_URL, params)
logger.info(rsp) logger.info(rsp)
obj = json.loads(rsp) obj = json.loads(rsp)
user = OAuthUser() user = OAuthUser()
user.nickname = obj['nickname'] user.nickname = obj['nickname'] # cll 昵称
user.openid = openid user.openid = openid # cll 唯一标识
user.type = 'qq' user.type = 'qq'
user.token = self.access_token user.token = self.access_token
user.metadata = rsp user.metadata = rsp
if 'email' in obj: if 'email' in obj:
user.email = obj['email'] user.email = obj['email']
if 'figureurl' in obj: if 'figureurl' in obj:
user.picture = str(obj['figureurl']) user.picture = str(obj['figureurl']) # cll 头像URL
return user return user
def get_picture(self, metadata): def get_picture(self, metadata):
"""从元数据提取QQ用户头像"""
datas = json.loads(metadata) datas = json.loads(metadata)
return str(datas['figureurl']) return str(datas['figureurl'])
@cache_decorator(expiration=100 * 60) @cache_decorator(expiration=100 * 60)
def get_oauth_apps(): def get_oauth_apps(): #xy
"""获取所有启用的OAuth应用缓存100分钟"""
# cll 读取数据库中启用的OAuth配置
configs = OAuthConfig.objects.filter(is_enable=True).all() configs = OAuthConfig.objects.filter(is_enable=True).all()
if not configs: if not configs:
return [] return []
# 提取已启用的平台类型
configtypes = [x.type for x in configs] configtypes = [x.type for x in configs]
# cll 获取所有BaseOauthManager的子类各平台实现
applications = BaseOauthManager.__subclasses__() applications = BaseOauthManager.__subclasses__()
# cll 筛选出已启用的平台实例
apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes] apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes]
return apps return apps
def get_manager_by_type(type): def get_manager_by_type(type):
"""根据平台类型获取对应的OAuth管理器实例"""
applications = get_oauth_apps() applications = get_oauth_apps()
if applications: if applications:
# cll 匹配平台类型(不区分大小写)
finds = list( finds = list(
filter( filter(
lambda x: x.ICON_NAME.lower() == type.lower(), lambda x: x.ICON_NAME.lower() == type.lower(),

@ -37,7 +37,7 @@ class ServerManagerTest(TestCase):
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = user article.author = user
article.category = c article.category = c
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
s = TextMessage([]) s = TextMessage([])

@ -48,7 +48,7 @@ class AccountTest(TestCase):
article.body = "nicecontentaaa" article.body = "nicecontentaaa"
article.author = user article.author = user
article.category = category article.category = category
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
@ -96,7 +96,7 @@ class AccountTest(TestCase):
article.body = "nicecontentttt" article.body = "nicecontentttt"
article.author = user article.author = user
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()

@ -197,7 +197,7 @@ class ArticleDocumentManager():
pub_time=article.pub_time, pub_time=article.pub_time,
status=article.status, status=article.status,
comment_status=article.comment_status, comment_status=article.comment_status,
type=article.type, type=article.article_type,
views=article.views, views=article.views,
article_order=article.article_order) for article in articles] article_order=article.article_order) for article in articles]

@ -86,7 +86,7 @@ class Article(BaseModel):
max_length=1, max_length=1,
choices=COMMENT_STATUS, choices=COMMENT_STATUS,
default='o') default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') article_type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0) views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,

@ -59,7 +59,7 @@ class ArticleTest(TestCase):
article.body = "nicecontent" article.body = "nicecontent"
article.author = user article.author = user
article.category = category article.category = category
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
@ -74,7 +74,7 @@ class ArticleTest(TestCase):
article.body = "nicetitle" + str(i) article.body = "nicetitle" + str(i)
article.author = user article.author = user
article.category = category article.category = category
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
article.tags.add(tag) article.tags.add(tag)

@ -97,7 +97,7 @@ class IndexView(ArticleListView):
link_type = LinkShowType.I link_type = LinkShowType.I
def get_queryset_data(self): def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p') article_list = Article.objects.filter(article_type='a', status='p')
return article_list return article_list
def get_queryset_cache_key(self): def get_queryset_cache_key(self):
@ -216,7 +216,7 @@ class AuthorDetailView(ArticleListView):
def get_queryset_data(self): def get_queryset_data(self):
author_name = self.kwargs['author_name'] author_name = self.kwargs['author_name']
article_list = Article.objects.filter( article_list = Article.objects.filter(
author__username=author_name, type='a', status='p') author__username=author_name, article_type='a', status='p')
return article_list return article_list
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -238,7 +238,7 @@ class TagDetailView(ArticleListView):
tag_name = tag.name tag_name = tag.name
self.name = tag_name self.name = tag_name
article_list = Article.objects.filter( article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p') tags__name=tag_name, article_type='a', status='p')
return article_list return article_list
def get_queryset_cache_key(self): def get_queryset_cache_key(self):

@ -42,7 +42,7 @@ class CommentsTest(TransactionTestCase):
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = self.user article.author = self.user
article.category = category article.category = category
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()

@ -91,7 +91,7 @@ class ElasticSearchBackend(BaseSearchBackend):
search = ArticleDocument.search() \ search = ArticleDocument.search() \
.query('bool', filter=[q]) \ .query('bool', filter=[q]) \
.filter('term', status='p') \ .filter('term', status='p') \
.filter('term', type='a') \ .filter('term', article_type='a') \
.source(False)[start_offset: end_offset] .source(False)[start_offset: end_offset]
results = search.execute() results = search.execute()

@ -21,7 +21,7 @@ class DjangoBlogFeed(Feed):
return get_user_model().objects.first().get_absolute_url() return get_user_model().objects.first().get_absolute_url()
def items(self): def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] return Article.objects.filter(article_type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item): def item_title(self, item):
return item.title return item.title

@ -37,7 +37,7 @@ class ServerManagerTest(TestCase):
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = user article.author = user
article.category = c article.category = c
article.type = 'a' article.article_type = 'a'
article.status = 'p' article.status = 'p'
article.save() article.save()
s = TextMessage([]) s = TextMessage([])

@ -17,7 +17,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
<header class="archive-header"> <header class="archive-header">
@ -47,7 +47,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
</div><!-- #content --> </main><!-- #content -->
</div><!-- #primary --> </div><!-- #primary -->
{% endblock %} {% endblock %}

@ -5,10 +5,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
{% load_article_detail article False user %} {% load_article_detail article False user %}
{% if article.type == 'a' %} {% if article.article_type == 'a' %}
<nav class="nav-single"> <nav class="nav-single">
<h3 class="assistive-text">文章导航</h3> <h3 class="assistive-text">文章导航</h3>
{% if next_article %} {% if next_article %}
@ -24,7 +24,7 @@
</nav><!-- .nav-single --> </nav><!-- .nav-single -->
{% endif %} {% endif %}
</div><!-- #content --> </main><!-- #content -->
{% if article.comment_status == "o" and OPEN_SITE_COMMENT %} {% if article.comment_status == "o" and OPEN_SITE_COMMENT %}

@ -18,7 +18,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
{% if page_type and tag_name %} {% if page_type and tag_name %}
<header class="archive-header"> <header class="archive-header">
@ -33,7 +33,7 @@
{% load_pagination_info page_obj page_type tag_name %} {% load_pagination_info page_obj page_type tag_name %}
{% endif %} {% endif %}
</div><!-- #content --> </main><!-- #content -->
</div><!-- #primary --> </div><!-- #primary -->
{% endblock %} {% endblock %}

@ -26,13 +26,13 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
<header class="archive-header"> <header class="archive-header">
<h1 class="archive-title">{{ message }}</h1> <h1 class="archive-title">{{ message }}</h1>
</header><!-- .archive-header --> </header><!-- .archive-header -->
</div><!-- #content --> </main><!-- #content -->
</div><!-- #primary --> </div><!-- #primary -->
{% endblock %} {% endblock %}

@ -16,7 +16,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
<header class="archive-header"> <header class="archive-header">
@ -31,7 +31,7 @@
</li> </li>
{% endfor %} </ul> {% endfor %} </ul>
</div> </div>
</div><!-- #content --> </main><!-- #content -->
</div><!-- #primary --> </div><!-- #primary -->
{% endblock %} {% endblock %}

@ -37,7 +37,7 @@
</div> </div>
</div><!-- .comments-link --> </div><!-- .comments-link -->
<br/> <br/>
{% if article.type == 'a' %} {% if article.article_type == 'a' %}
{% if not isindex %} {% if not isindex %}
{% cache 36000 breadcrumb article.pk %} {% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %} {% load_breadcrumb article %}

@ -8,7 +8,7 @@
</a> </a>
{% if article.type == 'a' %} {% if article.article_type == 'a' %}
{% if article.tags.all %} {% if article.tags.all %}
{% trans 'and tagged' %} {% trans 'and tagged' %}

@ -4,7 +4,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
<header class="archive-header"> <header class="archive-header">
@ -17,6 +17,6 @@
| |
<a href="/">回到首页</a> <a href="/">回到首页</a>
</header><!-- .archive-header --> </header><!-- .archive-header -->
</div> </main><!-- #content -->
</div> </div>
{% endblock %} {% endblock %}

@ -12,7 +12,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
{% if query %} {% if query %}
<header class="archive-header"> <header class="archive-header">
{% if suggestion %} {% if suggestion %}
@ -54,7 +54,7 @@
<h1 class="archive-title">哎呀,关键字:<span>{{ query }}</span>没有找到结果,要不换个词再试试?</h1> <h1 class="archive-title">哎呀,关键字:<span>{{ query }}</span>没有找到结果,要不换个词再试试?</h1>
</header><!-- .archive-header --> </header><!-- .archive-header -->
{% endif %} {% endif %}
</div><!-- #content --> </main><!-- #content -->
</div><!-- #primary --> </div><!-- #primary -->
{% endblock %} {% endblock %}

@ -17,7 +17,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
<header class="archive-header"> <header class="archive-header">

@ -5,10 +5,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="primary" class="site-content"> <div id="primary" class="site-content">
<div id="content" role="main"> <main id="content">
{% load_article_detail article False user %} {% load_article_detail article False user %}
{% if article.type == 'a' %} {% if article.article_type == 'a' %}
<nav class="nav-single"> <nav class="nav-single">
<h3 class="assistive-text">文章导航</h3> <h3 class="assistive-text">文章导航</h3>
{% if next_article %} {% if next_article %}

@ -37,7 +37,7 @@
</div> </div>
</div><!-- .comments-link --> </div><!-- .comments-link -->
<br/> <br/>
{% if article.type == 'a' %} {% if article.article_type == 'a' %}
{% if not isindex %} {% if not isindex %}
{% cache 36000 breadcrumb article.pk %} {% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %} {% load_breadcrumb article %}
@ -69,6 +69,82 @@
</div><!-- .entry-content --> </div><!-- .entry-content -->
{% if not isindex %}
<div class="article-engagement" style="margin: 1rem 0; padding: 1rem; border-top: 1px solid var(--border-color);">
<button class="engagement-btn like-btn" data-article-id="{{ article.id }}">
<span class="icon">👍</span>
<span class="count">{{ article.likes_count|default:0 }}</span>
<span class="text">{% trans 'Like' %}</span>
</button>
<button class="engagement-btn favorite-btn" data-article-id="{{ article.id }}">
<span class="icon"></span>
<span class="count">{{ article.favorites_count|default:0 }}</span>
<span class="text">{% trans 'Favorite' %}</span>
</button>
{% if not user.is_authenticated %}
<div class="auth-hint">
<span><a href="{% url 'account:login' %}?next={{ request.path }}">登录</a> 后进行操作</span>
</div>
{% endif %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 点赞功能
document.querySelector('.like-btn').addEventListener('click', function() {
const articleId = this.dataset.articleId;
const countEl = this.querySelector('.count');
fetch(`/article/like/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({ article_id: articleId })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
this.classList.toggle('is-active');
countEl.textContent = data.count;
} else {
alert(data.message || '操作失败');
}
})
.catch(error => console.error('Error:', error));
});
// 收藏功能
document.querySelector('.favorite-btn').addEventListener('click', function() {
const articleId = this.dataset.articleId;
const countEl = this.querySelector('.count');
fetch(`/article/favorite/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({ article_id: articleId })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
this.classList.toggle('is-active');
countEl.textContent = data.count;
} else {
alert(data.message || '操作失败');
}
})
.catch(error => console.error('Error:', error));
});
});
</script>
{% endif %}
{% load_article_metas article user %} {% load_article_metas article user %}
</article><!-- #post --> </article><!-- #post -->

@ -8,7 +8,7 @@
</a> </a>
{% if article.type == 'a' %} {% if article.article_type == 'a' %}
{% if article.tags.all %} {% if article.tags.all %}
{% trans 'and tagged' %} {% trans 'and tagged' %}

@ -1,7 +1,7 @@
{% load blog_tags %} {% load blog_tags %}
{% load i18n %} {% load i18n %}
<div id="secondary" class="widget-area" role="complementary"> <aside id="secondary" class="widget-area" aria-label="侧边栏">
<aside id="search-2" class="widget widget_search"> <aside id="search-2" class="widget widget_search" aria-label="搜索功能">
<form role="search" method="get" id="searchform" class="searchform" action="/search"> <form role="search" method="get" id="searchform" class="searchform" action="/search">
<div> <div>
<label class="screen-reader-text" for="s">{% trans 'search' %}</label> <label class="screen-reader-text" for="s">{% trans 'search' %}</label>
@ -13,7 +13,7 @@
{% if extra_sidebars %} {% if extra_sidebars %}
{% for sidebar in extra_sidebars %} {% for sidebar in extra_sidebars %}
<aside class="widget_text widget widget_custom_html"><p class="widget-title"> <aside class="widget_text widget widget_custom_html" aria-label="自定义侧边栏"><p class="widget-title">
{{ sidebar.name }}</p> {{ sidebar.name }}</p>
<div class="textwidget custom-html-widget"> <div class="textwidget custom-html-widget">
{{ sidebar.content|custom_markdown|safe }} {{ sidebar.content|custom_markdown|safe }}
@ -23,7 +23,7 @@
{% endif %} {% endif %}
{% if most_read_articles %} {% if most_read_articles %}
<aside id="views-4" class="widget widget_views"><p class="widget-title">Views</p> <aside id="views-4" class="widget widget_views" aria-label="热门阅读"><p class="widget-title">Views</p>
<ul> <ul>
{% for a in most_read_articles %} {% for a in most_read_articles %}
<li> <li>
@ -37,7 +37,7 @@
</aside> </aside>
{% endif %} {% endif %}
{% if sidebar_categorys %} {% if sidebar_categorys %}
<aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms"><p class="widget-title">{% trans 'category' %}</p> <aside id="su_siloed_terms-2" class="widget widget_su_siloed_terms" aria-label="文章分类"><p class="widget-title">{% trans 'category' %}</p>
<ul> <ul>
{% for c in sidebar_categorys %} {% for c in sidebar_categorys %}
<li class="cat-item cat-item-184"><a href={{ c.get_absolute_url }}>{{ c.name }}</a> <li class="cat-item cat-item-184"><a href={{ c.get_absolute_url }}>{{ c.name }}</a>
@ -47,7 +47,7 @@
</aside> </aside>
{% endif %} {% endif %}
{% if sidebar_comments and open_site_comment %} {% if sidebar_comments and open_site_comment %}
<aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments"><p class="widget-title">{% trans 'recent comments' %}</p> <aside id="ds-recent-comments-4" class="widget ds-widget-recent-comments" aria-label="最新评论"><p class="widget-title">{% trans 'recent comments' %}</p>
<ul id="recentcomments"> <ul id="recentcomments">
{% for c in sidebar_comments %} {% for c in sidebar_comments %}
@ -62,7 +62,7 @@
</aside> </aside>
{% endif %} {% endif %}
{% if recent_articles %} {% if recent_articles %}
<aside id="recent-posts-2" class="widget widget_recent_entries"><p class="widget-title">{% trans 'recent articles' %}</p> <aside id="recent-posts-2" class="widget widget_recent_entries" aria-label="最新文章"><p class="widget-title">{% trans 'recent articles' %}</p>
<ul> <ul>
{% for a in recent_articles %} {% for a in recent_articles %}
@ -74,7 +74,7 @@
</aside> </aside>
{% endif %} {% endif %}
{% if sidabar_links %} {% if sidabar_links %}
<aside id="linkcat-0" class="widget widget_links"><p class="widget-title">{% trans 'bookmark' %}</p> <aside id="linkcat-0" class="widget widget_links" aria-label="书签链接"><p class="widget-title">{% trans 'bookmark' %}</p>
<ul class='xoxo blogroll'> <ul class='xoxo blogroll'>
{% for l in sidabar_links %} {% for l in sidabar_links %}
<li> <li>
@ -86,25 +86,24 @@
</aside> </aside>
{% endif %} {% endif %}
{% if show_google_adsense %} {% if show_google_adsense %}
<aside id="text-2" class="widget widget_text"><p class="widget-title">Google AdSense</p> <aside id="text-2" class="widget widget_text" aria-label="广告"><p class="widget-title">Google AdSense</p>
<div class="textwidget"> <div class="textwidget">
{{ google_adsense_codes|safe }} {{ google_adsense_codes|safe }}
</div> </div>
</aside> </aside>
{% endif %} {% endif %}
{% if sidebar_tags %} {% if sidebar_tags %}
<aside id="tag_cloud-2" class="widget widget_tag_cloud"><p class="widget-title">{% trans 'Tag Cloud' %}</p> <aside id="tag_cloud-2" class="widget widget_tag_cloud" aria-label="标签云"><p class="widget-title">{% trans 'Tag Cloud' %}</p>
<div class="tagcloud"> <div class="tagcloud">
{% for tag, count, size in sidebar_tags %} {% for tag, count, size in sidebar_tags %}
<a href="{{ tag.get_absolute_url }}" <a href="{{ tag.get_absolute_url }}" class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}"
class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}" style="font-size: '{{ size }}pt';"
style="font-size: {{ size }}pt;" title="{{ count }}个话题"> {{ tag.name }} title="{{ count }}个话题">{{ tag.name }}</a>
</a>
{% endfor %} {% endfor %}
</div> </div>
</aside> </aside>
{% endif %} {% endif %}
<aside id="text-2" class="widget widget_text"><p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p> <aside id="text-3" class="widget widget_text" aria-label="源码链接"><p class="widget-title">{% trans 'Welcome to star or fork the source code of this site' %}</p>
<div class="textwidget"> <div class="textwidget">
<p><a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img <p><a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
@ -115,7 +114,7 @@
</div> </div>
</aside> </aside>
<aside id="meta-3" class="widget widget_meta"><p class="widget-title">{% trans 'Function' %}</p> <aside id="meta-3" class="widget widget_meta" aria-label="功能链接"><p class="widget-title">{% trans 'Function' %}</p>
<ul> <ul>
<li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li> <li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li>
{% if user.is_authenticated %} {% if user.is_authenticated %}
@ -133,4 +132,4 @@
</aside> </aside>
<div id="rocket" class="show" title="{% trans 'Click me to return to the top' %}"></div> <div id="rocket" class="show" title="{% trans 'Click me to return to the top' %}"></div>
</div><!-- #secondary --> </aside><!-- #secondary -->

@ -0,0 +1,139 @@
{% extends 'blog/base.html' %}
{% load i18n %}
{% load comment_tags %}
{% load staticfiles %}
{% load custom_markdown %}
{% block title %}{% trans "我的收藏" %} - {{ SITE_NAME }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<div class="title">
<h2>{% trans "我的收藏" %}</h2>
</div>
</div>
<div class="panel-body">
{% if has_favorites %}
{% for article in article_list %}
<div class="article post-preview">
<div class="article-inner">
<h3 class="article-title">
<a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
</h3>
<div class="article-meta">
<span class="article-date">
<i class="fa fa-calendar"></i>
{{ article.created_time }}
</span>
<span class="article-category">
<i class="fa fa-folder-o"></i>
<a href="{{ article.category.get_absolute_url }}">{{ article.category.name }}</a>
</span>
<span class="article-author">
<i class="fa fa-user-o"></i>
<a href="{{ article.author.get_absolute_url }}">{{ article.author.nickname }}</a>
</span>
<span class="article-view-count">
<i class="fa fa-eye"></i>
{{ article.views }}
</span>
<span class="article-comment-count">
<i class="fa fa-comment-o"></i>
{% get_comment_count article %}
</span>
</div>
<div class="article-content">
<p>
{{ article.body|custom_markdown|truncatechars:100 }}
</p>
</div>
<div class="article-footer">
<div class="article-more-link">
<a href="{{ article.get_absolute_url }}" class="btn btn-default">
阅读全文
</a>
</div>
</div>
</div>
</div>
<hr>
{% endfor %}
{% if page_obj.has_previous or page_obj.has_next %}
<div class="pagination">
<ul class="pagination">
{% if page_obj.has_previous %}
<li>
<a href="?page={{ page_obj.previous_page_number }}">{% trans "上一页" %}</a>
</li>
{% endif %}
{% if page_obj.has_next %}
<li>
<a href="?page={{ page_obj.next_page_number }}">{% trans "下一页" %}</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{% else %}
<div class="no-favorites">
<p class="text-center">您还没有收藏任何文章</p>
<div class="text-center">
<a href="/" class="btn btn-primary">
去首页看看
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
{% include 'blog/tags.html' %}
{% include 'blog/hotest_articles.html' %}
{% include 'blog/archives.html' %}
</div>
</div>
</div>
{% endblock %}
{% block extra_footer %}
<style>
.article-meta {
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
.article-meta span {
margin-right: 15px;
}
.article-meta a {
color: #666;
}
.article-meta a:hover {
color: #337ab7;
text-decoration: none;
}
.article-title {
margin-bottom: 10px;
}
.article-title a {
color: #333;
}
.article-title a:hover {
color: #337ab7;
text-decoration: none;
}
.no-favorites {
padding: 30px 0;
}
.no-favorites p {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
</style>
{% endblock %}

@ -25,6 +25,11 @@
<a href="{% url "blog:archives" %}">{% trans 'Article archive' %}</a> <a href="{% url "blog:archives" %}">{% trans 'Article archive' %}</a>
</li> </li>
{% if user.is_authenticated %}
<li class="menu-item menu-item-type-taxonomy menu-item-object-category">
<a href="{% url "blog:user_favorites" %}">{% trans '我的收藏' %}</a>
</li>
{% endif %}
</ul> </ul>
</div> </div>
</nav><!-- #site-navigation --> </nav><!-- #site-navigation -->
Loading…
Cancel
Save