Compare commits

...

103 Commits
main ... main

Author SHA1 Message Date
dynastxu 263d370607 Merge branch 'zy_branch' into develop
2 months ago
xdqw166 756a12fe29 11周作业表格终章
2 months ago
xdqw166 e209e1b2d4 11周作业表格
2 months ago
dynastxu 1de159624a docs: 完成介绍视频
2 months ago
dynastxu bc085a06cc docs: 完成开源软件泛读、标注和维护报告文档.docx
2 months ago
dynastxu d8c637ba15 chore(gitignore): 更新.gitignore
3 months ago
dynastxu 8dda6211ec update subtree
3 months ago
dynastxu 78ead977fa Squashed 'src/DjangoBlog/' changes from cf3b252..be4c76b
3 months ago
dynastxu f8bd1d0438 Merge branch 'develop' into xjj_branch
3 months ago
dynastxu d106cb2792 Squashed 'src/DjangoBlog/' changes from cafdade..cf3b252
3 months ago
dynastxu eee1b8c98e update subtree
3 months ago
dynastxu 265045fe65 Merge remote-tracking branch 'origin/zy_branch' into develop
3 months ago
xdqw166 f30135dfd8 修改了base html的报错
3 months ago
xdqw166 90f48adaee 功能增加变换背景颜色
3 months ago
dynastxu 7316b4c8b6 Squashed 'src/DjangoBlog/' changes from b99778c..cafdade
3 months ago
dynastxu 46862ad679 update subtree
3 months ago
dynastxu 8fa2a1d76f chore: 更新.gitignore文件忽略规则
3 months ago
dynastxu 654d036823 update subtree
3 months ago
dynastxu 60561750c0 Squashed 'src/DjangoBlog/' changes from 0bb6193..b99778c
3 months ago
dynastxu fedfe15b64 feat(script): 添加 Git 清理脚本以自动化清理未跟踪文件夹
3 months ago
dynastxu d71b49f1ca docs(readme): 更新 README 文件内容
3 months ago
dynastxu e33542c8a0 chore: 添加 MIT 许可证文件
3 months ago
dynastxu 43ea653c11 update subtree
3 months ago
dynastxu 72fddfe377 Squashed 'src/DjangoBlog/' changes from 13ebbc8..0bb6193
3 months ago
dynastxu af79acffbc chore(gitignore): 更新.gitignore文件
3 months ago
dynastxu 8a37d8cc06 Merge branch 'lrj_branch' into develop
3 months ago
ZUOFEikabuto b48a67b203 feat: [lrj] 完成OAuth模块代码质量分析和注释
3 months ago
ZUOFEikabuto cd96dfe561 feat: 完成OAuth模块代码质量分析和注释
3 months ago
ZUOFEikabuto 26efdcb6f4 在更新后的代码基础上添加oauth注释
3 months ago
dynastxu 151535a74e 完善文档
3 months ago
dynastxu ef8f3f3d19 完成 编码规范.docx
3 months ago
dynastxu dcc31a2bdf 完成 开源软件的质量分析报告文档.docx 大部分内容
3 months ago
dynastxu afaacc22cf 删除无用文件
3 months ago
dynastxu 741cac2e1f Merge branch 'xjj_branch' into develop
3 months ago
dynastxu 854e8e28c7 feat(script): 添加推送子树的批处理脚本
3 months ago
dynastxu 400192beb7 Merge branch 'zy_branch' into develop
3 months ago
dynastxu aadcbfbfc3 Merge remote-tracking branch 'origin/shw_branch' into develop
3 months ago
dynastxu f285750263 Merge remote-tracking branch 'origin/bjy_branch' into develop
3 months ago
dynastxu a5637dad09 chore(subtree): 更新子树脚本逻辑
3 months ago
dynastxu eac243818d Squashed 'src/DjangoBlog/' changes from 408d19c..13ebbc8
3 months ago
dynastxu 3240542cb9 Merge commit 'eac243818d651281e841188481859d3e6e251cc8' into xjj_branch
3 months ago
dynastxu b85f85125b chore(scripts): 添加更新子树的批处理脚本
3 months ago
bu661 01f9792c1c blog注释
3 months ago
dynastxu 001bc85a81 Squashed 'src/DjangoBlog/' changes from 1f969cc..408d19c
3 months ago
dynastxu cf73d21b06 Merge commit '001bc85a8157f3742d79d513e5300bcf3b975edb' into xjj_branch
3 months ago
wei664 cf58b9ef18 accounts代码注释
3 months ago
xdqw166 7fc1b25b73 代码注释
3 months ago
xdqw166 e8698c02a4 代码注释
3 months ago
wei664 7c41266ac6 Merge branch 'develop' into shw_branch
3 months ago
xdqw166 5ef0f8fb0f 代码注释
3 months ago
xdqw166 0eb7232c46 Merge remote-tracking branch 'origin/develop' into zy_branch
3 months ago
wei664 285ac07e41 测试提交是否成功
3 months ago
dynastxu 7e7ba6f503 chore(submodule): 添加 DjangoBlog 子模块
3 months ago
dynastxu b3b6de311e Merge commit '4e5377d89454be93a12bdc5e1d147b806ae35205' as 'src/DjangoBlog'
3 months ago
dynastxu 4e5377d894 Squashed 'src/DjangoBlog/' content from commit 1f969cc
3 months ago
dynastxu e50afe5c6b 移除子树以便重新添加其他分支
3 months ago
dynastxu 6f8f33fc1c Merge commit '524e7d1ed53ff015743c250b8695258775e4ee8e' as 'src/DjangoBlog'
3 months ago
dynastxu 524e7d1ed5 Squashed 'src/DjangoBlog/' content from commit ef67f8d
3 months ago
dynastxu 40f99159c5 删除子模块引用
3 months ago
dynastxu 1410b5c9df Merge remote-tracking branch 'origin/develop'
3 months ago
dynastxu 4eefae61cf 完善文档
3 months ago
dynastxu 05188c4dfb Merge remote-tracking branch 'origin/xjj_branch' into develop
3 months ago
p7opzmxku 20bb6cb93c 重新上传文件
3 months ago
dynastxu ac34e1ff53 删除异常的文件
3 months ago
dynastxu 52a8438eac 重新上传上次提交
3 months ago
dynastxu 0b6d110372 完成软件界面设计说明书
3 months ago
xdqw166 78de182afd 删除错误的文档
3 months ago
xdqw166 ae476bbab2 Merge branch 'zy_branch_fix' into zy_branch
3 months ago
xdqw166 5614ee69bc 交了软件界面设计大作业
3 months ago
xdqw166 6be53f48bb 交了软件界面设计大作业
3 months ago
xdqw166 f91c8887e6 Merge remote-tracking branch 'origin/zy_branch' into zy_branch
3 months ago
xdqw166 249e858738 交了软件界面设计大作业
3 months ago
dynastxu 398a282889 Merge branch 'xjj_branch' into develop
3 months ago
dynastxu 9fcc4d5d93 修改数据模型报告
3 months ago
dynastxu 700f5f7ddc Merge branch 'xjj_branch' into develop
3 months ago
dynastxu 2fc3891ee3 撰写报告部分内容
3 months ago
dynastxu 6b840e838e chore: 重构项目结构
3 months ago
pk5ywhism 4fc3fd7c01 建模系统数据模型,第五周大作业
4 months ago
pk5ywhism 674cbb7960 第五周大作业
4 months ago
dynastxu 94e7af9d2c docs: 推送数据模型设计文档初稿
4 months ago
p7opzmxku ace21826e1 完成第四周作业
4 months ago
p7opzmxku 0059353ca6 合并 lrj_branch
4 months ago
dynastxu e7faf82173 docs: 删除无用文件
4 months ago
p7opzmxku c6e972f5ec 合并 bjy_branch
4 months ago
dynastxu efadb347bc docs: 删除无用文件
4 months ago
p7opzmxku f401f266f0 合并 shw_branch
4 months ago
dynastxu 7e7b376691 docs: 删除无用文件
4 months ago
dynastxu 4cb4b0158c docs: 删除无用文件
4 months ago
p7opzmxku cd3fc6fed9 合并 zy_branch
4 months ago
dynastxu b94181e8a0 docs: 删除无用文件
4 months ago
p7opzmxku 773412798d 完成泛读报告
4 months ago
dynastxu 15b9d12769 docs: 更新泛读报告
4 months ago
dynastxu b72011bb7a docs: 完成泛读报告
4 months ago
p7opzmxku 4359e3ffa8 完成代码泛读报告第一项及第二项
4 months ago
dynastxu 707d204cbf docs(architecture): 添加项目架构图和模块类图
4 months ago
pk5ywhism bd7d84ec4a Update 345.py
4 months ago
xdqw166 dbb933deed 改了一行代码
4 months ago
xdqw166 114143f90b Merge remote-tracking branch 'origin/develop' into zy_branch
4 months ago
p7opzmxku 89e3b1bdb7 添加 DjangoBlog 子模块
4 months ago
dynastxu ebfac634c1 chore(submodule): 添加 DjangoBlog 子模块
4 months ago
wei664 9e524cfbad 测试提交是否成功
4 months ago
bu661 efeca12632 132
4 months ago
xdqw166 4e76e19165 船舰草稿
4 months ago

9
.gitignore vendored

