add comment

flj_branch
flj666 5 months ago
parent 7ef173b7f1
commit 3e90329a16

@ -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 @@
# 这个文件定义了用户相关的数据模型
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
@ -9,19 +10,26 @@ from djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
# 用户昵称
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
# 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# 最后修改时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 来源
source = models.CharField(_('create source'), max_length=100, blank=True)
# 获取用户详情页的url
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
# 返回邮箱作为用户标识
def __str__(self):
return self.email
# 获取用户的完整url
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,

@ -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,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

@ -1,3 +1,5 @@
# 这个文件是博客相关的数据模型,定义了博客系统中所有的数据表结构
import logging
import re
from abc import abstractmethod
@ -8,35 +10,41 @@ from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from 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__)
# 友情链接的展示类型选择,用于控制链接在哪些页面显示
class LinkShowType(models.TextChoices):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
I = ('i', _('index')) # 只在首页显示
L = ('l', _('list')) # 只在列表页显示
P = ('p', _('post')) # 只在文章页显示
A = ('a', _('all')) # 在所有页面显示
S = ('s', _('slide')) # 以轮播形式显示
# 所有模型的基类,包含通用字段,避免重复代码
class BaseModel(models.Model):
id = models.AutoField(primary_key=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('modify time'), default=now)
id = models.AutoField(primary_key=True) # 主键,自动递增
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
# 重写保存方法自动处理slug字段用于生成友好的URL
def save(self, *args, **kwargs):
# 如果是更新文章浏览量,直接更新数据库,避免触发其他逻辑
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:
# 如果有slug字段自动从标题或名称生成slug
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
@ -45,79 +53,87 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
def get_full_url(self):
# 获取完整的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 # 这是一个抽象基类,不会创建数据库表
@abstractmethod
def get_absolute_url(self):
# 子类必须实现这个方法返回对象的URL
pass
class Article(BaseModel):
"""文章"""
"""文章模型,博客的核心内容"""
# 文章状态选择:草稿或已发布
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
('d', _('Draft')), # 草稿
('p', _('Published')), # 已发布
)
# 评论状态选择:开放或关闭
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
('o', _('Open')), # 开放评论
('c', _('Close')), # 关闭评论
)
# 内容类型选择:文章或页面
TYPE = (
('a', _('Article')),
('p', _('Page')),
('a', _('Article')), # 普通文章
('p', _('Page')), # 静态页面
)
title = models.CharField(_('title'), max_length=200, unique=True)
body = MDTextField(_('body'))
title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题
body = MDTextField(_('body')) # 文章正文支持Markdown格式
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
_('publish time'), blank=False, null=False, default=now) # 发布时间
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
default='p') # 文章状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
default='o') # 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型
views = models.PositiveIntegerField(_('views'), default=0) # 浏览次数
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
on_delete=models.CASCADE) # 作者,关联用户表
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
_('order'), blank=False, null=False, default=0) # 文章排序
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
null=False) # 分类,关联分类表
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 标签,多对多关系
def body_to_string(self):
# 将文章正文转换为字符串
return self.body
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'] # 按排序字段和发布时间倒序排列
verbose_name = _('article') # 在管理后台显示的名称
verbose_name_plural = verbose_name # 复数形式
get_latest_by = 'id' # 获取最新记录的依据
def get_absolute_url(self):
# 获取文章的URL地址包含年月日信息
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
@ -125,21 +141,24 @@ class Article(BaseModel):
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
# 获取文章所属分类的层级结构
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
# 保存文章
super().save(*args, **kwargs)
def viewed(self):
# 增加文章浏览次数
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
# 获取文章评论列表,使用缓存提高性能
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
@ -152,25 +171,23 @@ class Article(BaseModel):
return comments
def get_admin_url(self):
# 获取文章在管理后台的编辑URL
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
# 下一篇
# 获取下一篇已发布的文章
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def prev_article(self):
# 前一篇
# 获取上一篇已发布的文章
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
# 从文章正文中提取第一张图片的URL
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
@ -178,36 +195,35 @@ class Article(BaseModel):
class Category(BaseModel):
"""文章分类"""
name = models.CharField(_('category name'), max_length=30, unique=True)
"""文章分类模型,支持层级结构"""
name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
index = models.IntegerField(default=0, verbose_name=_('index'))
on_delete=models.CASCADE) # 父分类,支持多级分类
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好的标识符
index = models.IntegerField(default=0, verbose_name=_('index')) # 排序索引
class Meta:
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
ordering = ['-index'] # 按索引倒序排列
verbose_name = _('category') # 在管理后台显示的名称
verbose_name_plural = verbose_name # 复数形式
def get_absolute_url(self):
# 获取分类的URL地址
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
# 返回分类名称作为字符串表示
return self.name
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
# 递归获取分类的父级分类树(从当前分类到根分类)
categorys = []
def parse(category):
@ -218,12 +234,9 @@ class Category(BaseModel):
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
# 递归获取当前分类的所有子分类
categorys = []
all_categorys = Category.objects.all()
@ -241,136 +254,157 @@ class Category(BaseModel):
class Tag(BaseModel):
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
"""文章标签模型"""
name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好的标识符
def __str__(self):
# 返回标签名称作为字符串表示
return self.name
def get_absolute_url(self):
# 获取标签的URL地址
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10)
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_article_count(self):
# 获取使用该标签的文章数量
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
ordering = ['name'] # 按名称排序
verbose_name = _('tag') # 在管理后台显示的名称
verbose_name_plural = verbose_name # 复数形式
class Links(models.Model):
"""友情链接"""
"""友情链接模型"""
name = models.CharField(_('link name'), max_length=30, unique=True)
link = models.URLField(_('link'))
sequence = models.IntegerField(_('order'), unique=True)
name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称
link = models.URLField(_('link')) # 链接地址
sequence = models.IntegerField(_('order'), unique=True) # 排序序号
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
_('is show'), default=True, blank=False, null=False) # 是否启用
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
default=LinkShowType.I) # 显示类型
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 修改时间
class Meta:
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
ordering = ['sequence'] # 按序号排序
verbose_name = _('link') # 在管理后台显示的名称
verbose_name_plural = verbose_name # 复数形式
def __str__(self):
# 返回链接名称作为字符串表示
return self.name
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
name = models.CharField(_('title'), max_length=100)
content = models.TextField(_('content'))
sequence = models.IntegerField(_('order'), unique=True)
is_enable = models.BooleanField(_('is enable'), default=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
"""侧边栏模型可以展示一些HTML内容"""
name = models.CharField(_('title'), max_length=100) # 侧边栏标题
content = models.TextField(_('content')) # 侧边栏内容支持HTML
sequence = models.IntegerField(_('order'), unique=True) # 排序序号
is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
last_mod_time = models.DateTimeField(_('modify time'), default=now) # 修改时间
class Meta:
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
ordering = ['sequence'] # 按序号排序
verbose_name = _('sidebar') # 在管理后台显示的名称
verbose_name_plural = verbose_name # 复数形式
def __str__(self):
# 返回侧边栏标题作为字符串表示
return self.name
class BlogSettings(models.Model):
"""blog的配置"""
"""博客配置模型,存储网站的各种设置"""
# 网站基本信息
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
default='') # 网站名称
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
default='') # 网站描述
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
_('site seo description'), max_length=1000, null=False, blank=False, default='') # SEO描述
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
article_sub_length = models.IntegerField(_('article sub length'), default=300)
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
article_comment_count = models.IntegerField(_('article comment count'), default=5)
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
default='') # 网站关键词
# 显示设置
article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页评论数量
# 广告设置
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示广告
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='')
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
_('adsense code'), max_length=2000, null=True, blank=True, default='') # 广告代码
# 评论设置
open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开放评论
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False) # 评论是否需要审核
# 页面设置
global_header = models.TextField("公共头部", null=True, blank=True, default='') # 公共头部HTML
global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 公共尾部HTML
# 备案信息
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
default='') # ICP备案号
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
'是否显示公安备案号', default=False, null=False) # 是否显示公安备案号
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
default='') # 公安备案号
# 统计代码
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='') # 网站统计代码如百度统计、Google Analytics等
class Meta:
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
verbose_name = _('Website configuration') # 在管理后台显示的名称
verbose_name_plural = verbose_name # 复数形式
def __str__(self):
# 返回网站名称作为字符串表示
return self.site_name
def clean(self):
# 确保只能有一个配置实例
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
# 保存配置后清除缓存,确保设置立即生效
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()

