├── 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 |
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 |
10 |
11 | 你的账户没有权限访问此页。请使用有权限的账户重新登录。
12 |
13 | {% else %}
14 |
15 |
16 | 请登录来查看此页。
17 |
18 | {% endif %}
19 | {% endif %}
20 |
21 | {% if user.is_authenticated %}
22 |
25 | {% else %}
26 |
27 |
30 |
43 | {% if form.errors %}
44 |
45 |
46 | 用户名和密码不匹配, 请重试。
47 |
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 |
34 |
35 | {{ message }}
36 |
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 |
14 | {% for post in posts %}
15 | -
16 |
19 |
20 |
21 | - {{ post.author.username }}
22 | - {{ post.creation_time | date:"c" }}
23 | -
24 | {% for category in post.categories.all %}
25 | {{ category }}
26 | {% endfor %}
27 |
28 | -
29 | {% for tag in post.tags.all %}
30 | {{ tag }}
31 | {% endfor %}
32 |
33 |
34 |
35 |
36 | {{ post.body_html |truncatechars_html:200 | safe }}
37 |
38 |
39 | {% endfor %}
40 |
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 |
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 |
19 |
20 | - {{ post.author.username }}
21 | - {{ post.creation_time | date:"c" }}
22 | -
23 | {% for category in post.categories.all %}
24 | {{ category }}
25 | {% endfor %}
26 |
27 | -
28 | {% for tag in post.tags.all %}
29 | {{ tag }}
30 | {% endfor %}
31 |
32 |
33 |
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 |
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 |
--------------------------------------------------------------------------------
评论列表
47 |48 | {% for comment in comments %} 49 |-
50 |
51 |
52 |
63 |
64 | {% endfor %}
65 |
66 |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 |留下您的评论
67 | 87 |