# Conflicts: # doc/第一组 软件界面设计说明书.docxlrj_branch
@ -1,3 +0,0 @@
|
||||
[submodule "src/DjangoBlog"]
|
||||
path = src/DjangoBlog
|
||||
url = https://github.com/ETOofficial/DjangoBlog.git
|
||||
@ -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 = ""];
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 668 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 728 KiB |
|
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 : 路由配置
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 6854347d35f6619d9e16852d0d6b133ca645c6c5
|
||||
@ -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,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,79 @@
|
||||
# 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/
|
||||
@ -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,60 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.forms import UserChangeForm
|
||||
from django.contrib.auth.forms import UsernameField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Register your models here.
|
||||
from .models import BlogUser
|
||||
|
||||
|
||||
class BlogUserCreationForm(forms.ModelForm):
|
||||
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
|
||||
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
|
||||
|
||||
class Meta:
|
||||
model = BlogUser
|
||||
fields = ('email',)
|
||||
|
||||
def clean_password2(self):
|
||||
# Check that the two password entries match
|
||||
password1 = self.cleaned_data.get("password1")
|
||||
password2 = self.cleaned_data.get("password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise forms.ValidationError(_("passwords do not match"))
|
||||
return password2
|
||||
|
||||
def save(self, commit=True):
|
||||
# Save the provided password in hashed format
|
||||
user = super().save(commit=False)
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
if commit:
|
||||
user.source = 'adminsite'
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class BlogUserChangeForm(UserChangeForm):
|
||||
class Meta:
|
||||
model = BlogUser
|
||||
fields = '__all__'
|
||||
field_classes = {'username': UsernameField}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class BlogUserAdmin(UserAdmin):
|
||||
form = BlogUserChangeForm
|
||||
add_form = BlogUserCreationForm
|
||||
list_display = (
|
||||
'id',
|
||||
'nickname',
|
||||
'username',
|
||||
'email',
|
||||
'last_login',
|
||||
'date_joined',
|
||||
'source')
|
||||
list_display_links = ('id', 'username')
|
||||
ordering = ('-id',)
|
||||
search_fields = ('username', 'nickname', 'email')
|
||||
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = 'accounts'
|
||||
@ -0,0 +1,117 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model, password_validation
|
||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from . import utils
|
||||
from .models import BlogUser
|
||||
|
||||
|
||||
class LoginForm(AuthenticationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoginForm, self).__init__(*args, **kwargs)
|
||||
self.fields['username'].widget = widgets.TextInput(
|
||||
attrs={'placeholder': "username", "class": "form-control"})
|
||||
self.fields['password'].widget = widgets.PasswordInput(
|
||||
attrs={'placeholder': "password", "class": "form-control"})
|
||||
|
||||
|
||||
class RegisterForm(UserCreationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RegisterForm, self).__init__(*args, **kwargs)
|
||||
|
||||
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):
|
||||
email = self.cleaned_data['email']
|
||||
if get_user_model().objects.filter(email=email).exists():
|
||||
raise ValidationError(_("email already exists"))
|
||||
return email
|
||||
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = ("username", "email")
|
||||
|
||||
|
||||
class ForgetPasswordForm(forms.Form):
|
||||
new_password1 = forms.CharField(
|
||||
label=_("New password"),
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
'placeholder': _("New password")
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
new_password2 = forms.CharField(
|
||||
label="确认密码",
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
'placeholder': _("Confirm password")
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
email = forms.EmailField(
|
||||
label='邮箱',
|
||||
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):
|
||||
password1 = self.data.get("new_password1")
|
||||
password2 = self.data.get("new_password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise ValidationError(_("passwords do not match"))
|
||||
password_validation.validate_password(password2)
|
||||
|
||||
return password2
|
||||
|
||||
def clean_email(self):
|
||||
user_email = self.cleaned_data.get("email")
|
||||
if not BlogUser.objects.filter(
|
||||
email=user_email
|
||||
).exists():
|
||||
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
|
||||
raise ValidationError(_("email does not exist"))
|
||||
return user_email
|
||||
|
||||
def clean_code(self):
|
||||
code = self.cleaned_data.get("code")
|
||||
error = utils.verify(
|
||||
email=self.cleaned_data.get("email"),
|
||||
code=code,
|
||||
)
|
||||
if error:
|
||||
raise ValidationError(error)
|
||||
return code
|
||||
|
||||
|
||||
class ForgetPasswordCodeForm(forms.Form):
|
||||
email = forms.EmailField(
|
||||
label=_('Email'),
|
||||
)
|
||||
@ -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,35 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from djangoblog.utils import get_current_site
|
||||
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class BlogUser(AbstractUser):
|
||||
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||
source = models.CharField(_('create source'), max_length=100, blank=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'blog:author_detail', kwargs={
|
||||
'author_name': self.username})
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
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:
|
||||
ordering = ['-id']
|
||||
verbose_name = _('user')
|
||||
verbose_name_plural = verbose_name
|
||||
get_latest_by = 'id'
|
||||
@ -0,0 +1,207 @@
|
||||
from django.test import Client, RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.models import BlogUser
|
||||
from blog.models import Article, Category
|
||||
from djangoblog.utils import *
|
||||
from . import utils
|
||||
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class AccountTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.factory = RequestFactory()
|
||||
self.blog_user = BlogUser.objects.create_user(
|
||||
username="test",
|
||||
email="admin@admin.com",
|
||||
password="12345678"
|
||||
)
|
||||
self.new_test = "xxx123--="
|
||||
|
||||
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')
|
||||
|
||||
loginresult = self.client.login(
|
||||
username='liangliangyy1',
|
||||
password='qwer!@#$ggg')
|
||||
self.assertEqual(loginresult, True)
|
||||
response = self.client.get('/admin/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
category = Category()
|
||||
category.name = "categoryaaa"
|
||||
category.creation_time = timezone.now()
|
||||
category.last_modify_time = timezone.now()
|
||||
category.save()
|
||||
|
||||
article = Article()
|
||||
article.title = "nicetitleaaa"
|
||||
article.body = "nicecontentaaa"
|
||||
article.author = user
|
||||
article.category = category
|
||||
article.type = 'a'
|
||||
article.status = 'p'
|
||||
article.save()
|
||||
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_validate_register(self):
|
||||
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',
|
||||
})
|
||||
self.assertEquals(
|
||||
1, len(
|
||||
BlogUser.objects.filter(
|
||||
email='user123@user.com')))
|
||||
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
||||
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
|
||||
path = reverse('accounts:result')
|
||||
url = '{path}?type=validation&id={id}&sign={sign}'.format(
|
||||
path=path, id=user.id, sign=sign)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.client.login(username='user1233', password='password123!q@wE#R$T')
|
||||
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
||||
user.is_superuser = True
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
delete_sidebar_cache()
|
||||
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()
|
||||
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('account:logout'))
|
||||
self.assertIn(response.status_code, [301, 302, 200])
|
||||
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertIn(response.status_code, [301, 302, 200])
|
||||
|
||||
response = self.client.post(reverse('account:login'), {
|
||||
'username': 'user1233',
|
||||
'password': 'password123'
|
||||
})
|
||||
self.assertIn(response.status_code, [301, 302, 200])
|
||||
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertIn(response.status_code, [301, 302, 200])
|
||||
|
||||
def test_verify_email_code(self):
|
||||
to_email = "admin@admin.com"
|
||||
code = generate_code()
|
||||
utils.set_code(to_email, code)
|
||||
utils.send_verify_email(to_email, code)
|
||||
|
||||
err = utils.verify("admin@admin.com", code)
|
||||
self.assertEqual(err, None)
|
||||
|
||||
err = utils.verify("admin@123.com", code)
|
||||
self.assertEqual(type(err), str)
|
||||
|
||||
def test_forget_password_email_code_success(self):
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict(email="admin@admin.com")
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "ok")
|
||||
|
||||
def test_forget_password_email_code_fail(self):
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict()
|
||||
)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
||||
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict(email="admin@com")
|
||||
)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
||||
|
||||
def test_forget_password_email_success(self):
|
||||
code = generate_code()
|
||||
utils.set_code(self.blog_user.email, code)
|
||||
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)
|
||||
|
||||
# 验证用户密码是否修改成功
|
||||
blog_user = BlogUser.objects.filter(
|
||||
email=self.blog_user.email,
|
||||
).first() # type: BlogUser
|
||||
self.assertNotEqual(blog_user, None)
|
||||
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
|
||||
|
||||
def test_forget_password_email_not_user(self):
|
||||
data = dict(
|
||||
new_password1=self.new_test,
|
||||
new_password2=self.new_test,
|
||||
email="123@123.com",
|
||||
code="123456",
|
||||
)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password"),
|
||||
data=data
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
def test_forget_password_email_code_error(self):
|
||||
code = generate_code()
|
||||
utils.set_code(self.blog_user.email, code)
|
||||
data = dict(
|
||||
new_password1=self.new_test,
|
||||
new_password2=self.new_test,
|
||||
email=self.blog_user.email,
|
||||
code="111111",
|
||||
)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password"),
|
||||
data=data
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
from django.urls import path
|
||||
from django.urls import re_path
|
||||
|
||||
from . import views
|
||||
from .forms import LoginForm
|
||||
|
||||
app_name = "accounts"
|
||||
|
||||
urlpatterns = [re_path(r'^login/$',
|
||||
views.LoginView.as_view(success_url='/'),
|
||||
name='login',
|
||||
kwargs={'authentication_form': LoginForm}),
|
||||
re_path(r'^register/$',
|
||||
views.RegisterView.as_view(success_url="/"),
|
||||
name='register'),
|
||||
re_path(r'^logout/$',
|
||||
views.LogoutView.as_view(),
|
||||
name='logout'),
|
||||
path(r'account/result.html',
|
||||
views.account_result,
|
||||
name='result'),
|
||||
re_path(r'^forget_password/$',
|
||||
views.ForgetPasswordView.as_view(),
|
||||
name='forget_password'),
|
||||
re_path(r'^forget_password_code/$',
|
||||
views.ForgetPasswordEmailCode.as_view(),
|
||||
name='forget_password_code'),
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
|
||||
class EmailOrUsernameModelBackend(ModelBackend):
|
||||
"""
|
||||
允许使用用户名或邮箱登录
|
||||
"""
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
if '@' in username:
|
||||
kwargs = {'email': username}
|
||||
else:
|
||||
kwargs = {'username': username}
|
||||
try:
|
||||
user = get_user_model().objects.get(**kwargs)
|
||||
if user.check_password(password):
|
||||
return user
|
||||
except get_user_model().DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user(self, username):
|
||||
try:
|
||||
return get_user_model().objects.get(pk=username)
|
||||
except get_user_model().DoesNotExist:
|
||||
return None
|
||||
@ -0,0 +1,114 @@
|
||||
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
|
||||
|
||||
|
||||
class ArticleForm(forms.ModelForm):
|
||||
# body = forms.CharField(widget=AdminPagedownWidget())
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
def makr_article_publish(modeladmin, request, queryset):
|
||||
queryset.update(status='p')
|
||||
|
||||
|
||||
def draft_article(modeladmin, request, queryset):
|
||||
queryset.update(status='d')
|
||||
|
||||
|
||||
def close_article_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(comment_status='c')
|
||||
|
||||
|
||||
def open_article_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(comment_status='o')
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
class ArticlelAdmin(admin.ModelAdmin):
|
||||
list_per_page = 20
|
||||
search_fields = ('body', 'title')
|
||||
form = ArticleForm
|
||||
list_display = (
|
||||
'id',
|
||||
'title',
|
||||
'author',
|
||||
'link_to_category',
|
||||
'creation_time',
|
||||
'views',
|
||||
'status',
|
||||
'type',
|
||||
'article_order')
|
||||
list_display_links = ('id', 'title')
|
||||
list_filter = ('status', 'type', 'category')
|
||||
date_hierarchy = 'creation_time'
|
||||
filter_horizontal = ('tags',)
|
||||
exclude = ('creation_time', 'last_modify_time')
|
||||
view_on_site = True
|
||||
actions = [
|
||||
makr_article_publish,
|
||||
draft_article,
|
||||
close_article_commentstatus,
|
||||
open_article_commentstatus]
|
||||
raw_id_fields = ('author', 'category',)
|
||||
|
||||
def link_to_category(self, obj):
|
||||
info = (obj.category._meta.app_label, obj.category._meta.model_name)
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
|
||||
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
|
||||
|
||||
link_to_category.short_description = _('category')
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
|
||||
form.base_fields['author'].queryset = get_user_model(
|
||||
).objects.filter(is_superuser=True)
|
||||
return form
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ArticlelAdmin, self).save_model(request, obj, form, change)
|
||||
|
||||
def get_view_on_site_url(self, obj=None):
|
||||
if obj:
|
||||
url = obj.get_full_url()
|
||||
return url
|
||||
else:
|
||||
from djangoblog.utils import get_current_site
|
||||
site = get_current_site().domain
|
||||
return site
|
||||
|
||||
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
exclude = ('slug', 'last_mod_time', 'creation_time')
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'parent_category', 'index')
|
||||
exclude = ('slug', 'last_mod_time', 'creation_time')
|
||||
|
||||
|
||||
class LinksAdmin(admin.ModelAdmin):
|
||||
exclude = ('last_mod_time', 'creation_time')
|
||||
|
||||
|
||||
class SideBarAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'content', 'is_enable', 'sequence')
|
||||
exclude = ('last_mod_time', 'creation_time')
|
||||
|
||||
|
||||
class BlogSettingsAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
name = 'blog'
|
||||
@ -0,0 +1,43 @@
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from djangoblog.utils import cache, get_blog_setting
|
||||
from .models import Category, Article
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def seo_processor(requests):
|
||||
key = 'seo_processor'
|
||||
value = cache.get(key)
|
||||
if value:
|
||||
return value
|
||||
else:
|
||||
logger.info('set processor cache.')
|
||||
setting = get_blog_setting()
|
||||
value = {
|
||||
'SITE_NAME': setting.site_name,
|
||||
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
|
||||
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
|
||||
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
|
||||
'SITE_DESCRIPTION': setting.site_description,
|
||||
'SITE_KEYWORDS': setting.site_keywords,
|
||||
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
|
||||
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
|
||||
'nav_category_list': Category.objects.all(),
|
||||
'nav_pages': Article.objects.filter(
|
||||
type='p',
|
||||
status='p'),
|
||||
'OPEN_SITE_COMMENT': setting.open_site_comment,
|
||||
'BEIAN_CODE': setting.beian_code,
|
||||
'ANALYTICS_CODE': setting.analytics_code,
|
||||
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
|
||||
"SHOW_GONGAN_CODE": setting.show_gongan_code,
|
||||
"CURRENT_YEAR": timezone.now().year,
|
||||
"GLOBAL_HEADER": setting.global_header,
|
||||
"GLOBAL_FOOTER": setting.global_footer,
|
||||
"COMMENT_NEED_REVIEW": setting.comment_need_review,
|
||||
}
|
||||
cache.set(key, value, 60 * 60 * 10)
|
||||
return value
|
||||
@ -0,0 +1,213 @@
|
||||
import time
|
||||
|
||||
import elasticsearch.client
|
||||
from django.conf import settings
|
||||
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
|
||||
from elasticsearch_dsl.connections import connections
|
||||
|
||||
from blog.models import Article
|
||||
|
||||
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
|
||||
|
||||
if ELASTICSEARCH_ENABLED:
|
||||
connections.create_connection(
|
||||
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
|
||||
from elasticsearch import Elasticsearch
|
||||
|
||||
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||
from elasticsearch.client import IngestClient
|
||||
|
||||
c = IngestClient(es)
|
||||
try:
|
||||
c.get_pipeline('geoip')
|
||||
except elasticsearch.exceptions.NotFoundError:
|
||||
c.put_pipeline('geoip', body='''{
|
||||
"description" : "Add geoip info",
|
||||
"processors" : [
|
||||
{
|
||||
"geoip" : {
|
||||
"field" : "ip"
|
||||
}
|
||||
}
|
||||
]
|
||||
}''')
|
||||
|
||||
|
||||
class GeoIp(InnerDoc):
|
||||
continent_name = Keyword()
|
||||
country_iso_code = Keyword()
|
||||
country_name = Keyword()
|
||||
location = GeoPoint()
|
||||
|
||||
|
||||
class UserAgentBrowser(InnerDoc):
|
||||
Family = Keyword()
|
||||
Version = Keyword()
|
||||
|
||||
|
||||
class UserAgentOS(UserAgentBrowser):
|
||||
pass
|
||||
|
||||
|
||||
class UserAgentDevice(InnerDoc):
|
||||
Family = Keyword()
|
||||
Brand = Keyword()
|
||||
Model = Keyword()
|
||||
|
||||
|
||||
class UserAgent(InnerDoc):
|
||||
browser = Object(UserAgentBrowser, required=False)
|
||||
os = Object(UserAgentOS, required=False)
|
||||
device = Object(UserAgentDevice, required=False)
|
||||
string = Text()
|
||||
is_bot = Boolean()
|
||||
|
||||
|
||||
class ElapsedTimeDocument(Document):
|
||||
url = Keyword()
|
||||
time_taken = Long()
|
||||
log_datetime = Date()
|
||||
ip = Keyword()
|
||||
geoip = Object(GeoIp, required=False)
|
||||
useragent = Object(UserAgent, required=False)
|
||||
|
||||
class Index:
|
||||
name = 'performance'
|
||||
settings = {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0
|
||||
}
|
||||
|
||||
class Meta:
|
||||
doc_type = 'ElapsedTime'
|
||||
|
||||
|
||||
class ElaspedTimeDocumentManager:
|
||||
@staticmethod
|
||||
def build_index():
|
||||
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():
|
||||
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):
|
||||
ElaspedTimeDocumentManager.build_index()
|
||||
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
|
||||
|
||||
doc = ElapsedTimeDocument(
|
||||
meta={
|
||||
'id': int(
|
||||
round(
|
||||
time.time() *
|
||||
1000))
|
||||
},
|
||||
url=url,
|
||||
time_taken=time_taken,
|
||||
log_datetime=log_datetime,
|
||||
useragent=ua, ip=ip)
|
||||
doc.save(pipeline="geoip")
|
||||
|
||||
|
||||
class ArticleDocument(Document):
|
||||
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
|
||||
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
|
||||
author = Object(properties={
|
||||
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||
'id': Integer()
|
||||
})
|
||||
category = Object(properties={
|
||||
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||
'id': Integer()
|
||||
})
|
||||
tags = Object(properties={
|
||||
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||
'id': Integer()
|
||||
})
|
||||
|
||||
pub_time = Date()
|
||||
status = Text()
|
||||
comment_status = Text()
|
||||
type = Text()
|
||||
views = Integer()
|
||||
article_order = Integer()
|
||||
|
||||
class Index:
|
||||
name = 'blog'
|
||||
settings = {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0
|
||||
}
|
||||
|
||||
class Meta:
|
||||
doc_type = 'Article'
|
||||
|
||||
|
||||
class ArticleDocumentManager():
|
||||
|
||||
def __init__(self):
|
||||
self.create_index()
|
||||
|
||||
def create_index(self):
|
||||
ArticleDocument.init()
|
||||
|
||||
def delete_index(self):
|
||||
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):
|
||||
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):
|
||||
ArticleDocument.init()
|
||||
articles = articles if articles else Article.objects.all()
|
||||
docs = self.convert_to_doc(articles)
|
||||
for doc in docs:
|
||||
doc.save()
|
||||
|
||||
def update_docs(self, docs):
|
||||
for doc in docs:
|
||||
doc.save()
|
||||
@ -0,0 +1,19 @@
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
from haystack.forms import SearchForm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlogSearchForm(SearchForm):
|
||||
querydata = forms.CharField(required=True)
|
||||
|
||||
def search(self):
|
||||
datas = super(BlogSearchForm, self).search()
|
||||
if not self.is_valid():
|
||||
return self.no_query_found()
|
||||
|
||||
if self.cleaned_data['querydata']:
|
||||
logger.info(self.cleaned_data['querydata'])
|
||||
return datas
|
||||
@ -0,0 +1,18 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
|
||||
ELASTICSEARCH_ENABLED
|
||||
|
||||
|
||||
# TODO 参数化
|
||||
class Command(BaseCommand):
|
||||
help = 'build search index'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if ELASTICSEARCH_ENABLED:
|
||||
ElaspedTimeDocumentManager.build_index()
|
||||
manager = ElapsedTimeDocument()
|
||||
manager.init()
|
||||
manager = ArticleDocumentManager()
|
||||
manager.delete_index()
|
||||
manager.rebuild()
|
||||
@ -0,0 +1,13 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from blog.models import Tag, Category
|
||||
|
||||
|
||||
# TODO 参数化
|
||||
class Command(BaseCommand):
|
||||
help = 'build search words'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
datas = set([t.name for t in Tag.objects.all()] +
|
||||
[t.name for t in Category.objects.all()])
|
||||
print('\n'.join(datas))
|
||||
@ -0,0 +1,11 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from djangoblog.utils import cache
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'clear the whole cache'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
cache.clear()
|
||||
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
|
||||
@ -0,0 +1,40 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from blog.models import Article, Tag, Category
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'create test datas'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
user = get_user_model().objects.get_or_create(
|
||||
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
|
||||
|
||||
pcategory = Category.objects.get_or_create(
|
||||
name='我是父类目', parent_category=None)[0]
|
||||
|
||||
category = Category.objects.get_or_create(
|
||||
name='子类目', parent_category=pcategory)[0]
|
||||
|
||||
category.save()
|
||||
basetag = Tag()
|
||||
basetag.name = "标签"
|
||||
basetag.save()
|
||||
for i in range(1, 20):
|
||||
article = Article.objects.get_or_create(
|
||||
category=category,
|
||||
title='nice title ' + str(i),
|
||||
body='nice content ' + str(i),
|
||||
author=user)[0]
|
||||
tag = Tag()
|
||||
tag.name = "标签" + str(i)
|
||||
tag.save()
|
||||
article.tags.add(tag)
|
||||
article.tags.add(basetag)
|
||||
article.save()
|
||||
|
||||
from djangoblog.utils import cache
|
||||
cache.clear()
|
||||
self.stdout.write(self.style.SUCCESS('created test datas \n'))
|
||||
@ -0,0 +1,50 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from djangoblog.spider_notify import SpiderNotify
|
||||
from djangoblog.utils import get_current_site
|
||||
from blog.models import Article, Tag, Category
|
||||
|
||||
site = get_current_site().domain
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'notify baidu url'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
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')
|
||||
|
||||
def get_full_url(self, path):
|
||||
url = "https://{site}{path}".format(site=site, path=path)
|
||||
return url
|
||||
|
||||
def handle(self, *args, **options):
|
||||
type = options['data_type']
|
||||
self.stdout.write('start get %s' % type)
|
||||
|
||||
urls = []
|
||||
if type == 'article' or type == 'all':
|
||||
for article in Article.objects.filter(status='p'):
|
||||
urls.append(article.get_full_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))
|
||||
if type == 'category' or type == 'all':
|
||||
for category in Category.objects.all():
|
||||
url = category.get_absolute_url()
|
||||
urls.append(self.get_full_url(url))
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
'start notify %d urls' %
|
||||
len(urls)))
|
||||
SpiderNotify.baidu_notify(urls)
|
||||
self.stdout.write(self.style.SUCCESS('finish notify'))
|
||||
@ -0,0 +1,47 @@
|
||||
import requests
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.templatetags.static import static
|
||||
|
||||
from djangoblog.utils import save_user_avatar
|
||||
from oauth.models import OAuthUser
|
||||
from oauth.oauthmanager import get_manager_by_type
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'sync user avatar'
|
||||
|
||||
def test_picture(self, url):
|
||||
try:
|
||||
if requests.get(url, timeout=2).status_code == 200:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
def handle(self, *args, **options):
|
||||
static_url = static("../")
|
||||
users = OAuthUser.objects.all()
|
||||
self.stdout.write(f'开始同步{len(users)}个用户头像')
|
||||
for u in users:
|
||||
self.stdout.write(f'开始同步:{u.nickname}')
|
||||
url = u.picture
|
||||
if url:
|
||||
if url.startswith(static_url):
|
||||
if self.test_picture(url):
|
||||
continue
|
||||
else:
|
||||
if u.metadata:
|
||||
manage = get_manager_by_type(u.type)
|
||||
url = manage.get_picture(u.metadata)
|
||||
url = save_user_avatar(url)
|
||||
else:
|
||||
url = static('blog/img/avatar.png')
|
||||
else:
|
||||
url = save_user_avatar(url)
|
||||
else:
|
||||
url = static('blog/img/avatar.png')
|
||||
if url:
|
||||
self.stdout.write(
|
||||
f'结束同步:{u.nickname}.url:{url}')
|
||||
u.picture = url
|
||||
u.save()
|
||||
self.stdout.write('结束同步')
|
||||
@ -0,0 +1,42 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from ipware import get_client_ip
|
||||
from user_agents import parse
|
||||
|
||||
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OnlineMiddleware(object):
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, request):
|
||||
''' page render time '''
|
||||
start_time = time.time()
|
||||
response = self.get_response(request)
|
||||
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
ip, _ = get_client_ip(request)
|
||||
user_agent = parse(http_user_agent)
|
||||
if not response.streaming:
|
||||
try:
|
||||
cast_time = time.time() - start_time
|
||||
if ELASTICSEARCH_ENABLED:
|
||||
time_taken = round((cast_time) * 1000, 2)
|
||||
url = request.path
|
||||
from django.utils import timezone
|
||||
ElaspedTimeDocumentManager.create(
|
||||
url=url,
|
||||
time_taken=time_taken,
|
||||
log_datetime=timezone.now(),
|
||||
useragent=user_agent,
|
||||
ip=ip)
|
||||
response.content = response.content.replace(
|
||||
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
|
||||
except Exception as e:
|
||||
logger.error("Error OnlineMiddleware: %s" % e)
|
||||
|
||||
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,23 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-29 06:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='global_footer',
|
||||
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='global_header',
|
||||
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-09 07:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('blog', '0002_blogsettings_global_footer_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='comment_need_review',
|
||||
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-09 07:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('blog', '0003_blogsettings_comment_need_review'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='analyticscode',
|
||||
new_name='analytics_code',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='beiancode',
|
||||
new_name='beian_code',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='sitename',
|
||||
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,17 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-26 02:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blogsettings',
|
||||
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,376 @@
|
||||
import logging
|
||||
import re
|
||||
from abc import abstractmethod
|
||||
|
||||
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 _
|
||||
from mdeditor.fields import MDTextField
|
||||
from uuslug import slugify
|
||||
|
||||
from djangoblog.utils import cache_decorator, cache
|
||||
from djangoblog.utils import get_current_site
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LinkShowType(models.TextChoices):
|
||||
I = ('i', _('index'))
|
||||
L = ('l', _('list'))
|
||||
P = ('p', _('post'))
|
||||
A = ('a', _('all'))
|
||||
S = ('s', _('slide'))
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||
last_modify_time = models.DateTimeField(_('modify time'), default=now)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_update_views = isinstance(
|
||||
self,
|
||||
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
|
||||
if is_update_views:
|
||||
Article.objects.filter(pk=self.pk).update(views=self.views)
|
||||
else:
|
||||
if 'slug' in self.__dict__:
|
||||
slug = getattr(
|
||||
self, 'title') if 'title' in self.__dict__ else getattr(
|
||||
self, 'name')
|
||||
setattr(self, 'slug', slugify(slug))
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
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:
|
||||
abstract = True
|
||||
|
||||
@abstractmethod
|
||||
def get_absolute_url(self):
|
||||
pass
|
||||
|
||||
|
||||
class Article(BaseModel):
|
||||
"""文章"""
|
||||
STATUS_CHOICES = (
|
||||
('d', _('Draft')),
|
||||
('p', _('Published')),
|
||||
)
|
||||
COMMENT_STATUS = (
|
||||
('o', _('Open')),
|
||||
('c', _('Close')),
|
||||
)
|
||||
TYPE = (
|
||||
('a', _('Article')),
|
||||
('p', _('Page')),
|
||||
)
|
||||
title = models.CharField(_('title'), max_length=200, unique=True)
|
||||
body = MDTextField(_('body'))
|
||||
pub_time = models.DateTimeField(
|
||||
_('publish time'), blank=False, null=False, default=now)
|
||||
status = models.CharField(
|
||||
_('status'),
|
||||
max_length=1,
|
||||
choices=STATUS_CHOICES,
|
||||
default='p')
|
||||
comment_status = models.CharField(
|
||||
_('comment status'),
|
||||
max_length=1,
|
||||
choices=COMMENT_STATUS,
|
||||
default='o')
|
||||
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
|
||||
views = models.PositiveIntegerField(_('views'), default=0)
|
||||
author = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=_('author'),
|
||||
blank=False,
|
||||
null=False,
|
||||
on_delete=models.CASCADE)
|
||||
article_order = models.IntegerField(
|
||||
_('order'), blank=False, null=False, default=0)
|
||||
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
|
||||
category = models.ForeignKey(
|
||||
'Category',
|
||||
verbose_name=_('category'),
|
||||
on_delete=models.CASCADE,
|
||||
blank=False,
|
||||
null=False)
|
||||
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
|
||||
|
||||
def body_to_string(self):
|
||||
return self.body
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
ordering = ['-article_order', '-pub_time']
|
||||
verbose_name = _('article')
|
||||
verbose_name_plural = verbose_name
|
||||
get_latest_by = 'id'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('blog:detailbyid', kwargs={
|
||||
'article_id': self.id,
|
||||
'year': self.creation_time.year,
|
||||
'month': self.creation_time.month,
|
||||
'day': self.creation_time.day
|
||||
})
|
||||
|
||||
@cache_decorator(60 * 60 * 10)
|
||||
def get_category_tree(self):
|
||||
tree = self.category.get_category_tree()
|
||||
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
|
||||
|
||||
return names
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def viewed(self):
|
||||
self.views += 1
|
||||
self.save(update_fields=['views'])
|
||||
|
||||
def comment_list(self):
|
||||
cache_key = 'article_comments_{id}'.format(id=self.id)
|
||||
value = cache.get(cache_key)
|
||||
if value:
|
||||
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
|
||||
|
||||
def get_admin_url(self):
|
||||
info = (self._meta.app_label, self._meta.model_name)
|
||||
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
|
||||
|
||||
@cache_decorator(expiration=60 * 100)
|
||||
def next_article(self):
|
||||
# 下一篇
|
||||
return Article.objects.filter(
|
||||
id__gt=self.id, status='p').order_by('id').first()
|
||||
|
||||
@cache_decorator(expiration=60 * 100)
|
||||
def prev_article(self):
|
||||
# 前一篇
|
||||
return Article.objects.filter(id__lt=self.id, status='p').first()
|
||||
|
||||
def get_first_image_url(self):
|
||||
"""
|
||||
Get the first image url from article.body.
|
||||
:return:
|
||||
"""
|
||||
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
class Category(BaseModel):
|
||||
"""文章分类"""
|
||||
name = models.CharField(_('category name'), max_length=30, unique=True)
|
||||
parent_category = models.ForeignKey(
|
||||
'self',
|
||||
verbose_name=_('parent category'),
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE)
|
||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
||||
index = models.IntegerField(default=0, verbose_name=_('index'))
|
||||
|
||||
class Meta:
|
||||
ordering = ['-index']
|
||||
verbose_name = _('category')
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
'blog:category_detail', kwargs={
|
||||
'category_name': self.slug})
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@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
|
||||
|
||||
@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
|
||||
|
||||
|
||||
class Tag(BaseModel):
|
||||
"""文章标签"""
|
||||
name = models.CharField(_('tag name'), max_length=30, unique=True)
|
||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
|
||||
|
||||
@cache_decorator(60 * 60 * 10)
|
||||
def get_article_count(self):
|
||||
return Article.objects.filter(tags__name=self.name).distinct().count()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = _('tag')
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
|
||||
class Links(models.Model):
|
||||
"""友情链接"""
|
||||
|
||||
name = models.CharField(_('link name'), max_length=30, unique=True)
|
||||
link = models.URLField(_('link'))
|
||||
sequence = models.IntegerField(_('order'), unique=True)
|
||||
is_enable = models.BooleanField(
|
||||
_('is show'), default=True, blank=False, null=False)
|
||||
show_type = models.CharField(
|
||||
_('show type'),
|
||||
max_length=1,
|
||||
choices=LinkShowType.choices,
|
||||
default=LinkShowType.I)
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
||||
|
||||
class Meta:
|
||||
ordering = ['sequence']
|
||||
verbose_name = _('link')
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class SideBar(models.Model):
|
||||
"""侧边栏,可以展示一些html内容"""
|
||||
name = models.CharField(_('title'), max_length=100)
|
||||
content = models.TextField(_('content'))
|
||||
sequence = models.IntegerField(_('order'), unique=True)
|
||||
is_enable = models.BooleanField(_('is enable'), default=True)
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
||||
|
||||
class Meta:
|
||||
ordering = ['sequence']
|
||||
verbose_name = _('sidebar')
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class BlogSettings(models.Model):
|
||||
"""blog的配置"""
|
||||
site_name = models.CharField(
|
||||
_('site name'),
|
||||
max_length=200,
|
||||
null=False,
|
||||
blank=False,
|
||||
default='')
|
||||
site_description = models.TextField(
|
||||
_('site description'),
|
||||
max_length=1000,
|
||||
null=False,
|
||||
blank=False,
|
||||
default='')
|
||||
site_seo_description = models.TextField(
|
||||
_('site seo description'), max_length=1000, null=False, blank=False, default='')
|
||||
site_keywords = models.TextField(
|
||||
_('site keywords'),
|
||||
max_length=1000,
|
||||
null=False,
|
||||
blank=False,
|
||||
default='')
|
||||
article_sub_length = models.IntegerField(_('article sub length'), default=300)
|
||||
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
|
||||
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
|
||||
article_comment_count = models.IntegerField(_('article comment count'), default=5)
|
||||
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
|
||||
google_adsense_codes = models.TextField(
|
||||
_('adsense code'), max_length=2000, null=True, blank=True, default='')
|
||||
open_site_comment = models.BooleanField(_('open site comment'), default=True)
|
||||
global_header = models.TextField("公共头部", null=True, blank=True, default='')
|
||||
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
|
||||
beian_code = models.CharField(
|
||||
'备案号',
|
||||
max_length=2000,
|
||||
null=True,
|
||||
blank=True,
|
||||
default='')
|
||||
analytics_code = models.TextField(
|
||||
"网站统计代码",
|
||||
max_length=1000,
|
||||
null=False,
|
||||
blank=False,
|
||||
default='')
|
||||
show_gongan_code = models.BooleanField(
|
||||
'是否显示公安备案号', default=False, null=False)
|
||||
gongan_beiancode = models.TextField(
|
||||
'公安备案号',
|
||||
max_length=2000,
|
||||
null=True,
|
||||
blank=True,
|
||||
default='')
|
||||
comment_need_review = models.BooleanField(
|
||||
'评论是否需要审核', default=False, null=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Website configuration')
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.site_name
|
||||
|
||||
def clean(self):
|
||||
if BlogSettings.objects.exclude(id=self.id).count():
|
||||
raise ValidationError(_('There can only be one configuration'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
from djangoblog.utils import cache
|
||||
cache.clear()
|
||||
@ -0,0 +1,13 @@
|
||||
from haystack import indexes
|
||||
|
||||
from blog.models import Article
|
||||
|
||||
|
||||
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
|
||||
text = indexes.CharField(document=True, use_template=True)
|
||||
|
||||
def get_model(self):
|
||||
return Article
|
||||
|
||||
def index_queryset(self, using=None):
|
||||
return self.get_model().objects.filter(status='p')
|
||||
@ -0,0 +1,9 @@
|
||||
.button {
|
||||
border: none;
|
||||
padding: 4px 80px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
margin: 4px 2px;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
let wait = 60;
|
||||
|
||||
function time(o) {
|
||||
if (wait == 0) {
|
||||
o.removeAttribute("disabled");
|
||||
o.value = "获取验证码";
|
||||
wait = 60
|
||||
return false
|
||||
} else {
|
||||
o.setAttribute("disabled", true);
|
||||
o.value = "重新发送(" + wait + ")";
|
||||
wait--;
|
||||
setTimeout(function () {
|
||||
time(o)
|
||||
},
|
||||
1000)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("btn").onclick = function () {
|
||||
let id_email = $("#id_email")
|
||||
let token = $("*[name='csrfmiddlewaretoken']").val()
|
||||
let ts = this
|
||||
let myErr = $("#myErr")
|
||||
$.ajax(
|
||||
{
|
||||
url: "/forget_password_code/",
|
||||
type: "POST",
|
||||
data: {
|
||||
"email": id_email.val(),
|
||||
"csrfmiddlewaretoken": token
|
||||
},
|
||||
success: function (result) {
|
||||
if (result != "ok") {
|
||||
myErr.remove()
|
||||
id_email.after("<ul className='errorlist' id='myErr'><li>" + result + "</li></ul>")
|
||||
return
|
||||
}
|
||||
myErr.remove()
|
||||
time(ts)
|
||||
},
|
||||
error: function (e) {
|
||||
alert("发送失败,请重试")
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 221 B |
@ -0,0 +1,51 @@
|
||||
// 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.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function emulatedIEMajorVersion() {
|
||||
var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
|
||||
if (groups === null) {
|
||||
return null
|
||||
}
|
||||
var ieVersionNum = parseInt(groups[1], 10)
|
||||
var ieMajorVersion = Math.floor(ieVersionNum)
|
||||
return ieMajorVersion
|
||||
}
|
||||
|
||||
function actualNonEmulatedIEMajorVersion() {
|
||||
// Detects the actual version of IE in use, even if it's in an older-IE emulation mode.
|
||||
// IE JavaScript conditional compilation docs: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx
|
||||
// @cc_on docs: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx
|
||||
var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line
|
||||
if (jscriptVersion === undefined) {
|
||||
return 11 // IE11+ not in emulation mode
|
||||
}
|
||||
if (jscriptVersion < 9) {
|
||||
return 8 // IE8 (or lower; haven't tested on IE<8)
|
||||
}
|
||||
return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode
|
||||
}
|
||||
|
||||
var ua = window.navigator.userAgent
|
||||
if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) {
|
||||
return // Opera, which might pretend to be IE
|
||||
}
|
||||
var emulated = emulatedIEMajorVersion()
|
||||
if (emulated === null) {
|
||||
return // Not IE
|
||||
}
|
||||
var nonEmulated = actualNonEmulatedIEMajorVersion()
|
||||
|
||||
if (emulated !== nonEmulated) {
|
||||
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,23 @@
|
||||
/*!
|
||||
* 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
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
|
||||
var msViewportStyle = document.createElement('style')
|
||||
msViewportStyle.appendChild(
|
||||
document.createTextNode(
|
||||
'@-ms-viewport{width:auto!important}'
|
||||
)
|
||||
)
|
||||
document.querySelector('head').appendChild(msViewportStyle)
|
||||
}
|
||||
|
||||
})();
|
||||
@ -0,0 +1,273 @@
|
||||
/*
|
||||
Styles for older IE versions (previous to IE9).
|
||||
*/
|
||||
|
||||
body {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
body.custom-background-empty {
|
||||
background-color: #fff;
|
||||
}
|
||||
body.custom-background-empty .site,
|
||||
body.custom-background-white .site {
|
||||
box-shadow: none;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.assistive-text,
|
||||
.site .screen-reader-text {
|
||||
clip: rect(1px 1px 1px 1px);
|
||||
}
|
||||
.full-width .site-content {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
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 */
|
||||
}
|
||||
.author-avatar {
|
||||
float: left;
|
||||
margin-top: 8px;
|
||||
margin-top: 0.571428571rem;
|
||||
}
|
||||
.author-description {
|
||||
float: right;
|
||||
width: 80%;
|
||||
}
|
||||
.site {
|
||||
box-shadow: 0 2px 6px rgba(100, 100, 100, 0.3);
|
||||
margin: 48px auto;
|
||||
max-width: 960px;
|
||||
overflow: hidden;
|
||||
padding: 0 40px;
|
||||
}
|
||||
.site-content {
|
||||
float: left;
|
||||
width: 65.104166667%;
|
||||
}
|
||||
body.template-front-page .site-content,
|
||||
body.attachment .site-content,
|
||||
body.full-width .site-content {
|
||||
width: 100%;
|
||||
}
|
||||
.widget-area {
|
||||
float: right;
|
||||
width: 26.041666667%;
|
||||
}
|
||||
.site-header h1,
|
||||
.site-header h2 {
|
||||
text-align: left;
|
||||
}
|
||||
.site-header h1 {
|
||||
font-size: 26px;
|
||||
line-height: 1.846153846;
|
||||
}
|
||||
.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%;
|
||||
}
|
||||
.main-navigation ul {
|
||||
margin: 0;
|
||||
text-indent: 0;
|
||||
}
|
||||
.main-navigation li a,
|
||||
.main-navigation li {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ie7 .main-navigation li a,
|
||||
.ie7 .main-navigation li {
|
||||
display: inline;
|
||||
}
|
||||
.main-navigation li a {
|
||||
border-bottom: 0;
|
||||
color: #6a6a6a;
|
||||
line-height: 3.692307692;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.main-navigation li a:hover {
|
||||
color: #000;
|
||||
}
|
||||
.main-navigation li {
|
||||
margin: 0 40px 0 0;
|
||||
position: relative;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.ie7 .main-navigation li ul {
|
||||
clip: inherit;
|
||||
display: none;
|
||||
left: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
.main-navigation li ul ul,
|
||||
.ie7 .main-navigation li ul ul {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.ie7 .main-navigation ul li:hover > ul,
|
||||
.ie7 .main-navigation ul li:focus > ul {
|
||||
display: block;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.main-navigation li ul li a:hover {
|
||||
background: #e3e3e3;
|
||||
color: #444;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.main-navigation .menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
.entry-header .entry-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
#respond form input[type="text"] {
|
||||
width: 46.333333333%;
|
||||
}
|
||||
#respond form textarea.blog-textarea {
|
||||
width: 79.666666667%;
|
||||
}
|
||||
.template-front-page .site-content,
|
||||
.template-front-page article {
|
||||
overflow: hidden;
|
||||
}
|
||||
.template-front-page.has-post-thumbnail article {
|
||||
float: left;
|
||||
width: 47.916666667%;
|
||||
}
|
||||
.entry-page-image {
|
||||
float: right;
|
||||
margin-bottom: 0;
|
||||
width: 47.916666667%;
|
||||
}
|
||||
/* IE Front Page Template Widget fix */
|
||||
.template-front-page .widget-area {
|
||||
clear: both;
|
||||
}
|
||||
.template-front-page .widget {
|
||||
width: 100% !important;
|
||||
border: none;
|
||||
}
|
||||
.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%;
|
||||
}
|
||||
.template-front-page .second.front-widgets,
|
||||
.template-front-page .widget-area .widget:nth-child(odd) {
|
||||
clear: right;
|
||||
}
|
||||
.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%;
|
||||
}
|
||||
.template-front-page.two-sidebars .widget,
|
||||
.template-front-page.two-sidebars .widget:nth-child(even) {
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
/* add input font for <IE9 Password Box to make the bullets show up */
|
||||
input[type="password"] {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* RTL overrides for IE7 and IE8
|
||||
-------------------------------------------------------------- */
|
||||
.rtl .site-header h1,
|
||||
.rtl .site-header h2 {
|
||||
text-align: right;
|
||||
}
|
||||
.rtl .widget-area,
|
||||
.rtl .author-description {
|
||||
float: left;
|
||||
}
|
||||
.rtl .author-avatar,
|
||||
.rtl .site-content {
|
||||
float: right;
|
||||
}
|
||||
.rtl .main-navigation ul.nav-menu,
|
||||
.rtl .main-navigation div.nav-menu > ul {
|
||||
text-align: right;
|
||||
}
|
||||
.rtl .main-navigation ul li ul li,
|
||||
.rtl .main-navigation ul li ul li ul li {
|
||||
margin-left: 40px;
|
||||
margin-right: auto;
|
||||
}
|
||||
.rtl .main-navigation li ul ul {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.ie7 .rtl .main-navigation li ul ul {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.ie7 .rtl .main-navigation ul li {
|
||||
z-index: 99;
|
||||
}
|
||||
.ie7 .rtl .main-navigation li ul {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.ie7 .rtl .main-navigation li {
|
||||
margin-right: auto;
|
||||
margin-left: 40px;
|
||||
}
|
||||
.ie7 .rtl .main-navigation li ul ul ul {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
/* Make clicks pass-through */
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: red;
|
||||
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
|
||||
opacity: 1.0;
|
||||
|
||||
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
||||
|
||||
/* Remove these to get rid of the spinner */
|
||||
#nprogress .spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
|
||||
border: solid 2px transparent;
|
||||
border-top-color: red;
|
||||
border-left-color: red;
|
||||
border-radius: 50%;
|
||||
|
||||
-webkit-animation: nprogress-spinner 400ms linear infinite;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent #nprogress .spinner,
|
||||
.nprogress-custom-parent #nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@-webkit-keyframes nprogress-spinner {
|
||||
0% { -webkit-transform: rotate(0deg); }
|
||||
100% { -webkit-transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes nprogress-spinner {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@ -0,0 +1,305 @@
|
||||
|
||||
.icon-sn-google {
|
||||
background-position: 0 -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-google {
|
||||
background-color: #4285f4;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.fa-sn-google {
|
||||
color: #4285f4;
|
||||
}
|
||||
|
||||
.icon-sn-github {
|
||||
background-position: -28px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-github {
|
||||
background-color: #333;
|
||||
background-position: -28px 0;
|
||||
}
|
||||
|
||||
.fa-sn-github {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.icon-sn-weibo {
|
||||
background-position: -56px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-weibo {
|
||||
background-color: #e90d24;
|
||||
background-position: -56px 0;
|
||||
}
|
||||
|
||||
.fa-sn-weibo {
|
||||
color: #e90d24;
|
||||
}
|
||||
|
||||
.icon-sn-qq {
|
||||
background-position: -84px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-qq {
|
||||
background-color: #0098e6;
|
||||
background-position: -84px 0;
|
||||
}
|
||||
|
||||
.fa-sn-qq {
|
||||
color: #0098e6;
|
||||
}
|
||||
|
||||
.icon-sn-twitter {
|
||||
background-position: -112px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-twitter {
|
||||
background-color: #50abf1;
|
||||
background-position: -112px 0;
|
||||
}
|
||||
|
||||
.fa-sn-twitter {
|
||||
color: #50abf1;
|
||||
}
|
||||
|
||||
.icon-sn-facebook {
|
||||
background-position: -140px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-facebook {
|
||||
background-color: #4862a3;
|
||||
background-position: -140px 0;
|
||||
}
|
||||
|
||||
.fa-sn-facebook {
|
||||
color: #4862a3;
|
||||
}
|
||||
|
||||
.icon-sn-renren {
|
||||
background-position: -168px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-renren {
|
||||
background-color: #197bc8;
|
||||
background-position: -168px 0;
|
||||
}
|
||||
|
||||
.fa-sn-renren {
|
||||
color: #197bc8;
|
||||
}
|
||||
|
||||
.icon-sn-tqq {
|
||||
background-position: -196px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-tqq {
|
||||
background-color: #1f9ed2;
|
||||
background-position: -196px 0;
|
||||
}
|
||||
|
||||
.fa-sn-tqq {
|
||||
color: #1f9ed2;
|
||||
}
|
||||
|
||||
.icon-sn-douban {
|
||||
background-position: -224px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-douban {
|
||||
background-color: #279738;
|
||||
background-position: -224px 0;
|
||||
}
|
||||
|
||||
.fa-sn-douban {
|
||||
color: #279738;
|
||||
}
|
||||
|
||||
.icon-sn-weixin {
|
||||
background-position: -252px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-weixin {
|
||||
background-color: #00b500;
|
||||
background-position: -252px 0;
|
||||
}
|
||||
|
||||
.fa-sn-weixin {
|
||||
color: #00b500;
|
||||
}
|
||||
|
||||
.icon-sn-dotted {
|
||||
background-position: -280px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-dotted {
|
||||
background-color: #eee;
|
||||
background-position: -280px 0;
|
||||
}
|
||||
|
||||
.fa-sn-dotted {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.icon-sn-site {
|
||||
background-position: -308px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-site {
|
||||
background-color: #00b500;
|
||||
background-position: -308px 0;
|
||||
}
|
||||
|
||||
.fa-sn-site {
|
||||
color: #00b500;
|
||||
}
|
||||
|
||||
.icon-sn-linkedin {
|
||||
background-position: -336px -28px;
|
||||
}
|
||||
|
||||
.icon-sn-bg-linkedin {
|
||||
background-color: #0077b9;
|
||||
background-position: -336px 0;
|
||||
}
|
||||
|
||||
.fa-sn-linkedin {
|
||||
color: #0077b9;
|
||||
}
|
||||
|
||||
[class*=icon-sn-] {
|
||||
display: inline-block;
|
||||
background-image: url('../img/icon-sn.svg');
|
||||
background-repeat: no-repeat;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
vertical-align: middle;
|
||||
background-size: auto 56px;
|
||||
}
|
||||
|
||||
[class*=icon-sn-]:hover {
|
||||
opacity: .8;
|
||||
filter: alpha(opacity=80);
|
||||
}
|
||||
|
||||
.btn-sn-google {
|
||||
background: #4285f4;
|
||||
}
|
||||
|
||||
.btn-sn-google:active, .btn-sn-google:focus, .btn-sn-google:hover {
|
||||
background: #2a75f3;
|
||||
}
|
||||
|
||||
.btn-sn-github {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.btn-sn-github:active, .btn-sn-github:focus, .btn-sn-github:hover {
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
.btn-sn-weibo {
|
||||
background: #e90d24;
|
||||
}
|
||||
|
||||
.btn-sn-weibo:active, .btn-sn-weibo:focus, .btn-sn-weibo:hover {
|
||||
background: #d10c20;
|
||||
}
|
||||
|
||||
.btn-sn-qq {
|
||||
background: #0098e6;
|
||||
}
|
||||
|
||||
.btn-sn-qq:active, .btn-sn-qq:focus, .btn-sn-qq:hover {
|
||||
background: #0087cd;
|
||||
}
|
||||
|
||||
.btn-sn-twitter {
|
||||
background: #50abf1;
|
||||
}
|
||||
|
||||
.btn-sn-twitter:active, .btn-sn-twitter:focus, .btn-sn-twitter:hover {
|
||||
background: #38a0ef;
|
||||
}
|
||||
|
||||
.btn-sn-facebook {
|
||||
background: #4862a3;
|
||||
}
|
||||
|
||||
.btn-sn-facebook:active, .btn-sn-facebook:focus, .btn-sn-facebook:hover {
|
||||
background: #405791;
|
||||
}
|
||||
|
||||
.btn-sn-renren {
|
||||
background: #197bc8;
|
||||
}
|
||||
|
||||
.btn-sn-renren:active, .btn-sn-renren:focus, .btn-sn-renren:hover {
|
||||
background: #166db1;
|
||||
}
|
||||
|
||||
.btn-sn-tqq {
|
||||
background: #1f9ed2;
|
||||
}
|
||||
|
||||
.btn-sn-tqq:active, .btn-sn-tqq:focus, .btn-sn-tqq:hover {
|
||||
background: #1c8dbc;
|
||||
}
|
||||
|
||||
.btn-sn-douban {
|
||||
background: #279738;
|
||||
}
|
||||
|
||||
.btn-sn-douban:active, .btn-sn-douban:focus, .btn-sn-douban:hover {
|
||||
background: #228330;
|
||||
}
|
||||
|
||||
.btn-sn-weixin {
|
||||
background: #00b500;
|
||||
}
|
||||
|
||||
.btn-sn-weixin:active, .btn-sn-weixin:focus, .btn-sn-weixin:hover {
|
||||
background: #009c00;
|
||||
}
|
||||
|
||||
.btn-sn-dotted {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.btn-sn-dotted:active, .btn-sn-dotted:focus, .btn-sn-dotted:hover {
|
||||
background: #e1e1e1;
|
||||
}
|
||||
|
||||
.btn-sn-site {
|
||||
background: #00b500;
|
||||
}
|
||||
|
||||
.btn-sn-site:active, .btn-sn-site:focus, .btn-sn-site:hover {
|
||||
background: #009c00;
|
||||
}
|
||||
|
||||
.btn-sn-linkedin {
|
||||
background: #0077b9;
|
||||
}
|
||||
|
||||
.btn-sn-linkedin:active, .btn-sn-linkedin:focus, .btn-sn-linkedin:hover {
|
||||
background: #0067a0;
|
||||
}
|
||||
|
||||
[class*=btn-sn-], [class*=btn-sn-]:active, [class*=btn-sn-]:focus, [class*=btn-sn-]:hover {
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-sn-more {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-sn-more, .btn-sn-more:active, .btn-sn-more:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[class*=btn-sn-] [class*=icon-sn-] {
|
||||
background-color: transparent;
|
||||
}
|
||||