@ -1,62 +1,76 @@
# 博客应用的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'), # 清除缓存接口
]

@ -1,23 +1,24 @@
# 博客视图文件,处理博客相关的页面请求
import logging
import os
import uuid
from django.conf import settings
from django.core.paginator import Paginator
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 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
@ -25,6 +26,7 @@ logger = logging.getLogger(__name__)
class ArticleListView(ListView):
"""文章列表视图基类,提供通用的文章列表功能"""
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
@ -33,15 +35,17 @@ class ArticleListView(ListView):
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
paginate_by = settings.PAGINATE_BY # 每页显示的文章数量
page_kwarg = 'page' # URL中页码参数名
link_type = LinkShowType.L # 友情链接显示类型
def get_view_cache_key(self):
# 获取视图缓存键(这个方法似乎有问题,应该返回字符串)
return self.request.get['pages']
@property
def page_number(self):
# 获取当前页码
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
@ -49,21 +53,21 @@ class ArticleListView(ListView):
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
子类必须重写此方法返回查询集的缓存键
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
子类必须重写此方法返回查询集的数据
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
缓存获取页面数据提高性能
:param cache_key: 缓存
:return: 文章列表数据
'''
value = cache.get(cache_key)
if value:
@ -77,50 +81,56 @@ class ArticleListView(ListView):
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
重写默认方法从缓存获取数据
:return: 文章查询集
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
# 为模板添加上下文数据
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
'''
首页
首页视图显示最新的已发布文章
'''
# 友情链接类型
# 友情链接类型:只在首页显示
link_type = LinkShowType.I
def get_queryset_data(self):
# 获取所有已发布的文章
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
# 生成首页的缓存键
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
文章详情页面视图显示单篇文章的完整内容
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
template_name = 'blog/article_detail.html' # 使用的模板
model = Article # 关联的模型
pk_url_kwarg = 'article_id' # URL中的主键参数名
context_object_name = "article" # 模板中的对象变量名
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)
# 为文章详情页准备上下文数据
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
@ -131,30 +141,35 @@ class ArticleDetailView(DetailView):
if page > paginator.num_pages:
page = paginator.num_pages
p_comments = paginator.page(page)
p_comments = paginator.page(page) # 获取当前页的评论
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
# 生成评论分页链接
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
# 添加评论相关数据到上下文
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
# 添加上一篇和下一篇文章
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
# 插件钩子:通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
# 插件钩子:允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
@ -163,23 +178,27 @@ class ArticleDetailView(DetailView):
class CategoryDetailView(ArticleListView):
'''
分类目录列表
分类目录列表视图显示指定分类下的所有文章
'''
page_type = "分类目录归档"
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')
return article_list
def get_queryset_cache_key(self):
# 生成分类页面的缓存键
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
@ -189,9 +208,10 @@ class CategoryDetailView(ArticleListView):
return cache_key
def get_context_data(self, **kwargs):
# 为分类页面添加上下文数据
categoryname = self.categoryname
try:
# 提取分类名称的最后一部分(处理多级分类)
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
@ -202,11 +222,12 @@ class CategoryDetailView(ArticleListView):
class AuthorDetailView(ArticleListView):
'''
作者详情页
作者详情页视图显示指定作者的所有文章
'''
page_type = '作者文章归档'
def get_queryset_cache_key(self):
# 生成作者页面的缓存键
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
@ -214,12 +235,14 @@ class AuthorDetailView(ArticleListView):
return cache_key
def get_queryset_data(self):
# 获取指定作者的所有已发布文章
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
# 为作者页面添加上下文数据
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
@ -228,11 +251,12 @@ class AuthorDetailView(ArticleListView):
class TagDetailView(ArticleListView):
'''
标签列表页面
标签列表页面视图显示指定标签下的所有文章
'''
page_type = '分类标签归档'
def get_queryset_data(self):
# 获取指定标签下的所有已发布文章
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@ -242,6 +266,7 @@ class TagDetailView(ArticleListView):
return article_list
def get_queryset_cache_key(self):
# 生成标签页面的缓存键
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
@ -251,7 +276,7 @@ class TagDetailView(ArticleListView):
return cache_key
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
@ -260,39 +285,46 @@ class TagDetailView(ArticleListView):
class ArchivesView(ArticleListView):
'''
文章归档页面
文章归档页面视图显示所有已发布文章的时间线
'''
page_type = '文章归档'
paginate_by = None
paginate_by = None # 不分页,显示所有文章
page_kwarg = None
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
# 获取所有已发布的文章
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
# 归档页面的缓存键
cache_key = 'archives'
return cache_key
class LinkListView(ListView):
"""友情链接列表视图"""
model = Links
template_name = 'blog/links_list.html'
def get_queryset(self):
# 获取所有启用的友情链接
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
"""Elasticsearch搜索视图"""
def get_context(self):
# 为搜索页面准备上下文数据
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
"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())
@ -300,38 +332,52 @@ class EsSearchView(SearchView):
return context
@csrf_exempt
@csrf_exempt # 跳过CSRF验证因为这是文件上传接口
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
文件上传接口提供图床功能
需要提供正确的签名才能上传文件
:param request: HTTP请求对象
:return: 上传成功返回文件URL列表失败返回错误信息
"""
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
url = static(savepath)
response.append(url)
return HttpResponse(response)
@ -344,6 +390,7 @@ def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
"""404错误页面处理函数"""
if exception:
logger.error(exception)
url = request.get_full_path()
@ -355,6 +402,7 @@ def page_not_found_view(
def server_error_view(request, template_name='blog/error_page.html'):
"""500服务器错误页面处理函数"""
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
@ -366,6 +414,7 @@ def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
"""403权限拒绝页面处理函数"""
if exception:
logger.error(exception)
return render(
@ -375,5 +424,6 @@ def permission_denied_view(
def clean_cache_view(request):
"""清除缓存的管理接口"""
cache.clear()
return HttpResponse('ok')

@ -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 = 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'

@ -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)

Loading…
Cancel
Save