@ -1 +1,8 @@
.idea
/.idea/
# DjangoBlog
.env
/logs/
/collectedstatic/
/djangoblog/
/static/

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,2 +1,4 @@
# 阅读和分析开源软件
- 软件名:[**DjangoBlog**](https://github.com/liangliangyy/DjangoBlog)
- 协议:[**MIT License**](src/DjangoBlog/LICENSE)

@ -0,0 +1,53 @@
digraph "djangoblog" {
splines = ortho;
fontname = "Inconsolata";
node [colorscheme = ylgnbu4];
edge [colorscheme = dark28, dir = both];
accounts_bloguser [shape = record, pos = "6.458,23.583!" , label = "{ accounts_bloguser | password : varchar(128)\l last_login : datetime(6)\l is_superuser : tinyint(1)\l username : varchar(150)\l first_name : varchar(150)\l last_name : varchar(150)\l email : varchar(254)\l is_staff : tinyint(1)\l is_active : tinyint(1)\l date_joined : datetime(6)\l nickname : varchar(100)\l source : varchar(100)\l creation_time : datetime(6)\l last_modify_time : datetime(6)\l| id : bigint(20)\l }"];
accounts_bloguser_groups [shape = record, pos = "2.597,19.375!" , label = "{ accounts_bloguser_groups | bloguser_id : bigint(20)\l group_id : int(11)\l| id : bigint(20)\l }"];
accounts_bloguser_user_permissions [shape = record, pos = "1.444,22.750!" , label = "{ accounts_bloguser_user_permissions | bloguser_id : bigint(20)\l permission_id : int(11)\l| id : bigint(20)\l }"];
auth_group [shape = record, pos = "0.069,19.778!" , label = "{ auth_group | name : varchar(150)\l| id : int(11)\l }"];
auth_group_permissions [shape = record, pos = "-2.167,22.111!" , label = "{ auth_group_permissions | group_id : int(11)\l permission_id : int(11)\l| id : bigint(20)\l }"];
auth_permission [shape = record, pos = "-0.556,25.153!" , label = "{ auth_permission | name : varchar(255)\l content_type_id : int(11)\l codename : varchar(100)\l| id : int(11)\l }"];
blog_article [shape = record, pos = "10.875,25.528!" , label = "{ blog_article | title : varchar(200)\l body : longtext\l pub_time : datetime(6)\l status : varchar(1)\l comment_status : varchar(1)\l type : varchar(1)\l views : int(10) unsigned\l article_order : int(11)\l show_toc : tinyint(1)\l author_id : bigint(20)\l category_id : int(11)\l creation_time : datetime(6)\l last_modify_time : datetime(6)\l| id : int(11)\l }"];
blog_article_tags [shape = record, pos = "14.750,22.736!" , label = "{ blog_article_tags | article_id : int(11)\l tag_id : int(11)\l| id : bigint(20)\l }"];
blog_blogsettings [shape = record, pos = "-2.167,12.972!" , label = "{ blog_blogsettings | site_name : varchar(200)\l site_description : longtext\l site_seo_description : longtext\l site_keywords : longtext\l article_sub_length : int(11)\l sidebar_article_count : int(11)\l sidebar_comment_count : int(11)\l article_comment_count : int(11)\l show_google_adsense : tinyint(1)\l google_adsense_codes : longtext\l open_site_comment : tinyint(1)\l beian_code : varchar(2000)\l analytics_code : longtext\l show_gongan_code : tinyint(1)\l gongan_beiancode : longtext\l global_footer : longtext\l global_header : longtext\l comment_need_review : tinyint(1)\l| id : bigint(20)\l }"];
blog_category [shape = record, pos = "12.556,28.889!" , label = "{ blog_category | name : varchar(30)\l slug : varchar(60)\l index : int(11)\l parent_category_id : int(11)\l creation_time : datetime(6)\l last_modify_time : datetime(6)\l| id : int(11)\l }"];
blog_links [shape = record, pos = "5.097,12.972!" , label = "{ blog_links | name : varchar(30)\l link : varchar(200)\l sequence : int(11)\l is_enable : tinyint(1)\l show_type : varchar(1)\l last_mod_time : datetime(6)\l creation_time : datetime(6)\l| id : bigint(20)\l }"];
blog_sidebar [shape = record, pos = "8.361,12.972!" , label = "{ blog_sidebar | name : varchar(100)\l content : longtext\l sequence : int(11)\l is_enable : tinyint(1)\l last_mod_time : datetime(6)\l creation_time : datetime(6)\l| id : bigint(20)\l }"];
blog_tag [shape = record, pos = "17.569,22.014!" , label = "{ blog_tag | name : varchar(30)\l slug : varchar(60)\l creation_time : datetime(6)\l last_modify_time : datetime(6)\l| id : int(11)\l }"];
comments_comment [shape = record, pos = "10.292,19.931!" , label = "{ comments_comment | body : longtext\l is_enable : tinyint(1)\l article_id : int(11)\l author_id : bigint(20)\l parent_comment_id : bigint(20)\l creation_time : datetime(6)\l last_modify_time : datetime(6)\l| id : bigint(20)\l }"];
django_admin_log [shape = record, pos = "4.917,27.264!" , label = "{ django_admin_log | action_time : datetime(6)\l object_id : longtext\l object_repr : varchar(200)\l action_flag : smallint(5) unsigned\l change_message : longtext\l content_type_id : int(11)\l user_id : bigint(20)\l| id : int(11)\l }"];
django_content_type [shape = record, pos = "1.625,27.236!" , label = "{ django_content_type | app_label : varchar(100)\l model : varchar(100)\l| id : int(11)\l }"];
django_migrations [shape = record, pos = "-2.167,5.722!" , label = "{ django_migrations | app : varchar(255)\l name : varchar(255)\l applied : datetime(6)\l| id : bigint(20)\l }"];
django_session [shape = record, pos = "0.917,5.722!" , label = "{ django_session | session_data : longtext\l expire_date : datetime(6)\l| session_key : varchar(40)\l }"];
django_site [shape = record, pos = "3.931,5.722!" , label = "{ django_site | domain : varchar(100)\l name : varchar(50)\l| id : int(11)\l }"];
oauth_oauthconfig [shape = record, pos = "1.611,12.972!" , label = "{ oauth_oauthconfig | type : varchar(10)\l appkey : varchar(200)\l appsecret : varchar(200)\l callback_url : varchar(200)\l is_enable : tinyint(1)\l creation_time : datetime(6)\l last_modify_time : datetime(6)\l| id : bigint(20)\l }"];
oauth_oauthuser [shape = record, pos = "6.472,17.667!" , label = "{ oauth_oauthuser | openid : varchar(50)\l nickname : varchar(50)\l token : varchar(150)\l picture : varchar(350)\l type : varchar(50)\l email : varchar(50)\l metadata : longtext\l author_id : bigint(20)\l creation_time : datetime(6)\l last_modify_time : datetime(6)\l| id : bigint(20)\l }"];
owntracks_owntracklog [shape = record, pos = "19.708,12.972!" , label = "{ owntracks_owntracklog | tid : varchar(100)\l lat : double\l lon : double\l creation_time : datetime(6)\l| id : bigint(20)\l }"];
servermanager_commands [shape = record, pos = "15.792,12.972!" , label = "{ servermanager_commands | title : varchar(300)\l command : varchar(2000)\l describe : varchar(300)\l creation_time : datetime(6)\l last_modify_time : datetime(6)\l| id : bigint(20)\l }"];
servermanager_emailsendlog [shape = record, pos = "11.625,12.972!" , label = "{ servermanager_emailsendlog | emailto : varchar(300)\l title : varchar(2000)\l content : longtext\l send_result : tinyint(1)\l creation_time : datetime(6)\l| id : bigint(20)\l }"];
accounts_bloguser_groups -> accounts_bloguser [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "bloguser_id:id", headlabel = ""];
accounts_bloguser_groups -> auth_group [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "group_id:id", headlabel = ""];
accounts_bloguser_user_permissions -> accounts_bloguser [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "bloguser_id:id", headlabel = ""];
accounts_bloguser_user_permissions -> auth_permission [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "permission_id:id", headlabel = ""];
auth_group_permissions -> auth_group [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "group_id:id", headlabel = ""];
auth_group_permissions -> auth_permission [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "permission_id:id", headlabel = ""];
auth_permission -> django_content_type [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "content_type_id:id", headlabel = ""];
blog_article -> accounts_bloguser [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "author_id:id", headlabel = ""];
blog_article -> blog_category [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "category_id:id", headlabel = ""];
blog_article_tags -> blog_article [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "article_id:id", headlabel = ""];
blog_article_tags -> blog_tag [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "tag_id:id", headlabel = ""];
blog_category -> blog_category [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "parent_category_id:id", headlabel = ""];
comments_comment -> accounts_bloguser [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "author_id:id", headlabel = ""];
comments_comment -> blog_article [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "article_id:id", headlabel = ""];
comments_comment -> comments_comment [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "parent_comment_id:id", headlabel = ""];
django_admin_log -> accounts_bloguser [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "user_id:id", headlabel = ""];
django_admin_log -> django_content_type [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "content_type_id:id", headlabel = ""];
oauth_oauthuser -> accounts_bloguser [color = "#595959", style = solid , arrowtail = none , arrowhead = normal , taillabel = "", label = "author_id:id", headlabel = ""];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

@ -0,0 +1,22 @@
graph TD
subgraph DjangoBlog Project
A[accounts] -->|用户体系| B[blog]
A -->|用户登录| C[comments]
A -->|用户绑定| D[oauth]
A -->|用户轨迹| E[owntracks]
B -->|文章内容| C
B -->|搜索功能| F[djangoblog]
B -->|插件扩展| G[plugins]
B -->|API接口| H[servermanager]
F -->|搜索引擎| B
F -->|站点地图| B
G -->|插件功能| B
H -->|邮件/命令| B
H -->|微信处理| B
D -->|第三方登录| A
end

@ -0,0 +1,162 @@
---
config:
layout: elk
---
erDiagram
BlogUser {
int id PK
string username
string email
string nickname
datetime creation_time
datetime last_modify_time
string source
}
Article {
int id PK
string title
text body
datetime pub_time
char status
char comment_status
char type
int views
int author_id FK
int article_order
boolean show_toc
int category_id FK
}
Category {
int id PK
string name
int parent_category_id FK
string slug
int index
}
Tag {
int id PK
string name
string slug
}
Comment {
int id PK
text body
datetime creation_time
datetime last_modify_time
int author_id FK
int article_id FK
int parent_comment_id FK
boolean is_enable
}
OAuthUser {
int id PK
int author_id FK
string openid
string nickname
string token
string picture
string type
string email
text metadata
datetime creation_time
datetime last_modify_time
}
OAuthConfig {
int id PK
string type
string appkey
string appsecret
string callback_url
boolean is_enable
datetime creation_time
datetime last_modify_time
}
BlogSettings {
int id PK
string site_name
text site_description
text site_seo_description
text site_keywords
int article_sub_length
int sidebar_article_count
int sidebar_comment_count
int article_comment_count
boolean show_google_adsense
text google_adsense_codes
boolean open_site_comment
text global_header
text global_footer
string beian_code
text analytics_code
boolean show_gongan_code
text gongan_beiancode
boolean comment_need_review
}
SideBar {
int id PK
string name
text content
int sequence
boolean is_enable
datetime creation_time
datetime last_mod_time
}
Links {
int id PK
string name
string link
int sequence
boolean is_enable
char show_type
datetime creation_time
datetime last_mod_time
}
OwnTrackLog {
int id PK
string tid
float lat
float lon
datetime creation_time
}
commands {
int id PK
string title
string command
string describe
datetime creation_time
datetime last_modify_time
}
EmailSendLog {
int id PK
string emailto
string title
text content
boolean send_result
datetime creation_time
}
BlogUser ||--o{ Article : "作者发表"
BlogUser ||--o{ Comment : "用户评论"
BlogUser ||--o{ OAuthUser : "关联第三方账号"
Article ||--o{ Comment : "文章拥有评论"
Article }o--|| Category : "属于分类"
Article }o--o{ Tag : "包含标签"
Category ||--o{ Category : "父分类"
Comment ||--o{ Comment : "父评论"
OAuthUser }o--|| OAuthConfig : "使用配置"

@ -0,0 +1,133 @@
---
config:
layout: elk
---
classDiagram
%% 模型类
class BlogUser {
+String nickname
+DateTime creation_time
+DateTime last_modify_time
+String source
+get_absolute_url()
+get_full_url()
}
%% 表单类
class BlogUserCreationForm {
+CharField password1
+CharField password2
+clean_password2()
+save()
}
class BlogUserChangeForm {
+__init__()
}
class LoginForm {
+__init__()
}
class RegisterForm {
+__init__()
+clean_email()
}
class ForgetPasswordForm {
+CharField new_password1
+CharField new_password2
+EmailField email
+CharField code
+clean_new_password2()
+clean_email()
+clean_code()
}
class ForgetPasswordCodeForm {
+EmailField email
}
%% 视图类
class RegisterView {
+form_valid()
}
class LogoutView {
+get()
}
class LoginView {
+get_context_data()
+form_valid()
+get_success_url()
}
class ForgetPasswordView {
+form_valid()
}
class ForgetPasswordEmailCode {
+post()
}
%% 认证后端
class EmailOrUsernameModelBackend {
+authenticate()
+get_user()
}
%% 应用配置
class AccountsConfig
%% 测试类
class AccountTest
%% 工具函数
class utils {
+send_verify_email()
+verify()
+set_code()
+get_code()
}
%% 继承关系
BlogUser --|> AbstractUser
BlogUserCreationForm --|> ModelForm
BlogUserChangeForm --|> UserChangeForm
LoginForm --|> AuthenticationForm
RegisterForm --|> UserCreationForm
ForgetPasswordForm --|> Form
ForgetPasswordCodeForm --|> Form
RegisterView --|> FormView
LogoutView --|> RedirectView
LoginView --|> FormView
ForgetPasswordView --|> FormView
ForgetPasswordEmailCode --|> View
EmailOrUsernameModelBackend --|> ModelBackend
AccountsConfig --|> AppConfig
AccountTest --|> TestCase
%% 关联关系
BlogUserCreationForm --> BlogUser : 创建
BlogUserChangeForm --> BlogUser : 修改
RegisterForm --> BlogUser : 注册
ForgetPasswordForm --> BlogUser : 重置密码
RegisterView --> RegisterForm : 使用
LoginView --> LoginForm : 使用
ForgetPasswordView --> ForgetPasswordForm : 使用
ForgetPasswordEmailCode --> ForgetPasswordCodeForm : 使用
EmailOrUsernameModelBackend --> BlogUser : 认证
AccountTest --> BlogUser : 测试
AccountTest --> utils : 测试
ForgetPasswordForm --> utils : 验证
ForgetPasswordEmailCode --> utils : 发送邮件
RegisterView --> utils : 发送邮件
%% 依赖关系
views ..> utils : 导入
forms ..> BlogUser : 导入
tests ..> BlogUser : 导入
tests ..> utils : 导入
admin ..> BlogUser : 导入

@ -0,0 +1,266 @@
---
config:
layout: elk
---
classDiagram
%% 基础模型类
class BaseModel {
<<abstract>>
+id
+creation_time
+last_modify_time
+save()
+get_full_url()
+get_absolute_url()*
}
%% 核心模型类
class Article {
+title
+body
+pub_time
+status
+comment_status
+type
+views
+article_order
+show_toc
+body_to_string()
+get_category_tree()
+viewed()
+comment_list()
+get_admin_url()
+next_article()
+prev_article()
+get_first_image_url()
}
class Category {
+name
+parent_category
+slug
+index
+get_category_tree()
+get_sub_categorys()
}
class Tag {
+name
+slug
+get_article_count()
}
class Links {
+name
+link
+sequence
+is_enable
+show_type
}
class SideBar {
+name
+content
+sequence
+is_enable
}
class BlogSettings {
+site_name
+site_description
+site_seo_description
+site_keywords
+article_sub_length
+sidebar_article_count
+sidebar_comment_count
+article_comment_count
+show_google_adsense
+google_adsense_codes
+open_site_comment
+global_header
+global_footer
+beian_code
+analytics_code
+show_gongan_code
+gongan_beiancode
+comment_need_review
+clean()
}
%% 枚举类型
class LinkShowType {
<<enumeration>>
I, L, P, A, S
}
%% 表单类
class BlogSearchForm {
+querydata
+search()
}
%% 管理类
class ArticlelAdmin {
+list_display
+list_filter
+actions
+link_to_category()
+get_form()
+get_view_on_site_url()
}
class TagAdmin
class CategoryAdmin
class LinksAdmin
class SideBarAdmin
class BlogSettingsAdmin
%% 视图类
class ArticleListView {
<<abstract>>
+page_type
+link_type
+get_queryset_cache_key()*
+get_queryset_data()*
+get_queryset()
+get_context_data()
}
class IndexView
class ArticleDetailView {
+get_context_data()
}
class CategoryDetailView
class AuthorDetailView
class TagDetailView
class ArchivesView
class LinkListView
class EsSearchView
%% 中间件类
class OnlineMiddleware {
+__call__()
}
%% 搜索索引类
class ArticleIndex {
+text
+get_model()
+index_queryset()
}
%% Elasticsearch 文档类
class GeoIp {
+continent_name
+country_iso_code
+country_name
+location
}
class UserAgentBrowser
class UserAgentOS
class UserAgentDevice
class UserAgent
class ElapsedTimeDocument {
+url
+time_taken
+log_datetime
+ip
+geoip
+useragent
}
class ElaspedTimeDocumentManager {
+build_index()
+delete_index()
+create()
}
class ArticleDocument {
+body
+title
+author
+category
+tags
+pub_time
+status
+comment_status
+type
+views
+article_order
}
class ArticleDocumentManager {
+create_index()
+delete_index()
+convert_to_doc()
+rebuild()
+update_docs()
}
%% 应用配置类
class BlogConfig
%% 测试类
class ArticleTest
%% 继承关系
BaseModel <|-- Article
BaseModel <|-- Category
BaseModel <|-- Tag
ArticleListView <|-- IndexView
ArticleListView <|-- CategoryDetailView
ArticleListView <|-- AuthorDetailView
ArticleListView <|-- TagDetailView
ArticleListView <|-- ArchivesView
ArticleDetailView --|> DetailView
LinkListView --|> ListView
EsSearchView --|> SearchView
UserAgentOS --|> UserAgentBrowser
%% 关联关系
Article --> Category : Foreign Key
Article --> Tag : Many-to-Many
Article --> settings.AUTH_USER_MODEL : Foreign Key
Category --> Category : Self-reference (Parent Category)
Links --> LinkShowType : Uses
ArticleDocument --> Article : Mapping
ElapsedTimeDocument --> GeoIp : Contains
ElapsedTimeDocument --> UserAgent : Contains
UserAgent --> UserAgentBrowser : Contains
UserAgent --> UserAgentOS : Contains
UserAgent --> UserAgentDevice : Contains
%% 管理关系
ArticlelAdmin --> Article : 管理
TagAdmin --> Tag : 管理
CategoryAdmin --> Category : 管理
LinksAdmin --> Links : 管理
SideBarAdmin --> SideBar : 管理
BlogSettingsAdmin --> BlogSettings : 管理
%% 视图与模型关系
IndexView --> Article : 查询
ArticleDetailView --> Article : 详情
CategoryDetailView --> Category : 查询
AuthorDetailView --> Article : 查询
TagDetailView --> Tag : 查询
ArchivesView --> Article : 查询
LinkListView --> Links : 查询
%% 搜索关系
ArticleIndex --> Article : 索引
BlogSearchForm --> SearchForm : 继承
%% 文档管理关系
ArticleDocumentManager --> ArticleDocument : 管理
ElaspedTimeDocumentManager --> ElapsedTimeDocument : 管理
%% 测试关系
ArticleTest --> Article : 测试
ArticleTest --> Category : 测试
ArticleTest --> Tag : 测试
ArticleTest --> SideBar : 测试
ArticleTest --> Links : 测试

@ -0,0 +1,77 @@
---
config:
layout: elk
---
classDiagram
%% 模型类
class Comment {
+body
+creation_time
+last_modify_time
+is_enable
+__str__()
}
%% 表单类
class CommentForm {
+parent_comment_id
}
%% 视图类
class CommentPostView {
+dispatch()
+get()
+form_invalid()
+form_valid()
}
%% 管理类
class CommentAdmin {
+list_display
+list_filter
+actions
+link_to_userinfo()
+link_to_article()
}
%% 应用配置类
class CommentsConfig
%% 测试类
class CommentsTest
%% 工具函数
class utils {
+send_comment_email()
}
%% 继承关系
CommentForm --|> ModelForm
CommentPostView --|> FormView
CommentAdmin --|> ModelAdmin
CommentsConfig --|> AppConfig
CommentsTest --|> TransactionTestCase
%% 关联关系
Comment --> AUTH_USER_MODEL : author
Comment --> Article : article
Comment --> Comment : parent_comment
CommentForm --> Comment : create
CommentPostView --> CommentForm : use
CommentPostView --> Comment : create
CommentAdmin --> Comment : manage
CommentsTest --> Comment : test
%% 依赖关系
CommentPostView ..> BlogUser : import
CommentPostView ..> Article : import
CommentsTest ..> BlogUser : import
CommentsTest ..> Article : import
CommentsTest ..> Category : import
utils ..> Comment : import
CommentAdmin ..> Comment : import
%% 方法调用关系
CommentPostView --> utils : possible_call
CommentsTest --> utils : test_call

@ -0,0 +1,264 @@
---
config:
layout: elk
---
classDiagram
%% 管理站点类
class DjangoBlogAdminSite {
+site_header
+site_title
+has_permission()
}
%% 应用配置类
class DjangoblogAppConfig {
+ready()
}
%% 信号处理器
class blog_signals {
<<module>>
+send_email_signal
+oauth_user_login_signal
+send_email_signal_handler()
+oauth_user_login_signal_handler()
+model_post_save_callback()
+user_auth_callback()
}
%% 搜索引擎相关类
class ElasticSearchBackend {
+manager
+search()
+get_suggestion()
+_create()
+_delete()
+_rebuild()
+update()
+remove()
+clear()
}
class ElasticSearchQuery {
+_convert_datetime()
+clean()
+build_query_fragment()
+get_count()
+get_spelling_suggestion()
}
class ElasticSearchModelSearchForm {
+search()
}
class ElasticSearchEngine
class WhooshSearchBackend {
+RESERVED_WORDS
+RESERVED_CHARACTERS
+setup()
+build_schema()
+update()
+remove()
+clear()
+delete_index()
+optimize()
+search()
+more_like_this()
+_process_results()
+create_spelling_suggestion()
}
class WhooshSearchQuery {
+_convert_datetime()
+clean()
+build_query_fragment()
}
class WhooshEngine
class WhooshHtmlFormatter {
+template
}
%% RSS订阅类
class DjangoBlogFeed {
+feed_type
+description
+title
+link
+author_name()
+author_link()
+items()
+item_title()
+item_description()
+feed_copyright()
+item_link()
+item_guid()
}
%% 日志管理类
class LogEntryAdmin {
+list_filter
+search_fields
+list_display
+has_add_permission()
+has_change_permission()
+has_delete_permission()
+object_link()
+user_link()
+get_queryset()
+get_actions()
}
%% 站点地图类
class StaticViewSitemap {
+priority
+changefreq
+items()
+location()
}
class ArticleSiteMap {
+changefreq
+priority
+items()
+lastmod()
}
class CategorySiteMap {
+changefreq
+priority
+items()
+lastmod()
}
class TagSiteMap {
+changefreq
+priority
+items()
+lastmod()
}
class UserSiteMap {
+changefreq
+priority
+items()
+lastmod()
}
%% 蜘蛛通知类
class SpiderNotify {
<<static>>
+baidu_notify()
+notify()
}
%% 测试类
class DjangoBlogTest
%% 工具类
class CommonMarkdown {
<<static>>
+_convert_markdown()
+get_markdown_with_toc()
+get_markdown()
}
%% 设置类(配置)
class settings {
<<module>>
+INSTALLED_APPS
+MIDDLEWARE
+DATABASES
+HAYSTACK_CONNECTIONS
+CACHES
+LOGGING
}
%% URL配置
class urls {
<<module>>
+urlpatterns
+handler404
+handler500
+handle403
}
%% WSGI配置
class wsgi {
<<module>>
+application
}
%% 继承关系
DjangoBlogAdminSite --|> AdminSite
DjangoblogAppConfig --|> AppConfig
ElasticSearchBackend --|> BaseSearchBackend
ElasticSearchQuery --|> BaseSearchQuery
ElasticSearchEngine --|> BaseEngine
WhooshSearchBackend --|> BaseSearchBackend
WhooshSearchQuery --|> BaseSearchQuery
WhooshEngine --|> BaseEngine
WhooshHtmlFormatter --|> HtmlFormatter
DjangoBlogFeed --|> Feed
LogEntryAdmin --|> ModelAdmin
StaticViewSitemap --|> Sitemap
ArticleSiteMap --|> Sitemap
CategorySiteMap --|> Sitemap
TagSiteMap --|> Sitemap
UserSiteMap --|> Sitemap
DjangoBlogTest --|> TestCase
%% 关联关系
DjangoBlogAdminSite --> ArticlelAdmin : 注册
DjangoBlogAdminSite --> CategoryAdmin : 注册
DjangoBlogAdminSite --> TagAdmin : 注册
DjangoBlogAdminSite --> LinksAdmin : 注册
DjangoBlogAdminSite --> SideBarAdmin : 注册
DjangoBlogAdminSite --> BlogSettingsAdmin : 注册
DjangoBlogAdminSite --> CommandsAdmin : 注册
DjangoBlogAdminSite --> EmailSendLogAdmin : 注册
DjangoBlogAdminSite --> BlogUserAdmin : 注册
DjangoBlogAdminSite --> CommentAdmin : 注册
DjangoBlogAdminSite --> OAuthUserAdmin : 注册
DjangoBlogAdminSite --> OAuthConfigAdmin : 注册
DjangoBlogAdminSite --> OwnTrackLogsAdmin : 注册
DjangoBlogAdminSite --> SiteAdmin : 注册
DjangoBlogAdminSite --> LogEntryAdmin : 注册
ElasticSearchBackend --> ArticleDocumentManager : 使用
ElasticSearchModelSearchForm --> ElasticSearchBackend : 配置
WhooshSearchBackend --> ChineseAnalyzer : 使用
blog_signals --> Comment : 信号处理
blog_signals --> OAuthUser : 信号处理
blog_signals --> LogEntry : 信号处理
blog_signals --> EmailSendLog : 记录
DjangoBlogFeed --> Article : 获取内容
ArticleSiteMap --> Article : 映射
CategorySiteMap --> Category : 映射
TagSiteMap --> Tag : 映射
UserSiteMap --> Article : 映射
%% 依赖关系
DjangoblogAppConfig ..> plugin_manage.loader : 导入
blog_signals ..> comments.utils : 导入
blog_signals ..> djangoblog.utils : 导入
blog_signals ..> djangoblog.spider_notify : 导入
ElasticSearchBackend ..> blog.documents : 导入
WhooshSearchBackend ..> jieba.analyse : 导入
utils ..> bleach : 导入
utils ..> markdown : 导入
%% 配置关系
settings --> INSTALLED_APPS : 包含所有应用
settings --> MIDDLEWARE : 包含中间件
urls --> admin_site : 包含管理路由
urls --> blog.urls : 包含博客路由
urls --> comments.urls : 包含评论路由
urls --> accounts.urls : 包含账户路由
urls --> oauth.urls : 包含OAuth路由
urls --> servermanager.urls : 包含服务器管理路由
urls --> owntracks.urls : 包含位置跟踪路由

@ -0,0 +1,194 @@
---
config:
layout: elk
---
classDiagram
%% Models
class OAuthUser {
+ForeignKey author
+CharField openid
+CharField nickname
+CharField token
+CharField picture
+CharField type
+CharField email
+TextField metadata
+DateTimeField creation_time
+DateTimeField last_modify_time
+__str__()
}
class OAuthConfig {
+CharField type
+CharField appkey
+CharField appsecret
+CharField callback_url
+BooleanField is_enable
+DateTimeField creation_time
+DateTimeField last_modify_time
+clean()
+__str__()
}
%% Admin Classes
class OAuthUserAdmin {
+search_fields
+list_per_page
+list_display
+list_display_links
+list_filter
+readonly_fields
+get_readonly_fields()
+has_add_permission()
+link_to_usermodel()
+show_user_image()
}
class OAuthConfigAdmin {
+list_display
+list_filter
}
%% Forms
class RequireEmailForm {
+EmailField email
+IntegerField oauthid
+__init__()
}
%% Views
class RequireEmailView {
+form_class
+template_name
+get()
+get_initial()
+get_context_data()
+form_valid()
}
%% OAuth Manager Classes
class BaseOauthManager {
<<Abstract>>
+AUTH_URL
+TOKEN_URL
+API_URL
+ICON_NAME
+access_token
+openid
+is_access_token_set
+is_authorized
+get_authorization_url()
+get_access_token_by_code()
+get_oauth_userinfo()
+get_picture()
+do_get()
+do_post()
+get_config()
}
class ProxyManagerMixin {
+proxies
+do_get()
+do_post()
}
class WBOauthManager {
+client_id
+client_secret
+callback_url
+get_authorization_url()
+get_access_token_by_code()
+get_oauth_userinfo()
+get_picture()
}
class GoogleOauthManager {
+client_id
+client_secret
+callback_url
+get_authorization_url()
+get_access_token_by_code()
+get_oauth_userinfo()
+get_picture()
}
class GitHubOauthManager {
+client_id
+client_secret
+callback_url
+get_authorization_url()
+get_access_token_by_code()
+get_oauth_userinfo()
+get_picture()
}
class FaceBookOauthManager {
+client_id
+client_secret
+callback_url
+get_authorization_url()
+get_access_token_by_code()
+get_oauth_userinfo()
+get_picture()
}
class QQOauthManager {
+client_id
+client_secret
+callback_url
+get_authorization_url()
+get_access_token_by_code()
+get_open_id()
+get_oauth_userinfo()
+get_picture()
}
%% Test Classes
class OAuthConfigTest {
+setUp()
+test_oauth_login_test()
}
class OauthLoginTest {
+setUp()
+init_apps()
+get_app_by_type()
+test_weibo_login()
+test_google_login()
+test_github_login()
+test_facebook_login()
+test_qq_login()
+test_weibo_authoriz_login_with_email()
+test_weibo_authoriz_login_without_email()
}
%% Exception Class
class OAuthAccessTokenException {
<<Exception>>
}
%% Relationships
OAuthUserAdmin --> OAuthUser : 管理
OAuthConfigAdmin --> OAuthConfig : 管理
RequireEmailView --> RequireEmailForm : 使用
BaseOauthManager <|-- WBOauthManager : 继承
BaseOauthManager <|-- QQOauthManager : 继承
BaseOauthManager <|-- GoogleOauthManager : 继承
ProxyManagerMixin <|.. GoogleOauthManager : 混入
BaseOauthManager <|-- GitHubOauthManager : 继承
ProxyManagerMixin <|.. GitHubOauthManager : 混入
BaseOauthManager <|-- FaceBookOauthManager : 继承
ProxyManagerMixin <|.. FaceBookOauthManager : 混入
BaseOauthManager --> OAuthUser : 创建
BaseOauthManager --> OAuthConfig : 配置依赖
OAuthConfigTest --> OAuthConfig : 测试
OauthLoginTest --> BaseOauthManager : 测试
OauthLoginTest --> OAuthConfig : 测试
OauthLoginTest --> OAuthUser : 测试

@ -0,0 +1,71 @@
---
config:
layout: elk
---
classDiagram
%% Models
class OwnTrackLog {
+CharField tid
+FloatField lat
+FloatField lon
+DateTimeField creation_time
+__str__()
}
%% Admin Classes
class OwnTrackLogsAdmin {
# 空管理类,使用默认配置
}
%% App Config
class OwntracksConfig {
+name
}
%% Test Classes
class OwnTrackLogTest {
+setUp()
+test_own_track_log()
}
%% Views (作为功能模块表示)
class ViewFunctions {
<<Module>>
+manage_owntrack_log()
+show_maps()
+show_log_dates()
+convert_to_amap()
+get_datas()
}
%% URL Patterns
class URLConfig {
<<Module>>
+logtracks
+show_maps
+get_datas
+show_dates
}
%% External Dependencies
class BlogUser {
<<External>>
+create_superuser()
}
class AMapAPI {
<<External>>
+坐标转换服务
}
%% Relationships
OwnTrackLogsAdmin --> OwnTrackLog : 管理
OwnTrackLogTest --> OwnTrackLog : 测试
OwnTrackLogTest --> BlogUser : 创建测试用户
ViewFunctions --> OwnTrackLog : 创建/查询
ViewFunctions --> AMapAPI : 调用坐标转换
URLConfig --> ViewFunctions : 路由映射
OwntracksConfig ..> OwnTrackLog : 应用配置

@ -0,0 +1,155 @@
---
config:
layout: elk
---
classDiagram
%% Models
class commands {
+CharField title
+CharField command
+CharField describe
+DateTimeField creation_time
+DateTimeField last_modify_time
+__str__()
}
class EmailSendLog {
+CharField emailto
+CharField title
+TextField content
+BooleanField send_result
+DateTimeField creation_time
+__str__()
}
%% Admin Classes
class CommandsAdmin {
+list_display
}
class EmailSendLogAdmin {
+list_display
+readonly_fields
+has_add_permission()
}
%% WeChat Robot Components
class MemcacheStorage {
+prefix
+cache
+is_available
+key_name()
+get()
+set()
+delete()
}
class MessageHandler {
+message
+session
+userid
+userinfo
+is_admin
+is_password_set
+save_session()
+handler()
}
class WxUserInfo {
+isAdmin
+isPasswordSet
+Count
+Command
}
%% External API Classes
class BlogApi {
<<External>>
+search_articles()
+get_category_lists()
+get_recent_articles()
}
class CommandHandler {
<<External>>
+run()
+get_help()
}
class ChatGPT {
<<External>>
+chat()
}
class WeRoBot {
<<External>>
+token
+config
+filter()
+handler()
}
%% Test Classes
class ServerManagerTest {
+setUp()
+test_chat_gpt()
+test_validate_comment()
}
%% App Config
class ServermanagerConfig {
+name
}
%% URL Configuration
class URLConfig {
+robot
}
%% External Models (for testing)
class BlogUser {
<<External>>
+create_superuser()
}
class Category {
<<External>>
}
class Article {
<<External>>
+title
+body
+get_full_url()
}
%% Relationships
CommandsAdmin --> commands : 管理
EmailSendLogAdmin --> EmailSendLog : 管理
MemcacheStorage ..> WeRoBot : 会话存储实现
MessageHandler o-- WxUserInfo : 组合
MessageHandler --> CommandHandler : 使用
MessageHandler --> ChatGPT : 使用
WeRoBot --> MessageHandler : 消息处理
WeRoBot --> BlogApi : 博客API调用
WeRoBot --> MemcacheStorage : 会话存储
%% Robot Filter Functions
WeRoBot ..> search : 注册过滤器
WeRoBot ..> category : 注册过滤器
WeRoBot ..> recents : 注册过滤器
WeRoBot ..> help : 注册过滤器
WeRoBot ..> weather : 注册过滤器
WeRoBot ..> idcard : 注册过滤器
%% Test Relationships
ServerManagerTest --> commands : 测试
ServerManagerTest --> MessageHandler : 测试
ServerManagerTest --> CommandHandler : 测试
ServerManagerTest --> BlogUser : 测试依赖
ServerManagerTest --> Category : 测试依赖
ServerManagerTest --> Article : 测试依赖
URLConfig --> WeRoBot : 路由配置

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,22 @@
@echo off
chcp 65001 >nul
echo 正在检查将要清理的文件和文件夹...
git clean -fd -n
set /p confirm=是否确认清理这些文件?(y/N):
if /i "%confirm%" neq "y" (
echo 操作已取消
pause
exit /b
)
echo 正在执行清理操作...
git clean -fd
if %errorlevel% equ 0 (
echo 清理完成!
) else (
echo 清理过程中出现错误,错误代码: %errorlevel%
)
pause

@ -0,0 +1,9 @@
@echo off
echo Pushing Git subtree...
git subtree push --prefix=src/DjangoBlog DjangoBlog g3f-CodeEdit
if %errorlevel% equ 0 (
echo Subtree push successful!
) else (
echo Subtree push failed!
)
pause

@ -0,0 +1,12 @@
bin/data/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
uploads/
settings_production.py
*.md
docs/
logs/
static/
.github/

@ -0,0 +1,6 @@
blog/static/* linguist-vendored
*.js linguist-vendored
*.css linguist-vendored
* text=auto
*.sh text eol=lf
*.conf text eol=lf

@ -0,0 +1,18 @@
<!--
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
-->
**我确定我已经查看了** (标注`[ ]`为`[x]`)
- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
----
**我要申请** (标注`[ ]`为`[x]`)
- [ ] BUG 反馈
- [ ] 添加新的特性或者功能
- [ ] 请求技术支持

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

@ -0,0 +1,49 @@
name: "CodeQL"
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- '**/*.yml'
- '**/*.txt'
schedule:
- cron: '30 1 * * 0'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: python
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

@ -0,0 +1,176 @@
name: 自动部署到生产环境
on:
workflow_run:
workflows: ["Django CI"]
types:
- completed
branches:
- master
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'production'
type: choice
options:
- production
- staging
image_tag:
description: '镜像标签 (默认: latest)'
required: false
default: 'latest'
type: string
skip_tests:
description: '跳过测试直接部署'
required: false
default: false
type: boolean
env:
REGISTRY: registry.cn-shenzhen.aliyuncs.com
IMAGE_NAME: liangliangyy/djangoblog
NAMESPACE: djangoblog
jobs:
deploy:
name: 构建镜像并部署到生产环境
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置部署参数
id: deploy-params
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "trigger_type=手动触发" >> $GITHUB_OUTPUT
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
echo "image_tag=${{ github.event.inputs.image_tag }}" >> $GITHUB_OUTPUT
echo "skip_tests=${{ github.event.inputs.skip_tests }}" >> $GITHUB_OUTPUT
else
echo "trigger_type=CI自动触发" >> $GITHUB_OUTPUT
echo "environment=production" >> $GITHUB_OUTPUT
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "skip_tests=false" >> $GITHUB_OUTPUT
fi
- name: 显示部署信息
run: |
echo "🚀 部署信息:"
echo " 触发方式: ${{ steps.deploy-params.outputs.trigger_type }}"
echo " 部署环境: ${{ steps.deploy-params.outputs.environment }}"
echo " 镜像标签: ${{ steps.deploy-params.outputs.image_tag }}"
echo " 跳过测试: ${{ steps.deploy-params.outputs.skip_tests }}"
- name: 设置Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 登录私有镜像仓库
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: 提取镜像元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=${{ steps.deploy-params.outputs.image_tag }}
- name: 构建并推送Docker镜像
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
- name: 部署到生产服务器
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_PORT || 22 }}
script: |
echo "🚀 开始部署 DjangoBlog..."
# 检查kubectl是否可用
if ! command -v kubectl &> /dev/null; then
echo "❌ 错误: kubectl 未安装或不在PATH中"
exit 1
fi
# 检查命名空间是否存在
if ! kubectl get namespace ${{ env.NAMESPACE }} &> /dev/null; then
echo "❌ 错误: 命名空间 ${{ env.NAMESPACE }} 不存在"
exit 1
fi
# 更新deployment镜像
echo "📦 更新deployment镜像为: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }}"
kubectl set image deployment/djangoblog \
djangoblog=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }} \
-n ${{ env.NAMESPACE }}
# 重启deployment
echo "🔄 重启deployment..."
kubectl -n ${{ env.NAMESPACE }} rollout restart deployment djangoblog
# 等待deployment完成
echo "⏳ 等待deployment完成..."
kubectl rollout status deployment/djangoblog -n ${{ env.NAMESPACE }} --timeout=300s
# 检查deployment状态
echo "✅ 检查deployment状态..."
kubectl get deployment djangoblog -n ${{ env.NAMESPACE }}
kubectl get pods -l app=djangoblog -n ${{ env.NAMESPACE }}
echo "🎉 部署完成!"
- name: 发送部署通知
if: always()
run: |
# 设置通知内容
if [ "${{ job.status }}" = "success" ]; then
TITLE="✅ DjangoBlog部署成功"
STATUS="成功"
else
TITLE="❌ DjangoBlog部署失败"
STATUS="失败"
fi
MESSAGE="部署状态: ${STATUS}
触发方式: ${{ steps.deploy-params.outputs.trigger_type }}
部署环境: ${{ steps.deploy-params.outputs.environment }}
镜像标签: ${{ steps.deploy-params.outputs.image_tag }}
提交者: ${{ github.actor }}
时间: $(date '+%Y-%m-%d %H:%M:%S')
查看详情: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# 发送Server酱通知
if [ -n "${{ secrets.SERVERCHAN_KEY }}" ]; then
echo "{\"title\": \"${TITLE}\", \"desp\": \"${MESSAGE}\"}" > /tmp/serverchan.json
curl --location "https://sctapi.ftqq.com/${{ secrets.SERVERCHAN_KEY }}.send" \
--header "Content-Type: application/json" \
--data @/tmp/serverchan.json \
--silent > /dev/null
rm -f /tmp/serverchan.json
echo "📱 部署通知已发送"
fi

@ -0,0 +1,371 @@
name: Django CI
on:
push:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
pull_request:
branches:
- master
- dev
paths-ignore:
- '**/*.md'
- '**/*.css'
- '**/*.js'
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# 标准测试 - Python 3.10
- python-version: "3.10"
test-type: "standard"
database: "mysql"
elasticsearch: false
coverage: false
# 标准测试 - Python 3.11
- python-version: "3.11"
test-type: "standard"
database: "mysql"
elasticsearch: false
coverage: false
# 完整测试 - 包含ES和覆盖率
- python-version: "3.11"
test-type: "full"
database: "mysql"
elasticsearch: true
coverage: true
# Docker构建测试
- python-version: "3.11"
test-type: "docker"
database: "none"
elasticsearch: false
coverage: false
name: Test (${{ matrix.test-type }}, Python ${{ matrix.python-version }})
steps:
- name: Checkout代码
uses: actions/checkout@v4
- name: 设置测试信息
id: test-info
run: |
echo "test_name=${{ matrix.test-type }}-py${{ matrix.python-version }}" >> $GITHUB_OUTPUT
if [ "${{ matrix.test-type }}" = "docker" ]; then
echo "skip_python_setup=true" >> $GITHUB_OUTPUT
else
echo "skip_python_setup=false" >> $GITHUB_OUTPUT
fi
# MySQL数据库设置 (只有需要数据库的测试才执行)
- name: 启动MySQL数据库
if: matrix.database == 'mysql'
uses: samin/mysql-action@v1.3
with:
host port: 3306
container port: 3306
character set server: utf8mb4
collation server: utf8mb4_general_ci
mysql version: latest
mysql root password: root
mysql database: djangoblog
mysql user: root
mysql password: root
# Elasticsearch设置 (只有完整测试才执行)
- name: 配置系统参数 (ES)
if: matrix.elasticsearch == true
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- name: 启动Elasticsearch
if: matrix.elasticsearch == true
uses: miyataka/elasticsearch-github-actions@1
with:
stack-version: '7.12.1'
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
# Python环境设置 (Docker测试跳过)
- name: 设置Python ${{ matrix.python-version }}
if: steps.test-info.outputs.skip_python_setup == 'false'
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: 'requirements.txt'
# 多层缓存策略优化
- name: 缓存Python依赖
if: steps.test-info.outputs.skip_python_setup == 'false'
uses: actions/cache@v4
with:
path: |
~/.cache/pip
.pytest_cache
key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-
${{ runner.os }}-python-${{ matrix.python-version }}-
${{ runner.os }}-python-
# Django缓存优化 (测试数据库等)
- name: 缓存Django资源
if: matrix.test-type != 'docker'
uses: actions/cache@v4
with:
path: |
.coverage*
htmlcov/
.django_cache/
key: ${{ runner.os }}-django-${{ matrix.test-type }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-django-${{ matrix.test-type }}-
${{ runner.os }}-django-
- name: 安装Python依赖
if: steps.test-info.outputs.skip_python_setup == 'false'
run: |
echo "📦 安装Python依赖 (Python ${{ matrix.python-version }})"
python -m pip install --upgrade pip setuptools wheel
# 安装基础依赖
pip install -r requirements.txt
# 根据测试类型安装额外依赖
if [ "${{ matrix.coverage }}" = "true" ]; then
echo "📊 安装覆盖率工具"
pip install coverage[toml]
fi
# 验证关键依赖
echo "🔍 验证关键依赖安装"
python -c "import django; print(f'Django version: {django.get_version()}')"
python -c "import MySQLdb; print('MySQL client: OK')" || python -c "import pymysql; print('PyMySQL client: OK')"
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
python -c "import elasticsearch; print('Elasticsearch client: OK')"
fi
# Django环境准备
- name: 准备Django环境
if: matrix.test-type != 'docker'
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
run: |
echo "🔧 准备Django测试环境"
# 等待数据库就绪
echo "⏳ 等待MySQL数据库启动..."
for i in {1..30}; do
if python -c "import MySQLdb; MySQLdb.connect(host='127.0.0.1', user='root', passwd='root', db='djangoblog')" 2>/dev/null; then
echo "✅ MySQL数据库连接成功"
break
fi
echo "🔄 等待数据库启动... ($i/30)"
sleep 2
done
# 等待Elasticsearch就绪 (如果启用)
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
echo "⏳ 等待Elasticsearch启动..."
for i in {1..30}; do
if curl -s http://127.0.0.1:9200/_cluster/health | grep -q '"status":"green"\|"status":"yellow"'; then
echo "✅ Elasticsearch连接成功"
break
fi
echo "🔄 等待Elasticsearch启动... ($i/30)"
sleep 2
done
fi
# Django测试执行
- name: 执行数据库迁移
if: matrix.test-type != 'docker'
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
run: |
echo "🗄️ 执行数据库迁移"
# 检查迁移文件
echo "📋 检查待应用的迁移..."
python manage.py showmigrations
# 检查是否有未创建的迁移
python manage.py makemigrations --check --verbosity 2
# 执行迁移
python manage.py migrate --verbosity 2
echo "✅ 数据库迁移完成"
- name: 运行Django测试
if: matrix.test-type != 'docker'
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
run: |
echo "🧪 开始执行 ${{ matrix.test-type }} 测试 (Python ${{ matrix.python-version }})"
# 显示Django配置信息
python manage.py diffsettings | head -20
# 运行测试
if [ "${{ matrix.coverage }}" = "true" ]; then
echo "📊 运行测试并生成覆盖率报告"
coverage run --source='.' --omit='*/venv/*,*/migrations/*,*/tests/*,manage.py' manage.py test --verbosity=2
echo "📈 生成覆盖率报告"
coverage xml
coverage report --show-missing
coverage html
echo "📋 覆盖率统计:"
coverage report | tail -1
else
echo "🧪 运行标准测试"
python manage.py test --verbosity=2 --failfast
fi
echo "✅ 测试执行完成"
# 覆盖率报告上传 (只有完整测试才执行)
- name: 上传覆盖率到Codecov
if: matrix.coverage == true && success()
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittests
name: codecov-${{ steps.test-info.outputs.test_name }}
fail_ci_if_error: false
verbose: true
- name: 上传覆盖率到Codecov (备用)
if: matrix.coverage == true && failure()
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
flags: unittests
name: codecov-${{ steps.test-info.outputs.test_name }}-fallback
fail_ci_if_error: false
verbose: true
# Docker构建测试
- name: 设置QEMU
if: matrix.test-type == 'docker'
uses: docker/setup-qemu-action@v3
- name: 设置Docker Buildx
if: matrix.test-type == 'docker'
uses: docker/setup-buildx-action@v3
- name: Docker构建测试
if: matrix.test-type == 'docker'
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: djangoblog/djangoblog:test-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# 收集测试工件 (失败时收集调试信息)
- name: 收集测试工件
if: failure() && matrix.test-type != 'docker'
run: |
echo "🔍 收集测试失败的调试信息"
# 收集Django日志
if [ -d "logs" ]; then
echo "📄 Django日志文件:"
ls -la logs/
if [ -f "logs/djangoblog.log" ]; then
echo "🔍 最新日志内容:"
tail -100 logs/djangoblog.log
fi
fi
# 显示数据库状态
echo "🗄️ 数据库连接状态:"
python -c "
try:
from django.db import connection
cursor = connection.cursor()
cursor.execute('SELECT VERSION()')
print(f'MySQL版本: {cursor.fetchone()[0]}')
cursor.execute('SHOW TABLES')
tables = cursor.fetchall()
print(f'数据库表数量: {len(tables)}')
except Exception as e:
print(f'数据库连接错误: {e}')
" || true
# Elasticsearch状态 (如果启用)
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
echo "🔍 Elasticsearch状态:"
curl -s http://127.0.0.1:9200/_cluster/health?pretty || true
fi
# 上传测试工件
- name: 上传覆盖率HTML报告
if: matrix.coverage == true && always()
uses: actions/upload-artifact@v4
with:
name: coverage-report-${{ steps.test-info.outputs.test_name }}
path: htmlcov/
retention-days: 30
# 性能统计
- name: 测试性能统计
if: always() && matrix.test-type != 'docker'
run: |
echo "⚡ 测试性能统计:"
echo " 开始时间: $(date -d '@${{ job.started_at }}' '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo '未知')"
echo " 当前时间: $(date '+%Y-%m-%d %H:%M:%S')"
# 系统资源使用情况
echo "💻 系统资源:"
echo " CPU使用: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)%"
echo " 内存使用: $(free -h | awk '/^Mem:/ {printf "%.1f%%", $3/$2 * 100}')"
echo " 磁盘使用: $(df -h / | awk 'NR==2{printf "%s", $5}')"
# 测试结果汇总
- name: 测试完成总结
if: always()
run: |
echo "📋 ============ 测试执行总结 ============"
echo " 🏷️ 测试类型: ${{ matrix.test-type }}"
echo " 🐍 Python版本: ${{ matrix.python-version }}"
echo " 🗄️ 数据库: ${{ matrix.database }}"
echo " 🔍 Elasticsearch: ${{ matrix.elasticsearch }}"
echo " 📊 覆盖率: ${{ matrix.coverage }}"
echo " ⚡ 状态: ${{ job.status }}"
echo " 📅 完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "============================================"
# 根据测试结果显示不同消息
if [ "${{ job.status }}" = "success" ]; then
echo "🎉 测试执行成功!"
else
echo "❌ 测试执行失败,请检查上面的日志"
fi

@ -0,0 +1,43 @@
name: docker
on:
push:
paths-ignore:
- '**/*.md'
- '**/*.yml'
branches:
- 'master'
- 'dev'
workflow_dispatch:
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set env to docker dev tag
if: endsWith(github.ref, '/dev')
run: |
echo "DOCKER_TAG=test" >> $GITHUB_ENV
- name: Set env to docker latest tag
if: endsWith(github.ref, '/master')
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}

@ -0,0 +1,39 @@
name: publish release
on:
release:
types: [ published ]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: name/app
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: |
linux/amd64
linux/arm64
linux/arm/v7
linux/arm/v6
linux/386
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}

@ -0,0 +1,84 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.pot
# Django stuff:
*.log
logs/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# PyCharm
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html
BingSiteAuth.xml
cb9339dbe2ff86a5aa169d28dba5f615.txt
werobot_session.*
django.jpg
uploads/
settings_production.py
werobot_session.db
bin/datas/
.env
# 目前似乎仅有测试代码会涉及到修改此文件夹,所以暂不进行版本管理
static/avatar/

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

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2025 车亮亮
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,158 @@
# DjangoBlog
<p align="center">
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml/badge.svg" alt="Django CI"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml/badge.svg" alt="CodeQL"></a>
<a href="https://codecov.io/gh/liangliangyy/DjangoBlog"><img src="https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg" alt="codecov"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/blob/master/LICENSE"><img src="https://img.shields.io/github/license/liangliangyy/djangoblog.svg" alt="license"></a>
</p>
<p align="center">
<b>一款功能强大、设计优雅的现代化博客系统</b>
<br>
<a href="/docs/README-en.md">English</a><b>简体中文</b>
</p>
---
DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能还通过一个灵活的插件系统让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。
## ✨ 特性亮点
- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。
- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。
- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能代码解耦易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能!
- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。
- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。
## 🛠️ 技术栈
- **后端**: Python 3.10, Django 4.0
- **数据库**: MySQL, SQLite (可配置)
- **缓存**: Redis
- **前端**: HTML5, CSS3, JavaScript
- **搜索**: Whoosh, Elasticsearch (可配置)
- **编辑器**: Markdown (mdeditor)
## 🚀 快速开始
### 1. 环境准备
确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。
### 2. 克隆与安装
```bash
# 克隆项目到本地
git clone https://github.com/liangliangyy/DjangoBlog.git
cd DjangoBlog
# 安装依赖
pip install -r requirements.txt
```
### 3. 项目配置
- **数据库**:
打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD': 'your_password',
'HOST': '127.0.0.1',
'PORT': 3306,
}
}
```
在 MySQL 中创建数据库:
```sql
CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
- **更多配置**:
关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。
### 4. 初始化数据库
```bash
python manage.py makemigrations
python manage.py migrate
# 创建一个超级管理员账户
python manage.py createsuperuser
```
### 5. 运行项目
```bash
# (可选) 生成一些测试数据
python manage.py create_testdata
# (可选) 收集和压缩静态文件
python manage.py collectstatic --noinput
python manage.py compress --force
# 启动开发服务器
python manage.py runserver
```
现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!
## 部署
- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。
## 🧩 插件系统
插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。
- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。
- **现有插件**: `view_count`(浏览计数), `seo_optimizer`SEO优化等都是通过插件系统实现的。
- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!
## 🤝 贡献指南
我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug请随时提交 Issue 或 Pull Request。
## 📄 许可证
本项目基于 [MIT License](LICENSE) 开源。
---
## ❤️ 支持与赞助
如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。
<p align="center">
<img src="/docs/imgs/alipay.jpg" width="150" alt="支付宝赞助">
<img src="/docs/imgs/wechat.jpg" width="150" alt="微信赞助">
</p>
<p align="center">
<i>(左) 支付宝 / (右) 微信</i>
</p>
## 🙏 鸣谢
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
<p align="center">
<a href="https://www.jetbrains.com/?from=DjangoBlog">
<img src="/docs/imgs/pycharm_logo.png" width="150" alt="JetBrains Logo">
</a>
</p>
---
> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。

