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 && \
apt-get install default-libmysqlclient-dev gettext -y && \
rm -rf /var/lib/apt/lists/*
ADD requirements.txt requirements.txt
COPY requirements.txt requirements.txt
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn[gevent] && \
pip cache purge
ADD . .
COPY . .
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]

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

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

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

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

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

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

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

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

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

@ -1,62 +1,90 @@
# 博客应用的URL配置文件
from django.urls import path
from django.views.decorators.cache import cache_page
from django.views.decorators.cache import cache_page # 页面缓存装饰器
from . import views
app_name = "blog"
app_name = "blog" # 应用命名空间
urlpatterns = [
# 首页相关
path(
r'',
views.IndexView.as_view(),
name='index'),
name='index'), # 首页
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
name='index_page'), # 首页分页
# 文章详情页
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
name='detailbyid'), # 文章详情页按ID
# 分类相关页面
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
name='category_detail'), # 分类页面
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
name='category_detail_page'), # 分类页面分页
# 作者相关页面
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
name='author_detail'), # 作者页面
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
name='author_detail_page'), # 作者页面分页
# 标签相关页面
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
name='tag_detail'), # 标签页面
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
name='tag_detail_page'), # 标签页面分页
# 其他页面
path(
'archives.html',
cache_page(
60 * 60)(
cache_page(60 * 60)( # 缓存1小时
views.ArchivesView.as_view()),
name='archives'),
name='archives'), # 文章归档页
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
name='links'), # 友情链接页
# 功能接口
path(
r'upload',
views.fileupload,
name='upload'),
name='upload'), # 文件上传接口
path(
r'clean',
views.clean_cache_view,
name='clean'),
name='clean'), # 清除缓存接口
# 点赞和收藏功能
path(
r'article/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 os
import uuid
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.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from django.views.decorators.csrf import csrf_exempt # 用于跳过CSRF验证
from django.views.generic.detail import DetailView # 详情页视图基类
from django.views.generic.list import ListView # 列表页视图基类
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 djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage import hooks # 插件管理
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
#flj 文章列表视图基类,提供通用的文章列表功能,负责处理文章列表的展示逻辑,包括分页、缓存等功能
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
template_name = 'blog/article_index.html' #flj 指定使用的模板文件
context_object_name = 'article_list' #flj 上下文变量名,在模板中使用该名字访问文章列表
page_type = '' #flj 页面类型,用于标识是分类目录、标签列表等
paginate_by = settings.PAGINATE_BY #flj 每页显示的文章数量
page_kwarg = 'page' #flj URL中页码参数名
link_type = LinkShowType.L #flj 友情链接显示类型
#flj 获取视图缓存键,注意:这个方法似乎有问题,应该返回字符串而不是字典
def get_view_cache_key(self):
return self.request.get['pages']
@property
#flj 获取当前页码从URL参数或kwargs中获取页码默认为1
def page_number(self):
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
#flj 子类必须重写此方法,返回查询集的缓存键,不同的列表视图需要不同的缓存键来区分
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
#flj 子类必须重写此方法,返回查询集的数据,每个子类根据不同的需求过滤文章数据
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
#flj 从缓存获取页面数据,提高性能,如果缓存不存在,则从数据库获取并存入缓存
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key)
if value:
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))
return article_list
#flj 重写默认方法,从缓存获取数据,优先使用缓存,提高页面响应速度
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
#flj 为模板添加上下文数据,添加友情链接类型等额外信息
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
#flj 首页视图,显示最新的已发布文章
class IndexView(ArticleListView):
'''
首页
首页视图显示最新的已发布文章
'''
# 友情链接类型
#flj 友情链接类型:只在首页显示
link_type = LinkShowType.I
#flj 获取首页文章数据,过滤条件为类型为文章(a)且状态为已发布(p)
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
#flj 生成首页的缓存键,包含页码信息
def get_queryset_cache_key(self):
# 生成首页的缓存键
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
#flj 文章详情视图,负责显示单篇文章的详细内容和评论
class ArticleDetailView(DetailView):
'''
文章详情页
文章详情页视图
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
template_name = 'blog/article_detail.html' #flj 使用的模板
model = Article #flj 关联的模型
pk_url_kwarg = 'article_id' #flj URL中的主键参数名
context_object_name = "article" #flj 模板中的对象变量名
#flj 获取文章详情页的上下文数据,包括评论表单、相关文章等
def get_context_data(self, **kwargs):
comment_form = CommentForm()
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
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)
#flj 调用父类方法获取基础上下文数据
kwargs = 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
article.viewed() # 增加浏览次数
#flj 添加评论表单
comments = article.comment_list()
comment_form = CommentForm()
#flj 添加点赞和收藏信息
is_liked = False
is_favorited = False
like_count = article.like_set.count()
favorite_count = article.favorite_set.count()
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):
'''
分类目录列表
分类详情页视图
'''
page_type = "分类目录归档"
page_type = "分类目录归档" #flj 页面类型标识
#flj 获取分类下的文章数据根据URL参数中的分类ID过滤
def get_queryset_data(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
# 使用局部变量避免未使用参数警告
#flj 获取分类ID
#flj 过滤该分类下的已发布文章
#flj 返回查询结果
return article_list
#flj 生成分类页面的缓存键包含分类ID和页码
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
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)
#flj 生成缓存键
return cache_key
#flj 添加分类信息到上下文
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
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 获取分类对象
#flj 添加到上下文
return super().get_context_data(**kwargs)
#flj 作者详情视图,显示指定作者的文章列表
class AuthorDetailView(ArticleListView):
'''
作者详情页
作者详情页视图
'''
page_type = '作者文章归档'
page_type = '作者文章归档' #flj 页面类型标识
#flj 生成作者页面的缓存键包含作者ID和页码
def get_queryset_cache_key(self):
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
#flj 生成缓存键
return cache_key
#flj 获取作者的文章数据根据URL参数中的作者ID过滤
def get_queryset_data(self):
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
# 使用局部变量避免未使用参数警告
#flj 获取作者ID
#flj 过滤该作者的已发布文章
return article_list
#flj 添加作者信息到上下文
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
#flj 获取作者对象
#flj 添加到上下文
return super().get_context_data(**kwargs)
#flj 标签详情视图,显示指定标签下的文章列表
class TagDetailView(ArticleListView):
'''
标签列表页面
标签详情页视图
'''
page_type = '分类标签归档'
page_type = '分类标签归档' #flj 页面类型标识
#flj 获取标签下的文章数据根据URL参数中的标签ID过滤
def get_queryset_data(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
# 使用局部变量避免未使用参数警告
#flj 获取标签ID
#flj 过滤该标签下的已发布文章
return article_list
#flj 生成标签页面的缓存键包含标签ID和页码
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
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)
#flj 生成缓存键
return cache_key
#flj 添加标签信息到上下文
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
#flj 获取标签对象
#flj 添加到上下文
return super().get_context_data(**kwargs)
#flj 文章归档视图,显示所有文章按时间分组
class ArchivesView(ArticleListView):
'''
文章归档页面
文章归档视图
'''
page_type = '文章归档'
paginate_by = None
page_type = '文章归档' #flj 页面类型标识
paginate_by = None #flj 不分页,显示所有文章
page_kwarg = None
template_name = 'blog/article_archives.html'
template_name = 'blog/article_archives.html' #flj 使用归档专用模板
#flj 获取所有已发布文章,按时间排序
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
#flj 获取所有已发布文章
return article_list
#flj 生成归档页面的缓存键
def get_queryset_cache_key(self):
cache_key = 'archives'
#flj 生成缓存键
return cache_key
#flj 友情链接列表视图
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):
return Links.objects.filter(is_enable=True)
#flj 过滤显示状态的友情链接并排序
return links
#flj 搜索视图,处理文章搜索功能
class EsSearchView(SearchView):
'''
搜索视图
'''
#flj 获取搜索结果的上下文数据
def get_context(self):
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
#flj 获取基础上下文
#flj 添加额外的搜索相关信息
return context
@csrf_exempt
#flj 文件上传接口,允许上传图片等文件
@csrf_exempt #flj 跳过CSRF验证因为这是文件上传接口
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
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 检查请求方法
#flj 验证权限
#flj 处理文件上传
#flj 保存文件到指定目录
#flj 返回文件URL
return HttpResponse(json.dumps(data), content_type="application/json")
#flj 404错误页面视图
#flj 处理页面未找到的情况
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
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)
template_name=ERROR_PAGE_TEMPLATE):
'''
404错误页面
'''
#flj 渲染错误页面
context = get_blog_setting()
return render(request, template_name, context, status=404)
#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(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
template_name=ERROR_PAGE_TEMPLATE):
'''
403错误页面
'''
#flj 渲染错误页面
context = get_blog_setting()
return render(request, template_name, context, status=403)
#flj 清理缓存视图,用于手动清理站点缓存
#flj 提供管理功能,清除系统缓存
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.db import models
from django.utils.timezone import now
@ -8,32 +9,41 @@ from blog.models import Article
# Create your models here.
#fkc 评论模型类
class Comment(models.Model):
#fkc 评论正文
body = models.TextField('正文', max_length=300)
#fkc 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#fkc 最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#fkc 评论作者(外键关联用户)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
#fkc 评论的文章(外键关联文章)
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
#fkc 父评论(支持回复功能)
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
#fkc 是否启用(审核功能)
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
ordering = ['-id']
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id'
ordering = ['-id'] #fkc 按ID倒序排列
verbose_name = _('comment') #fkc 在管理后台显示的名称
verbose_name_plural = verbose_name #fkc 复数形式
get_latest_by = 'id' #fkc 获取最新记录的依据
#fkc 返回评论正文作为字符串表示
def __str__(self):
return self.body

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

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

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

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

@ -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/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.conf.urls.i18n import i18n_patterns # 国际化URL模式
from django.conf.urls.static import static # 静态文件服务
from django.contrib.sitemaps.views import sitemap # 站点地图
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from haystack.views import search_view_factory # 搜索视图工厂
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.feeds import DjangoBlogFeed
from djangoblog.feeds import DjangoBlogFeed # RSS订阅源
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# 站点地图配置用于SEO优化
sitemaps = {
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
'blog': ArticleSiteMap, # 文章站点地图
'Category': CategorySiteMap, # 分类站点地图
'Tag': TagSiteMap, # 标签站点地图
'User': UserSiteMap, # 用户站点地图
'static': StaticViewSitemap # 静态页面站点地图
}
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
# 错误页面处理器
handler404 = 'blog.views.page_not_found_view' # 404错误页面
handler500 = 'blog.views.server_error_view' # 500错误页面
handle403 = 'blog.views.permission_denied_view' # 403权限拒绝页面
# 基础URL模式不包含语言前缀
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
path('i18n/', include('django.conf.urls.i18n')), # 国际化切换
]
# 带语言前缀的URL模式
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
re_path(r'', include('blog.urls', namespace='blog')),
re_path(r'mdeditor/', include('mdeditor.urls')),
re_path(r'', include('comments.urls', namespace='comment')),
re_path(r'', include('accounts.urls', namespace='account')),
re_path(r'', include('oauth.urls', namespace='oauth')),
re_path(r'^admin/', admin_site.urls), # 管理后台
re_path(r'', include('blog.urls', namespace='blog')), # 博客相关页面
re_path(r'mdeditor/', include('mdeditor.urls')), # Markdown编辑器
re_path(r'', include('comments.urls', namespace='comment')), # 评论系统
re_path(r'', include('accounts.urls', namespace='account')), # 用户账户
re_path(r'', include('oauth.urls', namespace='oauth')), # OAuth登录
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
name='django.contrib.sitemaps.views.sitemap'), # 站点地图
re_path(r'^feed/$', DjangoBlogFeed()), # RSS订阅源
re_path(r'^rss/$', DjangoBlogFeed()), # RSS订阅源别名
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
name='search'), # 搜索功能
re_path(r'', include('servermanager.urls', namespace='servermanager')), # 服务器管理
re_path(r'', include('owntracks.urls', namespace='owntracks')), # 位置追踪
prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# 开发环境下提供媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

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

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

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

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

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

@ -86,7 +86,7 @@ class Article(BaseModel):
max_length=1,
choices=COMMENT_STATUS,
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)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -37,7 +37,7 @@
</div>
</div><!-- .comments-link -->
<br/>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if not isindex %}
{% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %}
@ -69,6 +69,82 @@
</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 %}
</article><!-- #post -->

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

@ -1,7 +1,7 @@
{% load blog_tags %}
{% load i18n %}
<div id="secondary" class="widget-area" role="complementary">
<aside id="search-2" class="widget widget_search">
<aside id="secondary" class="widget-area" aria-label="侧边栏">
<aside id="search-2" class="widget widget_search" aria-label="搜索功能">
<form role="search" method="get" id="searchform" class="searchform" action="/search">
<div>
<label class="screen-reader-text" for="s">{% trans 'search' %}</label>
@ -13,7 +13,7 @@
{% if 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>
<div class="textwidget custom-html-widget">
{{ sidebar.content|custom_markdown|safe }}
@ -23,7 +23,7 @@
{% endif %}
{% 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>
{% for a in most_read_articles %}
<li>
@ -37,7 +37,7 @@
</aside>
{% endif %}
{% 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>
{% for c in sidebar_categorys %}
<li class="cat-item cat-item-184"><a href={{ c.get_absolute_url }}>{{ c.name }}</a>
@ -47,7 +47,7 @@
</aside>
{% endif %}
{% 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">
{% for c in sidebar_comments %}
@ -62,7 +62,7 @@
</aside>
{% endif %}
{% 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>
{% for a in recent_articles %}
@ -74,7 +74,7 @@
</aside>
{% endif %}
{% 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'>
{% for l in sidabar_links %}
<li>
@ -86,25 +86,24 @@
</aside>
{% endif %}
{% 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">
{{ google_adsense_codes|safe }}
</div>
</aside>
{% endif %}
{% 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">
{% for tag,count,size in sidebar_tags %}
<a href="{{ tag.get_absolute_url }}"
class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}"
style="font-size: {{ size }}pt;" title="{{ count }}个话题"> {{ tag.name }}
</a>
{% for tag, count, size in sidebar_tags %}
<a href="{{ tag.get_absolute_url }}" class="tag-link-{{ tag.id }} tag-link-position-{{ tag.id }}"
style="font-size: '{{ size }}pt';"
title="{{ count }}个话题">{{ tag.name }}</a>
{% endfor %}
</div>
</aside>
{% 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">
<p><a href="https://github.com/liangliangyy/DjangoBlog" rel="nofollow"><img
@ -115,7 +114,7 @@
</div>
</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>
<li><a href="/admin/" rel="nofollow">{% trans 'management site' %}</a></li>
{% if user.is_authenticated %}
@ -133,4 +132,4 @@
</aside>
<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>
</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>
</div>
</nav><!-- #site-navigation -->
Loading…
Cancel
Save