├── blog ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0007_auto_20160519_0926.py │ ├── 0005_auto_20160513_1128.py │ ├── 0008_auto_20160613_1410.py │ ├── 0006_link.py │ ├── 0003_auto_20160505_1634.py │ ├── 0004_auto_20160509_1603.py │ ├── 0002_comment.py │ ├── 0010_add_slug_to_post.py │ ├── 0001_initial.py │ └── 0009_auto_20160711_1512.py ├── templatetags │ ├── __init__.py │ └── blog_extras.py ├── templates │ ├── dashboard_base.html │ ├── 404.html │ ├── 500.html │ ├── registration │ │ ├── logged_out.html │ │ └── login.html │ ├── flatpages │ │ └── default.html │ ├── profile.html │ ├── change_profile.html │ ├── _sidebar.html │ ├── _navbar.html │ ├── base.html │ ├── index.html │ ├── edit_post.html │ └── post.html ├── tests.py ├── apps.py ├── admin.py ├── feeds.py ├── sitemaps.py ├── tools.py ├── urls.py ├── static │ └── blog │ │ ├── dist │ │ ├── css │ │ │ └── style.min.css │ │ └── js │ │ │ └── edit_post.min.js │ │ ├── css │ │ └── style.less │ │ └── js │ │ └── edit_post.js ├── models.py ├── forms.py └── views.py ├── django_blog ├── __init__.py ├── wsgi.py ├── urls.py └── settings.py ├── requirements ├── dev.txt └── common.txt ├── .editorconfig ├── manage.py ├── deployment ├── uwsgi_params ├── django_blog_uwsgi.ini └── django_blog_nginx.conf ├── package.json ├── README.md ├── gulpfile.js ├── LICENSE └── .gitignore /blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/templates/dashboard_base.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | name = 'blog' 6 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | django-debug-toolbar==1.5 2 | sqlparse==0.2.0 3 | fake-factory==0.5.10 4 | python-dateutil==2.5.3 5 | -------------------------------------------------------------------------------- /blog/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}404{% endblock %} 3 | {% block content %} 4 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /blog/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}500{% endblock %} 3 | {% block content %} 4 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /blog/templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}登出页{% endblock %} 4 | {% block content %} 5 |

登出网站