@ -0,0 +1,87 @@
#shw 导入Django的表单模块
from django import forms
#shw 导入Django后台管理的基础用户管理类
from django.contrib.auth.admin import UserAdmin
#shw 导入Django后台用于修改用户信息的表单
from django.contrib.auth.forms import UserChangeForm
#shw 导入Django后台用于用户名的字段类
from django.contrib.auth.forms import UsernameField
#shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _
#shw 注册你的模型到这里(这是一个注释提示,实际注册在文件末尾)
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
#shw 自定义用户创建表单用于在Django Admin后台添加新用户。
#shw 它继承自 ModelForm并增加了密码输入和确认的逻辑。
#shw 定义第一个密码字段使用PasswordInput控件隐藏输入内容
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
#shw 定义第二个密码字段,用于确认密码
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
#shw Meta类用于配置表单与模型的关联
model = BlogUser #shw 指定该表单对应的模型是 BlogUser
fields = ('email',) #shw 在创建用户时,除了密码外,只显示邮箱字段
def clean_password2(self):
#shw 自定义验证方法,用于检查两次输入的密码是否一致
#shw Check that the two password entries match
password1 = self.cleaned_data.get("password1") #shw 从已清洗的数据中获取第一个密码
password2 = self.cleaned_data.get("password2") #shw 从已清洗的数据中获取第二个密码
#shw 如果两个密码都存在且不相等,则抛出验证错误
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2 #shw 返回第二个密码作为清洗后的数据
def save(self, commit=True):
#shw 重写save方法以便在保存用户时处理密码哈希
#shw Save the provided password in hashed format
user = super().save(commit=False) #shw 调用父类的save方法但先不提交到数据库commit=False
user.set_password(self.cleaned_data["password1"]) #shw 使用Django的set_password方法将明文密码加密后存储
if commit:
user.source = 'adminsite' #shw 如果决定提交,则设置用户的来源为 'adminsite'
user.save() #shw 将用户对象保存到数据库
return user #shw 返回保存后的用户对象
class BlogUserChangeForm(UserChangeForm):
#shw 自定义用户修改表单用于在Django Admin后台编辑现有用户信息。
#shw 它继承自Django的UserChangeForm以复用大部分功能。
class Meta:
#shw Meta类用于配置表单与模型的关联
model = BlogUser #shw 指定该表单对应的模型是 BlogUser
fields = '__all__' #shw 在修改用户时,显示模型中的所有字段
#shw 指定 'username' 字段使用的字段类为 UsernameField
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
#shw 重写初始化方法,可以在这里添加自定义的初始化逻辑
super().__init__(*args, **kwargs) #shw 调用父类的初始化方法
class BlogUserAdmin(UserAdmin):
#shw 自定义用户管理类用于在Django Admin后台定制 BlogUser 模型的显示和操作方式。
#shw 它继承自Django的UserAdmin以复用用户管理的标准功能。
form = BlogUserChangeForm #shw 指定修改用户时使用的表单
add_form = BlogUserCreationForm #shw 指定添加用户时使用的表单
#shw 定义在用户列表页面显示的列
list_display = (
'id', #shw 用户ID
'nickname', #shw 昵称
'username', #shw 用户名
'email', #shw 邮箱
'last_login', #shw 最后登录时间
'date_joined', #shw 注册时间
'source') #shw 创建来源
#shw 定义在列表页面中,哪些列可以点击进入编辑页面
list_display_links = ('id', 'username')
#shw 定义用户列表的默认排序方式按ID降序排列
ordering = ('-id',)
#shw 定义搜索框可以搜索的字段
search_fields = ('username', 'nickname', 'email')

