diff --git a/src/DjangoBlog/accounts/forms.py b/src/DjangoBlog/accounts/forms.py index fce4137..fded37b 100644 --- a/src/DjangoBlog/accounts/forms.py +++ b/src/DjangoBlog/accounts/forms.py @@ -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'), # 邮箱字段,用于发送验证码 ) diff --git a/src/DjangoBlog/accounts/models.py b/src/DjangoBlog/accounts/models.py index 3baddbb..178ee7a 100644 --- a/src/DjangoBlog/accounts/models.py +++ b/src/DjangoBlog/accounts/models.py @@ -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, diff --git a/src/DjangoBlog/accounts/urls.py b/src/DjangoBlog/accounts/urls.py index 107a801..3f51980 100644 --- a/src/DjangoBlog/accounts/urls.py +++ b/src/DjangoBlog/accounts/urls.py @@ -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'), # 发送验证码接口 +] diff --git a/src/DjangoBlog/blog/forms.py b/src/DjangoBlog/blog/forms.py index 715be76..c12044b 100644 --- a/src/DjangoBlog/blog/forms.py +++ b/src/DjangoBlog/blog/forms.py @@ -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 diff --git a/src/DjangoBlog/blog/models.py b/src/DjangoBlog/blog/models.py index 083788b..e3e005c 100644 --- a/src/DjangoBlog/blog/models.py +++ b/src/DjangoBlog/blog/models.py @@ -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() diff --git a/src/DjangoBlog/blog/urls.py b/src/DjangoBlog/blog/urls.py index adf2703..14aedf2 100644 --- a/src/DjangoBlog/blog/urls.py +++ b/src/DjangoBlog/blog/urls.py @@ -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//', views.IndexView.as_view(), - name='index_page'), + name='index_page'), # 首页分页 + + # 文章详情页 path( r'article////.html', views.ArticleDetailView.as_view(), - name='detailbyid'), + name='detailbyid'), # 文章详情页(按ID) + + # 分类相关页面 path( r'category/.html', views.CategoryDetailView.as_view(), - name='category_detail'), + name='category_detail'), # 分类页面 path( r'category//.html', views.CategoryDetailView.as_view(), - name='category_detail_page'), + name='category_detail_page'), # 分类页面分页 + + # 作者相关页面 path( r'author/.html', views.AuthorDetailView.as_view(), - name='author_detail'), + name='author_detail'), # 作者页面 path( r'author//.html', views.AuthorDetailView.as_view(), - name='author_detail_page'), + name='author_detail_page'), # 作者页面分页 + + # 标签相关页面 path( r'tag/.html', views.TagDetailView.as_view(), - name='tag_detail'), + name='tag_detail'), # 标签页面 path( r'tag//.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'), # 清除缓存接口 ] diff --git a/src/DjangoBlog/blog/views.py b/src/DjangoBlog/blog/views.py index d5dc7ec..3441cb9 100644 --- a/src/DjangoBlog/blog/views.py +++ b/src/DjangoBlog/blog/views.py @@ -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') diff --git a/src/DjangoBlog/djangoblog/settings.py b/src/DjangoBlog/djangoblog/settings.py index cd3babd..340b902 100644 --- a/src/DjangoBlog/djangoblog/settings.py +++ b/src/DjangoBlog/djangoblog/settings.py @@ -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' diff --git a/src/DjangoBlog/djangoblog/urls.py b/src/DjangoBlog/djangoblog/urls.py index 4aae58a..07e1454 100644 --- a/src/DjangoBlog/djangoblog/urls.py +++ b/src/DjangoBlog/djangoblog/urls.py @@ -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)