6 | 您已经登出,感谢您的使用。您可以重新登录。 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /blog/templates/flatpages/default.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load blog_extras %} 3 | {% block title %}{{ flatpage.title }}{% endblock %} 4 | {% block navbar %}{% show_navbar 'about' user %}{% endblock %} 5 | {% block content %} 6 | {{ flatpage.content }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /requirements/common.txt: -------------------------------------------------------------------------------- 1 | Django==1.10 2 | pytz==2016.6.1 3 | django-widget-tweaks==1.4.1 4 | bleach==1.4.3 5 | six==1.10.0 6 | html5lib==0.9999999 7 | -e git+https://github.com/liuenyan/bleach-whitelist.git@master#egg=bleach-whitelist 8 | Markdown==2.6.6 9 | pymdown-extensions==1.1 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.py] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_blog.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /blog/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load blog_extras %} 3 | {% block content %} 4 | 7 | 8 |

9 |

名字: {{ user.first_name }}

10 |

姓氏: {{ user.last_name }}

11 |

电子邮件: {{ user.email }}

12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from blog.models import Post, Comment, Tag, Category, Link 3 | 4 | # Register your models here. 5 | 6 | class PostAdmin(admin.ModelAdmin): 7 | readonly_fields = ('body_html',) 8 | 9 | admin.site.register(Post, PostAdmin) 10 | admin.site.register(Comment) 11 | admin.site.register(Tag) 12 | admin.site.register(Category) 13 | admin.site.register(Link) 14 | -------------------------------------------------------------------------------- /django_blog/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_site project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_blog.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /blog/migrations/0007_auto_20160519_0926.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-05-19 01:26 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0006_link'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='comment', 17 | name='url', 18 | field=models.URLField(blank=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /blog/migrations/0005_auto_20160513_1128.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-05-13 03:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0004_auto_20160509_1603'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='post', 17 | name='timestamp', 18 | field=models.DateTimeField(auto_now_add=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /blog/feeds.py: -------------------------------------------------------------------------------- 1 | """ 2 | 这个文件定义了RSS输出。 3 | """ 4 | from django.contrib.syndication.views import Feed 5 | from django.utils.feedgenerator import Atom1Feed 6 | from blog.models import Post 7 | 8 | class PostFeed(Feed): 9 | """ 10 | 定义文章 ATOM 输出的类 11 | """ 12 | feed_type = Atom1Feed 13 | title = "学习笔记" 14 | link = "/" 15 | subtitle = "恩岩的学习笔记" 16 | 17 | def items(self): 18 | return Post.objects.order_by('-id') 19 | 20 | def item_title(self, item): 21 | return item.title 22 | 23 | def item_description(self, item): 24 | return item.body_html 25 | -------------------------------------------------------------------------------- /blog/sitemaps.py: -------------------------------------------------------------------------------- 1 | """ 2 | 定义站点地图。 3 | """ 4 | from django.contrib.sitemaps import Sitemap 5 | from blog.models import Post 6 | 7 | class StaticSitemap(Sitemap): 8 | """静态链接的站点地图""" 9 | changefreq = 'daily' 10 | priority = 0.5 11 | 12 | def items(self): 13 | return ['index', ] 14 | 15 | def location(self, item): 16 | from django.core.urlresolvers import reverse 17 | return reverse(item) 18 | 19 | class PostSitemap(Sitemap): 20 | """文章详情页面的站点地图""" 21 | changefreq = 'never' 22 | priority = 0.5 23 | 24 | def items(self): 25 | return Post.objects.all() 26 | 27 | def lastmod(self, obj): 28 | return obj.modification_time 29 | -------------------------------------------------------------------------------- /blog/templates/change_profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load widget_tweaks %} 3 | {% block content %} 4 | 7 |
8 | {% csrf_token %} 9 |
10 | {{ form.first_name.label_tag }} 11 | {{ form.first_name | add_class:"form-control" }} 12 |
13 |
14 | {{ form.last_name.label_tag }} 15 | {{ form.last_name | add_class:"form-control" }} 16 |
17 |
18 | {{ form.email.label_tag }} 19 | {{ form.email | add_class:"form-control" }} 20 |
21 | 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /deployment/uwsgi_params: -------------------------------------------------------------------------------- 1 | 2 | uwsgi_param QUERY_STRING $query_string; 3 | uwsgi_param REQUEST_METHOD $request_method; 4 | uwsgi_param CONTENT_TYPE $content_type; 5 | uwsgi_param CONTENT_LENGTH $content_length; 6 | 7 | uwsgi_param REQUEST_URI $request_uri; 8 | uwsgi_param PATH_INFO $document_uri; 9 | uwsgi_param DOCUMENT_ROOT $document_root; 10 | uwsgi_param SERVER_PROTOCOL $server_protocol; 11 | uwsgi_param REQUEST_SCHEME $scheme; 12 | uwsgi_param HTTPS $https if_not_empty; 13 | 14 | uwsgi_param REMOTE_ADDR $remote_addr; 15 | uwsgi_param REMOTE_PORT $remote_port; 16 | uwsgi_param SERVER_PORT $server_port; 17 | uwsgi_param SERVER_NAME $server_name; 18 | -------------------------------------------------------------------------------- /deployment/django_blog_uwsgi.ini: -------------------------------------------------------------------------------- 1 | # django_blog_uwsgi.ini file 2 | [uwsgi] 3 | 4 | # Django-related settings 5 | # the base directory (full path) 6 | chdir = /home/liuenyan/django-blog 7 | # Django's wsgi file 8 | module = django_blog.wsgi 9 | # the virtualenv (full path) 10 | home = /home/liuenyan/django_venv 11 | 12 | # process-related settings 13 | # master 14 | master = true 15 | # maximum number of worker processes 16 | processes = 10 17 | # the socket (use the full path to be safe 18 | socket = /tmp/django_blog.sock 19 | # ... with appropriate permissions - may be needed 20 | chmod-socket = 666 21 | # clear environment on exit 22 | vacuum = true 23 | # plugin 24 | plugins = python3 25 | -------------------------------------------------------------------------------- /blog/migrations/0008_auto_20160613_1410.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-06-13 06:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0007_auto_20160519_0926'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='post', 17 | old_name='body', 18 | new_name='body_html', 19 | ), 20 | migrations.AddField( 21 | model_name='post', 22 | name='body_markdown', 23 | field=models.TextField(default=''), 24 | preserve_default=False, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /blog/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | 这个文件定义了一些有用的工具函数。 3 | """ 4 | import bleach 5 | import markdown 6 | from bleach_whitelist import generally_xss_safe 7 | from blog.models import Post, Comment 8 | 9 | def clean_html_tags(data): 10 | return bleach.linkify( 11 | bleach.clean(data, generally_xss_safe), 12 | skip_pre=True 13 | ) 14 | 15 | def convert_to_html(markdown_text): 16 | md = markdown.Markdown( 17 | extensions=[ 18 | 'pymdownx.github', 19 | 'markdown.extensions.toc', 20 | ], 21 | extension_configs={ 22 | 'markdown.extensions.toc': 23 | { 24 | 'title': '目录', 25 | }, 26 | }, 27 | output_format="html5" 28 | ) 29 | return md.convert(markdown_text) 30 | -------------------------------------------------------------------------------- /blog/migrations/0006_link.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-05-15 03:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0005_auto_20160513_1128'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Link', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=128)), 20 | ('description', models.CharField(max_length=128)), 21 | ('link', models.URLField()), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /blog/migrations/0003_auto_20160505_1634.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-05-05 08:34 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0002_comment'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='comment', 17 | name='name', 18 | field=models.CharField(default='user', max_length=256), 19 | preserve_default=False, 20 | ), 21 | migrations.AddField( 22 | model_name='comment', 23 | name='url', 24 | field=models.URLField(default='http://www.example.com/'), 25 | preserve_default=False, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-blog", 3 | "version": "1.0.0", 4 | "description": "Personal blog powered by django web framework.", 5 | "main": "gulpfile.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "gulp": "^3.9.1", 9 | "gulp-less": "^3.1.0", 10 | "gulp-minify-css": "^1.2.4", 11 | "gulp-rename": "^1.2.2", 12 | "gulp-uglify": "^2.0.0" 13 | }, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/liuenyan/django-blog.git" 20 | }, 21 | "keywords": [ 22 | "django" 23 | ], 24 | "author": "Enyan Liu", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/liuenyan/django-blog/issues" 28 | }, 29 | "homepage": "https://github.com/liuenyan/django-blog#readme" 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Blog 2 | 3 | 一个使用Django框架开发的博客程序。 4 | 5 | ## 依赖包安装 6 | 7 | 1. 安装virtualenv和virtualenvwrapper。 8 | 2. 使用virtualenvwrapper创建虚拟环境: 9 | 10 | ``` 11 | mkvirtualenv django-blog 12 | ``` 13 | 14 | 3. 切换到虚拟环境并安装依赖。 15 | 16 | ``` 17 | workon django-blog 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | 4. 安装前端开发工具依赖: 22 | 23 | ``` 24 | npm install 25 | ``` 26 | 27 | ## 数据库初始化 28 | 29 | 1. 执行数据库迁移: 30 | 31 | ``` 32 | ./manage.py migrate 33 | ``` 34 | 35 | 2. 创建超级用户,根据提示完成用户的创建: 36 | 37 | ``` 38 | ./manage.py createsuperuser 39 | ``` 40 | 41 | ## 部署到服务器 42 | 43 | ### 基于 ubuntu 16.04 / nginx / uwsgi 的部署 44 | 45 | 1. 安装软件包 46 | 47 | ``` 48 | apt install uwsgi uwsgi-extra uwsgi-emperor uwsgi-plugin-python3 nginx 49 | ``` 50 | 51 | 2. 创建虚拟环境,见上述的依赖包安装部分。 52 | 3. 部署配置文件。配置文件内容参考deployment目录。 53 | -------------------------------------------------------------------------------- /blog/migrations/0004_auto_20160509_1603.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-05-09 08:03 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0003_auto_20160505_1634'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Tag', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('tag', models.CharField(max_length=256, unique=True)), 20 | ], 21 | ), 22 | migrations.AddField( 23 | model_name='post', 24 | name='tags', 25 | field=models.ManyToManyField(to='blog.Tag'), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | uglify = require('gulp-uglify'), 3 | minifyCSS = require('gulp-minify-css'), 4 | less = require('gulp-less'), 5 | rename = require('gulp-rename'); 6 | 7 | gulp.task('default', ['script', 'less']); 8 | 9 | gulp.task('script', function(){ 10 | gulp.src('blog/static/blog/js/*.js') 11 | .pipe(uglify()) 12 | .pipe(rename({suffix:'.min'})) 13 | .pipe(gulp.dest('blog/static/blog/dist/js/')); 14 | }); 15 | 16 | gulp.task('css', function(){ 17 | gulp.src('blog/static/blog/css/*.css') 18 | .pipe(minifyCSS()) 19 | .pipe(rename({suffix:'.min'})) 20 | .pipe(gulp.dest('blog/static/blog/dist/css/')); 21 | }); 22 | 23 | gulp.task('less', function(){ 24 | gulp.src('blog/static/blog/css/*.less') 25 | .pipe(less()) 26 | .pipe(minifyCSS()) 27 | .pipe(rename({suffix:'.min'})) 28 | .pipe(gulp.dest('blog/static/blog/dist/css/')); 29 | }); 30 | -------------------------------------------------------------------------------- /blog/migrations/0002_comment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-05-05 06:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('blog', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Comment', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('email', models.EmailField(max_length=254)), 21 | ('comment', models.TextField()), 22 | ('timestamp', models.DateTimeField(auto_now=True)), 23 | ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.Post')), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /django_blog/urls.py: -------------------------------------------------------------------------------- 1 | """django_site URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', admin.site.urls), 21 | url(r'^pages/', include('django.contrib.flatpages.urls')), 22 | url(r'^', include('blog.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /blog/migrations/0010_add_slug_to_post.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-07-11 08:51 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | def update_slug(apps, schema_editor): 9 | Post = apps.get_model('blog', 'Post') 10 | for post in Post.objects.all(): 11 | post.slug = str(post.id) 12 | post.save() 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [ 18 | ('blog', '0009_auto_20160711_1512'), 19 | ] 20 | 21 | operations = [ 22 | migrations.AddField( 23 | model_name='post', 24 | name='slug', 25 | field=models.SlugField(unique=False, default='') 26 | ), 27 | migrations.RunPython(update_slug, ), 28 | migrations.AlterField( 29 | model_name='post', 30 | name='slug', 31 | field=models.SlugField(unique=True) 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.5 on 2016-05-03 07:36 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Post', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('title', models.CharField(max_length=128)), 24 | ('body', models.TextField()), 25 | ('timestamp', models.DateTimeField(auto_now=True)), 26 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Enyan Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /blog/templates/_sidebar.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | #virtualenv 65 | venv/* 66 | 67 | #sqlite 68 | db.sqlite3 69 | 70 | #node_modules 71 | node_modules/ 72 | 73 | #dist css & javascript 74 | !blog/static/blog/dist/ 75 | -------------------------------------------------------------------------------- /blog/migrations/0009_auto_20160711_1512.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-07-11 07:12 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.db import migrations, models 7 | from django.utils.timezone import utc 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('blog', '0008_auto_20160613_1410'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Category', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('category', models.CharField(max_length=256, unique=True)), 22 | ], 23 | ), 24 | migrations.RenameField( 25 | model_name='post', 26 | old_name='timestamp', 27 | new_name='creation_time', 28 | ), 29 | migrations.AddField( 30 | model_name='post', 31 | name='modification_time', 32 | field=models.DateTimeField(auto_now=True, default=datetime.datetime(2016, 7, 11, 7, 12, 57, 207949, tzinfo=utc)), 33 | preserve_default=False, 34 | ), 35 | migrations.AddField( 36 | model_name='post', 37 | name='categories', 38 | field=models.ManyToManyField(to='blog.Category'), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /blog/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | 定义 blog 应用的 URL 映射。 3 | """ 4 | from django.conf.urls import url, include 5 | from django.contrib.sitemaps.views import sitemap 6 | from django.contrib.flatpages.sitemaps import FlatPageSitemap 7 | from blog.feeds import PostFeed 8 | from blog.sitemaps import StaticSitemap, PostSitemap 9 | from . import views 10 | 11 | sitemaps = { 12 | 'static': StaticSitemap, 13 | 'posts': PostSitemap, 14 | 'flatpages': FlatPageSitemap, 15 | } 16 | 17 | urlpatterns = [ 18 | url(r'^$', views.index, name='index'), 19 | url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, 20 | name='django.contrib.sitemaps.views.sitemap'), 21 | url(r'^auth/', include('django.contrib.auth.urls')), 22 | url(r'^post/([a-zA-Z0-9\+\-_]+)/$', views.post_detail, name='post'), 23 | url(r'^edit-post/([a-zA-Z0-9\+\-_]+)/$', views.edit_post, name='edit_post'), 24 | url(r'^new-post/$', views.new_post, name='new_post'), 25 | url(r'^delete-post/([a-zA-Z0-9\+\-_]+)/$', views.delete_post, name='delete_post'), 26 | url(r'^feed/$', PostFeed(), name='feed'), 27 | url(r'^category/([a-zA-Z0-9\+\-_]+)/$', views.category_posts, name='category'), 28 | url(r'^tag/([a-zA-Z0-9\+\-_]+)/$', views.tag_posts, name='tag'), 29 | url(r'^archive/(\d{4})/(\d{2})/$', views.archive, name='archive'), 30 | url(r'^profile/$', views.profile, name='profile'), 31 | url(r'^change-profile/$', views.change_profile, name='change_profile'), 32 | url(r'^new_category/$', views.new_category, name='new_category'), 33 | url(r'^new_tag/$', views.new_tag, name='new_tag'), 34 | ] 35 | -------------------------------------------------------------------------------- /blog/templatetags/blog_extras.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from django import template 3 | from django.db.models import Count 4 | from ..models import Post, Category, Tag, Link 5 | 6 | register = template.Library() 7 | 8 | @register.inclusion_tag('_navbar.html') 9 | def show_navbar(active, user): 10 | return {'active': active, 'user': user} 11 | 12 | @register.inclusion_tag('_sidebar.html') 13 | def show_sidebar(): 14 | categories = Category.objects.annotate(Count('post')).filter(post__count__gt=0).order_by('category') 15 | tags = Tag.objects.annotate(Count('post')).filter(post__count__gt=0).order_by('tag') 16 | archives = Post.objects.extra(select={ 17 | 'year': 'strftime("%Y", creation_time)', 18 | 'month': 'strftime("%m", creation_time)' 19 | }).values('year', 'month').annotate(Count('id')).order_by('-year', '-month') 20 | links = Link.objects.all() 21 | context = { 22 | 'categories': categories, 23 | 'tags': tags, 24 | 'archives': archives, 25 | 'links': links, 26 | } 27 | return context 28 | 29 | @register.filter 30 | def gravatar(email, size=60, default='identicon'): 31 | md5 = hashlib.md5(email.encode('utf8').lower()).hexdigest() 32 | gravatar_url = "//cdn.v2ex.com/gravatar/{0}?s={1}&d={2}".format(md5, str(size), default) 33 | return gravatar_url 34 | 35 | @register.filter 36 | def alert_class(level_tag): 37 | if level_tag == 'error': 38 | return 'alert-danger' 39 | elif level_tag in ['info', 'success', 'warning']: 40 | return 'alert-{0}'.format(level_tag) 41 | else: 42 | return 'alert-info' 43 | -------------------------------------------------------------------------------- /blog/static/blog/dist/css/style.min.css: -------------------------------------------------------------------------------- 1 | body,html{height:100%}.post-meta,body>footer.page-footer,ul.comments .comment-odd{background-color:#f5f5f5}.post-meta>ul>li>span>a,ul.comments>li span a{color:#eee}ul.comments>li,ul.posts>li,ul.sidebar>li{border-bottom-color:#ddd;border-bottom-width:2px;border-bottom-style:solid}.toc,ul.comments>li,ul.posts>li,ul.sidebar>li{border-bottom-style:solid}#id_categories,#id_tags,.post-meta>ul,.toc ul,ul.comments,ul.posts,ul.sidebar,ul.sidebar>li>ul{list-style-type:none}body{font-family:"Helvetica Neue","Luxi Sans","Droid Sans","DejaVu Sans",Tahoma,"Hiragino Sans GB","Microsoft Yahei","WenQuanYi Micro Hei","WenQuanYi Zen Hei",sans-serif}body>div.container{min-height:100%;padding-top:50px;padding-bottom:80px}body>footer.page-footer{height:80px;padding:10px;margin-top:-80px;text-align:center;border-top-color:#ddd}.post-body,.post-title{margin:30px 0}ul.posts{padding:0}ul.posts>li{padding:10px 0}.post-title a{text-decoration:none}.post-meta>ul{padding:10px 0}.post-meta>ul>li{display:inline;margin:5px}.post-controls{min-height:50px}.post-controls>a{margin:2px}ul.comments{margin:15px 0;padding:15px 0}ul.comments>li{min-height:80px}ul.comments .comment-even{background-color:#fff}ul.comments .comment-avatar{position:absolute;margin:10px 0}ul.comments .comment-body{margin:0 10px 0 70px;padding:10px 0}ul.sidebar,ul.sidebar>li>h4{padding:10px}ul.sidebar{margin:10px 0;border-radius:5px}ul.sidebar>li{padding:10px;background-color:#f5f5f5}ul.sidebar>li>ul{padding:10px 36px}img.align-center{display:block;margin:20px auto}.toc{border-bottom-color:#f5f5f5;border-bottom-width:1px}.toc ul{padding-left:25px;font-size:18px}.toctitle{text-align:center;font-size:18px;display:block}#id_categories>li,#id_tags>li{display:inline;margin:10px} -------------------------------------------------------------------------------- /blog/static/blog/dist/js/edit_post.min.js: -------------------------------------------------------------------------------- 1 | function getCookie(e){var t=null;if(document.cookie&&""!=document.cookie)for(var o=document.cookie.split(";"),a=0;a',a='";$li=$("
  • ").append($(a).prepend($(o))),$("#id_categories").append($li)}}else console.log("新分类的ajax请求失败:",t);$("#category-modal").modal("hide")},"json")}),$("#tag-modal .modal-footer button:first").click(function(){$.post("/new_tag/",{tag:$("#id_tag").val()},function(e,t){if("success"===t){if("success"===e.status){var o='',a='";$li=$("
  • ").append($(a).prepend($(o))),$("#id_tags").append($li)}}else console.log("新标签的ajax请求失败:",t);$("#tag-modal").modal("hide")},"json")}); -------------------------------------------------------------------------------- /blog/templates/_navbar.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /blog/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load widget_tweaks %} 3 | 4 | {% block title %}登录页{% endblock %} 5 | {% block content %} 6 | 7 | {% if next %} 8 | {% if user.is_authenticated %} 9 | 13 | {% else %} 14 | 18 | {% endif %} 19 | {% endif %} 20 | 21 | {% if user.is_authenticated %} 22 | 25 | {% else %} 26 |
    27 | 30 |
    31 | {% csrf_token %} 32 |
    33 | {{ form.username.label_tag }} 34 | {{ form.username | add_class:'form-control' }} 35 |
    36 |
    37 | {{ form.password.label_tag }} 38 | {{ form.password | add_class:'form-control' }} 39 |
    40 | 41 | 42 |
    43 | {% if form.errors %} 44 | 48 | {% endif %} 49 | {# Assumes you setup the password_reset view in your URLconf #} 50 | {#

    忘记密码?

    #} 51 |
    52 | {% endif %} 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /deployment/django_blog_nginx.conf: -------------------------------------------------------------------------------- 1 | # django_blog_nginx.conf 2 | 3 | # the upstream component nginx needs to connect to 4 | upstream django { 5 | server unix:///tmp/django_blog.sock; # for a file socket 6 | # server 127.0.0.1:8001; # for a web port socket (we'll use this first) 7 | } 8 | 9 | # configuration of the server 10 | server { 11 | # the port your site will be served on 12 | listen 443 ssl; 13 | # the domain name it will serve for 14 | server_name enyan.me; # substitute your machine's IP address or FQDN 15 | charset utf-8; 16 | 17 | # max upload size 18 | client_max_body_size 75M; # adjust to taste 19 | 20 | # robots.txt 21 | #location /robots.txt { 22 | # alias /home/liuenyan/django-blog/robots.txt 23 | #} 24 | 25 | # favicon 26 | location /favicon.ico { 27 | alias /home/liuenyan/django-blog/favicon.ico; 28 | } 29 | # Django media 30 | location /media { 31 | alias /home/liuenyan/django-blog/media; # your Django project's media files - amend as required 32 | } 33 | 34 | location /static { 35 | alias /home/liuenyan/django-blog/static; # your Django project's static files - amend as required 36 | } 37 | 38 | # Finally, send all non-media requests to the Django server. 39 | location / { 40 | uwsgi_pass django; 41 | include /home/liuenyan/django-blog/deployment/uwsgi_params; # the uwsgi_params file you installed 42 | } 43 | ssl on; 44 | ssl_certificate /etc/letsencrypt/live/enyan.me/fullchain.pem; 45 | ssl_certificate_key /etc/letsencrypt/live/enyan.me/privkey.pem; 46 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 47 | } 48 | 49 | # redirect http to https 50 | server { 51 | # the port your site will be served on 52 | listen 80; 53 | # the domain name it will serve for 54 | server_name enyan.me; 55 | return 301 https://$server_name$request_uri; 56 | } 57 | 58 | # redirect www.enyan.me to https://enyan.me 59 | server { 60 | server_name www.enyan.me; 61 | rewrite ^(.*) https://enyan.me$1 permanent; 62 | } 63 | -------------------------------------------------------------------------------- /blog/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | 这个文件定义 blog 应用的数据模型。 3 | """ 4 | from django.db import models 5 | from django.contrib.sitemaps import ping_google 6 | 7 | # Create your models here. 8 | 9 | class Tag(models.Model): 10 | """标签的数据模型""" 11 | tag = models.CharField(max_length=256, unique=True) 12 | 13 | def __str__(self): 14 | return self.tag 15 | 16 | 17 | class Category(models.Model): 18 | """分类的数据模型""" 19 | category = models.CharField(max_length=256, unique=True) 20 | 21 | def __str__(self): 22 | return self.category 23 | 24 | 25 | class Post(models.Model): 26 | """文章的数据模型""" 27 | title = models.CharField(max_length=128) 28 | slug = models.SlugField(unique=True) 29 | creation_time = models.DateTimeField(auto_now_add=True) 30 | modification_time = models.DateTimeField(auto_now=True) 31 | body_markdown = models.TextField() 32 | body_html = models.TextField() 33 | author = models.ForeignKey('auth.User') 34 | categories = models.ManyToManyField(Category) 35 | tags = models.ManyToManyField(Tag) 36 | 37 | def save(self, force_insert=False, force_update=False, using=None, 38 | update_fields=None): 39 | super(Post, self).save(force_insert, force_update, using, update_fields) 40 | try: 41 | ping_google() 42 | except Exception: 43 | pass 44 | 45 | def get_absolute_url(self): 46 | from django.core.urlresolvers import reverse 47 | return reverse('post', args=[self.slug]) 48 | 49 | def __str__(self): 50 | return self.title 51 | 52 | 53 | class Comment(models.Model): 54 | """评论的数据模型""" 55 | name = models.CharField(max_length=256) 56 | email = models.EmailField() 57 | url = models.URLField(blank=True) 58 | comment = models.TextField() 59 | timestamp = models.DateTimeField(auto_now=True) 60 | post = models.ForeignKey('Post') 61 | 62 | def __str__(self): 63 | return self.comment 64 | 65 | 66 | class Link(models.Model): 67 | """链接的数据模型""" 68 | name = models.CharField(max_length=128) 69 | description = models.CharField(max_length=128) 70 | link = models.URLField() 71 | 72 | def __str__(self): 73 | return self.name 74 | -------------------------------------------------------------------------------- /blog/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles blog_extras %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}{% endblock %} 10 | 11 | {% block styles %} 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | {% endblock %} 23 | 24 | 25 | {% block navbar %} 26 | {% show_navbar 'home' user %} 27 | {% endblock %} 28 |
    29 | {% if messages %} 30 |
    31 | 32 | {% for message in messages %} 33 | 37 | {% endfor %} 38 |
    39 | {% endif %} 40 |
    41 | {% block content %} 42 | {% endblock %} 43 |
    44 |
    45 | 48 | {% block modals %} 49 | {% endblock %} 50 | {% block scripts %} 51 | 52 | 53 | 54 | 55 | {% endblock %} 56 | 57 | 58 | -------------------------------------------------------------------------------- /blog/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | 这个文件定义表单信息。 3 | """ 4 | from django import forms 5 | from django.forms.widgets import CheckboxSelectMultiple 6 | from blog.models import Post, Category, Tag 7 | 8 | 9 | class PostForm(forms.ModelForm): 10 | """文章内容编辑表单""" 11 | class Meta: 12 | model = Post 13 | fields = ['title', 'slug', 'categories', 'tags', 'body_markdown'] 14 | widgets = { 15 | 'categories': CheckboxSelectMultiple, 16 | 'tags': CheckboxSelectMultiple, 17 | } 18 | labels = { 19 | 'title': '标题', 20 | 'slug': '缩略名', 21 | 'categories': '分类', 22 | 'tags': '标签', 23 | 'body_markdown': '内容', 24 | } 25 | help_texts = { 26 | 'title': '您的文章标题', 27 | 'slug': '缩略名(slug)是文章标题的URL友好型版本', 28 | 'tags': '使用标签将更具体的关键字与您的文章关联起来', 29 | 'categories': '使用类别按主题对您的文章进行分组', 30 | 'body_markdown': '使用Markdown语法编辑文章', 31 | } 32 | error_messages = { 33 | 'title': { 34 | 'required': '标题不能为空', 35 | }, 36 | 'slug': { 37 | 'required': 'slug不能为空', 38 | 'invalid': 'slug无效', 39 | }, 40 | 'body_markdown': { 41 | 'required': '文章内容不能为空', 42 | }, 43 | } 44 | 45 | class CategoryForm(forms.ModelForm): 46 | """"分类表单""" 47 | class Meta: 48 | model = Category 49 | fields = ['category',] 50 | labels = { 51 | 'category': '分类', 52 | } 53 | 54 | 55 | class TagForm(forms.ModelForm): 56 | """标签表单""" 57 | class Meta: 58 | model = Tag 59 | fields = ['tag',] 60 | labels = { 61 | 'tag': '标签', 62 | } 63 | 64 | 65 | class CommentForm(forms.Form): 66 | """评论表单""" 67 | name = forms.CharField( 68 | label='姓名', 69 | max_length=256, 70 | error_messages={ 71 | 'required': '姓名不能为空', 72 | } 73 | ) 74 | url = forms.URLField( 75 | label='网站', 76 | required=False 77 | ) 78 | email = forms.EmailField( 79 | label='邮件地址', 80 | error_messages={ 81 | 'required': 'email不能为空', 82 | } 83 | ) 84 | comment = forms.CharField( 85 | label='评论', 86 | widget=forms.Textarea, 87 | error_messages={ 88 | 'required': '评论内容不能为空', 89 | } 90 | ) 91 | 92 | 93 | class EditProfileForm(forms.Form): 94 | """个人资料编辑表单""" 95 | first_name = forms.CharField( 96 | label='名字', 97 | max_length=30 98 | ) 99 | last_name = forms.CharField( 100 | label='姓氏', 101 | max_length=30 102 | ) 103 | email = forms.EmailField(label='邮件地址') 104 | -------------------------------------------------------------------------------- /blog/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load blog_extras %} 3 | 4 | {% block title %}{% if title %}{{ title }}{% else %}主页 - 学习笔记{% endif %}{% endblock %} 5 | 6 | {% block styles %} 7 | {{ block.super }} 8 | 9 | {% endblock %} 10 | {% block content %} 11 | {{ block.super }} 12 |
    13 | 41 | 55 |
    56 |
    57 | {% show_sidebar %} 58 |
    59 | {% endblock %} 60 | 61 | {% block scripts %} 62 | {{ block.super }} 63 | 64 | 65 | 66 | 82 | {% endblock %} 83 | -------------------------------------------------------------------------------- /blog/static/blog/css/style.less: -------------------------------------------------------------------------------- 1 | @border-color: #ddd; 2 | @bg-color: #f5f5f5; 3 | 4 | .border-bottom-mixin(@color, @style, @width){ 5 | border-bottom-color: @color; 6 | border-bottom-style: @style; 7 | border-bottom-width: @width; 8 | } 9 | 10 | html { 11 | height: 100%; 12 | } 13 | 14 | body { 15 | height: 100%; 16 | font-family: "Helvetica Neue", "Luxi Sans", "Droid Sans", "DejaVu Sans", 17 | Tahoma, "Hiragino Sans GB", "Microsoft Yahei", "WenQuanYi Micro Hei", 18 | "WenQuanYi Zen Hei", sans-serif; 19 | > div.container { 20 | min-height: 100%; 21 | padding-top: 50px; /* for navbar */ 22 | padding-bottom: 80px; /*for footer */ 23 | } 24 | > footer.page-footer { 25 | height: 80px; 26 | padding: 10px; 27 | margin-top: -80px; 28 | text-align: center; 29 | background-color: @bg-color; 30 | border-top-color:@border-color; 31 | } 32 | } 33 | 34 | ul.posts { 35 | list-style-type: none; 36 | padding: 0; 37 | > li { 38 | padding: 10px 0; 39 | .border-bottom-mixin(@border-color, solid, 2px); 40 | } 41 | } 42 | 43 | .post-title { 44 | margin: 30px 0; 45 | a { 46 | text-decoration: none; 47 | } 48 | } 49 | 50 | .post-body { margin: 30px 0 } 51 | 52 | .post-meta { 53 | background-color: @bg-color; 54 | > ul { 55 | list-style-type: none; 56 | padding: 10px 0; 57 | > li { 58 | display: inline; 59 | margin: 5px; 60 | > span > a { 61 | color: #eee; 62 | } 63 | } 64 | } 65 | } 66 | 67 | .post-controls { 68 | min-height: 50px; 69 | > a { 70 | margin: 2px; 71 | } 72 | } 73 | 74 | ul.comments { 75 | margin: 15px 0; 76 | padding: 15px 0; 77 | list-style-type: none; 78 | > li { 79 | .border-bottom-mixin(@border-color, solid, 2px); 80 | min-height: 80px; 81 | } 82 | .comment-odd { background-color: @bg-color } 83 | .comment-even { background-color: #fff } 84 | > li span a { 85 | color: #eee; 86 | } 87 | .comment-avatar { 88 | position: absolute; 89 | margin: 10px 0; 90 | } 91 | .comment-body { 92 | margin: 0 10px 0 70px; 93 | padding: 10px 0; 94 | } 95 | } 96 | 97 | ul.sidebar { 98 | margin: 10px 0; 99 | padding: 10px 10px; 100 | border-radius: 5px; 101 | list-style-type: none; 102 | > li { 103 | padding: 10px; 104 | .border-bottom-mixin(@border-color, solid, 2px); 105 | background-color: @bg-color; 106 | > h4 { padding: 10px } 107 | > ul { 108 | list-style-type: none; 109 | padding: 10px 36px; 110 | } 111 | } 112 | } 113 | 114 | img.align-center { 115 | display: block; 116 | margin: 20px auto; 117 | } 118 | 119 | .toc { 120 | .border-bottom-mixin(@bg-color, solid, 1px); 121 | ul { 122 | list-style-type: none; 123 | padding-left: 25px; 124 | font-size: 18px; 125 | } 126 | } 127 | 128 | .toctitle { 129 | text-align: center; 130 | font-size: 18px; 131 | display: block; 132 | } 133 | 134 | #id_categories, #id_tags { list-style-type: none } 135 | 136 | #id_categories > li, 137 | #id_tags > li { 138 | display: inline; 139 | margin: 10px; 140 | } 141 | -------------------------------------------------------------------------------- /blog/static/blog/js/edit_post.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $('[data-toggle="tooltip"]').tooltip(); 3 | var editor = new EpicEditor({ 4 | basePath: "//cdn.bootcss.com/epiceditor/0.2.2", 5 | clientSideStorage: false, 6 | theme: { 7 | base: '/themes/base/epiceditor.css', 8 | preview: '/themes/preview/github.css', 9 | editor: '/themes/editor/epic-dark.css' 10 | }, 11 | autogrow: { 12 | minHeight: 250 13 | }, 14 | button: { 15 | bar: true 16 | } 17 | }).load(); 18 | }); 19 | function getCookie(name) { 20 | var cookieValue = null; 21 | if (document.cookie && document.cookie != '') { 22 | var cookies = document.cookie.split(';'); 23 | for (var i = 0; i < cookies.length; i++) { 24 | var cookie = jQuery.trim(cookies[i]); 25 | // Does this cookie string begin with the name we want? 26 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 27 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 28 | break; 29 | } 30 | } 31 | } 32 | return cookieValue; 33 | } 34 | 35 | function csrfSafeMethod(method) { 36 | // these HTTP methods do not require CSRF protection 37 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 38 | } 39 | 40 | var csrftoken = getCookie('csrftoken'); 41 | 42 | $.ajaxSetup({ 43 | beforeSend: function(xhr, settings) { 44 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 45 | xhr.setRequestHeader("X-CSRFToken", csrftoken); 46 | } 47 | } 48 | }); 49 | 50 | $('#category-modal .modal-footer button:first').click(function(){ 51 | $.post('/new_category/', { 52 | 'category': $('#id_category').val(), 53 | }, function(data, textStatus){ 54 | if(textStatus === 'success') { 55 | if (data['status']==='success'){ 56 | var checkbox = ''; 61 | var label = ''; 64 | $li = $('
  • ').append($(label).prepend($(checkbox))); 65 | $('#id_categories').append($li); 66 | } 67 | }else{ 68 | console.log('新分类的ajax请求失败:', textStatus); 69 | } 70 | $('#category-modal').modal('hide'); 71 | }, 'json'); 72 | }); 73 | 74 | $('#tag-modal .modal-footer button:first').click(function(){ 75 | $.post('/new_tag/', { 76 | 'tag': $('#id_tag').val(), 77 | }, function(data, textStatus){ 78 | if(textStatus === 'success'){ 79 | if (data['status'] === 'success'){ 80 | var checkbox = ''; 85 | var label = ''; 88 | $li = $('
  • ').append($(label).prepend($(checkbox))); 89 | $('#id_tags').append($li); 90 | } 91 | }else{ 92 | console.log('新标签的ajax请求失败:', textStatus); 93 | } 94 | $('#tag-modal').modal('hide'); 95 | }, 'json'); 96 | }); 97 | -------------------------------------------------------------------------------- /blog/templates/edit_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load staticfiles widget_tweaks %} 3 | {% block title %}编辑文章{% endblock %} 4 | 5 | {% block styles %} 6 | {{ block.super }} 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {{ block.super }} 11 |
    12 | 15 |
    16 | {% csrf_token %} 17 |
    18 | {{ post_form.title.label_tag }} 19 | {{ post_form.title | add_class:'form-control' }} 20 |
    21 |
    22 | {{ post_form.slug.label_tag }} 23 | {{ post_form.slug | add_class:'form-control' }} 24 |
    25 |
    26 | {{ post_form.categories.label_tag }} 27 | {{ post_form.categories }} 28 | 29 |
    30 |
    31 | {{ post_form.tags.label_tag }} 32 | {{ post_form.tags }} 33 | 34 |
    35 |
    36 | {{ post_form.body_markdown.label_tag }} 37 |
    {{ post_form.body_markdown | add_class:'form-control' }}
    38 |
    39 | 40 |
    41 |
    42 | {% endblock %} 43 | 44 | {% block modals %} 45 | 68 | 91 | {% endblock %} 92 | 93 | {% block scripts %} 94 | {{ block.super }} 95 | 96 | 97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /django_blog/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_blog project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '@p934wrm2ee8y5^9zx4&wz79wh2vy$hw!mjvpj&vz_k6meug*q' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = False 27 | 28 | ALLOWED_HOSTS = ['*',] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'blog', 35 | 'widget_tweaks', 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | 'django.contrib.sitemaps', 43 | 'django.contrib.sites', 44 | 'django.contrib.flatpages', 45 | #'debug_toolbar', 46 | ] 47 | 48 | MIDDLEWARE_CLASSES = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'django_blog.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'django_blog.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 83 | 84 | DATABASES = { 85 | 'default': { 86 | 'ENGINE': 'django.db.backends.sqlite3', 87 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'zh-hans' 115 | 116 | TIME_ZONE = 'Asia/Shanghai' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | STATIC_ROOT = os.path.join(BASE_DIR, 'static/') 130 | 131 | # auth 132 | LOGIN_REDIRECT_URL = '/' 133 | LOGIN_URL = '/auth/login/' 134 | LOGOUT_URL = '/auth/logout/' 135 | # INTERNAL_IPS for debug https://docs.djangoproject.com/en/1.9/ref/settings/#internal-ips 136 | INTERNAL_IPS = ['0.0.0.0', '127.0.0.1',] 137 | 138 | #site 139 | SITE_ID = 1 140 | 141 | # default comments provider setting. default or disqus 142 | DEFAULT_COMMENTS_PROVIDER = 'disqus' 143 | -------------------------------------------------------------------------------- /blog/templates/post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load widget_tweaks blog_extras %} 3 | 4 | {% block title %}{{ post.title }}{% endblock %} 5 | 6 | {% block styles %} 7 | {{ block.super }} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | {{ block.super }} 13 |
    14 |
    15 |
    16 |

    {{ post.title }}

    17 |
    18 | 34 |
    35 | {{ post.body_html | safe }} 36 |
    37 |
    38 | {% if post.author.id == user.id %} 39 | 编辑 40 | 删除 41 | {% endif %} 42 |
    43 |
    44 | {% if comments_provider == 'default' %} 45 |
    46 |

    评论列表

    47 |
      48 | {% for comment in comments %} 49 |
    • 50 | 51 |
      52 |

      53 | #{{forloop.counter }} 54 | {% if comment.url %} 55 | {{ comment.name }} 56 | {% else %} 57 | {{ comment.name }} 58 | {% endif %} 59 | 发布于 {{ comment.timestamp | date:'c' }} 60 |

      61 | {{ comment.comment | safe }} 62 |
      63 |
    • 64 | {% endfor %} 65 |
    66 |

    留下您的评论

    67 |
    68 | {% csrf_token %} 69 |
    70 | {{ form.name.label_tag }} 71 | {{ form.name | add_class:'form-control'| attr:'placeholder:您的名字' }} 72 |
    73 |
    74 | {{ form.url.label_tag }} 75 | {{ form.url | add_class:'form-control' | attr:'placeholder:http://'}} 76 |
    77 |
    78 | {{ form.email.label_tag }} 79 | {{ form.email | add_class:'form-control' |attr:'placeholder:user@example.com'}} 80 |
    81 |
    82 | {{ form.comment.label_tag }} 83 | {{ form.comment | add_class:'form-control' |attr:'placeholder:输入您的评论'}} 84 |
    85 | 86 |
    87 |
    88 | {% endif %} 89 | {% if comments_provider == 'disqus' %} 90 |
    91 | {% endif %} 92 |
    93 |
    94 | {% show_sidebar %} 95 |
    96 | {% endblock %} 97 | 98 | {% block scripts %} 99 | {{ block.super }} 100 | 101 | 102 | 103 | {% if comments_provider == 'disqus' and not debug %} 104 | 120 | 121 | {% endif %} 122 | 137 | {% endblock %} 138 | -------------------------------------------------------------------------------- /blog/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | 博客应用的视图函数。 3 | """ 4 | import json 5 | from django.shortcuts import render, redirect, get_object_or_404 6 | from django.contrib.auth.decorators import login_required 7 | from django.core.paginator import Paginator, InvalidPage 8 | from django.contrib import messages 9 | from django.conf import settings 10 | from django.http import HttpResponse 11 | from django.views.decorators.http import require_POST 12 | from blog.models import Post, Comment, Tag, Category 13 | from blog.forms import PostForm, CommentForm, EditProfileForm, CategoryForm, TagForm 14 | from blog.tools import clean_html_tags, convert_to_html 15 | # Create your views here. 16 | 17 | def index(request): 18 | """首页的视图函数""" 19 | post_list = Post.objects.all().order_by('-id') 20 | paginator = Paginator(post_list, 10) 21 | page = request.GET.get('page') 22 | try: 23 | posts = paginator.page(page) 24 | except InvalidPage: 25 | posts = paginator.page(1) 26 | return render(request, "index.html", context={'posts': posts}) 27 | 28 | 29 | def post_detail(request, slug): 30 | """文章页面的视图函数""" 31 | post = get_object_or_404(Post, slug=slug) 32 | context = { 33 | 'comments_provider': settings.DEFAULT_COMMENTS_PROVIDER, 34 | 'post': post, 35 | } 36 | if settings.DEFAULT_COMMENTS_PROVIDER == 'default': 37 | if request.method == 'POST': 38 | form = CommentForm(request.POST) 39 | if form.is_valid(): 40 | comment = Comment( 41 | name=form.cleaned_data['name'], 42 | url=form.cleaned_data['url'], 43 | email=form.cleaned_data['email'], 44 | comment=clean_html_tags(form.cleaned_data['comment']), 45 | post=post 46 | ) 47 | comment.save() 48 | return redirect('post', slug) 49 | else: 50 | messages.add_message(request, messages.ERROR, form.errors) 51 | form = CommentForm() 52 | comments = Comment.objects.filter(post=post) 53 | context['form'] = form 54 | context['comments'] = comments 55 | return render(request, 'post.html', context) 56 | 57 | 58 | @login_required 59 | def edit_post(request, slug): 60 | """文章编辑页面的视图函数""" 61 | post = get_object_or_404(Post, slug=slug) 62 | if request.user.id != post.author.id: 63 | return redirect('post', slug) 64 | if request.method == 'POST': 65 | post_form = PostForm(request.POST, instance=post) 66 | if post_form.is_valid(): 67 | post.body_html = convert_to_html(post_form.cleaned_data['body_markdown']) 68 | post_form.save() 69 | messages.add_message(request, messages.SUCCESS, '文章已更新') 70 | return redirect('post', post.slug) 71 | else: 72 | messages.add_message(request, messages.ERROR, post_form.errors) 73 | context = { 74 | 'post_form': post_form, 75 | 'category_form': CategoryForm(), 76 | 'tag_form': TagForm(), 77 | } 78 | return render(request, 'edit_post.html', context) 79 | context = { 80 | 'post_form': PostForm(instance=post), 81 | 'category_form': CategoryForm(), 82 | 'tag_form': TagForm(), 83 | } 84 | return render(request, 'edit_post.html', context) 85 | 86 | 87 | @login_required 88 | def new_post(request): 89 | """文章新建页面的视图函数""" 90 | if request.method == 'POST': 91 | post_form = PostForm(request.POST) 92 | if post_form.is_valid(): 93 | post = post_form.save(commit=False) 94 | post.body_html = convert_to_html(post_form.cleaned_data['body_markdown']) 95 | post.author = request.user 96 | post.save() 97 | post_form.save_m2m() 98 | messages.add_message(request, messages.SUCCESS, '文章已发布') 99 | return redirect('post', post.slug) 100 | else: 101 | messages.add_message(request, messages.ERROR, post_form.errors) 102 | context = { 103 | 'post_form': post_form, 104 | 'category_form': CategoryForm(), 105 | 'tag_form': TagForm(), 106 | } 107 | return render(request, 'edit_post.html', context) 108 | context = { 109 | 'post_form': PostForm(), 110 | 'category_form': CategoryForm(), 111 | 'tag_form': TagForm(), 112 | } 113 | return render(request, 'edit_post.html', context) 114 | 115 | 116 | @login_required 117 | def delete_post(request, slug): 118 | """文章删除的视图函数""" 119 | post = get_object_or_404(Post, id=slug) 120 | if request.user.id != post.author.id: 121 | return redirect('post', slug) 122 | post.delete() 123 | return redirect('index') 124 | 125 | 126 | def category_posts(request, category_name): 127 | """分类页面的视图函数""" 128 | category_object = get_object_or_404(Category, category=category_name) 129 | post_list = category_object.post_set.order_by('-id') 130 | paginator = Paginator(post_list, 10) 131 | page = request.GET.get('page') 132 | try: 133 | posts = paginator.page(page) 134 | except InvalidPage: 135 | posts = paginator.page(1) 136 | title = '分类为{0}的文章'.format(category_name) 137 | return render(request, 'index.html', context={'title': title, 'posts': posts}) 138 | 139 | 140 | @login_required 141 | @require_POST 142 | def new_category(request): 143 | """新建分类的处理函数""" 144 | form = CategoryForm(request.POST) 145 | if form.is_valid(): 146 | category = form.save() 147 | result = { 148 | 'status': 'success', 149 | 'category': { 150 | 'id': category.id, 151 | 'category': category.category, 152 | }, 153 | } 154 | return HttpResponse(json.dumps(result), content_type="text/json") 155 | else: 156 | result = { 157 | 'status': 'fail', 158 | 'errors': form.category.errors, 159 | } 160 | return HttpResponse(json.dumps(result), content="text/json") 161 | 162 | 163 | @login_required 164 | @require_POST 165 | def new_tag(request): 166 | """新建标签的处理函数""" 167 | form = TagForm(request.POST) 168 | if form.is_valid(): 169 | tag = form.save() 170 | result = { 171 | 'status': 'success', 172 | 'tag': { 173 | 'id': tag.id, 174 | 'tag': tag.tag, 175 | } 176 | } 177 | return HttpResponse(json.dumps(result), content_type="text/json") 178 | else: 179 | result = { 180 | 'status': 'fail', 181 | 'errors': form.errors, 182 | } 183 | return HttpResponse(json.dumps(result), content="text/json") 184 | 185 | 186 | def tag_posts(request, tagname): 187 | """标签页面的视图函数""" 188 | tag_object = get_object_or_404(Tag, tag=tagname) 189 | post_list = tag_object.post_set.order_by('-id') 190 | paginator = Paginator(post_list, 10) 191 | page = request.GET.get('page') 192 | try: 193 | posts = paginator.page(page) 194 | except InvalidPage: 195 | posts = paginator.page(1) 196 | title = '标签为{0}的文章'.format(tagname) 197 | return render(request, 'index.html', context={'title': title, 'posts': posts}) 198 | 199 | 200 | def archive(request, year, month): 201 | """归档页面的视图函数""" 202 | post_list = Post.objects.filter( 203 | creation_time__year=year, 204 | creation_time__month=month 205 | ).order_by('-id') 206 | paginator = Paginator(post_list, 10) 207 | page = request.GET.get('page') 208 | try: 209 | posts = paginator.page(page) 210 | except InvalidPage: 211 | posts = paginator.page(1) 212 | title = '{0}年{1}月的归档'.format(year, month) 213 | return render(request, 'index.html', context={'title': title, 'posts': posts}) 214 | 215 | 216 | @login_required 217 | def profile(request): 218 | """个人资料页面的视图函数""" 219 | return render(request, 'profile.html') 220 | 221 | 222 | @login_required 223 | def change_profile(request): 224 | """修改个人资料的视图函数""" 225 | current_user = request.user 226 | if request.method == 'POST': 227 | form = EditProfileForm(request.POST) 228 | if form.is_valid(): 229 | current_user.first_name = form.cleaned_data['first_name'] 230 | current_user.last_name = form.cleaned_data['last_name'] 231 | current_user.email = form.cleaned_data['email'] 232 | current_user.save() 233 | messages.add_message(request, messages.SUCCESS, '个人资料已更新') 234 | return redirect('profile') 235 | else: 236 | messages.add_message(request, messages.ERROR, form.errors) 237 | data = { 238 | 'first_name': current_user.first_name, 239 | 'last_name': current_user.last_name, 240 | 'email': current_user.email 241 | } 242 | form = EditProfileForm(data) 243 | return render(request, 'change_profile.html', context={'form': form}) 244 | --------------------------------------------------------------------------------