@ -0,0 +1,11 @@
#shw 导入Django的应用配置基类
from django.apps import AppConfig
class AccountsConfig(AppConfig):
#shw 这是accounts应用的配置类。
#shw 它用于定义该应用的各种元数据和行为。
#shw 指定这个配置类所属的应用的完整Python路径。
#shw Django通过这个name来找到并加载这个应用。
name = 'accounts'

@ -0,0 +1,153 @@
#shw 导入Django的表单模块
from django import forms
#shw 导入获取当前激活用户模型的函数
from django.contrib.auth import get_user_model, password_validation
#shw 导入Django内置的认证表单和用户创建表单
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
#shw 导入Django的验证错误类
from django.core.exceptions import ValidationError
#shw 导入Django的表单控件模块
from django.forms import widgets
#shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _
from . import utils
#shw 导入本地的BlogUser模型
from .models import BlogUser
class LoginForm(AuthenticationForm):
#shw 自定义登录表单继承自Django的AuthenticationForm。
#shw 主要用于自定义前端显示的样式。
def __init__(self, *args, **kwargs):
#shw 重写初始化方法,以自定义字段的控件属性。
super(LoginForm, self).__init__(*args, **kwargs) #shw 调用父类的初始化方法
#shw 自定义 'username' 字段的控件添加placeholder和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
#shw 自定义 'password' 字段的控件添加placeholder和CSS类
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
#shw 自定义注册表单继承自Django的UserCreationForm。
#shw 增加了邮箱唯一性验证和前端样式自定义。
def __init__(self, *args, **kwargs):
#shw 重写初始化方法,以自定义字段的控件属性。
super(RegisterForm, self).__init__(*args, **kwargs) #shw 调用父类的初始化方法
#shw 为各个字段添加Bootstrap风格的CSS类和placeholder
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
#shw 自定义邮箱字段的验证方法,确保邮箱在系统中是唯一的。
email = self.cleaned_data['email'] #shw 获取清洗后的邮箱数据
#shw 检查数据库中是否已存在该邮箱
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists")) #shw 如果存在,抛出验证错误
return email #shw 返回清洗后的邮箱
class Meta:
#shw Meta类用于配置表单与模型的关联
model = get_user_model() #shw 动态获取用户模型而不是硬编码BlogUser更具可复用性
fields = ("username", "email") #shw 指定注册表单中显示的字段
class ForgetPasswordForm(forms.Form):
#shw 忘记密码/重置密码表单继承自基础的Form类。
#shw 它不直接与模型关联,用于处理通过邮箱和验证码重置密码的流程。
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
new_password2 = forms.CharField(
label="确认密码", #shw 这里使用了中文硬编码,建议使用 _("Confirm password") 以支持国际化
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
email = forms.EmailField(
label='邮箱', #shw 这里使用了中文硬编码,建议使用 _("Email")
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
#shw 自定义验证方法,检查两次输入的新密码是否一致,并验证密码强度。
password1 = self.data.get("new_password1") #shw 从原始数据中获取密码1
password2 = self.data.get("new_password2") #shw 从原始数据中获取密码2
#shw 检查两次密码是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
#shw 使用Django内置的密码验证器来检查密码强度
password_validation.validate_password(password2)
return password2 #shw 返回验证通过的新密码
def clean_email(self):
#shw 自定义验证方法,检查输入的邮箱是否存在于数据库中。
user_email = self.cleaned_data.get("email") #shw 获取清洗后的邮箱
#shw 检查该邮箱是否已注册
if not BlogUser.objects.filter(
email=user_email
).exists():
#shwtodo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
#shw 这是一个安全提示,直接告诉攻击者邮箱未注册可能会被利用。
raise ValidationError(_("email does not exist"))
return user_email #shw 返回清洗后的邮箱
def clean_code(self):
#shw 自定义验证方法,验证邮箱验证码是否正确。
code = self.cleaned_data.get("code") #shw 获取清洗后的验证码
#shw 调用工具函数验证邮箱和验证码是否匹配
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
#shw 如果工具函数返回错误信息,则抛出验证错误
if error:
raise ValidationError(error)
return code #shw 返回验证通过的验证码
class ForgetPasswordCodeForm(forms.Form):
#shw 发送忘记密码验证码的表单。
#shw 它只包含一个邮箱字段,用于用户输入接收验证码的邮箱地址。
email = forms.EmailField(
label=_('Email'), #shw 邮箱字段,标签支持国际化
)

@ -0,0 +1,49 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='BlogUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': '用户',
'verbose_name_plural': '用户',
'ordering': ['-id'],
'get_latest_by': 'id',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -0,0 +1,46 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -0,0 +1,54 @@
#shw 导入Django内置的抽象用户模型基类
from django.contrib.auth.models import AbstractUser
#shw 导入Django的数据库模型模块
from django.db import models
#shw 导入Django的URL反向解析函数
from django.urls import reverse
#shw 导入Django的时区工具用于获取当前时间
from django.utils.timezone import now
#shw 导入Django的国际化和翻译工具
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
#shw 在这里创建你的模型。
class BlogUser(AbstractUser):
#shw 自定义用户模型继承自Django的AbstractUser。
#shw 它扩展了默认用户模型,增加了博客系统所需的额外字段。
#shw 用户昵称字段,可为空
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
#shw 用户创建时间字段,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
#shw 用户最后修改时间字段,默认为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
#shw 用户创建来源字段(如:'adminsite', 'register'),可为空
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
#shw 定义获取用户详情页绝对路径的方法。
#shw Django Admin和其他地方会使用这个方法来获取对象的URL。
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username}) #shw 反向解析到博客应用的作者详情页URL参数为用户名
def __str__(self):
#shw 定义对象的字符串表示形式。
#shw 在Django Admin或打印对象时会显示这个字符串。
return self.email #shw 返回用户的邮箱作为其字符串表示
def get_full_url(self):
#shw 定义获取用户详情页完整URL包含域名的方法。
site = get_current_site().domain #shw 获取当前站点的域名
#shw 拼接协议、域名和绝对路径形成完整的URL
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
#shw Meta类用于定义模型的元数据选项。
ordering = ['-id'] #shw 默认按ID降序排列
verbose_name = _('user') #shw 在Django Admin中显示的单数名称支持国际化
verbose_name_plural = verbose_name #shw 在Django Admin中显示的复数名称
get_latest_by = 'id' #shw 当使用 .latest() 方法时,默认按 'id' 字段查找

@ -0,0 +1,247 @@
#shw 导入Django的测试客户端、请求工厂和测试用例基类
from django.test import Client, RequestFactory, TestCase
#shw 导入Django的URL反向解析函数
from django.urls import reverse
#shw 导入Django的时区工具
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
#shw 导入本地的BlogUser模型
from accounts.models import BlogUser
#shw 导入博客应用的Article和Category模型
from blog.models import Article, Category
#shw 从项目工具模块导入所有函数
from djangoblog.utils import *
#shw 导入本地的工具模块
from . import utils
#shw 在这里创建你的测试。
class AccountTest(TestCase):
#shw 账户应用的测试用例集继承自Django的TestCase。
#shw TestCase提供了数据库事务回滚和客户端模拟等功能。
def setUp(self):
#shw 每个测试方法执行前都会运行的初始化方法。
#shw 用于创建测试所需的公共数据和环境。
self.client = Client() #shw 创建一个模拟的HTTP客户端用于发送请求
self.factory = RequestFactory() #shw 创建一个请求工厂,用于生成请求对象
#shw 创建一个普通用户用于测试
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" #shw 定义一个测试用的新密码
def test_validate_account(self):
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1') #shw 从数据库中获取刚创建的超级用户
#shw 使用client模拟登录
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) #shw 断言登录成功
response = self.client.get('/admin/') #shw 模拟访问后台管理页面
self.assertEqual(response.status_code, 200) #shw 断言访问成功状态码为200
#shw 创建一个文章分类用于测试
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
#shw 创建一篇文章用于测试
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
#shw 模拟访问文章的后台编辑页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200) #shw 断言访问成功
def test_validate_register(self):
#shw 测试用户注册、邮箱验证、登录、登出等一系列流程。
#shw 断言注册前,数据库中不存在该邮箱的用户
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
#shw 断言注册后,数据库中存在该邮箱的用户
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0] #shw 获取新注册的用户
#shw 生成用于邮箱验证的签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result') #shw 获取验证结果页面的URL路径
#shw 构造完整的验证URL
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
response = self.client.get(url) #shw 模拟用户点击邮箱中的验证链接
self.assertEqual(response.status_code, 200) #shw 断言访问成功
#shw 模拟用户登录
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0] #shw 重新获取用户对象
user.is_superuser = True #shw 将用户提升为超级用户,以便访问后台
user.is_staff = True
user.save()
delete_sidebar_cache() #shw 删除侧边栏缓存
#shw 创建测试用的分类和文章
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
#shw 登录状态下访问文章后台页面
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
#shw 模拟用户登出
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200]) #shw 断言登出成功重定向或OK
#shw 登出后再次访问文章后台页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200]) #shw 断言被重定向到登录页
#shw 模拟使用错误的密码登录
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123' #shw 错误的密码
})
self.assertIn(response.status_code, [301, 302, 200]) #shw 断言登录失败,页面重定向
#shw 登录失败后访问文章后台页面
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200]) #shw 断言依然无法访问
def test_verify_email_code(self):
#shw 测试邮箱验证码的生成、发送和验证功能。
to_email = "admin@admin.com"
code = generate_code() #shw 生成一个验证码
utils.set_code(to_email, code) #shw 将验证码与邮箱关联(通常是存入缓存或数据库)
utils.send_verify_email(to_email, code) #shw 发送验证码邮件(测试环境中可能不会真的发送)
#shw 使用正确的邮箱和验证码进行验证
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None) #shw 断言验证成功,无错误信息返回
#shw 使用错误的邮箱进行验证
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str) #shw 断言验证失败,返回一个字符串类型的错误信息
def test_forget_password_email_code_success(self):
#shw 测试成功发送忘记密码验证码的场景。
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com") #shw 使用一个已存在的邮箱
)
self.assertEqual(resp.status_code, 200) #shw 断言请求成功
self.assertEqual(resp.content.decode("utf-8"), "ok") #shw 断言返回内容为"ok"
def test_forget_password_email_code_fail(self):
#shw 测试发送忘记密码验证码失败的场景(如邮箱格式错误)。
#shw 测试不提供邮箱的情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #shw 断言返回错误提示
#shw 测试提供格式错误的邮箱的情况
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") #shw 断言返回错误提示
def test_forget_password_email_success(self):
#shw 测试成功重置密码的场景。
code = generate_code() #shw 生成一个验证码
utils.set_code(self.blog_user.email, code) #shw 为测试用户设置验证码
#shw 构造重置密码的请求数据
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302) #shw 断言请求成功并重定向
#shw 验证用户密码是否真的被修改了
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None) #shw 断言用户依然存在
self.assertEqual(blog_user.check_password(data["new_password1"]), True) #shw 断言新密码是正确的
def test_forget_password_email_not_user(self):
#shw 测试重置一个不存在用户的密码的场景。
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com", #shw 使用一个不存在的邮箱
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) #shw 断言请求未重定向,停留在原页面并显示错误
def test_forget_password_email_code_error(self):
#shw 测试使用错误验证码重置密码的场景。
code = generate_code() #shw 生成一个验证码
utils.set_code(self.blog_user.email, code) #shw 为测试用户设置验证码
#shw 构造重置密码的请求数据,但验证码是错误的
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111", #shw 错误的验证码
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200) #shw 断言请求未重定向,停留在原页面并显示错误

@ -0,0 +1,44 @@
#shw 导入Django的path和re_path函数用于定义URL路由
from django.urls import path, re_path
#shw 导入本地的视图模块
from . import views
#shw 导入本地的LoginForm表单用于传递给登录视图
from .forms import LoginForm
#shw 定义应用的命名空间用于在模板中反向解析URL如 {% url 'accounts:login' %}
app_name = "accounts"
#shw 定义URL模式列表
urlpatterns = [
#shw 定义登录页面的URL路由
re_path(r'^login/$',
views.LoginView.as_view(success_url='/'), #shw 关联到LoginView类视图并指定登录成功后重定向到根路径
name='login', #shw 为这个URL模式命名为 'login'
kwargs={'authentication_form': LoginForm}), #shw 向LoginView传递额外的关键字参数指定使用自定义的LoginForm
#shw 定义注册页面的URL路由
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"), #shw 关联到RegisterView类视图并指定注册成功后重定向到根路径
name='register'), #shw 命名为 'register'
#shw 定义登出页面的URL路由
re_path(r'^logout/$',
views.LogoutView.as_view(), #shw 关联到LogoutView类视图
name='logout'), #shw 命名为 'logout'
#shw 定义注册/操作结果页面的URL路由
path(r'account/result.html',
views.account_result, #shw 关联到account_result函数视图
name='result'), #shw 命名为 'result'
#shw 定义忘记密码页面的URL路由用于输入新密码和验证码
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(), #shw 关联到ForgetPasswordView类视图
name='forget_password'), #shw 命名为 'forget_password'
#shw 定义发送忘记密码验证码的URL路由
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(), #shw 关联到ForgetPasswordEmailCode类视图
name='forget_password_code'), #shw 命名为 'forget_password_code'
]

@ -0,0 +1,40 @@
#shw 导入Django的获取用户模型函数
from django.contrib.auth import get_user_model
#shw 导入Django默认的基于模型的认证后端基类
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
#shw 自定义认证后端继承自ModelBackend。
#shw 它扩展了Django默认的认证功能使用户既可以使用用户名也可以使用邮箱进行登录。
def authenticate(self, request, username=None, password=None, **kwargs):
#shw 重写authenticate方法这是认证的核心逻辑。
#shw Django的login视图会调用这个方法来验证用户身份。
#shw 判断用户输入的 'username' 字段是否包含 '@' 符号,以此来区分是邮箱还是用户名
if '@' in username:
kwargs = {'email': username} #shw 如果是邮箱,则设置查询条件为 email
else:
kwargs = {'username': username} #shw 如果是用户名,则设置查询条件为 username
try:
#shw 使用动态构建的查询条件email或username去数据库中查找用户
user = get_user_model().objects.get(**kwargs)
#shw 如果找到了用户,则调用 check_password 方法来验证密码是否正确
if user.check_password(password):
return user #shw 密码正确,返回用户对象,认证成功
except get_user_model().DoesNotExist:
#shw 如果根据email或username找不到用户捕获异常
return None #shw 返回None表示认证失败
def get_user(self, username):
#shw 重写get_user方法。
#shw Django的认证中间件会在每个请求中调用此方法根据session中的user_id来获取用户对象。
try:
#shw 注意这里的参数名是username但实际传入的是用户的主键pk通常是ID
return get_user_model().objects.get(pk=username) #shw 根据主键pk查找用户
except get_user_model().DoesNotExist:
#shw 如果根据主键找不到用户,捕获异常
return None #shw 返回None

@ -0,0 +1,63 @@
#shw 导入类型提示模块,用于增强代码可读性和健壮性
import typing
#shw 导入时间间隔类,用于定义验证码有效期
from datetime import timedelta
#shw 导入Django的缓存模块
from django.core.cache import cache
#shw 导入Django的翻译函数
from django.utils.translation import gettext
#shw 导入Django的延迟翻译函数用于在类或模型定义等场景
from django.utils.translation import gettext_lazy as _
#shw 从项目工具模块导入发送邮件的函数
from djangoblog.utils import send_email
#shw 定义验证码的生存时间Time To Live为5分钟
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
to_mail: 接受邮箱
subject: 邮件主题
code: 验证码
"""
#shw 构造邮件的HTML内容使用国际化字符串并将验证码动态插入
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
#shw 调用项目通用的邮件发送函数来完成发送
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方就需要对error进行处理
"""
#shw 从缓存中获取指定邮箱对应的验证码
cache_code = get_code(email)
#shw 比较用户输入的验证码和缓存中的验证码是否一致
if cache_code != code:
#shw 如果不一致,返回一个翻译后的错误信息字符串
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code"""
#shw 将验证码存入缓存键为邮箱值为验证码并设置5分钟的过期时间
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
#shw 从缓存中根据邮箱(键)获取验证码(值)
return cache.get(email)

@ -0,0 +1,255 @@
#shw 导入日志模块
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
#shw 导入Django的认证模块
from django.contrib import auth
#shw 导入登录后重定向字段的常量名
from django.contrib.auth import REDIRECT_FIELD_NAME
#shw 导入获取用户模型的函数
from django.contrib.auth import get_user_model
#shw 导入登出函数
from django.contrib.auth import logout
#shw 导入Django内置的认证表单
from django.contrib.auth.forms import AuthenticationForm
#shw 导入密码哈希生成函数
from django.contrib.auth.hashers import make_password
#shw 导入HTTP响应相关类
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
#shw 导入Django的快捷函数
from django.shortcuts import get_object_or_404
from django.shortcuts import render
#shw 导入URL反向解析函数
from django.urls import reverse
#shw 导入方法装饰器
from django.utils.decorators import method_decorator
#shw 导入URL安全检查函数
from django.utils.http import url_has_allowed_host_and_scheme
#shw 导入Django的视图基类
from django.views import View
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
#shw 从项目工具模块导入所需函数
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
#shw 导入本地的工具模块和表单
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
#shw 导入本地的模型
from .models import BlogUser
#shw 获取当前模块的日志记录器
logger = logging.getLogger(__name__)
#shw 在这里创建你的视图。
class RegisterView(FormView):
#shw 用户注册视图继承自FormView用于处理用户注册逻辑。
form_class = RegisterForm #shw 指定使用的表单类
template_name = 'account/registration_form.html' #shw 指定渲染的模板
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
#shw 为视图的dispatch方法添加CSRF保护
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
#shw 当表单验证通过时执行此方法
if form.is_valid(): #shw 再次确认表单有效
user = form.save(False) #shw 保存表单数据但先不提交到数据库commit=False
user.is_active = False #shw 将用户状态设为未激活,需要邮箱验证
user.source = 'Register' #shw 设置用户来源为注册
user.save(True) #shw 现在将用户对象保存到数据库
site = get_current_site().domain #shw 获取当前站点域名
#shw 生成用于邮箱验证的双重哈希签名
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
#shw 如果是调试模式,则使用本地地址
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result') #shw 获取结果页面的URL路径
#shw 构造完整的邮箱验证链接
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
#shw 构造邮件内容
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
#shw 发送验证邮件
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
#shw 构造注册成功后的跳转URL提示用户去查收邮件
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url) #shw 重定向到结果页面
else:
#shw 如果表单无效,重新渲染注册页面并显示错误
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
#shw 用户登出视图继承自RedirectView用于处理用户登出逻辑。
url = '/login/' #shw 登出后重定向的URL
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#shw 为视图添加never_cache装饰器确保该页面不被缓存
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request) #shw 调用Django的logout函数清除session信息
delete_sidebar_cache() #shw 删除侧边栏缓存
return super(LogoutView, self).get(request, *args, **kwargs) #shw 执行重定向
class LoginView(FormView):
#shw 用户登录视图继承自FormView用于处理用户登录逻辑。
form_class = LoginForm #shw 指定使用的表单类
template_name = 'account/login.html' #shw 指定渲染的模板
success_url = '/' #shw 登录成功后默认的重定向URL
redirect_field_name = REDIRECT_FIELD_NAME #shw 指定包含重定向URL的GET参数名
login_ttl = 2626560 # 一个月的时间用于“记住我”功能的session过期时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
#shw 为视图添加多个装饰器保护密码参数、CSRF保护、禁止缓存
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
#shw 向模板上下文中添加额外的数据
redirect_to = self.request.GET.get(self.redirect_field_name) #shw 获取GET参数中的重定向URL
if redirect_to is None:
redirect_to = '/' #shw 如果没有,则默认为根路径
kwargs['redirect_to'] = redirect_to #shw 将重定向URL添加到上下文
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
#shw 当表单验证通过时执行此方法
#shw 使用Django内置的AuthenticationForm再次验证因为它会调用自定义的认证后端
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache() #shw 删除侧边栏缓存
logger.info(self.redirect_field_name) #shw 记录日志
auth.login(self.request, form.get_user()) #shw 调用Django的login函数将用户信息存入session
#shw 如果用户勾选了“记住我”
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl) #shw 设置session的过期时间为一个月
return super(LoginView, self).form_valid(form) #shw 调用父类方法,处理重定向
else:
#shw 如果验证失败,重新渲染登录页面并显示错误
return self.render_to_response({
'form': form
})
def get_success_url(self):
#shw 获取登录成功后应重定向的URL
redirect_to = self.request.POST.get(self.redirect_field_name) #shw 从POST数据中获取重定向URL
#shw 检查URL是否安全防止开放重定向攻击
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url #shw 如果URL不安全则使用默认的success_url
return redirect_to
def account_result(request):
#shw 函数视图,用于处理注册和邮箱验证的结果展示。
type = request.GET.get('type') #shw 获取URL参数中的类型
id = request.GET.get('id') #shw 获取URL参数中的用户ID
user = get_object_or_404(get_user_model(), id=id) #shw 根据ID获取用户对象如果不存在则返回404
logger.info(type) #shw 记录日志
if user.is_active: #shw 如果用户已经激活,则直接跳转到首页
return HttpResponseRedirect('/')
#shw 处理两种类型:注册成功提示和邮箱验证
if type and type in ['register', 'validation']:
if type == 'register':
#shw 如果是注册类型,显示注册成功、请查收邮件的提示
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
#shw 如果是验证类型,需要验证签名
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) #shw 重新计算正确的签名
sign = request.GET.get('sign') #shw 获取URL中的签名
if sign != c_sign: #shw 比较签名如果不一致则返回403禁止访问
return HttpResponseForbidden()
user.is_active = True #shw 激活用户
user.save() #shw 保存用户状态
#shw 显示验证成功的提示
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
#shw 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
#shw 如果类型不匹配,则跳转到首页
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
#shw 忘记密码视图,用于处理通过验证码重置密码的逻辑。
form_class = ForgetPasswordForm #shw 指定使用的表单
template_name = 'account/forget_password.html' #shw 指定渲染的模板
def form_valid(self, form):
#shw 当表单验证通过时执行此方法
if form.is_valid(): #shw 再次确认表单有效
#shw 根据邮箱获取用户对象
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
#shw 使用make_password对新密码进行哈希处理
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save() #shw 保存用户的新密码
return HttpResponseRedirect('/login/') #shw 重定向到登录页面
else:
#shw 如果表单无效,重新渲染页面并显示错误
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
#shw 发送忘记密码验证码的视图继承自基础的View。
@staticmethod
def post(request: HttpRequest):
#shw 只处理POST请求
form = ForgetPasswordCodeForm(request.POST) #shw 用POST数据实例化表单
if not form.is_valid(): #shw 验证表单(主要是验证邮箱格式)
return HttpResponse("错误的邮箱") #shw 如果无效,返回错误信息
to_email = form.cleaned_data["email"] #shw 获取清洗后的邮箱
code = generate_code() #shw 生成一个验证码
utils.send_verify_email(to_email, code) #shw 调用工具函数发送验证邮件
utils.set_code(to_email, code) #shw 调用工具函数将验证码存入缓存
return HttpResponse("ok") #shw 返回成功信息

@ -0,0 +1,166 @@
# bjy: 从Django中导入所需的模块和类
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article, Category, Tag, Links, SideBar, BlogSettings
# bjy: 为Article模型创建一个自定义的ModelForm
class ArticleForm(forms.ModelForm):
# bjy: 示例如果使用Pagedown编辑器可以取消下面这行的注释
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
# bjy: 指定这个表单对应的模型是Article
model = Article
# bjy: 表示在表单中包含模型的所有字段
fields = '__all__'
# bjy: 定义一个admin动作用于将选中的文章发布
def makr_article_publish(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的状态为'p'(已发布)
queryset.update(status='p')
# bjy: 定义一个admin动作用于将选中的文章设为草稿
def draft_article(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的状态为'd'(草稿)
queryset.update(status='d')
# bjy: 定义一个admin动作用于关闭选中文章的评论功能
def close_article_commentstatus(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的评论状态为'c'(关闭)
queryset.update(comment_status='c')
# bjy: 定义一个admin动作用于开启选中文章的评论功能
def open_article_commentstatus(modeladmin, request, queryset):
# bjy: 批量更新查询集中所有文章的评论状态为'o'(开启)
queryset.update(comment_status='o')
# bjy: 为admin动作设置在后台显示的描述文本
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
# bjy: 为Article模型自定义Admin管理界面
class ArticlelAdmin(admin.ModelAdmin):
# bjy: 设置每页显示20条记录
list_per_page = 20
# bjy: 启用搜索功能搜索范围包括文章内容body和标题title
search_fields = ('body', 'title')
# bjy: 指定使用的自定义表单
form = ArticleForm
# bjy: 在列表视图中显示的字段
list_display = (
'id',
'title',
'author',
'link_to_category', # bjy: 自定义方法,显示指向分类的链接
'creation_time',
'views',
'status',
'type',
'article_order')
# bjy: 设置列表视图中可点击进入编辑页面的链接字段
list_display_links = ('id', 'title')
# bjy: 启用右侧筛选栏,可按状态、类型、分类进行筛选
list_filter = ('status', 'type', 'category')
# bjy: 启用日期层次导航,按创建时间进行分层
date_hierarchy = 'creation_time'
# bjy: 为多对多字段tags提供一个水平筛选的界面
filter_horizontal = ('tags',)
# bjy: 在编辑页面中排除的字段,这些字段将自动处理
exclude = ('creation_time', 'last_modify_time')
# bjy: 在列表页面显示“在站点上查看”的按钮
view_on_site = True
# bjy: 将自定义的admin动作添加到动作下拉列表中
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
# bjy: 对于外键字段author, category显示为一个输入框用于输入ID而不是下拉列表
raw_id_fields = ('author', 'category',)
# bjy: 自定义方法,用于在列表页面显示一个指向文章分类的链接
def link_to_category(self, obj):
# bjy: 获取分类模型的app_label和model_name用于构建admin URL
info = (obj.category._meta.app_label, obj.category._meta.model_name)
# bjy: 生成指向该分类编辑页面的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
# bjy: 使用format_html安全地生成HTML链接
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
# bjy: 设置该方法在列表页面列标题的显示文本
link_to_category.short_description = _('category')
# bjy: 重写get_form方法用于动态修改表单
def get_form(self, request, obj=None, **kwargs):
# bjy: 获取父类的表单
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
# bjy: 修改author字段的查询集只显示超级用户
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
# bjy: 重写save_model方法在保存模型时执行额外操作
def save_model(self, request, obj, form, change):
# bjy: 调用父类的save_model方法执行默认保存操作
super(ArticlelAdmin, self).save_model(request, obj, form, change)
# bjy: 重写get_view_on_site_url方法自定义“在站点上查看”的URL
def get_view_on_site_url(self, obj=None):
if obj:
# bjy: 如果对象存在则调用模型的get_full_url方法获取URL
url = obj.get_full_url()
return url
else:
# bjy: 如果对象不存在例如在添加新对象时则返回网站首页URL
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
# bjy: 为Tag模型自定义Admin管理界面
class TagAdmin(admin.ModelAdmin):
# bjy: 在编辑页面中排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# bjy: 为Category模型自定义Admin管理界面
class CategoryAdmin(admin.ModelAdmin):
# bjy: 在列表视图中显示的字段
list_display = ('name', 'parent_category', 'index')
# bjy: 在编辑页面中排除的字段
exclude = ('slug', 'last_mod_time', 'creation_time')
# bjy: 为Links模型自定义Admin管理界面
class LinksAdmin(admin.ModelAdmin):
# bjy: 在编辑页面中排除的字段
exclude = ('last_mod_time', 'creation_time')
# bjy: 为SideBar模型自定义Admin管理界面
class SideBarAdmin(admin.ModelAdmin):
# bjy: 在列表视图中显示的字段
list_display = ('name', 'content', 'is_enable', 'sequence')
# bjy: 在编辑页面中排除的字段
exclude = ('last_mod_time', 'creation_time')
# bjy: 为BlogSettings模型自定义Admin管理界面
class BlogSettingsAdmin(admin.ModelAdmin):
# bjy: 使用默认配置,无需自定义
pass

@ -0,0 +1,8 @@
# bjy: 从Django中导入AppConfig基类用于配置应用程序
from django.apps import AppConfig
# bjy: 定义一个名为BlogConfig的配置类它继承自AppConfig
class BlogConfig(AppConfig):
# bjy: 指定这个配置类对应的应用程序名称通常是Python包的路径
name = 'blog'

@ -0,0 +1,76 @@
# bjy: 导入日志模块
import logging
# bjy: 从Django中导入时区工具
from django.utils import timezone
# bjy: 从项目工具模块中导入缓存和获取博客设置的函数
from djangoblog.utils import cache, get_blog_setting
# bjy: 从当前应用的models中导入Category和Article模型
from .models import Category, Article
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个上下文处理器,用于在所有模板中注入全局变量
def seo_processor(requests):
# bjy: 定义一个缓存键名
key = 'seo_processor'
# bjy: 尝试从缓存中获取数据
value = cache.get(key)
# bjy: 如果缓存中存在数据,则直接返回
if value:
return value
else:
# bjy: 如果缓存中没有数据,则记录一条日志
logger.info('set processor cache.')
# bjy: 获取博客的设置对象
setting = get_blog_setting()
# bjy: 构建一个包含所有SEO和全局设置的字典
value = {
# bjy: 网站名称
'SITE_NAME': setting.site_name,
# bjy: 是否显示Google AdSense广告
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
# bjy: Google AdSense的广告代码
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
# bjy: 网站的SEO描述
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
# bjy: 网站的普通描述
'SITE_DESCRIPTION': setting.site_description,
# bjy: 网站的关键词
'SITE_KEYWORDS': setting.site_keywords,
# bjy: 网站的完整基础URL协议+域名)
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
# bjy: 文章列表页的摘要长度
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
# bjy: 用于导航栏的所有分类列表
'nav_category_list': Category.objects.all(),
# bjy: 用于导航栏的所有已发布的“页面”类型的文章
'nav_pages': Article.objects.filter(
type='p', # bjy: 类型为'p'page
status='p'), # bjy: 状态为'p'published
# bjy: 是否开启全站评论功能
'OPEN_SITE_COMMENT': setting.open_site_comment,
# bjy: 网站的ICP备案号
'BEIAN_CODE': setting.beian_code,
# bjy: 网站统计代码如Google Analytics
'ANALYTICS_CODE': setting.analytics_code,
# bjy: 公安备案号
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
# bjy: 是否显示公安备案号
"SHOW_GONGAN_CODE": setting.show_gongan_code,
# bjy: 当前年份,用于页脚版权信息
"CURRENT_YEAR": timezone.now().year,
# bjy: 全局页头HTML代码
"GLOBAL_HEADER": setting.global_header,
# bjy: 全局页脚HTML代码
"GLOBAL_FOOTER": setting.global_footer,
# bjy: 评论是否需要审核
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
# bjy: 将构建好的字典存入缓存缓存时间为10小时60*60*10秒
cache.set(key, value, 60 * 60 * 10)
# bjy: 返回这个字典,它将被注入到所有模板的上下文中
return value

@ -0,0 +1,288 @@
# bjy: 导入时间模块
import time
# bjy: 导入Elasticsearch的客户端模块和异常类
import elasticsearch.client
import elasticsearch.exceptions
# bjy: 导入Django的设置
from django.conf import settings
# bjy: 从elasticsearch_dsl中导入文档、内部文档、字段类型和连接管理器
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
# bjy: 从blog应用中导入Article模型
from blog.models import Article
# bjy: 检查Django设置中是否配置了ELASTICSEARCH_DSL以决定是否启用Elasticsearch功能
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
# bjy: 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
# bjy: 根据Django设置创建到Elasticsearch的连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
# bjy: 导入并实例化Elasticsearch客户端
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# bjy: 导入并实例化Ingest客户端用于管理管道
from elasticsearch.client import IngestClient
c = IngestClient(es)
# bjy: 尝试获取名为'geoip'的管道
try:
c.get_pipeline('geoip')
# bjy: 如果管道不存在,则创建它
except elasticsearch.exceptions.NotFoundError:
# bjy: 创建一个geoip管道用于根据IP地址添加地理位置信息
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
# bjy: 定义一个内部文档InnerDoc结构用于存储IP地理位置信息
class GeoIp(InnerDoc):
# bjy: 大洲名称
continent_name = Keyword()
# bjy: 国家ISO代码
country_iso_code = Keyword()
# bjy: 国家名称
country_name = Keyword()
# bjy: 地理坐标(经纬度)
location = GeoPoint()
# bjy: 定义内部文档用于存储用户代理User-Agent中的浏览器信息
class UserAgentBrowser(InnerDoc):
# bjy: 浏览器家族如Chrome, Firefox
Family = Keyword()
# bjy: 浏览器版本
Version = Keyword()
# bjy: 定义内部文档,用于存储用户代理中的操作系统信息
class UserAgentOS(UserAgentBrowser):
# bjy: 继承自UserAgentBrowser结构相同
pass
# bjy: 定义内部文档,用于存储用户代理中的设备信息
class UserAgentDevice(InnerDoc):
# bjy: 设备家族如iPhone, Android
Family = Keyword()
# bjy: 设备品牌如Apple, Samsung
Brand = Keyword()
# bjy: 设备型号如iPhone 12
Model = Keyword()
# bjy: 定义内部文档,用于存储完整的用户代理信息
class UserAgent(InnerDoc):
# bjy: 嵌套浏览器信息
browser = Object(UserAgentBrowser, required=False)
# bjy: 嵌套操作系统信息
os = Object(UserAgentOS, required=False)
# bjy: 嵌套设备信息
device = Object(UserAgentDevice, required=False)
# bjy: 原始User-Agent字符串
string = Text()
# bjy: 是否为爬虫或机器人
is_bot = Boolean()
# bjy: 定义一个Elasticsearch文档用于存储页面性能数据如响应时间
class ElapsedTimeDocument(Document):
# bjy: 请求的URL
url = Keyword()
# bjy: 请求耗时(毫秒)
time_taken = Long()
# bjy: 日志记录时间
log_datetime = Date()
# bjy: 客户端IP地址
ip = Keyword()
# bjy: 嵌套的IP地理位置信息
geoip = Object(GeoIp, required=False)
# bjy: 嵌套的用户代理信息
useragent = Object(UserAgent, required=False)
class Index:
# bjy: 指定索引名称为'performance'
name = 'performance'
# bjy: 设置索引的分片和副本数
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
# bjy: 指定文档类型
doc_type = 'ElapsedTime'
# bjy: 定义一个管理类用于操作ElapsedTimeDocument索引
class ElaspedTimeDocumentManager:
@staticmethod
def build_index():
# bjy: 如果索引不存在,则创建它
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
ElapsedTimeDocument.init()
@staticmethod
def delete_index():
# bjy: 删除'performance'索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
# bjy: 确保索引存在
ElaspedTimeDocumentManager.build_index()
# bjy: 构建UserAgent内部文档对象
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# bjy: 创建ElapsedTimeDocument文档实例
doc = ElapsedTimeDocument(
meta={
# bjy: 使用当前时间的毫秒数作为文档ID
'id': int(
round(
time.time() *
1000))
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
# bjy: 保存文档,并使用'geoip'管道处理IP地址
doc.save(pipeline="geoip")
# bjy: 定义一个Elasticsearch文档用于存储博客文章数据以支持全文搜索
class ArticleDocument(Document):
# bjy: 文章内容使用ik分词器进行索引和搜索
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# bjy: 文章标题使用ik分词器
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# bjy: 作者信息,为一个对象类型
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# bjy: 分类信息,为一个对象类型
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# bjy: 标签信息,为一个对象类型
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# bjy: 发布时间
pub_time = Date()
# bjy: 文章状态
status = Text()
# bjy: 评论状态
comment_status = Text()
# bjy: 文章类型
type = Text()
# bjy: 浏览量
views = Integer()
# bjy: 文章排序权重
article_order = Integer()
class Index:
# bjy: 指定索引名称为'blog'
name = 'blog'
# bjy: 设置索引的分片和副本数
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
# bjy: 指定文档类型
doc_type = 'Article'
# bjy: 定义一个管理类用于操作ArticleDocument索引
class ArticleDocumentManager():
def __init__(self):
# bjy: 初始化时创建索引
self.create_index()
def create_index(self):
# bjy: 创建'blog'索引
ArticleDocument.init()
def delete_index(self):
# bjy: 删除'blog'索引
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
# bjy: 将Django的Article查询集转换为ArticleDocument对象列表
return [
ArticleDocument(
meta={
'id': article.id},
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
# bjy: 重建索引。如果未提供articles则使用所有文章
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
# bjy: 遍历并保存每个文档
for doc in docs:
doc.save()
def update_docs(self, docs):
# bjy: 更新一组文档
for doc in docs:
doc.save()

@ -0,0 +1,32 @@
# bjy: 导入日志模块
import logging
# bjy: 从Django中导入表单模块
from django import forms
# bjy: 从haystack一个Django搜索框架中导入基础搜索表单
from haystack.forms import SearchForm
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个自定义的博客搜索表单继承自Haystack的SearchForm
class BlogSearchForm(SearchForm):
# bjy: 定义一个名为querydata的字符字段用于接收用户输入的搜索关键词并设置为必填
querydata = forms.CharField(required=True)
# bjy: 重写search方法用于执行搜索逻辑
def search(self):
# bjy: 调用父类的search方法执行默认的搜索并返回结果集
datas = super(BlogSearchForm, self).search()
# bjy: 检查表单数据是否有效
if not self.is_valid():
# bjy: 如果表单无效则调用no_query_found方法通常返回一个空的结果集
return self.no_query_found()
# bjy: 如果用户在querydata字段中输入了内容
if self.cleaned_data['querydata']:
# bjy: 将用户输入的搜索关键词记录到日志中
logger.info(self.cleaned_data['querydata'])
# bjy: 返回搜索结果
return datas

@ -0,0 +1,23 @@
# TODO 参数化
# bjy: 定义一个继承自BaseCommand的命令类用于执行构建搜索索引的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"构建搜索索引"
help = 'build search index'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 检查Elasticsearch功能是否已启用确保在启用状态下才执行索引操作
if ELASTICSEARCH_ENABLED:
# bjy: 调用ElapsedTimeDocumentManager的类方法构建用于记录耗时的文档索引
ElaspedTimeDocumentManager.build_index()
# bjy: 创建ElapsedTimeDocument的实例并调用其init方法进行初始化可能是数据同步或设置
manager = ElapsedTimeDocument()
manager.init()
# bjy: 创建ArticleDocumentManager的实例用于管理文章的搜索索引
manager = ArticleDocumentManager()
# bjy: 删除现有的文章索引,为重建做准备,防止旧数据冲突
manager.delete_index()
# bjy: 重新构建文章索引将数据库中的最新文章数据同步到Elasticsearch
manager.rebuild()

@ -0,0 +1,20 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从当前应用的models模块导入Tag和Category模型用于获取数据
from blog.models import Tag, Category
# TODO 参数化
# bjy: 定义一个继承自BaseCommand的命令类用于执行构建搜索词的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"构建搜索词"
help = 'build search words'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 使用集合推导式获取所有Tag和Category的name字段并自动去重
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
# bjy: 将去重后的搜索词集合中的每个元素用换行符连接,并打印到标准输出
print('\n'.join(datas))

@ -0,0 +1,18 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从项目工具模块导入cache实例用于操作缓存
from djangoblog.utils import cache
# bjy: 定义一个继承自BaseCommand的命令类用于执行清空缓存的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"清空所有缓存"
help = 'clear the whole cache'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 调用cache实例的clear方法清空所有缓存
cache.clear()
# bjy: 使用成功样式向标准输出写入操作成功的信息,并附带换行符
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -0,0 +1,65 @@
# bjy: 从Django的auth模块导入get_user_model函数用于动态获取当前项目激活的用户模型
from django.contrib.auth import get_user_model
# bjy: 从Django的auth模块导入make_password函数用于创建加密后的密码哈希
from django.contrib.auth.hashers import make_password
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从当前应用的models模块导入Article, Tag, Category模型用于创建测试数据
from blog.models import Article, Tag, Category
# bjy: 定义一个继承自BaseCommand的命令类用于执行创建测试数据的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"创建测试数据"
help = 'create test datas'
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 获取或创建一个测试用户,如果不存在则创建,密码已加密
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
# bjy: 获取或创建一个父级分类parent_category为None表示它是顶级分类
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
# bjy: 获取或创建一个子分类,并设置其父分类为上面创建的父级分类
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
# bjy: 显式保存子分类实例确保数据已写入数据库虽然get_or_create通常会保存
category.save()
# bjy: 创建一个基础标签,所有文章都将共用此标签
basetag = Tag()
basetag.name = "标签"
basetag.save()
# bjy: 循环19次创建19篇测试文章和对应的标签
for i in range(1, 20):
# bjy: 获取或创建一篇文章,关联到上面创建的分类、用户,并设置标题和内容
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
# bjy: 为每篇文章创建一个专属标签
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
# bjy: 将专属标签和基础标签都添加到当前文章的标签集合中
article.tags.add(tag)
article.tags.add(basetag)
# bjy: 保存文章,使标签关联生效
article.save()
# bjy: 导入项目的cache工具用于清理缓存
from djangoblog.utils import cache
# bjy: 清空所有缓存,以确保新创建的数据能被正确加载
cache.clear()
# bjy: 使用成功样式向标准输出写入操作完成的信息
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -0,0 +1,70 @@
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
from djangoblog.spider_notify import SpiderNotify
# bjy: 从项目工具模块导入get_current_site函数用于获取当前站点域名等信息
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
# bjy: 获取当前站点的域名用于拼接完整URL
site = get_current_site().domain
# bjy: 定义一个继承自BaseCommand的命令类用于执行通知百度抓取URL的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"通知百度URL"
help = 'notify baidu url'
# bjy: 为命令添加参数,允许用户指定通知的数据类型
def add_arguments(self, parser):
# bjy: 添加一个名为data_type的位置参数类型为字符串且只能从给定的选项中选择
parser.add_argument(
'data_type',
type=str,
choices=[
'all',
'article',
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
# bjy: 定义一个辅助方法用于根据路径拼接完整的URL
@staticmethod
def get_full_url(path):
# bjy: 使用https协议和当前站点域名拼接完整URL
url = "https://{site}{path}".format(site=site, path=path)
return url
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 获取用户指定的data_type参数决定通知哪些类型的URL
type = options['data_type']
# bjy: 输出开始获取指定类型URL的信息
self.stdout.write('start get %s' % type)
# bjy: 初始化一个空列表用于收集所有待通知的URL
urls = []
# bjy: 如果类型为article或all则收集所有已发布文章的完整URL
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
# bjy: 如果类型为tag或all则收集所有标签的完整URL
if type == 'tag' or type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
# bjy: 如果类型为category或all则收集所有分类的完整URL
if type == 'category' or type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
# bjy: 输出开始通知URL的数量信息使用成功样式
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
# bjy: 调用SpiderNotify的百度通知方法将收集到的URL发送给百度
SpiderNotify.baidu_notify(urls)
# bjy: 输出通知完成的信息,使用成功样式
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -0,0 +1,76 @@
# bjy: 导入requests库用于发起HTTP请求检测头像URL是否可访问
import requests
# bjy: 从Django核心管理模块导入BaseCommand基类用于创建自定义管理命令
from django.core.management.base import BaseCommand
# bjy: 从Django模板标签模块导入static函数用于生成静态文件的URL
from django.templatetags.static import static
# bjy: 从项目工具模块导入save_user_avatar函数用于保存用户头像到本地
from djangoblog.utils import save_user_avatar
# bjy: 从oauth应用导入OAuthUser模型用于获取所有OAuth用户数据
from oauth.models import OAuthUser
# bjy: 从oauth应用导入get_manager_by_type函数用于根据OAuth类型获取对应的管理器
from oauth.oauthmanager import get_manager_by_type
# bjy: 定义一个继承自BaseCommand的命令类用于执行同步用户头像的任务
class Command(BaseCommand):
# bjy: 设置命令的帮助信息,描述该命令的功能是"同步用户头像"
help = 'sync user avatar'
# bjy: 定义一个辅助方法用于测试给定的URL是否可访问返回200状态码
@staticmethod
def test_picture(url):
try:
# bjy: 尝试GET请求设置2秒超时如果状态码为200则返回True
if requests.get(url, timeout=2).status_code == 200:
return True
except:
# bjy: 任何异常都视为不可访问,静默忽略
pass
# bjy: 定义命令的执行逻辑,当命令被调用时此方法会运行
def handle(self, *args, **options):
# bjy: 获取项目静态文件的基础URL用于判断头像是否为本地静态文件
static_url = static("../")
# bjy: 获取所有OAuth用户
users = OAuthUser.objects.all()
# bjy: 输出开始同步用户头像的总数信息
self.stdout.write(f'开始同步{len(users)}个用户头像')
# bjy: 遍历每个用户,进行头像同步
for u in users:
# bjy: 输出当前正在同步的用户昵称
self.stdout.write(f'开始同步:{u.nickname}')
# bjy: 获取用户当前的头像URL
url = u.picture
# bjy: 如果头像URL不为空则执行同步逻辑
if url:
# bjy: 如果当前头像URL是本地静态文件路径
if url.startswith(static_url):
# bjy: 测试该静态文件是否可访问,若可访问则跳过此用户
if self.test_picture(url):
continue
else:
# bjy: 如果不可访问且用户有metadata信息则尝试通过OAuth管理器重新获取头像URL并保存
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
# bjy: 如果没有metadata则使用默认头像
url = static('blog/img/avatar.png')
else:
# bjy: 如果头像URL不是本地静态文件则直接保存到本地
url = save_user_avatar(url)
else:
# bjy: 如果头像URL为空则使用默认头像
url = static('blog/img/avatar.png')
# bjy: 如果最终得到的URL不为空则更新用户头像并保存
if url:
# bjy: 输出同步完成后的用户昵称和头像URL
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
# bjy: 输出同步全部结束的信息
self.stdout.write('结束同步')

@ -0,0 +1,68 @@
# bjy: 导入日志模块
import logging
# bjy: 导入时间模块,用于计算页面渲染时间
import time
# bjy: 从ipware库导入get_client_ip函数用于获取客户端真实IP
from ipware import get_client_ip
# bjy: 从user_agents库导入parse函数用于解析User-Agent字符串
from user_agents import parse
# bjy: 从blog应用的documents模块中导入Elasticsearch是否启用的标志和性能文档管理器
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个中间件类,用于记录页面性能和在线用户信息
class OnlineMiddleware(object):
# bjy: Django 1.10+ 兼容的初始化方法
def __init__(self, get_response=None):
# bjy: 保存get_response可调用对象它是Django请求-响应链中的下一个处理器
self.get_response = get_response
# bjy: 调用父类的初始化方法
super().__init__()
# bjy: 中间件的核心调用方法,每个请求都会经过这里
def __call__(self, request):
""" page render time """
# bjy: 记录页面渲染开始时间
start_time = time.time()
# bjy: 调用下一个中间件或视图,获取响应对象
response = self.get_response(request)
# bjy: 从请求头中获取User-Agent字符串
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
# bjy: 使用ipware库获取客户端的IP地址
ip, _ = get_client_ip(request)
# bjy: 解析User-Agent字符串得到结构化的用户代理信息
user_agent = parse(http_user_agent)
# bjy: 检查响应是否为流式响应(如文件下载),如果不是,则进行处理
if not response.streaming:
try:
# bjy: 计算页面渲染耗时(秒)
cast_time = time.time() - start_time
# bjy: 如果启用了Elasticsearch
if ELASTICSEARCH_ENABLED:
# bjy: 将耗时转换为毫秒并四舍五入
time_taken = round((cast_time) * 1000, 2)
# bjy: 获取请求的URL路径
url = request.path
# bjy: 导入Django的时区工具
from django.utils import timezone
# bjy: 调用文档管理器将性能数据保存到Elasticsearch
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
# bjy: 将页面渲染耗时替换到响应内容的特定占位符<!!LOAD_TIMES!!>中
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
# bjy: 捕获并记录处理过程中可能发生的任何异常
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
# bjy: 返回最终的响应对象
return response

@ -0,0 +1,137 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
},
),
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
('link', models.URLField(verbose_name='链接地址')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
},
),
migrations.CreateModel(
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='标题')),
('content', models.TextField(verbose_name='内容')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
],
options={
'verbose_name': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
},
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
},
),
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,35 @@
# bjy: 此文件由Django 4.1.7于2023-03-29 06:08自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations和models用于定义迁移操作和模型字段
from django.db import migrations, models
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0001_initial迁移确保基础表已存在
dependencies = [
('blog', '0001_initial'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作1为BlogSettings模型添加一个名为'global_footer'的字段
migrations.AddField(
# bjy: 指定要操作的模型名称
model_name='blogsettings',
# bjy: 指定新字段的名称
name='global_footer',
# bjy: 定义新字段的类型和属性文本类型可为空默认空字符串并设置verbose_name
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
# bjy: 操作2为BlogSettings模型添加一个名为'global_header'的字段
migrations.AddField(
# bjy: 指定要操作的模型名称
model_name='blogsettings',
# bjy: 指定新字段的名称
name='global_header',
# bjy: 定义新字段的类型和属性文本类型可为空默认空字符串并设置verbose_name
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -0,0 +1,25 @@
# bjy: 此文件由Django 4.2.1于2023-05-09 07:45自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations和models用于定义迁移操作和模型字段
from django.db import migrations, models
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0002_blogsettings_global_footer_and_more迁移
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作为BlogSettings模型添加一个新字段
migrations.AddField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定新字段的名称为'comment_need_review'
name='comment_need_review',
# bjy: 定义新字段的类型和属性布尔类型默认值为False并设置verbose_name
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -0,0 +1,43 @@
# bjy: 此文件由Django 4.2.1于2023-05-09 07:51自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations用于定义迁移操作
from django.db import migrations
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0003_blogsettings_comment_need_review迁移
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作1重命名BlogSettings模型中的一个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'analyticscode'
old_name='analyticscode',
# bjy: 指定字段的新名称为'analytics_code'
new_name='analytics_code',
),
# bjy: 操作2重命名BlogSettings模型中的另一个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'beiancode'
old_name='beiancode',
# bjy: 指定字段的新名称为'beian_code'
new_name='beian_code',
),
# bjy: 操作3重命名BlogSettings模型中的第三个字段
migrations.RenameField(
# bjy: 指定要操作的模型名称为'blogsettings'
model_name='blogsettings',
# bjy: 指定字段的原始名称为'sitename'
old_name='sitename',
# bjy: 指定字段的新名称为'site_name'
new_name='site_name',
),
]

@ -0,0 +1,300 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
),
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
),
migrations.RemoveField(
model_name='article',
name='created_time',
),
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
migrations.RemoveField(
model_name='category',
name='created_time',
),
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
migrations.RemoveField(
model_name='links',
name='created_time',
),
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
),
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
),
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
),
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -0,0 +1,24 @@
# bjy: 此文件由Django 4.2.7于2024-01-26 02:41自动生成用于数据库结构迁移
# bjy: 从Django数据库模块导入migrations用于定义迁移操作
from django.db import migrations
# bjy: 定义一个迁移类用于对blog应用进行数据库结构变更
class Migration(migrations.Migration):
# bjy: 定义此迁移的依赖关系它依赖于blog应用的0005迁移
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
# bjy: 定义此迁移要执行的一系列操作
operations = [
# bjy: 操作修改BlogSettings模型的Meta选项更新verbose_name为英文
migrations.AlterModelOptions(
# bjy: 指定要操作的模型名称为'blogsettings'
name='blogsettings',
# bjy: 更新模型的verbose_name和verbose_name_plural为'Website configuration'
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-11-13 13:53
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='article',
name='users_like',
field=models.ManyToManyField(blank=True, related_name='articles_liked', to=settings.AUTH_USER_MODEL, verbose_name='点赞用户'),
),
]

@ -0,0 +1,499 @@
# bjy: 导入日志模块
import logging
# bjy: 导入正则表达式模块
import re
# bjy: 导入抽象基类模块,用于定义抽象方法
from abc import abstractmethod
# bjy: 从Django中导入设置、异常、模型、URL反向解析、时区和国际化工具
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
# bjy: 从Django MDEditor中导入Markdown文本字段
from mdeditor.fields import MDTextField
# bjy: 从uuslug中导入slugify函数用于生成URL友好的slug
from uuslug import slugify
# bjy: 从项目工具模块中导入缓存装饰器和缓存对象
from djangoblog.utils import cache_decorator, cache
# bjy: 从项目工具模块中导入获取当前站点的函数
from djangoblog.utils import get_current_site
# bjy: 获取一个名为__name__的logger实例用于记录日志
logger = logging.getLogger(__name__)
# bjy: 定义一个文本选择类,用于链接显示类型
class LinkShowType(models.TextChoices):
# bjy: 首页
I = ('i', _('index'))
# bjy: 列表页
L = ('l', _('list'))
# bjy: 文章页
P = ('p', _('post'))
# bjy: 所有页面
A = ('a', _('all'))
# bjy: 幻灯片
S = ('s', _('slide'))
# bjy: 定义一个基础模型类,作为其他模型的父类
class BaseModel(models.Model):
# bjy: 自增主键
id = models.AutoField(primary_key=True)
# bjy: 创建时间,默认为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间,默认为当前时间
last_modify_time = models.DateTimeField(_('modify time'), default=now)
# bjy: 重写save方法以实现自定义逻辑
def save(self, *args, **kwargs):
# bjy: 检查是否是更新文章浏览量的操作
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# bjy: 如果是则直接更新数据库中的views字段避免触发其他save逻辑
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# bjy: 如果模型有slug字段则根据title或name自动生成slug
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
# bjy: 调用父类的save方法
super().save(*args, **kwargs)
# bjy: 获取模型的完整URL包括域名
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
# bjy: 设置为抽象模型,不会在数据库中创建表
abstract = True
# bjy: 定义一个抽象方法,要求子类必须实现
@abstractmethod
def get_absolute_url(self):
pass
# bjy: 定义文章模型
class Article(BaseModel):
"""文章"""
# bjy: 文章状态选择
STATUS_CHOICES = (
('d', _('Draft')),
('p', _('Published')),
)
# bjy: 评论状态选择
COMMENT_STATUS = (
('o', _('Open')),
('c', _('Close')),
)
# bjy: 文章类型选择
TYPE = (
('a', _('Article')),
('p', _('Page')),
)
# bjy: 文章标题,唯一
title = models.CharField(_('title'), max_length=200, unique=True)
# bjy: 文章正文使用Markdown编辑器
body = MDTextField(_('body'))
# bjy: 发布时间
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now)
# bjy: 文章状态
status = models.CharField(
_('status'),
max_length=1,
choices=STATUS_CHOICES,
default='p')
# bjy: 评论状态
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
default='o')
# bjy: 文章类型
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
# bjy: 浏览量
views = models.PositiveIntegerField(_('views'), default=0)
# bjy: 作者,外键关联到用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
# bjy: 文章排序权重
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
# bjy: 是否显示目录
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
# bjy: 分类外键关联到Category模型
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
# bjy: 标签多对多关联到Tag模型
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
users_like = models.ManyToManyField(
settings.AUTH_USER_MODEL, # 关联到用户模型
related_name='articles_liked', # 反向关系名称user.articles_liked.all()可获取用户点赞的所有文章
blank=True, # 允许文章没有被任何用户点赞
verbose_name='点赞用户' # 在Admin后台显示的字段名称
)
# bjy: 将body字段转换为字符串
def body_to_string(self):
return self.body
# bjy: 定义文章的字符串表示
def __str__(self):
return self.title
class Meta:
# bjy: 默认排序方式
ordering = ['-article_order', '-pub_time']
# bjy: 模型的单数和复数名称
verbose_name = _('article')
verbose_name_plural = verbose_name
# bjy: 指定按哪个字段获取最新对象
get_latest_by = 'id'
# bjy: 获取文章的绝对URL
def get_absolute_url(self):
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
# bjy: 获取文章的分类树路径,带缓存
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
# bjy: 重写save方法
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# bjy: 增加浏览量
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
# bjy: 获取文章的评论列表,带缓存
def comment_list(self):
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
# bjy: 获取文章在Admin后台的编辑URL
def get_admin_url(self):
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
# bjy: 获取下一篇文章,带缓存
@cache_decorator(expiration=60 * 100)
def next_article(self):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
# bjy: 获取上一篇文章,带缓存
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
# bjy: 从文章正文中提取第一张图片的URL
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
# bjy: 定义分类模型
class Category(BaseModel):
"""文章分类"""
# bjy: 分类名称,唯一
name = models.CharField(_('category name'), max_length=30, unique=True)
# bjy: 父分类,自关联外键
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
# bjy: URL友好的别名
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# bjy: 排序索引
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
# bjy: 按索引降序排列
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
# bjy: 获取分类的绝对URL
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
# bjy: 定义分类的字符串表示
def __str__(self):
return self.name
# bjy: 递归获取分类的所有父级分类,带缓存
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
categorys = []
def parse(category):
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
# bjy: 获取当前分类的所有子分类,带缓存
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
categorys = []
all_categorys = Category.objects.all()
def parse(category):
if category not in categorys:
categorys.append(category)
childs = all_categorys.filter(parent_category=category)
for child in childs:
if category not in categorys:
categorys.append(child)
parse(child)
parse(self)
return categorys
# bjy: 定义标签模型
class Tag(BaseModel):
"""文章标签"""
# bjy: 标签名称,唯一
name = models.CharField(_('tag name'), max_length=30, unique=True)
# bjy: URL友好的别名
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
# bjy: 定义标签的字符串表示
def __str__(self):
return self.name
# bjy: 获取标签的绝对URL
def get_absolute_url(self):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
# bjy: 获取使用该标签的文章数量,带缓存
@cache_decorator(60 * 60 * 10)
def get_article_count(self):
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
# bjy: 按名称排序
ordering = ['name']
verbose_name = _('tag')
verbose_name_plural = verbose_name
# bjy: 定义友情链接模型
class Links(models.Model):
"""友情链接"""
# bjy: 链接名称,唯一
name = models.CharField(_('link name'), max_length=30, unique=True)
# bjy: 链接URL
link = models.URLField(_('link'))
# bjy: 排序权重,唯一
sequence = models.IntegerField(_('order'), unique=True)
# bjy: 是否启用
is_enable = models.BooleanField(
_('is show'), default=True, blank=False, null=False)
# bjy: 显示类型
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
# bjy: 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
# bjy: 按排序权重升序排列
ordering = ['sequence']
verbose_name = _('link')
verbose_name_plural = verbose_name
# bjy: 定义链接的字符串表示
def __str__(self):
return self.name
# bjy: 定义侧边栏模型
class SideBar(models.Model):
"""侧边栏,可以展示一些html内容"""
# bjy: 侧边栏标题
name = models.CharField(_('title'), max_length=100)
# bjy: 侧边栏内容HTML
content = models.TextField(_('content'))
# bjy: 排序权重,唯一
sequence = models.IntegerField(_('order'), unique=True)
# bjy: 是否启用
is_enable = models.BooleanField(_('is enable'), default=True)
# bjy: 创建时间
creation_time = models.DateTimeField(_('creation time'), default=now)
# bjy: 最后修改时间
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
# bjy: 按排序权重升序排列
ordering = ['sequence']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
# bjy: 定义侧边栏的字符串表示
def __str__(self):
return self.name
# bjy: 定义博客设置模型
class BlogSettings(models.Model):
"""blog的配置"""
# bjy: 网站名称
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
# bjy: 网站描述
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
# bjy: SEO描述
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
# bjy: 网站关键词
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
default='')
# bjy: 文章摘要长度
article_sub_length = models.IntegerField(_('article sub length'), default=300)
# bjy: 侧边栏文章数量
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
# bjy: 侧边栏评论数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
# bjy: 文章页评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5)
# bjy: 是否显示Google AdSense
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
# bjy: Google AdSense代码
google_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
# bjy: 是否开启全站评论
open_site_comment = models.BooleanField(_('open site comment'), default=True)
# bjy: 公共头部HTML代码
global_header = models.TextField("公共头部", null=True, blank=True, default='')
# bjy: 公共尾部HTML代码
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
# bjy: ICP备案号
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
# bjy: 网站统计代码
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
# bjy: 是否显示公安备案号
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
# bjy: 公安备案号
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
# bjy: 评论是否需要审核
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
class Meta:
# bjy: 模型的单数和复数名称
verbose_name = _('Website configuration')
verbose_name_plural = verbose_name
# bjy: 定义设置的字符串表示
def __str__(self):
return self.site_name
# bjy: 重写clean方法用于模型验证
def clean(self):
# bjy: 确保数据库中只能有一条配置记录
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
# bjy: 重写save方法保存后清除缓存
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()

@ -0,0 +1,21 @@
# bjy: 从haystack框架中导入indexes模块用于创建搜索索引
from haystack import indexes
# bjy: 从blog应用中导入Article模型
from blog.models import Article
# bjy: 为Article模型定义一个搜索索引类
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
# bjy: 定义一个主文本字段,`document=True`表示这是搜索的主要字段
# bjy: `use_template=True`表示该字段的内容将由一个模板来生成
text = indexes.CharField(document=True, use_template=True)
# bjy: `get_model`方法必须实现,用于返回此索引对应的模型类
def get_model(self):
return Article
# bjy: `index_queryset`方法定义了哪些模型实例应该被建立索引
def index_queryset(self, using=None):
# bjy: 这里只返回状态为'p'(已发布)的文章
return self.get_model().objects.filter(status='p')

@ -0,0 +1,17 @@
/* bjy: 定义一个名为.button的CSS类用于设置按钮的通用样式 */
.button {
/* bjy: 移除按钮的默认边框 */
border: none;
/* bjy: 设置按钮的内边距上下4像素左右80像素 */
padding: 4px 80px;
/* bjy: 设置按钮内部文本的水平居中对齐 */
text-align: center;
/* bjy: 移除文本装饰(如下划线),通常用于链接样式的按钮 */
text-decoration: none;
/* bjy: 将按钮设置为行内块级元素,使其可以设置宽高并与其他元素在同一行显示 */
display: inline-block;
/* bjy: 设置按钮内部文本的字体大小为16像素 */
font-size: 16px;
/* bjy: 设置按钮的外边距上下4像素左右2像素用于控制按钮之间的间距 */
margin: 4px 2px;
}

@ -0,0 +1,78 @@
// bjy: 声明一个全局变量wait用于倒计时初始值为60秒
let wait = 60;
// bjy: 定义一个名为time的函数用于处理按钮的倒计时效果
// bjy: 参数o代表触发倒计时的按钮元素
function time(o) {
// bjy: 如果倒计时结束wait为0
if (wait == 0) {
// bjy: 移除按钮的disabled属性使其重新可点击
o.removeAttribute("disabled");
// bjy: 将按钮的显示文本恢复为“获取验证码”
o.value = "获取验证码";
// bjy: 重置倒计时变量为60以便下次使用
wait = 60
// bjy: 结束函数执行
return false
} else {
// bjy: 如果倒计时未结束,禁用按钮,防止重复点击
o.setAttribute("disabled", true);
// bjy: 更新按钮的显示文本,显示剩余的倒计时秒数
o.value = "重新发送(" + wait + ")";
// bjy: 倒计时秒数减一
wait--;
// bjy: 设置一个1秒1000毫秒后执行的定时器
setTimeout(function () {
// bjy: 定时器回调函数中递归调用time函数实现每秒更新一次倒计时
time(o)
},
1000)
}
}
// bjy: 为ID为"btn"的元素绑定点击事件处理函数
document.getElementById("btn").onclick = function () {
// bjy: 使用jQuery选择器获取邮箱输入框元素
let id_email = $("#id_email")
// bjy: 使用jQuery选择器获取CSRF令牌的值用于Django的POST请求安全验证
let token = $("*[name='csrfmiddlewaretoken']").val()
// bjy: 将this即被点击的按钮的引用保存到ts变量中以便在AJAX回调中使用
let ts = this
// bjy: 使用jQuery选择器获取用于显示错误信息的元素
let myErr = $("#myErr")
// bjy: 使用jQuery发起一个AJAX请求
$.ajax(
{
// bjy: 请求的URL地址
url: "/forget_password_code/",
// bjy: 请求的类型为POST
type: "POST",
// bjy: 发送到服务器的数据包含邮箱和CSRF令牌
data: {
"email": id_email.val(),
"csrfmiddlewaretoken": token
},
// bjy: 定义请求成功时的回调函数result是服务器返回的数据
success: function (result) {
// bjy: 如果服务器返回的结果不是"ok"(表示发送失败或有错误)
if (result != "ok") {
// bjy: 移除页面上可能存在的旧错误提示
myErr.remove()
// bjy: 在邮箱输入框后面动态添加一个错误提示列表,显示服务器返回的错误信息
id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>")
// bjy: 结束函数执行
return
}
// bjy: 如果发送成功,移除页面上可能存在的旧错误提示
myErr.remove()
// bjy: 调用time函数开始按钮的倒计时效果
time(ts)
},
// bjy: 定义请求失败时的回调函数e是错误对象
error: function (e) {
// bjy: 弹出一个警告框,提示用户发送失败
alert("发送失败,请重试")
}
}
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,13 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*
* See the Getting Started docs for more information:
* http://getbootstrap.com/getting-started/#support-ie10-width
*/
@-ms-viewport { width: device-width; }
@-o-viewport { width: device-width; }
@viewport { width: device-width; }

@ -0,0 +1,58 @@
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #fff;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin-heading {
margin: 0 0 15px;
font-size: 18px;
font-weight: 400;
color: #555;
}
.form-signin .checkbox {
margin-bottom: 10px;
font-weight: normal;
}
.form-signin .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: 10px;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
}
.card {
width: 304px;
padding: 20px 25px 30px;
margin: 0 auto 25px;
background-color: #f7f7f7;
border-radius: 2px;
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
}
.card-signin {
width: 354px;
padding: 40px;
}
.card-signin .profile-img {
display: block;
width: 96px;
height: 96px;
margin: 0 auto 10px;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

@ -0,0 +1,71 @@
// NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT
// IT'S JUST JUNK FOR OUR DOCS!
// ++++++++++++++++++++++++++++++++++++++++++
/*!
* Copyright 2014-2015 Twitter, Inc.
*
* Licensed under the Creative Commons Attribution 3.0 Unported License. For
* details, see https://creativecommons.org/licenses/by/3.0/.
*/
// Intended to prevent false-positive bug reports about Bootstrap not working properly in old versions of IE due to folks testing using IE's unreliable emulation modes.
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function () {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict';
// bjy: 定义一个函数用于从用户代理字符串中获取IE的模拟版本号
function emulatedIEMajorVersion() {
// bjy: 使用正则表达式匹配用户代理字符串中的 "MSIE x.x" 部分
var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
// bjy: 如果匹配不到说明不是IE或版本号无法识别返回null
if (groups === null) {
return null
}
// bjy: 将匹配到的版本号字符串(如 "10.0")转换为整数
var ieVersionNum = parseInt(groups[1], 10)
// bjy: 取整数部分作为主版本号
var ieMajorVersion = Math.floor(ieVersionNum)
// bjy: 返回模拟的IE主版本号
return ieMajorVersion
}
// bjy: 定义一个函数用于检测当前浏览器实际运行的IE版本即使它处于旧版IE的模拟模式下
function actualNonEmulatedIEMajorVersion() {
// bjy: 此函数通过IE特有的JScript条件编译来检测真实版本
// bjy: IE JavaScript条件编译文档: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx
// bjy: @cc_on 文档: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx
// bjy: 创建一个新的Function其内容是IE的条件编译语句用于获取JScript版本
var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line
// bjy: 如果jscriptVersion未定义说明是IE11或更高版本且不在模拟模式下
if (jscriptVersion === undefined) {
return 11 // IE11+ not in emulation mode
}
// bjy: 如果JScript版本小于9则判断为IE8或更低版本
if (jscriptVersion < 9) {
return 8 // IE8 (or lower; haven't tested on IE<8)
}
// bjy: 否则返回JScript版本这对应于IE9或IE10在任何模式下或IE11在非IE11模式下
return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode
}
// bjy: 获取当前浏览器的用户代理字符串
var ua = window.navigator.userAgent
// bjy: 检查用户代理中是否包含'Opera'或'Presto'Opera的旧版渲染引擎
if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) {
return // Opera, which might pretend to be IE
}
// bjy: 调用函数获取模拟的IE版本号
var emulated = emulatedIEMajorVersion()
// bjy: 如果模拟版本为null说明不是IE浏览器直接返回
if (emulated === null) {
return // Not IE
}
// bjy: 调用函数获取实际的IE版本号
var nonEmulated = actualNonEmulatedIEMajorVersion()
// bjy: 比较模拟版本和实际版本如果不相同说明IE正处于模拟模式下
if (emulated !== nonEmulated) {
// bjy: 弹出一个警告框提示用户当前正处于IE模拟模式并警告其行为可能与真实旧版IE不同
window.alert('WARNING: You appear to be using IE' + nonEmulated + ' in IE' + emulated + ' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!')
}
})();

@ -0,0 +1,33 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
// See the Getting Started docs for more information:
// http://getbootstrap.com/getting-started/#support-ie10-width
// bjy: 使用一个立即执行函数表达式IIFE来创建一个独立的作用域避免污染全局命名空间
(function () {
// bjy: 启用严格模式,有助于捕获常见的编码错误和“不安全”的操作
'use strict';
// bjy: 检查当前浏览器的用户代理字符串User Agent是否匹配IEMobile/10.0
// bjy: 这是为了专门识别运行在Windows Phone 8上的IE10移动版浏览器
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
// bjy: 如果匹配,则创建一个新的<style>元素
var msViewportStyle = document.createElement('style')
// bjy: 向这个<style>元素中添加一个CSS规则
msViewportStyle.appendChild(
// bjy: 创建一个包含CSS规则的文本节点
document.createTextNode(
// bjy: CSS规则@-ms-viewport是IE10的一个特有规则用于设置视口大小
// bjy: width:auto!important 覆盖了IE10错误的默认设置强制视口宽度为设备宽度
'@-ms-viewport{width:auto!important}'
)
)
// bjy: 将这个新创建的<style>元素添加到文档的<head>部分使CSS规则生效
document.querySelector('head').appendChild(msViewportStyle)
}
})();

@ -0,0 +1,319 @@
/*
bjy: IEIE9
*/
/* bjy: 为body元素设置浅灰色背景 */
body {
background-color: #e6e6e6;
}
/* bjy: 当body没有自定义背景时设置背景为白色 */
body.custom-background-empty {
background-color: #fff;
}
/* bjy: 在没有自定义背景或背景为白色的页面上,移除网站容器的阴影、边距和内边距 */
body.custom-background-empty .site,
body.custom-background-white .site {
box-shadow: none;
margin-bottom: 0;
margin-top: 0;
padding: 0;
}
/* bjy: 隐藏辅助性文本和屏幕阅读器专用文本,通过裁剪使其在视觉上不可见 */
.assistive-text,
.site .screen-reader-text {
clip: rect(1px 1px 1px 1px);
}
/* bjy: 在全宽布局下,使内容区域占满整个宽度,并取消浮动 */
.full-width .site-content {
float: none;
width: 100%;
}
/* bjy: 防止在IE8中带有height和width属性的图片被拉伸设置width为auto */
img.size-full,
img.size-large,
img.header-image,
img.wp-post-image,
img[class*="align"],
img[class*="wp-image-"],
img[class*="attachment-"] {
width: auto; /* Prevent stretching of full-size and large-size images with height and width attributes in IE8 */
}
/* bjy: 作者头像向左浮动,并设置上边距 */
.author-avatar {
float: left;
margin-top: 8px;
margin-top: 0.571428571rem;
}
/* bjy: 作者描述向右浮动宽度占80% */
.author-description {
float: right;
width: 80%;
}
/* bjy: 网站主容器样式:居中、最大宽度、阴影、溢出隐藏、内边距 */
.site {
box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
margin: 48px auto;
max-width: 960px;
overflow: hidden;
padding: 0 40px;
}
/* bjy: 网站内容区域向左浮动宽度约为65.1% */
.site-content {
float: left;
width: 65.104166667%;
}
/* bjy: 在首页模板、附件页面或全宽布局下内容区域宽度为100% */
body.template-front-page .site-content,
body.attachment .site-content,
body.full-width .site-content {
width: 100%;
}
/* bjy: 小工具区域向右浮动宽度约为26.04% */
.widget-area {
float: right;
width: 26.041666667%;
}
/* bjy: 网站标题和副标题左对齐 */
.site-header h1,
.site-header h2 {
text-align: left;
}
/* bjy: 网站标题的字体大小和行高 */
.site-header h1 {
font-size: 26px;
line-height: 1.846153846;
}
/* bjy: 主导航菜单样式:上下边框、行内块显示、左对齐、全宽 */
.main-navigation ul.nav-menu,
.main-navigation div.nav-menu > ul {
border-bottom: 1px solid #ededed;
border-top: 1px solid #ededed;
display: inline-block !important;
text-align: left;
width: 100%;
}
/* bjy: 重置主导航ul的默认外边距和文本缩进 */
.main-navigation ul {
margin: 0;
text-indent: 0;
}
/* bjy: 主导航的链接和列表项设置为行内块,无文本装饰 */
.main-navigation li a,
.main-navigation li {
display: inline-block;
text-decoration: none;
}
/* bjy: 针对IE7的特殊处理将主导航的链接和列表项设置为行内 */
.ie7 .main-navigation li a,
.ie7 .main-navigation li {
display: inline;
}
/* bjy: 主导航链接样式:无边框、颜色、行高、大写转换 */
.main-navigation li a {
border-bottom: 0;
color: #6a6a6a;
line-height: 3.692307692;
text-transform: uppercase;
}
/* bjy: 主导航链接悬停时颜色变黑 */
.main-navigation li a:hover {
color: #000;
}
/* bjy: 主导航列表项样式:右边距、相对定位 */
.main-navigation li {
margin: 0 40px 0 0;
position: relative;
}
/* bjy: 主导航下拉子菜单样式:绝对定位、隐藏(通过裁剪) */
.main-navigation li ul {
margin: 0;
padding: 0;
position: absolute;
top: 100%;
z-index: 1;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
/* bjy: 针对IE7的下拉菜单特殊处理使用display:none隐藏 */
.ie7 .main-navigation li ul {
clip: inherit;
display: none;
left: 0;
overflow: visible;
}
/* bjy: 二级及更深层的下拉菜单定位 */
.main-navigation li ul ul,
.ie7 .main-navigation li ul ul {
top: 0;
left: 100%;
}
/* bjy: 当鼠标悬停或聚焦于主导航项时,显示其子菜单 */
.main-navigation ul li:hover > ul,
.main-navigation ul li:focus > ul,
.main-navigation .focus > ul {
border-left: 0;
clip: inherit;
overflow: inherit;
height: inherit;
width: inherit;
}
/* bjy: 针对IE7当鼠标悬停或聚焦时使用display:block显示子菜单 */
.ie7 .main-navigation ul li:hover > ul,
.ie7 .main-navigation ul li:focus > ul {
display: block;
}
/* bjy: 下拉菜单中的链接样式:背景、边框、块级显示、字体、内边距、宽度 */
.main-navigation li ul li a {
background: #efefef;
border-bottom: 1px solid #ededed;
display: block;
font-size: 11px;
line-height: 2.181818182;
padding: 8px 10px;
width: 180px;
}
/* bjy: 下拉菜单链接悬停时的背景和颜色 */
.main-navigation li ul li a:hover {
background: #e3e3e3;
color: #444;
}
/* bjy: 当前菜单项或其祖先项的链接样式:加粗 */
.main-navigation .current-menu-item > a,
.main-navigation .current-menu-ancestor > a,
.main-navigation .current_page_item > a,
.main-navigation .current_page_ancestor > a {
color: #636363;
font-weight: bold;
}
/* bjy: 隐藏主导航的移动端菜单切换按钮 */
.main-navigation .menu-toggle {
display: none;
}
/* bjy: 文章标题的字体大小 */
.entry-header .entry-title {
font-size: 22px;
}
/* bjy: 评论表单中文本输入框的宽度 */
#respond form input[type="text"] {
width: 46.333333333%;
}
/* bjy: 评论表单中文本域的宽度 */
#respond form textarea.blog-textarea {
width: 79.666666667%;
}
/* bjy: 首页模板的内容区域和文章设置溢出隐藏 */
.template-front-page .site-content,
.template-front-page article {
overflow: hidden;
}
/* bjy: 首页模板中有特色图片的文章向左浮动宽度约为47.92% */
.template-front-page.has-post-thumbnail article {
float: left;
width: 47.916666667%;
}
/* bjy: 首页模板中的特色图片向右浮动宽度约为47.92% */
.entry-page-image {
float: right;
margin-bottom: 0;
width: 47.916666667%;
}
/* bjy: IE首页模板小工具修复清除浮动 */
.template-front-page .widget-area {
clear: both;
}
/* bjy: IE首页模板小工具修复设置小工具宽度为100%,无边框 */
.template-front-page .widget {
width: 100% !important;
border: none;
}
/* bjy: 首页模板小工具区域的布局:左浮动、宽度、边距 */
.template-front-page .widget-area .widget,
.template-front-page .first.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets {
float: left;
margin-bottom: 24px;
width: 51.875%;
}
/* bjy: 首页模板特定小工具的布局:清除右浮动 */
.template-front-page .second.front-widgets,
.template-front-page .widget-area .widget:nth-child(odd) {
clear: right;
}
/* bjy: 首页模板另一组小工具的布局:右浮动、宽度、边距 */
.template-front-page .first.front-widgets,
.template-front-page .second.front-widgets,
.template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets {
float: right;
margin: 0 0 24px;
width: 39.0625%;
}
/* bjy: 双侧边栏首页模板的小工具布局:取消浮动,宽度自适应 */
.template-front-page.two-sidebars .widget,
.template-front-page.two-sidebars .widget:nth-child(even) {
float: none;
width: auto;
}
/* bjy: 为IE9以下的密码输入框添加字体以确保密码圆点显示正常 */
input[type="password"] {
font-family: Helvetica, Arial, sans-serif;
}
/* bjy: RTLIE7IE8
-------------------------------------------------------------- */
/* bjy: RTL布局下网站标题右对齐 */
.rtl .site-header h1,
.rtl .site-header h2 {
text-align: right;
}
/* bjy: RTL布局下小工具区域和作者描述向左浮动 */
.rtl .widget-area,
.rtl .author-description {
float: left;
}
/* bjy: RTL布局下作者头像和内容区域向右浮动 */
.rtl .author-avatar,
.rtl .site-content {
float: right;
}
/* bjy: RTL布局下主导航菜单右对齐 */
.rtl .main-navigation ul.nav-menu,
.rtl .main-navigation div.nav-menu > ul {
text-align: right;
}
/* bjy: RTL布局下下拉菜单项的左边距 */
.rtl .main-navigation ul li ul li,
.rtl .main-navigation ul li ul li ul li {
margin-left: 40px;
margin-right: auto;
}
/* bjy: RTL布局下二级下拉菜单位于父菜单的左侧 */
.rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
/* bjy: IE7 RTL布局下二级下拉菜单位置调整 */
.ie7 .rtl .main-navigation li ul ul {
position: absolute;
bottom: 0;
right: 100%;
z-index: 1;
}
/* bjy: IE7 RTL布局下为主导航列表项设置堆叠顺序 */
.ie7 .rtl .main-navigation ul li {
z-index: 99;
}
/* bjy: IE7 RTL布局下一级下拉菜单位于父菜单上方 */
.ie7 .rtl .main-navigation li ul {
position: absolute;
bottom: 100%;
right: 0;
z-index: 1;
}
/* bjy: IE7 RTL布局下主导航列表项的边距调整 */
.ie7 .rtl .main-navigation li {
margin-right: auto;
marg

@ -0,0 +1,87 @@
/* bjy: 使点击事件可以穿透进度条元素,不影响下方页面的交互 */
#nprogress {
pointer-events: none;
}
/* bjy: 进度条主体样式 */
#nprogress .bar {
/* bjy: 进度条背景色为红色 */
background: red;
/* bjy: 固定定位,使进度条始终在页面顶部 */
position: fixed;
/* bjy: 设置较高的堆叠顺序,确保进度条在最上层 */
z-index: 1031;
top: 0;
left: 0;
/* bjy: 进度条宽度和高度 */
width: 100%;
height: 2px;
}
/* bjy: 进度条右侧的“闪光”或“模糊”效果 */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
/* bjy: 使用box-shadow创建一个发光的阴影效果 */
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1.0;
/* bjy: 对peg元素进行轻微的旋转和位移增加动感 */
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* bjy: 进度指示器(右上角的旋转图标)样式。注释提示:删除这些样式可以去掉旋转图标 */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
/* bjy: 旋转图标本身的样式 */
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
/* bjy: 设置边框,顶部和左侧为红色,形成旋转动画的视觉效果 */
border: solid 2px transparent;
border-top-color: red;
border-left-color: red;
border-radius: 50%;
/* bjy: 应用旋转动画 */
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
/* bjy: 自定义父容器样式,用于将进度条限制在特定区域内 */
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
/* bjy: 当进度条在自定义父容器内时将其定位方式从fixed改为absolute */
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
/* bjy: 定义旋转动画的关键帧兼容旧版WebKit内核浏览器 */
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
/* bjy: 定义旋转动画的关键帧(标准语法) */
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save