├── .coveragerc
├── .dockerignore
├── .gitignore
├── .idea
├── .gitignore
├── HelloDjango-blog-tutorial.iml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── blog
├── __init__.py
├── admin.py
├── apps.py
├── elasticsearch2_ik_backend.py
├── feeds.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20190711_1802.py
│ ├── 0003_auto_20191011_2326.py
│ ├── 0004_post_views.py
│ └── __init__.py
├── models.py
├── search_indexes.py
├── static
│ └── blog
│ │ ├── css
│ │ ├── bootstrap.min.css
│ │ ├── custom.css
│ │ └── pace.css
│ │ └── js
│ │ ├── bootstrap.min.js
│ │ ├── jquery-2.1.3.min.js
│ │ ├── modernizr.custom.js
│ │ ├── pace.min.js
│ │ └── script.js
├── templatetags
│ ├── __init__.py
│ └── blog_extras.py
├── tests
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_smoke.py
│ ├── test_templatetags.py
│ ├── test_utils.py
│ └── test_views.py
├── urls.py
├── utils.py
└── views.py
├── blogproject
├── __init__.py
├── settings
│ ├── __init__.py
│ ├── common.py
│ ├── local.py
│ └── production.py
├── urls.py
└── wsgi.py
├── comments
├── __init__.py
├── admin.py
├── apps.py
├── forms.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20191011_2326.py
│ └── __init__.py
├── models.py
├── templatetags
│ ├── __init__.py
│ └── comments_extras.py
├── tests
│ ├── __init__.py
│ ├── base.py
│ ├── test_models.py
│ ├── test_templatetags.py
│ └── test_views.py
├── urls.py
└── views.py
├── compose
├── local
│ ├── Dockerfile
│ └── start.sh
└── production
│ ├── django
│ ├── Dockerfile
│ └── start.sh
│ ├── elasticsearch
│ ├── Dockerfile
│ ├── elasticsearch-analysis-ik-1.10.6.zip
│ ├── elasticsearch-analysis-ik-5.6.16.zip
│ └── elasticsearch.yml
│ └── nginx
│ ├── Dockerfile
│ ├── hellodjango-blog-tutorial.conf-tmpl
│ └── sources.list
├── cover.jpg
├── database
└── readme.md
├── fabfile.py
├── local.yml
├── manage.py
├── production.yml
├── requirements.txt
├── scripts
├── __init__.py
├── fake.py
└── md.sample
└── templates
├── base.html
├── blog
├── detail.html
├── inclusions
│ ├── _archives.html
│ ├── _categories.html
│ ├── _recent_posts.html
│ └── _tags.html
└── index.html
├── comments
├── inclusions
│ ├── _form.html
│ └── _list.html
└── preview.html
├── pure_pagination
└── pagination.html
└── search
├── indexes
└── blog
│ └── post_text.txt
└── search.html
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = .
4 | omit =
5 | _credentials.py
6 | manage.py
7 | blogproject/settings/*
8 | fabfile.py
9 | scripts/fake.py
10 | */migrations/*
11 | blogproject\wsgi.py
12 |
13 | [report]
14 | show_missing = True
15 | skip_covered = True
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .*
2 | _credentials.py
3 | fabfile.py
4 | *.sqlite3
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Python template
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | .hypothesis/
50 | .pytest_cache/
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 | local_settings.py
59 | db.sqlite3
60 |
61 | # Flask stuff:
62 | instance/
63 | .webassets-cache
64 |
65 | # Scrapy stuff:
66 | .scrapy
67 |
68 | # Sphinx documentation
69 | docs/_build/
70 |
71 | # PyBuilder
72 | target/
73 |
74 | # Jupyter Notebook
75 | .ipynb_checkpoints
76 |
77 | # pyenv
78 | .python-version
79 |
80 | # celery beat schedule file
81 | celerybeat-schedule
82 |
83 | # SageMath parsed files
84 | *.sage.py
85 |
86 | # Environments
87 | .env
88 | .venv
89 | env/
90 | venv/
91 | ENV/
92 | env.bak/
93 | venv.bak/
94 |
95 | # Spyder project settings
96 | .spyderproject
97 | .spyproject
98 |
99 | # Rope project settings
100 | .ropeproject
101 |
102 | # mkdocs documentation
103 | /site
104 |
105 | # mypy
106 | *.sqlite3
107 | media/
108 | _credentials.py
109 | .production
110 |
111 | # Pycharm .idea folder, see following link to know which files should be ignored:
112 | # https://www.jetbrains.com/help/pycharm/synchronizing-and-sharing-settings.html#7e81d3cb
113 | .idea/workspace.xml
114 | .idea/dataSources.*
115 | .idea/tasks.xml
116 | .idea/dictionaries/
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Datasource local storage ignored files
3 | /dataSources/
--------------------------------------------------------------------------------
/.idea/HelloDjango-blog-tutorial.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 | fabric = "*"
8 | coverage = "*"
9 |
10 | [packages]
11 | django = "~=2.2"
12 | markdown = "*"
13 | gunicorn = "*"
14 | faker = "*"
15 | django-pure-pagination = "*"
16 | elasticsearch = ">=2,<3"
17 | django-haystack = "*"
18 |
19 | [requires]
20 | python_version = "3"
21 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "3e8e98278df6986cc6156a4f03f636b9a2e0b91ffa35f8fbfafb6957bcbd1720"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "django": {
20 | "hashes": [
21 | "sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a",
22 | "sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038"
23 | ],
24 | "index": "pypi",
25 | "version": "==2.2.10"
26 | },
27 | "django-haystack": {
28 | "hashes": [
29 | "sha256:8b54bcc926596765d0a3383d693bcdd76109c7abb6b2323b3984a39e3576028c"
30 | ],
31 | "index": "pypi",
32 | "version": "==2.8.1"
33 | },
34 | "django-pure-pagination": {
35 | "hashes": [
36 | "sha256:02b42561b8afb09f1fb6ac6dc81db13374f5f748640f31c8160a374274b54713"
37 | ],
38 | "index": "pypi",
39 | "version": "==0.3.0"
40 | },
41 | "elasticsearch": {
42 | "hashes": [
43 | "sha256:bb8f9a365ba6650d599428538c8aed42033264661d8f7d353da59d5892305f72",
44 | "sha256:fead47ebfcaabd1c53dbfc21403eb99ac207eef76de8002fe11a1c8ec9589ce2"
45 | ],
46 | "index": "pypi",
47 | "version": "==2.4.1"
48 | },
49 | "faker": {
50 | "hashes": [
51 | "sha256:440d68fe0e46c1658b1975b2497abe0c24a7f772e3892253f31e713ffcc48965",
52 | "sha256:ee24608768549c2c69e593e9d7a3b53c9498ae735534243ec8390cae5d529f8b"
53 | ],
54 | "index": "pypi",
55 | "version": "==4.0.1"
56 | },
57 | "gunicorn": {
58 | "hashes": [
59 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
60 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
61 | ],
62 | "index": "pypi",
63 | "version": "==20.0.4"
64 | },
65 | "markdown": {
66 | "hashes": [
67 | "sha256:90fee683eeabe1a92e149f7ba74e5ccdc81cd397bd6c516d93a8da0ef90b6902",
68 | "sha256:e4795399163109457d4c5af2183fbe6b60326c17cfdf25ce6e7474c6624f725d"
69 | ],
70 | "index": "pypi",
71 | "version": "==3.2.1"
72 | },
73 | "python-dateutil": {
74 | "hashes": [
75 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
76 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
77 | ],
78 | "version": "==2.8.1"
79 | },
80 | "pytz": {
81 | "hashes": [
82 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
83 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
84 | ],
85 | "version": "==2019.3"
86 | },
87 | "six": {
88 | "hashes": [
89 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
90 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
91 | ],
92 | "version": "==1.14.0"
93 | },
94 | "sqlparse": {
95 | "hashes": [
96 | "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
97 | "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
98 | ],
99 | "version": "==0.3.1"
100 | },
101 | "text-unidecode": {
102 | "hashes": [
103 | "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8",
104 | "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"
105 | ],
106 | "version": "==1.3"
107 | },
108 | "urllib3": {
109 | "hashes": [
110 | "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
111 | "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
112 | ],
113 | "version": "==1.25.8"
114 | }
115 | },
116 | "develop": {
117 | "bcrypt": {
118 | "hashes": [
119 | "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89",
120 | "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42",
121 | "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294",
122 | "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161",
123 | "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752",
124 | "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31",
125 | "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5",
126 | "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c",
127 | "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0",
128 | "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de",
129 | "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e",
130 | "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052",
131 | "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09",
132 | "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105",
133 | "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133",
134 | "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1",
135 | "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7",
136 | "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"
137 | ],
138 | "version": "==3.1.7"
139 | },
140 | "cffi": {
141 | "hashes": [
142 | "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
143 | "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
144 | "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
145 | "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
146 | "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
147 | "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
148 | "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
149 | "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
150 | "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
151 | "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
152 | "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
153 | "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
154 | "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
155 | "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
156 | "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
157 | "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
158 | "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
159 | "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
160 | "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
161 | "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
162 | "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
163 | "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
164 | "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
165 | "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
166 | "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
167 | "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
168 | "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
169 | "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
170 | ],
171 | "version": "==1.14.0"
172 | },
173 | "coverage": {
174 | "hashes": [
175 | "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3",
176 | "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c",
177 | "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0",
178 | "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477",
179 | "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a",
180 | "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf",
181 | "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691",
182 | "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73",
183 | "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987",
184 | "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894",
185 | "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e",
186 | "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef",
187 | "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf",
188 | "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68",
189 | "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8",
190 | "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954",
191 | "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2",
192 | "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40",
193 | "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc",
194 | "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc",
195 | "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e",
196 | "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d",
197 | "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f",
198 | "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc",
199 | "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301",
200 | "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea",
201 | "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb",
202 | "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af",
203 | "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52",
204 | "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37",
205 | "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"
206 | ],
207 | "index": "pypi",
208 | "version": "==5.0.3"
209 | },
210 | "cryptography": {
211 | "hashes": [
212 | "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
213 | "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
214 | "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
215 | "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
216 | "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
217 | "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
218 | "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
219 | "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
220 | "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
221 | "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
222 | "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
223 | "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
224 | "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
225 | "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
226 | "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
227 | "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
228 | "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
229 | "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
230 | "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
231 | "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
232 | "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
233 | ],
234 | "version": "==2.8"
235 | },
236 | "fabric": {
237 | "hashes": [
238 | "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389",
239 | "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"
240 | ],
241 | "index": "pypi",
242 | "version": "==2.5.0"
243 | },
244 | "invoke": {
245 | "hashes": [
246 | "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132",
247 | "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134",
248 | "sha256:de3f23bfe669e3db1085789fd859eb8ca8e0c5d9c20811e2407fa042e8a5e15d"
249 | ],
250 | "version": "==1.4.1"
251 | },
252 | "paramiko": {
253 | "hashes": [
254 | "sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f",
255 | "sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"
256 | ],
257 | "version": "==2.7.1"
258 | },
259 | "pycparser": {
260 | "hashes": [
261 | "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
262 | ],
263 | "version": "==2.19"
264 | },
265 | "pynacl": {
266 | "hashes": [
267 | "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255",
268 | "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c",
269 | "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e",
270 | "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae",
271 | "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621",
272 | "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56",
273 | "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39",
274 | "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310",
275 | "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1",
276 | "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5",
277 | "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a",
278 | "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786",
279 | "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b",
280 | "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b",
281 | "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f",
282 | "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20",
283 | "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415",
284 | "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715",
285 | "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92",
286 | "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1",
287 | "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"
288 | ],
289 | "version": "==1.3.0"
290 | },
291 | "six": {
292 | "hashes": [
293 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
294 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
295 | ],
296 | "version": "==1.14.0"
297 | }
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
HelloDjango-blog-tutorial
6 | 完全免费、开源的 HelloDjango 系列教程之博客开发。
7 | 基于 django 2.2,带你从零开始一步步创建属于自己的博客网站。
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | **特别说明**:本项目不仅仅是教程用的演示项目!我们的目标是开发一个功能完善、测试充分、可用于生产环境的开源博客系统。和其他开源博客系统不同点在于,我们以教程的形式详细记录项目从 0 到 1 的开发过程。
18 |
19 | ## 分支说明
20 |
21 | master 分支为项目的主分支,每一步关键功能的开发都对应一篇详细的教程,并和历史提交以及标签一一对应。例如第一篇教程对应第一个 commit,对应标签为 step1,依次类推。
22 |
23 | ## 资源列表
24 |
25 | - [成品在线预览](https://hellodjango-blog-tutorial-demo.zmrenwu.com/)
26 | - 教程首发 HelloGitHub 微信公众号和 [追梦人物的博客](https://www.zmrenwu.com/),在线学习地址:[HelloDjango - Django博客教程(第二版)](https://zmrenwu.com/courses/hellodjango-blog-tutorial/)
27 | - 项目 [源码仓库](https://github.com/HelloGitHub-Team/HelloDjango-blog-tutorial)
28 | - 项目 [前端模板源码仓库](https://github.com/zmrenwu/django-blog-tutorial-templates)
29 |
30 | ## 本地运行
31 |
32 | 可以使用 Virtualenv、Pipenv、Docker 等在本地运行项目,每种方式都只需运行简单的几条命令就可以了。
33 |
34 | > **注意:**
35 | >
36 | > 因为博客全文搜索功能依赖 Elasticsearch 服务,如果使用 Virtualenv 或者 Pipenv 启动项目而不想搭建 Elasticsearch 服务的话,请先设置环境变量 `ENABLE_HAYSTACK_REALTIME_SIGNAL_PROCESSOR=no` 以关闭实时索引,否则无法创建博客文章。如果关闭实时索引,全文搜索功能将不可用。
37 | >
38 | > Windows 设置环境变量的方式:`set ENABLE_HAYSTACK_REALTIME_SIGNAL_PROCESSOR=no`
39 | >
40 | > Linux 或者 macOS:`export ENABLE_HAYSTACK_REALTIME_SIGNAL_PROCESSOR=no`
41 | >
42 | > 使用 Docker 启动则无需设置,因为会自动启动一个包含 Elasticsearch 服务的 Docker 容器。
43 |
44 | 无论采用何种方式,先克隆代码到本地:
45 |
46 | ```bash
47 | $ git clone https://github.com/HelloGitHub-Team/HelloDjango-blog-tutorial.git
48 | ```
49 |
50 | ### Virtualenv
51 |
52 | 1. 创建虚拟环境并激活虚拟环境,具体方法可参考:[开始进入 django 开发之旅:使用虚拟环境](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/59/#%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E7%8E%AF%E5%A2%83)
53 |
54 | 2. 安装项目依赖
55 |
56 | ```bash
57 | $ cd HelloDjango-blog-tutorial
58 | $ pip install -r requirements.txt
59 | ```
60 |
61 | 3. 迁移数据库
62 |
63 | ```bash
64 | $ python manage.py migrate
65 | ```
66 |
67 | 4. 创建后台管理员账户
68 |
69 | ```bash
70 | $ python manage.py createsuperuser
71 | ```
72 |
73 | 具体请参阅 [创作后台开启,请开始你的表演](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/65/)。
74 |
75 | 5. 运行开发服务器
76 |
77 | ```bash
78 | $ python manage.py runserver
79 | ```
80 |
81 | 6. 浏览器访问 http://127.0.0.1:8000/admin,使用第 4 步创建的管理员账户登录后台发布文章,如何发布文章可参考:[创作后台开启,请开始你的表演](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/65/)。
82 |
83 | 或者执行 fake 脚本批量生成测试数据:
84 |
85 | ```bash
86 | $ python -m scripts.fake
87 | ```
88 |
89 | > 批量脚本会清除全部已有数据,包括第 4 步创建的后台管理员账户。脚本会再默认生成一个管理员账户,用户名和密码都是 admin。
90 |
91 | 9. 浏览器访问:http://127.0.0.1:8000,可进入到博客首页
92 |
93 | ### Pipenv
94 |
95 | 1. 安装 Pipenv(已安装可跳过)
96 |
97 | ```bash
98 | $ pip install pipenv
99 | ```
100 |
101 | 2. 安装项目依赖
102 |
103 | ```bash
104 | $ cd HelloDjango-blog-tutorial
105 | $ pipenv install --dev
106 | ```
107 |
108 | 关于如何使用 Pipenv,参阅:[开始进入 django 开发之旅](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/59/) 的 Pipenv 创建和管理虚拟环境部分。
109 |
110 | 3. 迁移数据库
111 |
112 | 在项目根目录运行如下命令迁移数据库:
113 | ```bash
114 | $ pipenv run python manage.py migrate
115 | ```
116 |
117 | 4. 创建后台管理员账户
118 |
119 | 在项目根目录运行如下命令创建后台管理员账户
120 |
121 | ```bash
122 | $ pipenv run python manage.py createsuperuser
123 | ```
124 |
125 | 具体请参阅 [创作后台开启,请开始你的表演](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/65/)。
126 |
127 | 5. 运行开发服务器
128 |
129 | 在项目根目录运行如下命令开启开发服务器:
130 |
131 | ```bash
132 | $ pipenv run python manage.py runserver
133 | ```
134 |
135 | 6. 浏览器访问 http://127.0.0.1:8000/admin,使用第 4 步创建的管理员账户登录后台发布文章,如何发布文章可参考:[创作后台开启,请开始你的表演](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/65/)。
136 |
137 | 或者执行 fake 脚本批量生成测试数据:
138 |
139 | ```bash
140 | $ pipenv run python -m scripts.fake
141 | ```
142 |
143 | > 批量脚本会清除全部已有数据,包括第 4 步创建的后台管理员账户。脚本会再默认生成一个管理员账户,用户名和密码都是 admin。
144 |
145 | 7. 在浏览器访问:http://127.0.0.1:8000/,可进入到博客首页。
146 |
147 | ### Docker
148 |
149 | 1. 安装 Docker 和 Docker Compose
150 |
151 | 3. 构建和启动容器
152 |
153 | ```bash
154 | $ docker-compose -f local.yml build
155 | $ docker-compose -f local.yml up
156 | ```
157 |
158 | 4. 创建后台管理员账户
159 |
160 | ```bash
161 | $ docker exec -it hellodjango_blog_tutorial_local python manage.py createsuperuser
162 | ```
163 |
164 | 其中 hellodjango_blog_tutorial_local 为项目预定义容器名。
165 |
166 | 4. 浏览器访问 http://127.0.0.1:8000/admin,使用第 3 步创建的管理员账户登录后台发布文章,如何发布文章可参考:[创作后台开启,请开始你的表演](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/65/)。
167 |
168 | 或者执行 fake 脚本批量生成测试数据:
169 |
170 | ```bash
171 | $ docker exec -it hellodjango_blog_tutorial_local python -m scripts.fake
172 | ```
173 |
174 | > 批量脚本会清除全部已有数据,包括第 3 步创建的后台管理员账户。脚本会再默认生成一个管理员账户,用户名和密码都是 admin。
175 |
176 | 5. 为 fake 脚本生成的博客文章创建索引,这样就可以使用 Elasticsearch 服务搜索文章
177 |
178 | ```bash
179 | $ docker exec -it hellodjango_blog_tutorial_local python manage.py rebuild_index
180 | ```
181 |
182 | > 通过 admin 后台添加的文章会自动创建索引。
183 |
184 | 6. 在浏览器访问:http://127.0.0.1:8000/,可进入到博客首页。
185 |
186 | ### 线上部署
187 |
188 | 线上部署的详细文档请参考下方教程目录索引中的 **部署篇** 部分,如果不想了解细节或已了解细节,使用 Docker 仅需以下几个简单步骤就可以上线运行:
189 |
190 | > **小贴士:**
191 | >
192 | > 国内服务器请设置好镜像加速,否则 Docker 构建容器的过程会非常缓慢!具体可参考 **部署篇** Docker 部署 django 中线上部署部分的内容。
193 |
194 | 1. 克隆代码到服务器
195 |
196 | ```bash
197 | $ git clone https://github.com/HelloGitHub-Team/HelloDjango-blog-tutorial.git
198 | ```
199 |
200 | 2. 创建环境变量文件用于存放项目敏感信息
201 |
202 | ```bash
203 | $ cd HelloDjango-blog-tutorial
204 | $ mkdir .envs
205 | $ touch .envs/.production
206 | ```
207 |
208 | 3. 在 .production 文件写入下面的内容并保存
209 |
210 | ```
211 | # django 用于签名和加密等功能的密钥,泄露会严重降低网站的安全性
212 | # 推荐使用这个工具生成:https://miniwebtool.com/django-secret-key-generator/
213 | DJANGO_SECRET_KEY=0p72%e@r3qr$bq%%&bxj#_bem+na2t^0(#((fom6eewrg)gyb^
214 |
215 | # 设置 django 启动时加载的配置文件
216 | DJANGO_SETTINGS_MODULE=blogproject.settings.production
217 | ```
218 |
219 | 4. 修改 Nginx 配置:复制 compose/nginx/hellodjango-blog-tutorial.conf-tmpl 到同一目录,并重命名为 hellodjango-blog-tutorial.conf,修改第 6 行的 server_name 为自己的域名(如果没有域名就改为服务器的公网 ip 地址)
220 |
221 | 5. 启动容器
222 |
223 | ```bash
224 | $ docker-compose -f production.yml up --build -d
225 | ```
226 |
227 | 6. 执行 docker ps 检查容器启动状况,看到如下的 3 个容器说明启动成功:
228 |
229 | - hellodjango_blog_tutorial_nginx
230 | - hellodjango_blog_tutorial_elasticsearch
231 | - hellodjango_blog_tutorial
232 |
233 | 7. 配置 HTTPS 证书(没有配置域名无法配置,只能通过服务器 ip 以 HTTP 协议访问)
234 |
235 | ```bash
236 | $ docker exec -it hellodjango_blog_tutorial_nginx certbot --nginx -n --agree-tos --redirect --email email@hellodjango.com -d hellodjango-blog-tutorial-demo.zmrenwu.com
237 | ```
238 |
239 | 解释一下各参数的含义:
240 |
241 | - --nginx,使用 Nginx 插件
242 | - -n 非交互式,否则会弹出询问框
243 | - --redirect,自动配置 Nginx,将所有 http 请求都重定向到 https
244 | - --email xxx@xxx.com,替换为自己的 email,用于接收通知
245 | - -d 域名列表,开启 https 的域名,替换为自己的域名,多个域名用逗号分隔
246 |
247 | 8. 浏览器访问域名或者服务器 ip 即可进入博客首页
248 |
249 | ## 教程目录索引
250 |
251 | **基础篇**
252 |
253 | 1. [开始进入 django 开发之旅](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/59/)
254 | 2. ["空空如也"的博客应用](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/60/)
255 | 3. [创建 Django 博客的数据库模型](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/61/)
256 | 4. [Django 迁移、操作数据库](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/62/)
257 | 5. [Django 的接客之道](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/63/)
258 | 6. [博客从“裸奔”到“有皮肤”](https://www.zmrenwu.com/courseqs/hellodjango-blog-tutorial/materials/64/)
259 | 7. [创作后台开启,请开始你的表演](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/65/)
260 | 8. [开发博客文章详情页](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/66/)
261 | 9. [让博客支持 Markdown 语法和代码高亮](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/67/)
262 | 10. [Markdown 文章自动生成目录,提升阅读体验](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/68/)
263 | 11. [自动生成文章摘要](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/69/)
264 | 12. [页面侧边栏:使用自定义模板标签](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/70/)
265 | 13. [分类、归档和标签页](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/71/)
266 | 14. [交流的桥梁:评论功能](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/72/)
267 | 15. [优化博客功能细节,提升使用体验](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/73/)
268 |
269 | **部署篇**
270 |
271 | 16. [Nginx+Gunicorn+Supervisor 部署 Django 博客应用](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/74/)
272 | 17. [使用 Fabric 自动化部署](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/75/)
273 | 18. [使用 Certbot 向 Let's Encrypt 免费申请 HTTPS 证书](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/76/)
274 | 19. [使用 Docker 让部署 Django 项目更加轻松](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/77/)
275 |
276 | **进阶篇**
277 |
278 | 20. [开发博客文章阅读量统计功能](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/78/)
279 | 21. [Django 官方推荐的姿势:类视图](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/79/)
280 | 22. [在脚本中使用 ORM:Faker 批量生成测试数据](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/80/)
281 | 23. [通过 Django Pagination 实现简单分页](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/81/)
282 | 24. [稳定易用的 Django 分页库,完善分页功能](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/82/)
283 | 25. [统计各个分类和标签下的文章数](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/83/)
284 | 26. [开启 Django 博客的 RSS 功能](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/84/)
285 | 27. [Django 博客实现简单的全文搜索](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/85/)
286 | 28. [Django Haystack 全文检索与关键词高亮](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/86/)
287 |
288 | **测试篇**
289 |
290 | 29. [单元测试:测试 blog 应用](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/87/)
291 | 30. [单元测试:测试评论应用](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/88/)
292 | 31. [Coverage.py 统计测试覆盖率](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/89/)
293 |
294 | ## 继续学习
295 | 有了以上的 django 基础,让我继续学习 [django REST framework 教程](https://www.zmrenwu.com/courses/django-rest-framework-tutorial/)
296 |
297 | ## 公众号
298 |
299 | 
300 | 欢迎关注 HelloGitHub 公众号,获取更多开源项目的资料和内容。
301 |
302 |
303 |
304 | ## QQ 群
305 |
306 | 加入 QQ 群和更多的 django 开发者进行交流:
307 |
308 | Django学习小组主群:696899473
309 |
310 | ## 版权声明
311 |
312 | 
本作品采用署名-非商业性使用-禁止演绎 4.0 国际 进行许可。
--------------------------------------------------------------------------------
/blog/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/blog/__init__.py
--------------------------------------------------------------------------------
/blog/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Post, Category, Tag
3 |
4 |
5 | class PostAdmin(admin.ModelAdmin):
6 | list_display = ['title', 'created_time', 'modified_time', 'views', 'category', 'author']
7 | fields = ['title', 'body', 'excerpt', 'category', 'tags']
8 |
9 | def save_model(self, request, obj, form, change):
10 | obj.author = request.user
11 | super().save_model(request, obj, form, change)
12 |
13 |
14 | admin.site.register(Post, PostAdmin)
15 | admin.site.register(Category)
16 | admin.site.register(Tag)
17 |
--------------------------------------------------------------------------------
/blog/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class BlogConfig(AppConfig):
5 | name = 'blog'
6 | verbose_name = '博客'
7 |
--------------------------------------------------------------------------------
/blog/elasticsearch2_ik_backend.py:
--------------------------------------------------------------------------------
1 | from haystack.backends.elasticsearch2_backend import Elasticsearch2SearchBackend, Elasticsearch2SearchEngine
2 |
3 | DEFAULT_FIELD_MAPPING = {'type': 'string', "analyzer": "ik_max_word", "search_analyzer": "ik_smart"}
4 |
5 |
6 | class Elasticsearch2IkSearchBackend(Elasticsearch2SearchBackend):
7 |
8 | def __init__(self, *args, **kwargs):
9 | self.DEFAULT_SETTINGS['settings']['analysis']['analyzer']['ik_analyzer'] = {
10 | "type": "custom",
11 | "tokenizer": "ik_max_word",
12 | }
13 | super(Elasticsearch2IkSearchBackend, self).__init__(*args, **kwargs)
14 |
15 |
16 | class Elasticsearch2IkSearchEngine(Elasticsearch2SearchEngine):
17 | backend = Elasticsearch2IkSearchBackend
18 |
--------------------------------------------------------------------------------
/blog/feeds.py:
--------------------------------------------------------------------------------
1 | from django.contrib.syndication.views import Feed
2 |
3 | from .models import Post
4 |
5 |
6 | class AllPostsRssFeed(Feed):
7 | # 显示在聚合阅读器上的标题
8 | title = "HelloDjango-blog-tutorial"
9 |
10 | # 通过聚合阅读器跳转到网站的地址
11 | link = "/"
12 |
13 | # 显示在聚合阅读器上的描述信息
14 | description = "HelloDjango-blog-tutorial 全部文章"
15 |
16 | # 需要显示的内容条目
17 | def items(self):
18 | return Post.objects.all()
19 |
20 | # 聚合器中显示的内容条目的标题
21 | def item_title(self, item):
22 | return "[%s] %s" % (item.category, item.title)
23 |
24 | # 聚合器中显示的内容条目的描述
25 | def item_description(self, item):
26 | return item.body_html
27 |
--------------------------------------------------------------------------------
/blog/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.3 on 2019-07-05 14:06
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
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 | ('name', models.CharField(max_length=100)),
22 | ],
23 | ),
24 | migrations.CreateModel(
25 | name='Tag',
26 | fields=[
27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
28 | ('name', models.CharField(max_length=100)),
29 | ],
30 | ),
31 | migrations.CreateModel(
32 | name='Post',
33 | fields=[
34 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
35 | ('title', models.CharField(max_length=70)),
36 | ('body', models.TextField()),
37 | ('created_time', models.DateTimeField()),
38 | ('modified_time', models.DateTimeField()),
39 | ('excerpt', models.CharField(blank=True, max_length=200)),
40 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
41 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.Category')),
42 | ('tags', models.ManyToManyField(blank=True, to='blog.Tag')),
43 | ],
44 | ),
45 | ]
46 |
--------------------------------------------------------------------------------
/blog/migrations/0002_auto_20190711_1802.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.3 on 2019-07-11 10:02
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import django.utils.timezone
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('blog', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterModelOptions(
17 | name='category',
18 | options={'verbose_name': '分类', 'verbose_name_plural': '分类'},
19 | ),
20 | migrations.AlterModelOptions(
21 | name='post',
22 | options={'verbose_name': '文章', 'verbose_name_plural': '文章'},
23 | ),
24 | migrations.AlterModelOptions(
25 | name='tag',
26 | options={'verbose_name': '标签', 'verbose_name_plural': '标签'},
27 | ),
28 | migrations.AlterField(
29 | model_name='category',
30 | name='name',
31 | field=models.CharField(max_length=100, verbose_name='分类名'),
32 | ),
33 | migrations.AlterField(
34 | model_name='post',
35 | name='author',
36 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者'),
37 | ),
38 | migrations.AlterField(
39 | model_name='post',
40 | name='body',
41 | field=models.TextField(verbose_name='正文'),
42 | ),
43 | migrations.AlterField(
44 | model_name='post',
45 | name='category',
46 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.Category', verbose_name='分类'),
47 | ),
48 | migrations.AlterField(
49 | model_name='post',
50 | name='created_time',
51 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间'),
52 | ),
53 | migrations.AlterField(
54 | model_name='post',
55 | name='excerpt',
56 | field=models.CharField(blank=True, max_length=200, verbose_name='摘要'),
57 | ),
58 | migrations.AlterField(
59 | model_name='post',
60 | name='modified_time',
61 | field=models.DateTimeField(verbose_name='修改时间'),
62 | ),
63 | migrations.AlterField(
64 | model_name='post',
65 | name='tags',
66 | field=models.ManyToManyField(blank=True, to='blog.Tag', verbose_name='标签'),
67 | ),
68 | migrations.AlterField(
69 | model_name='post',
70 | name='title',
71 | field=models.CharField(max_length=70, verbose_name='标题'),
72 | ),
73 | migrations.AlterField(
74 | model_name='tag',
75 | name='name',
76 | field=models.CharField(max_length=100, verbose_name='标签名'),
77 | ),
78 | ]
79 |
--------------------------------------------------------------------------------
/blog/migrations/0003_auto_20191011_2326.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.5 on 2019-10-11 15:26
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('blog', '0002_auto_20190711_1802'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='post',
15 | options={'ordering': ['-created_time'], 'verbose_name': '文章', 'verbose_name_plural': '文章'},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/blog/migrations/0004_post_views.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.4 on 2019-10-20 11:18
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('blog', '0003_auto_20191011_2326'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='post',
15 | name='views',
16 | field=models.PositiveIntegerField(default=0, editable=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/blog/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/blog/migrations/__init__.py
--------------------------------------------------------------------------------
/blog/models.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import markdown
4 | from django.contrib.auth.models import User
5 | from django.db import models
6 | from django.urls import reverse
7 | from django.utils import timezone
8 | from django.utils.functional import cached_property
9 | from django.utils.html import strip_tags
10 | from django.utils.text import slugify
11 | from markdown.extensions.toc import TocExtension
12 |
13 |
14 | def generate_rich_content(value):
15 | md = markdown.Markdown(
16 | extensions=[
17 | "markdown.extensions.extra",
18 | "markdown.extensions.codehilite",
19 | # 记得在顶部引入 TocExtension 和 slugify
20 | TocExtension(slugify=slugify),
21 | ]
22 | )
23 | content = md.convert(value)
24 | m = re.search(r'', md.toc, re.S)
25 | toc = m.group(1) if m is not None else ""
26 | return {"content": content, "toc": toc}
27 |
28 |
29 | class Category(models.Model):
30 | """
31 | Django 要求模型必须继承 models.Model 类。
32 | Category 只需要一个简单的分类名 name 就可以了。
33 | CharField 指定了分类名 name 的数据类型,CharField 是字符型,
34 | CharField 的 max_length 参数指定其最大长度,超过这个长度的分类名就不能被存入数据库。
35 | 当然 Django 还为我们提供了多种其它的数据类型,如日期时间类型 DateTimeField、整数类型 IntegerField 等等。
36 | Django 内置的全部类型可查看文档:
37 | https://docs.djangoproject.com/en/2.2/ref/models/fields/#field-types
38 | """
39 |
40 | name = models.CharField("分类名", max_length=100)
41 |
42 | class Meta:
43 | verbose_name = "分类"
44 | verbose_name_plural = verbose_name
45 |
46 | def __str__(self):
47 | return self.name
48 |
49 |
50 | class Tag(models.Model):
51 | """
52 | 标签 Tag 也比较简单,和 Category 一样。
53 | 再次强调一定要继承 models.Model 类!
54 | """
55 |
56 | name = models.CharField("标签名", max_length=100)
57 |
58 | class Meta:
59 | verbose_name = "标签"
60 | verbose_name_plural = verbose_name
61 |
62 | def __str__(self):
63 | return self.name
64 |
65 |
66 | class Post(models.Model):
67 | """
68 | 文章的数据库表稍微复杂一点,主要是涉及的字段更多。
69 | """
70 |
71 | # 文章标题
72 | title = models.CharField("标题", max_length=70)
73 |
74 | # 文章正文,我们使用了 TextField。
75 | # 存储比较短的字符串可以使用 CharField,但对于文章的正文来说可能会是一大段文本,因此使用 TextField 来存储大段文本。
76 | body = models.TextField("正文")
77 |
78 | # 这两个列分别表示文章的创建时间和最后一次修改时间,存储时间的字段用 DateTimeField 类型。
79 | created_time = models.DateTimeField("创建时间", default=timezone.now)
80 | modified_time = models.DateTimeField("修改时间")
81 |
82 | # 文章摘要,可以没有文章摘要,但默认情况下 CharField 要求我们必须存入数据,否则就会报错。
83 | # 指定 CharField 的 blank=True 参数值后就可以允许空值了。
84 | excerpt = models.CharField("摘要", max_length=200, blank=True)
85 |
86 | # 这是分类与标签,分类与标签的模型我们已经定义在上面。
87 | # 我们在这里把文章对应的数据库表和分类、标签对应的数据库表关联了起来,但是关联形式稍微有点不同。
88 | # 我们规定一篇文章只能对应一个分类,但是一个分类下可以有多篇文章,所以我们使用的是 ForeignKey,即一对多的关联关系。
89 | # 且自 django 2.0 以后,ForeignKey 必须传入一个 on_delete 参数用来指定当关联的数据被删除时,被关联的数据的行为,
90 | # 我们这里假定当某个分类被删除时,该分类下全部文章也同时被删除,因此使用 models.CASCADE 参数,意为级联删除。
91 | # 而对于标签来说,一篇文章可以有多个标签,同一个标签下也可能有多篇文章,所以我们使用 ManyToManyField,表明这是多对多的关联关系。
92 | # 同时我们规定文章可以没有标签,因此为标签 tags 指定了 blank=True。
93 | # 如果你对 ForeignKey、ManyToManyField 不了解,请看教程中的解释,亦可参考官方文档:
94 | # https://docs.djangoproject.com/en/2.2/topics/db/models/#relationships
95 | category = models.ForeignKey(Category, verbose_name="分类", on_delete=models.CASCADE)
96 | tags = models.ManyToManyField(Tag, verbose_name="标签", blank=True)
97 |
98 | # 文章作者,这里 User 是从 django.contrib.auth.models 导入的。
99 | # django.contrib.auth 是 Django 内置的应用,专门用于处理网站用户的注册、登录等流程,User 是 Django 为我们已经写好的用户模型。
100 | # 这里我们通过 ForeignKey 把文章和 User 关联了起来。
101 | # 因为我们规定一篇文章只能有一个作者,而一个作者可能会写多篇文章,因此这是一对多的关联关系,和 Category 类似。
102 | author = models.ForeignKey(User, verbose_name="作者", on_delete=models.CASCADE)
103 |
104 | # 新增 views 字段记录阅读量
105 | views = models.PositiveIntegerField(default=0, editable=False)
106 |
107 | class Meta:
108 | verbose_name = "文章"
109 | verbose_name_plural = verbose_name
110 | ordering = ["-created_time"]
111 |
112 | def __str__(self):
113 | return self.title
114 |
115 | def save(self, *args, **kwargs):
116 | self.modified_time = timezone.now()
117 |
118 | # 首先实例化一个 Markdown 类,用于渲染 body 的文本。
119 | # 由于摘要并不需要生成文章目录,所以去掉了目录拓展。
120 | md = markdown.Markdown(
121 | extensions=["markdown.extensions.extra", "markdown.extensions.codehilite",]
122 | )
123 |
124 | # 先将 Markdown 文本渲染成 HTML 文本
125 | # strip_tags 去掉 HTML 文本的全部 HTML 标签
126 | # 从文本摘取前 54 个字符赋给 excerpt
127 | self.excerpt = strip_tags(md.convert(self.body))[:54]
128 |
129 | super().save(*args, **kwargs)
130 |
131 | # 自定义 get_absolute_url 方法
132 | # 记得从 django.urls 中导入 reverse 函数
133 | def get_absolute_url(self):
134 | return reverse("blog:detail", kwargs={"pk": self.pk})
135 |
136 | def increase_views(self):
137 | self.views += 1
138 | self.save(update_fields=["views"])
139 |
140 | @property
141 | def toc(self):
142 | return self.rich_content.get("toc", "")
143 |
144 | @property
145 | def body_html(self):
146 | return self.rich_content.get("content", "")
147 |
148 | @cached_property
149 | def rich_content(self):
150 | return generate_rich_content(self.body)
151 |
--------------------------------------------------------------------------------
/blog/search_indexes.py:
--------------------------------------------------------------------------------
1 | from haystack import indexes
2 | from .models import Post
3 |
4 |
5 | class PostIndex(indexes.SearchIndex, indexes.Indexable):
6 | text = indexes.CharField(document=True, use_template=True)
7 |
8 | def get_model(self):
9 | return Post
10 |
11 | def index_queryset(self, using=None):
12 | return self.get_model().objects.all()
13 |
--------------------------------------------------------------------------------
/blog/static/blog/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Table of Contents
3 | *
4 | * 1.0 - Google Font
5 | * 2.0 - General Elements
6 | * 3.0 - Site Header
7 | * 3.1 - Logo
8 | * 3.2 - Main Navigation
9 | * 3.2.1 - Main Nav CSS 3 Hover Effect
10 | * 4.0 - Home/Blog
11 | * 4.1 - Read More Button CSS 3 style
12 | * 5.0 - Widget
13 | * 6.0 - Footer
14 | * 7.0 - Header Search Bar
15 | * 8.0 - Mobile Menu
16 | * 9.0 - Contact Page Social
17 | * 10.0 - Contact Form
18 | * 11.0 - Media Query
19 | * 12.0 - Comment
20 | * 13.0 - Pagination
21 | */
22 |
23 | /**
24 | * 1.0 - Google Font
25 | */
26 |
27 | /**
28 | * 2.0 - General Elements
29 | */
30 |
31 | * {
32 | outline: none;
33 | }
34 |
35 | h1,
36 | h2,
37 | h3,
38 | h4,
39 | h5,
40 | h6 {
41 | margin-top: 0;
42 | }
43 |
44 | b {
45 | font-weight: 400;
46 | }
47 |
48 | a {
49 | color: #333;
50 | }
51 |
52 | a:hover, a:focus {
53 | text-decoration: none;
54 | color: #000;
55 | }
56 |
57 | ::selection {
58 | background-color: #eee;
59 | }
60 |
61 | body {
62 | color: #444;
63 | font-family: 'Lato', sans-serif;
64 | }
65 |
66 | p {
67 | font-family: 'Ubuntu', sans-serif;
68 | font-weight: 400;
69 | word-spacing: 1px;
70 | letter-spacing: 0.01em;
71 | }
72 |
73 | #single p,
74 | #page p {
75 | margin-bottom: 25px;
76 | }
77 |
78 | .page-title {
79 | text-align: center;
80 | }
81 |
82 | .title {
83 | margin-bottom: 30px;
84 | }
85 |
86 | figure {
87 | margin-bottom: 25px;
88 | }
89 |
90 | img {
91 | max-width: 100%;
92 | }
93 |
94 | .img-responsive-center img {
95 | margin: 0 auto;
96 | }
97 |
98 | .height-40px {
99 | margin-bottom: 40px;
100 | }
101 |
102 | /**
103 | * 3.0 - Site Header
104 | */
105 |
106 | #site-header {
107 | background-color: #FFF;
108 | padding: 25px 20px;
109 | margin-bottom: 40px;
110 | border-bottom: 1px solid #e7e7e7;
111 | }
112 |
113 | .copyrights {
114 | text-indent: -9999px;
115 | height: 0;
116 | line-height: 0;
117 | font-size: 0;
118 | overflow: hidden;
119 | }
120 |
121 | /**
122 | * 3.1 - Logo
123 | */
124 |
125 | .logo h1 a {
126 | color: #000;
127 | }
128 |
129 | .logo h1 a:hover {
130 | text-decoration: none;
131 | border-bottom: none;
132 | }
133 |
134 | .logo h1 {
135 | margin: 0;
136 | font-family: 'Lato', sans-serif;
137 | font-weight: 300;
138 | }
139 |
140 | /**
141 | * 3.2 - Main Navigation
142 | */
143 |
144 | .main-nav {
145 | margin-top: 11px;
146 | max-width: 95%;
147 | }
148 |
149 | .main-nav a {
150 | color: #000000 !important;
151 | padding: 0 0 5px 0 !important;
152 | margin-right: 30px;
153 | font-family: 'Lato', sans-serif;
154 | font-weight: 300;
155 | font-size: 24px;
156 | }
157 |
158 | .main-nav a:active,
159 | .main-nav a:focus,
160 | .main-nav a:hover {
161 | background-color: transparent !important;
162 | border-bottom: 0;
163 | }
164 |
165 | .navbar-toggle {
166 | margin: 0;
167 | border: 0;
168 | padding: 0;
169 | margin-right: 25px;
170 | }
171 |
172 | .navbar-toggle span {
173 | font-size: 2em;
174 | color: #000;
175 | }
176 |
177 | /**
178 | * 3.2.1 - Main Nav CSS 3 Hover Effect
179 | */
180 |
181 | .cl-effect-11 a {
182 | padding: 10px 0;
183 | color: #0972b4;
184 | text-shadow: none;
185 | }
186 |
187 | .cl-effect-11 a::before {
188 | position: absolute;
189 | top: 0;
190 | left: 0;
191 | overflow: hidden;
192 | padding: 0 0 5px 0 !important;
193 | max-width: 0;
194 | border-bottom: 1px solid #000;
195 | color: #000;
196 | content: attr(data-hover);
197 | white-space: nowrap;
198 | -webkit-transition: max-width 0.5s;
199 | -moz-transition: max-width 0.5s;
200 | transition: max-width 0.5s;
201 | }
202 |
203 | .cl-effect-11 a:hover::before,
204 | .cl-effect-11 a:focus::before {
205 | max-width: 100%;
206 | }
207 |
208 | /**
209 | * 4.0 - Home/Blog
210 | */
211 |
212 | .content-body {
213 | padding-bottom: 4em;
214 | }
215 |
216 | .post {
217 | background: #fff;
218 | padding: 30px 30px 0;
219 | }
220 |
221 | .entry-title {
222 | text-align: center;
223 | font-size: 1.9em;
224 | margin-bottom: 20px;
225 | line-height: 1.6;
226 | padding: 10px 20px 0;
227 | }
228 |
229 | .entry-meta {
230 | text-align: center;
231 | color: #DDDDDD;
232 | font-size: 13px;
233 | margin-bottom: 30px;
234 | }
235 |
236 | .entry-content {
237 | font-size: 18px;
238 | line-height: 1.9;
239 | font-weight: 300;
240 | color: #000;
241 | }
242 |
243 | .post-category::after,
244 | .post-date::after,
245 | .post-author::after,
246 | .comments-link::after {
247 | content: ' ·';
248 | color: #000;
249 | }
250 |
251 | /**
252 | * 4.1 - Read More Button CSS 3 style
253 | */
254 |
255 | .read-more {
256 | font-family: 'Ubuntu', sans-serif;
257 | font-weight: 400;
258 | word-spacing: 1px;
259 | letter-spacing: 0.01em;
260 | text-align: center;
261 | margin-top: 20px;
262 | }
263 |
264 | .cl-effect-14 a {
265 | padding: 0 20px;
266 | height: 45px;
267 | line-height: 45px;
268 | position: relative;
269 | display: inline-block;
270 | margin: 15px 25px;
271 | letter-spacing: 1px;
272 | font-weight: 400;
273 | text-shadow: 0 0 1px rgba(255, 255, 255, 0.3);
274 | }
275 |
276 | .cl-effect-14 a::before,
277 | .cl-effect-14 a::after {
278 | position: absolute;
279 | width: 45px;
280 | height: 1px;
281 | background: #C3C3C3;
282 | content: '';
283 | -webkit-transition: all 0.3s;
284 | -moz-transition: all 0.3s;
285 | transition: all 0.3s;
286 | pointer-events: none;
287 | }
288 |
289 | .cl-effect-14 a::before {
290 | top: 0;
291 | left: 0;
292 | -webkit-transform: rotate(90deg);
293 | -moz-transform: rotate(90deg);
294 | transform: rotate(90deg);
295 | -webkit-transform-origin: 0 0;
296 | -moz-transform-origin: 0 0;
297 | transform-origin: 0 0;
298 | }
299 |
300 | .cl-effect-14 a::after {
301 | right: 0;
302 | bottom: 0;
303 | -webkit-transform: rotate(90deg);
304 | -moz-transform: rotate(90deg);
305 | transform: rotate(90deg);
306 | -webkit-transform-origin: 100% 0;
307 | -moz-transform-origin: 100% 0;
308 | transform-origin: 100% 0;
309 | }
310 |
311 | .cl-effect-14 a:hover::before,
312 | .cl-effect-14 a:hover::after,
313 | .cl-effect-14 a:focus::before,
314 | .cl-effect-14 a:focus::after {
315 | background: #000;
316 | }
317 |
318 | .cl-effect-14 a:hover::before,
319 | .cl-effect-14 a:focus::before {
320 | left: 50%;
321 | -webkit-transform: rotate(0deg) translateX(-50%);
322 | -moz-transform: rotate(0deg) translateX(-50%);
323 | transform: rotate(0deg) translateX(-50%);
324 | }
325 |
326 | .cl-effect-14 a:hover::after,
327 | .cl-effect-14 a:focus::after {
328 | right: 50%;
329 | -webkit-transform: rotate(0deg) translateX(50%);
330 | -moz-transform: rotate(0deg) translateX(50%);
331 | transform: rotate(0deg) translateX(50%);
332 | }
333 |
334 | /**
335 | * 5.0 - Widget
336 | */
337 |
338 | .widget {
339 | background: #fff;
340 | padding: 30px 0 0;
341 | }
342 |
343 | .widget-title {
344 | font-size: 1.5em;
345 | margin-bottom: 20px;
346 | line-height: 1.6;
347 | padding: 10px 0 0;
348 | font-weight: 400;
349 | }
350 |
351 | .widget-recent-posts ul {
352 | padding: 0;
353 | margin: 0;
354 | padding-left: 20px;
355 | }
356 |
357 | .widget-recent-posts ul li {
358 | list-style-type: none;
359 | position: relative;
360 | line-height: 170%;
361 | margin-bottom: 10px;
362 | }
363 |
364 | .widget-recent-posts ul li::before {
365 | content: '\f3d3';
366 | font-family: "Ionicons";
367 | position: absolute;
368 | left: -17px;
369 | top: 3px;
370 | font-size: 16px;
371 | color: #000;
372 | }
373 |
374 | .widget-archives ul {
375 | padding: 0;
376 | margin: 0;
377 | padding-left: 25px;
378 | }
379 |
380 | .widget-archives ul li {
381 | list-style-type: none;
382 | position: relative;
383 | line-height: 170%;
384 | margin-bottom: 10px;
385 | }
386 |
387 | .widget-archives ul li::before {
388 | content: '\f3f3';
389 | font-family: "Ionicons";
390 | position: absolute;
391 | left: -25px;
392 | top: 1px;
393 | font-size: 16px;
394 | color: #000;
395 | }
396 |
397 | .widget-category ul {
398 | padding: 0;
399 | margin: 0;
400 | padding-left: 25px;
401 | }
402 |
403 | .widget-category ul li {
404 | list-style-type: none;
405 | position: relative;
406 | line-height: 170%;
407 | margin-bottom: 10px;
408 | }
409 |
410 | .widget-category ul li::before {
411 | content: '\f3fe';
412 | font-family: "Ionicons";
413 | position: absolute;
414 | left: -25px;
415 | top: 1px;
416 | font-size: 18px;
417 | color: #000;
418 | }
419 |
420 | .widget-tag-cloud ul {
421 | padding: 0;
422 | margin: 0;
423 | margin-right: -10px;
424 | }
425 |
426 | .widget-tag-cloud ul li {
427 | list-style-type: none;
428 | font-size: 13px;
429 | display: inline-block;
430 | margin-right: 10px;
431 | margin-bottom: 10px;
432 | padding: 3px 8px;
433 | border: 1px solid #ddd;
434 | }
435 |
436 | .widget-content ul ul {
437 | margin-top: 10px;
438 | }
439 |
440 | .widget-content ul li {
441 | margin-bottom: 10px;
442 | }
443 |
444 | .rss {
445 | font-size: 21px;
446 | margin-top: 30px;
447 | }
448 |
449 | .rss a {
450 | color: #444;
451 | }
452 |
453 | /**
454 | * 6.0 - Footer
455 | */
456 |
457 | #site-footer {
458 | padding-top: 10px;
459 | padding: 0 0 1.5em 0;
460 | }
461 |
462 | .copyright {
463 | text-align: center;
464 | padding-top: 1em;
465 | margin: 0;
466 | border-top: 1px solid #eee;
467 | color: #666;
468 | }
469 |
470 | /**
471 | * 7.0 - Header Search Bar
472 | */
473 |
474 | #header-search-box {
475 | position: absolute;
476 | right: 38px;
477 | top: 8px;
478 | }
479 |
480 | .search-form {
481 | display: none;
482 | width: 25%;
483 | position: absolute;
484 | min-width: 200px;
485 | right: -6px;
486 | top: 33px;
487 | }
488 |
489 | #search-menu span {
490 | font-size: 20px;
491 | }
492 |
493 | #searchform {
494 | position: relative;
495 | border: 1px solid #ddd;
496 | min-height: 42px;
497 | }
498 |
499 | #searchform input[type=search] {
500 | width: 100%;
501 | border: none;
502 | position: absolute;
503 | left: 0;
504 | padding: 10px 30px 10px 10px;
505 | z-index: 99;
506 | }
507 |
508 | #searchform button {
509 | position: absolute;
510 | right: 6px;
511 | top: 4px;
512 | z-index: 999;
513 | background: transparent;
514 | border: 0;
515 | padding: 0;
516 | }
517 |
518 | #searchform button span {
519 | font-size: 22px;
520 | }
521 |
522 | #search-menu span.ion-ios-close-empty {
523 | font-size: 40px;
524 | line-height: 0;
525 | position: relative;
526 | top: -6px;
527 | }
528 |
529 | /**
530 | * 8.0 - Mobile Menu
531 | */
532 |
533 | .overlay {
534 | position: fixed;
535 | width: 100%;
536 | height: 100%;
537 | top: 0;
538 | left: 0;
539 | background: #fff;
540 | }
541 |
542 | .overlay .overlay-close {
543 | position: absolute;
544 | right: 25px;
545 | top: 10px;
546 | padding: 0;
547 | overflow: hidden;
548 | border: none;
549 | color: transparent;
550 | background-color: transparent;
551 | z-index: 100;
552 | }
553 |
554 | .overlay-hugeinc.open .ion-ios-close-empty {
555 | color: #000;
556 | font-size: 50px;
557 | }
558 |
559 | .overlay nav {
560 | text-align: center;
561 | position: relative;
562 | top: 50%;
563 | height: 60%;
564 | font-size: 54px;
565 | -webkit-transform: translateY(-50%);
566 | transform: translateY(-50%);
567 | }
568 |
569 | .overlay ul {
570 | list-style: none;
571 | padding: 0;
572 | margin: 0 auto;
573 | display: inline-block;
574 | height: 100%;
575 | position: relative;
576 | }
577 |
578 | .overlay ul li {
579 | display: block;
580 | height: 20%;
581 | height: calc(100% / 5);
582 | min-height: 54px;
583 | }
584 |
585 | .overlay ul li a {
586 | font-weight: 300;
587 | display: block;
588 | -webkit-transition: color 0.2s;
589 | transition: color 0.2s;
590 | }
591 |
592 | .overlay ul li a:hover,
593 | .overlay ul li a:focus {
594 | color: #000;
595 | }
596 |
597 | .overlay-hugeinc {
598 | opacity: 0;
599 | visibility: hidden;
600 | -webkit-transition: opacity 0.5s, visibility 0s 0.5s;
601 | transition: opacity 0.5s, visibility 0s 0.5s;
602 | }
603 |
604 | .overlay-hugeinc.open {
605 | opacity: 1;
606 | visibility: visible;
607 | -webkit-transition: opacity 0.5s;
608 | transition: opacity 0.5s;
609 | }
610 |
611 | .overlay-hugeinc nav {
612 | -webkit-perspective: 1200px;
613 | perspective: 1200px;
614 | }
615 |
616 | .overlay-hugeinc nav ul {
617 | opacity: 0.4;
618 | -webkit-transform: translateY(-25%) rotateX(35deg);
619 | transform: translateY(-25%) rotateX(35deg);
620 | -webkit-transition: -webkit-transform 0.5s, opacity 0.5s;
621 | transition: transform 0.5s, opacity 0.5s;
622 | }
623 |
624 | .overlay-hugeinc.open nav ul {
625 | opacity: 1;
626 | -webkit-transform: rotateX(0deg);
627 | transform: rotateX(0deg);
628 | }
629 |
630 | .overlay-hugeinc.close nav ul {
631 | -webkit-transform: translateY(25%) rotateX(-35deg);
632 | transform: translateY(25%) rotateX(-35deg);
633 | }
634 |
635 | /**
636 | * 9.0 - Contact Page Social
637 | */
638 |
639 | .social {
640 | list-style-type: none;
641 | padding: 0;
642 | margin: 0;
643 | text-align: center;
644 | }
645 |
646 | .social li {
647 | display: inline-block;
648 | margin-right: 10px;
649 | margin-bottom: 20px;
650 | }
651 |
652 | .social li a {
653 | border: 1px solid #888;
654 | font-size: 22px;
655 | color: #888;
656 | transition: all 0.3s ease-in;
657 | }
658 |
659 | .social li a:hover {
660 | background-color: #333;
661 | color: #fff;
662 | }
663 |
664 | .facebook a {
665 | padding: 12px 21px;
666 | }
667 |
668 | .twitter a {
669 | padding: 12px 15px;
670 | }
671 |
672 | .google-plus a {
673 | padding: 12px 15px;
674 | }
675 |
676 | .tumblr a {
677 | padding: 12px 20px;
678 | }
679 |
680 | /**
681 | * 10.0 - Contact Form
682 | */
683 |
684 | .contact-form input, .comment-form input {
685 | border: 1px solid #aaa;
686 | margin-bottom: 15px;
687 | width: 100%;
688 | padding: 15px 15px;
689 | font-size: 16px;
690 | line-height: 100%;
691 | transition: 0.4s border-color linear;
692 | }
693 |
694 | .contact-form textarea, .comment-form textarea {
695 | border: 1px solid #aaa;
696 | margin-bottom: 15px;
697 | width: 100%;
698 | padding: 15px 15px;
699 | font-size: 16px;
700 | line-height: 20px !important;
701 | min-height: 183px;
702 | transition: 0.4s border-color linear;
703 | }
704 |
705 | .contact-form input:focus, .comment-form input:focus,
706 | .contact-form textarea:focus, .comment-form textarea:focus {
707 | border-color: #666;
708 | }
709 |
710 | .btn-send {
711 | background: none;
712 | border: 1px solid #aaa;
713 | cursor: pointer;
714 | padding: 25px 80px;
715 | display: inline-block;
716 | letter-spacing: 1px;
717 | position: relative;
718 | transition: all 0.3s;
719 | }
720 |
721 | .btn-5 {
722 | color: #666;
723 | height: 70px;
724 | min-width: 260px;
725 | line-height: 15px;
726 | font-size: 16px;
727 | overflow: hidden;
728 | -webkit-backface-visibility: hidden;
729 | -moz-backface-visibility: hidden;
730 | backface-visibility: hidden;
731 | }
732 |
733 | .btn-5 span {
734 | display: inline-block;
735 | width: 100%;
736 | height: 100%;
737 | -webkit-transition: all 0.3s;
738 | -webkit-backface-visibility: hidden;
739 | -moz-transition: all 0.3s;
740 | -moz-backface-visibility: hidden;
741 | transition: all 0.3s;
742 | backface-visibility: hidden;
743 | }
744 |
745 | .btn-5:before {
746 | position: absolute;
747 | height: 100%;
748 | width: 100%;
749 | line-height: 2.5;
750 | font-size: 180%;
751 | -webkit-transition: all 0.3s;
752 | -moz-transition: all 0.3s;
753 | transition: all 0.3s;
754 | }
755 |
756 | .btn-5:active:before {
757 | color: #703b87;
758 | }
759 |
760 | .btn-5b:hover span {
761 | -webkit-transform: translateX(200%);
762 | -moz-transform: translateX(200%);
763 | -ms-transform: translateX(200%);
764 | transform: translateX(200%);
765 | }
766 |
767 | .btn-5b:before {
768 | left: -100%;
769 | top: 0;
770 | }
771 |
772 | .btn-5b:hover:before {
773 | left: 0;
774 | }
775 |
776 | /**
777 | * 11.0 - Media Query
778 | */
779 |
780 | @media (max-width: 991px) {
781 | .main-nav a {
782 | margin-right: 20px;
783 | }
784 |
785 | #header-search-box {
786 | position: absolute;
787 | right: 20px;
788 | }
789 | }
790 |
791 | @media (max-width: 767px) {
792 | #header-search-box {
793 | right: 20px;
794 | top: 9px;
795 | }
796 |
797 | .main-nav {
798 | margin-top: 2px;
799 | }
800 |
801 | .btn-5 span {
802 | display: none;
803 | }
804 |
805 | .btn-5b:before {
806 | left: 0;
807 | }
808 | }
809 |
810 | @media (max-width: 431px) {
811 | .logo h1 {
812 | margin-top: 8px;
813 | font-size: 24px;
814 | }
815 |
816 | .post {
817 | background: #fff;
818 | padding: 0 10px 0;
819 | }
820 |
821 | .more-link {
822 | font-size: 0.9em;
823 | line-height: 100%;
824 | }
825 | }
826 |
827 | @media screen and (max-height: 30.5em) {
828 | .overlay nav {
829 | height: 70%;
830 | font-size: 34px;
831 | }
832 |
833 | .overlay ul li {
834 | min-height: 34px;
835 | }
836 | }
837 |
838 | /**
839 | * 12.0 - Comment
840 | */
841 | .comment-area {
842 | padding: 0 30px 0;
843 | }
844 |
845 | .comment-form {
846 | margin-top: 15px;
847 | }
848 |
849 | .comment-form .comment-btn {
850 | background-color: #fff;
851 | border: 1px solid #aaa;
852 | font-size: 16px;
853 | padding: 5px 10px;
854 | }
855 |
856 | .comment-list-panel {
857 | margin-top: 30px;
858 | }
859 |
860 | .comment-list {
861 | margin-top: 15px;
862 | }
863 |
864 | .comment-item:not(:last-child) {
865 | border-bottom: 1px #ccc solid;
866 | margin-bottom: 20px;
867 | padding-bottom: 20px;
868 | }
869 |
870 | .comment-item .nickname,
871 | .comment-item .submit-date {
872 | color: #777;
873 | font-size: 14px;
874 | }
875 |
876 | .comment-item .nickname:after {
877 | content: ' ·';
878 | }
879 |
880 | .comment-item .text {
881 | padding-top: 5px;
882 | font-size: 16px;
883 | }
884 |
885 | /**
886 | * 13.0 - Pagination
887 | */
888 | .pagination-simple {
889 | padding-left: 30px;
890 | font-size: 16px;
891 | }
892 |
893 | .pagination ul {
894 | list-style: none;
895 | }
896 |
897 | .pagination ul li {
898 | display: inline-block;
899 | font-size: 16px;
900 | margin-right: 5px;
901 | }
902 |
903 | .current a {
904 | color: red;
905 | }
906 |
--------------------------------------------------------------------------------
/blog/static/blog/css/pace.css:
--------------------------------------------------------------------------------
1 | .pace .pace-progress {
2 | background: #000;
3 | position: fixed;
4 | z-index: 2000;
5 | top: 0;
6 | left: 0;
7 | height: 1px;
8 | transition: width 1s;
9 | }
10 |
11 | .pace-inactive {
12 | display: none;
13 | }
--------------------------------------------------------------------------------
/blog/static/blog/js/bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.3.2 (http://getbootstrap.com)
3 | * Copyright 2011-2015 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.2",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.2",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.2",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.2",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.2",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(b){if(/(38|40|27|32)/.test(b.which)&&!/input|textarea/i.test(b.target.tagName)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var e=c(d),g=e.hasClass("open");if(!g&&27!=b.which||g&&27==b.which)return 27==b.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.divider):visible a",i=e.find('[role="menu"]'+h+', [role="listbox"]'+h);if(i.length){var j=i.index(b.target);38==b.which&&j>0&&j--,40==b.which&&j').prependTo(this.$element).on("click.dismiss.bs.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),f&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;f?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var g=function(){d.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):g()}else b&&b()},c.prototype.handleUpdate=function(){this.options.backdrop&&this.adjustBackdrop(),this.adjustDialog()},c.prototype.adjustBackdrop=function(){this.$backdrop.css("height",0).css("height",this.$element[0].scrollHeight)},c.prototype.adjustDialog=function(){var a=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){this.bodyIsOverflowing=document.body.scrollHeight>document.documentElement.clientHeight,this.scrollbarWidth=this.measureScrollbar()},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css("padding-right")||0,10);this.bodyIsOverflowing&&this.$body.css("padding-right",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css("padding-right","")},c.prototype.measureScrollbar=function(){var a=document.createElement("div");a.className="modal-scrollbar-measure",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(c){var d=a(this),e=d.attr("href"),f=a(d.attr("data-target")||e&&e.replace(/.*(?=#[^\s]+$)/,"")),g=f.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is("a")&&c.preventDefault(),f.one("show.bs.modal",function(a){a.isDefaultPrevented()||f.one("hidden.bs.modal",function(){d.is(":visible")&&d.trigger("focus")})}),b.call(f,g,this)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof b&&b;(e||"destroy"!=b)&&(e||d.data("bs.tooltip",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};c.VERSION="3.3.2",c.TRANSITION_DURATION=150,c.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-mp.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||"destroy"!=b)&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.2",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},c.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){var e=a.proxy(this.process,this);this.$body=a("body"),this.$scrollElement=a(a(c).is("body")?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",e),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.2",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b="offset",c=0;a.isWindow(this.$scrollElement[0])||(b="position",c=this.$scrollElement.scrollTop()),this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight();var d=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+c,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){d.offsets.push(this[0]),d.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,this.clear();var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")},b.prototype.clear=function(){a(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.3.2",c.TRANSITION_DURATION=150,c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a"),f=a.Event("hide.bs.tab",{relatedTarget:b[0]}),g=a.Event("show.bs.tab",{relatedTarget:e[0]});if(e.trigger(f),b.trigger(g),!g.isDefaultPrevented()&&!f.isDefaultPrevented()){var h=a(d);this.activate(b.closest("li"),c),this.activate(h,h.parent(),function(){e.trigger({type:"hidden.bs.tab",relatedTarget:b[0]}),b.trigger({type:"shown.bs.tab",relatedTarget:e[0]})})}}},c.prototype.activate=function(b,d,e){function f(){g.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()
7 | }var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.2",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a("body").height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);
--------------------------------------------------------------------------------
/blog/static/blog/js/modernizr.custom.js:
--------------------------------------------------------------------------------
1 | /* Modernizr 2.7.1 (Custom Build) | MIT & BSD
2 | * Build: http://modernizr.com/download/#-csstransitions-shiv-cssclasses-prefixed-testprop-testallprops-domprefixes-load
3 | */
4 | ;window.Modernizr=function(a,b,c){function x(a){j.cssText=a}function y(a,b){return x(prefixes.join(a+";")+(b||""))}function z(a,b){return typeof a===b}function A(a,b){return!!~(""+a).indexOf(b)}function B(a,b){for(var d in a){var e=a[d];if(!A(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function C(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:z(f,"function")?f.bind(d||b):f}return!1}function D(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+n.join(d+" ")+d).split(" ");return z(b,"string")||z(b,"undefined")?B(e,b):(e=(a+" "+o.join(d+" ")+d).split(" "),C(e,b,c))}var d="2.7.1",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m="Webkit Moz O ms",n=m.split(" "),o=m.toLowerCase().split(" "),p={},q={},r={},s=[],t=s.slice,u,v={}.hasOwnProperty,w;!z(v,"undefined")&&!z(v.call,"undefined")?w=function(a,b){return v.call(a,b)}:w=function(a,b){return b in a&&z(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=t.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(t.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(t.call(arguments)))};return e}),p.csstransitions=function(){return D("transition")};for(var E in p)w(p,E)&&(u=E.toLowerCase(),e[u]=p[E](),s.push((e[u]?"":"no-")+u));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)w(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},x(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._domPrefixes=o,e._cssomPrefixes=n,e.testProp=function(a){return B([a])},e.testAllProps=D,e.prefixed=function(a,b,c){return b?D(a,b,c):D(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+s.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;fb;b++)if(b in this&&this[b]===a)return b;return-1};for(t={catchupTime:500,initialRate:.03,minTime:500,ghostTime:500,maxProgressPerFrame:10,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},B=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},D=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,s=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==D&&(D=function(a){return setTimeout(a,50)},s=function(a){return clearTimeout(a)}),F=function(a){var b,c;return b=B(),(c=function(){var d;return d=B()-b,d>=33?(b=B(),a(d,function(){return D(c)})):setTimeout(c,33-d)})()},E=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?W.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},u=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?W.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)X.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?u(b[a],e):b[a]=e);return b},p=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},w=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];cP;P++)J=T[P],C[J]===!0&&(C[J]=t[J]);i=function(a){function b(){return U=b.__super__.constructor.apply(this,arguments)}return Y(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(C.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='\n',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b;return null==document.querySelector(C.target)?!1:(a=this.getElement(),a.children[0].style.width=""+this.progress+"%",(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?b="99":(b=this.progress<10?"0":"",b+=0|this.progress),a.children[0].setAttribute("data-progress",""+b)),this.lastRenderedProgress=this.progress)},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),O=window.XMLHttpRequest,N=window.XDomainRequest,M=window.WebSocket,v=function(a,b){var c,d,e,f;f=[];for(d in b.prototype)try{e=b.prototype[d],f.push(null==a[d]&&"function"!=typeof e?a[d]=e:void 0)}catch(g){c=g}return f},z=[],Pace.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?W.call(arguments,1):[],z.unshift("ignore"),c=b.apply(null,a),z.shift(),c},Pace.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?W.call(arguments,1):[],z.unshift("track"),c=b.apply(null,a),z.shift(),c},I=function(a){var b;if(null==a&&(a="GET"),"track"===z[0])return"force";if(!z.length&&C.ajax){if("socket"===a&&C.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),Z.call(C.ajax.trackMethods,b)>=0)return!0}return!1},j=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e){return I(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new O(b),a(c),c};try{v(window.XMLHttpRequest,O)}catch(d){}if(null!=N){window.XDomainRequest=function(){var b;return b=new N,a(b),b};try{v(window.XDomainRequest,N)}catch(d){}}if(null!=M&&C.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new M(a,b):new M(a),I("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{v(window.WebSocket,M)}catch(d){}}}return Y(b,a),b}(h),Q=null,x=function(){return null==Q&&(Q=new j),Q},H=function(a){var b,c,d,e;for(e=C.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},x().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,H(g)?void 0:Pace.running||C.restartOnRequestAfter===!1&&"force"!==I(f)?void 0:(d=arguments,c=C.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,j;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(Pace.restart(),i=Pace.sources,j=[],c=0,g=i.length;g>c;c++){if(J=i[c],J instanceof a){J.watch.apply(J,d);break}j.push(void 0)}return j}},c))}),a=function(){function a(){var a=this;this.elements=[],x().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,H(e)?void 0:(c="socket"===d?new m(b):new n(b),this.elements.push(c))},a}(),n=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return h.progress=a.lengthComputable?100*a.loaded/a.total:h.progress+(100-h.progress)/2}),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100});else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),m=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100})}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},C.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=B(),b=setInterval(function(){var g;return g=B()-c-50,c=B(),e.push(g),e.length>C.eventLag.sampleCount&&e.shift(),a=p(e),++d>=C.eventLag.minSamples&&a=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/C.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,C.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+C.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),K=null,G=null,q=null,L=null,o=null,r=null,Pace.running=!1,y=function(){return C.restartOnPushState?Pace.restart():void 0},null!=window.history.pushState&&(S=window.history.pushState,window.history.pushState=function(){return y(),S.apply(window.history,arguments)}),null!=window.history.replaceState&&(V=window.history.replaceState,window.history.replaceState=function(){return y(),V.apply(window.history,arguments)}),k={ajax:a,elements:d,document:c,eventLag:f},(A=function(){var a,c,d,e,f,g,h,i;for(Pace.sources=K=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],C[a]!==!1&&K.push(new k[a](C[a]));for(i=null!=(h=C.extraSources)?h:[],d=0,f=i.length;f>d;d++)J=i[d],K.push(new J(C));return Pace.bar=q=new b,G=[],L=new l})(),Pace.stop=function(){return Pace.trigger("stop"),Pace.running=!1,q.destroy(),r=!0,null!=o&&("function"==typeof s&&s(o),o=null),A()},Pace.restart=function(){return Pace.trigger("restart"),Pace.stop(),Pace.start()},Pace.go=function(){var a;return Pace.running=!0,q.render(),a=B(),r=!1,o=F(function(b,c){var d,e,f,g,h,i,j,k,m,n,o,p,s,t,u,v;for(k=100-q.progress,e=o=0,f=!0,i=p=0,t=K.length;t>p;i=++p)for(J=K[i],n=null!=G[i]?G[i]:G[i]=[],h=null!=(v=J.elements)?v:[J],j=s=0,u=h.length;u>s;j=++s)g=h[j],m=null!=n[j]?n[j]:n[j]=new l(g),f&=m.done,m.done||(e++,o+=m.tick(b));return d=o/e,q.update(L.tick(b,d)),q.done()||f||r?(q.update(100),Pace.trigger("done"),setTimeout(function(){return q.finish(),Pace.running=!1,Pace.trigger("hide")},Math.max(C.ghostTime,Math.max(C.minTime-(B()-a),0)))):c()})},Pace.start=function(a){u(C,a),Pace.running=!0;try{q.render()}catch(b){i=b}return document.querySelector(".pace")?(Pace.trigger("start"),Pace.go()):setTimeout(Pace.start,50)},"function"==typeof define&&define.amd?define(function(){return Pace}):"object"==typeof exports?module.exports=Pace:C.startOnPageLoad&&Pace.start()}).call(this);
--------------------------------------------------------------------------------
/blog/static/blog/js/script.js:
--------------------------------------------------------------------------------
1 | var searchvisible = 0;
2 |
3 | $("#search-menu").click(function(e){
4 | //This stops the page scrolling to the top on a # link.
5 | e.preventDefault();
6 |
7 | var val = $('#search-icon');
8 | if(val.hasClass('ion-ios-search-strong')){
9 | val.addClass('ion-ios-close-empty');
10 | val.removeClass('ion-ios-search-strong');
11 | }
12 | else{
13 | val.removeClass('ion-ios-close-empty');
14 | val.addClass('ion-ios-search-strong');
15 | }
16 |
17 |
18 | if (searchvisible ===0) {
19 | //Search is currently hidden. Slide down and show it.
20 | $("#search-form").slideDown(200);
21 | $("#s").focus(); //Set focus on the search input field.
22 | searchvisible = 1; //Set search visible flag to visible.
23 | }
24 |
25 | else {
26 | //Search is currently showing. Slide it back up and hide it.
27 | $("#search-form").slideUp(200);
28 | searchvisible = 0;
29 | }
30 | });
31 |
32 | /*!
33 | * classie - class helper functions
34 | * from bonzo https://github.com/ded/bonzo
35 | *
36 | * classie.has( elem, 'my-class' ) -> true/false
37 | * classie.add( elem, 'my-new-class' )
38 | * classie.remove( elem, 'my-unwanted-class' )
39 | * classie.toggle( elem, 'my-class' )
40 | */
41 |
42 | /*jshint browser: true, strict: true, undef: true */
43 | /*global define: false */
44 |
45 | ( function( window ) {
46 |
47 | 'use strict';
48 |
49 | // class helper functions from bonzo https://github.com/ded/bonzo
50 |
51 | function classReg( className ) {
52 | return new RegExp("(^|\\s+)" + className + "(\\s+|$)");
53 | }
54 |
55 | // classList support for class management
56 | // altho to be fair, the api sucks because it won't accept multiple classes at once
57 | var hasClass, addClass, removeClass;
58 |
59 | if ( 'classList' in document.documentElement ) {
60 | hasClass = function( elem, c ) {
61 | return elem.classList.contains( c );
62 | };
63 | addClass = function( elem, c ) {
64 | elem.classList.add( c );
65 | };
66 | removeClass = function( elem, c ) {
67 | elem.classList.remove( c );
68 | };
69 | }
70 | else {
71 | hasClass = function( elem, c ) {
72 | return classReg( c ).test( elem.className );
73 | };
74 | addClass = function( elem, c ) {
75 | if ( !hasClass( elem, c ) ) {
76 | elem.className = elem.className + ' ' + c;
77 | }
78 | };
79 | removeClass = function( elem, c ) {
80 | elem.className = elem.className.replace( classReg( c ), ' ' );
81 | };
82 | }
83 |
84 | function toggleClass( elem, c ) {
85 | var fn = hasClass( elem, c ) ? removeClass : addClass;
86 | fn( elem, c );
87 | }
88 |
89 | var classie = {
90 | // full names
91 | hasClass: hasClass,
92 | addClass: addClass,
93 | removeClass: removeClass,
94 | toggleClass: toggleClass,
95 | // short names
96 | has: hasClass,
97 | add: addClass,
98 | remove: removeClass,
99 | toggle: toggleClass
100 | };
101 |
102 | // transport
103 | if ( typeof define === 'function' && define.amd ) {
104 | // AMD
105 | define( classie );
106 | } else {
107 | // browser global
108 | window.classie = classie;
109 | }
110 |
111 | })( window );
112 |
113 | (function() {
114 | var triggerBttn = document.getElementById( 'trigger-overlay' ),
115 | overlay = document.querySelector( 'div.overlay' ),
116 | closeBttn = overlay.querySelector( 'button.overlay-close' );
117 | transEndEventNames = {
118 | 'WebkitTransition': 'webkitTransitionEnd',
119 | 'MozTransition': 'transitionend',
120 | 'OTransition': 'oTransitionEnd',
121 | 'msTransition': 'MSTransitionEnd',
122 | 'transition': 'transitionend'
123 | },
124 | transEndEventName = transEndEventNames[ Modernizr.prefixed( 'transition' ) ],
125 | support = { transitions : Modernizr.csstransitions };
126 |
127 | function toggleOverlay() {
128 | if( classie.has( overlay, 'open' ) ) {
129 | classie.remove( overlay, 'open' );
130 | classie.add( overlay, 'close' );
131 | var onEndTransitionFn = function( ev ) {
132 | if( support.transitions ) {
133 | if( ev.propertyName !== 'visibility' ) return;
134 | this.removeEventListener( transEndEventName, onEndTransitionFn );
135 | }
136 | classie.remove( overlay, 'close' );
137 | };
138 | if( support.transitions ) {
139 | overlay.addEventListener( transEndEventName, onEndTransitionFn );
140 | }
141 | else {
142 | onEndTransitionFn();
143 | }
144 | }
145 | else if( !classie.has( overlay, 'close' ) ) {
146 | classie.add( overlay, 'open' );
147 | }
148 | }
149 |
150 | triggerBttn.addEventListener( 'click', toggleOverlay );
151 | closeBttn.addEventListener( 'click', toggleOverlay );
152 | })();
--------------------------------------------------------------------------------
/blog/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/blog/templatetags/__init__.py
--------------------------------------------------------------------------------
/blog/templatetags/blog_extras.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.db.models.aggregates import Count
3 |
4 | from ..models import Post, Category, Tag
5 |
6 | register = template.Library()
7 |
8 |
9 | @register.inclusion_tag('blog/inclusions/_recent_posts.html', takes_context=True)
10 | def show_recent_posts(context, num=5):
11 | return {
12 | 'recent_post_list': Post.objects.all()[:num],
13 | }
14 |
15 |
16 | @register.inclusion_tag('blog/inclusions/_archives.html', takes_context=True)
17 | def show_archives(context):
18 | return {
19 | 'date_list': Post.objects.dates('created_time', 'month', order='DESC'),
20 | }
21 |
22 |
23 | @register.inclusion_tag('blog/inclusions/_categories.html', takes_context=True)
24 | def show_categories(context):
25 | category_list = Category.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)
26 | return {
27 | 'category_list': category_list,
28 | }
29 |
30 |
31 | @register.inclusion_tag('blog/inclusions/_tags.html', takes_context=True)
32 | def show_tags(context):
33 | tag_list = Tag.objects.annotate(num_posts=Count('post')).filter(num_posts__gt=0)
34 | return {
35 | 'tag_list': tag_list,
36 | }
37 |
--------------------------------------------------------------------------------
/blog/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/blog/tests/__init__.py
--------------------------------------------------------------------------------
/blog/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.contrib.auth.models import User
3 | from django.test import TestCase
4 | from django.urls import reverse
5 |
6 | from ..models import Category, Post, Tag
7 | from ..search_indexes import PostIndex
8 |
9 |
10 | class CategoryModelTestCase(TestCase):
11 | def setUp(self):
12 | self.cate = Category.objects.create(name="测试")
13 |
14 | def test_str_representation(self):
15 | self.assertEqual(self.cate.__str__(), self.cate.name)
16 |
17 |
18 | class TagModelTestCase(TestCase):
19 | def setUp(self):
20 | self.tag = Tag.objects.create(name="测试")
21 |
22 | def test_str_representation(self):
23 | self.assertEqual(self.tag.__str__(), self.tag.name)
24 |
25 |
26 | class PostModelTestCase(TestCase):
27 | def setUp(self):
28 | # 断开 haystack 的 signal,测试生成的文章无需生成索引
29 | apps.get_app_config("haystack").signal_processor.teardown()
30 | user = User.objects.create_superuser(
31 | username="admin", email="admin@hellogithub.com", password="admin"
32 | )
33 | cate = Category.objects.create(name="测试")
34 | self.post = Post.objects.create(
35 | title="测试标题", body="测试内容", category=cate, author=user,
36 | )
37 |
38 | def test_str_representation(self):
39 | self.assertEqual(self.post.__str__(), self.post.title)
40 |
41 | def test_auto_populate_modified_time(self):
42 | self.assertIsNotNone(self.post.modified_time)
43 |
44 | old_post_modified_time = self.post.modified_time
45 | self.post.body = "新的测试内容"
46 | self.post.save()
47 | self.post.refresh_from_db()
48 | self.assertTrue(self.post.modified_time > old_post_modified_time)
49 |
50 | def test_auto_populate_excerpt(self):
51 | self.assertIsNotNone(self.post.excerpt)
52 | self.assertTrue(0 < len(self.post.excerpt) <= 54)
53 |
54 | def test_get_absolute_url(self):
55 | expected_url = reverse("blog:detail", kwargs={"pk": self.post.pk})
56 | self.assertEqual(self.post.get_absolute_url(), expected_url)
57 |
58 | def test_increase_views(self):
59 | self.post.increase_views()
60 | self.post.refresh_from_db()
61 | self.assertEqual(self.post.views, 1)
62 |
63 | self.post.increase_views()
64 | self.post.refresh_from_db()
65 | self.assertEqual(self.post.views, 2)
66 |
67 |
68 | class SearchIndexesTestCase(TestCase):
69 | def setUp(self):
70 | apps.get_app_config("haystack").signal_processor.teardown()
71 | user = User.objects.create_superuser(
72 | username="admin", email="admin@hellogithub.com", password="admin"
73 | )
74 | cate = Category.objects.create(name="测试")
75 | Post.objects.create(
76 | title="测试标题", body="测试内容", category=cate, author=user,
77 | )
78 | another_cate = Category.objects.create(name="另一个测试")
79 | Post.objects.create(
80 | title="另一个测试标题", body="另一个测试内容", category=another_cate, author=user,
81 | )
82 | self.index_instance = PostIndex()
83 |
84 | def test_get_model(self):
85 | self.assertTrue(issubclass(self.index_instance.get_model(), Post))
86 |
87 | def test_index_queryset(self):
88 | expected_qs = Post.objects.all()
89 | self.assertQuerysetEqual(
90 | self.index_instance.index_queryset(), [repr(p) for p in expected_qs]
91 | )
92 |
--------------------------------------------------------------------------------
/blog/tests/test_smoke.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 |
4 | class SmokeTestCase(TestCase):
5 | def test_smoke(self):
6 | self.assertEqual(1 + 1, 2)
7 |
--------------------------------------------------------------------------------
/blog/tests/test_templatetags.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from django.apps import apps
4 | from django.contrib.auth.models import User
5 | from django.template import Context, Template
6 | from django.test import TestCase
7 | from django.urls import reverse
8 | from django.utils import timezone
9 |
10 | from ..models import Category, Post, Tag
11 | from ..templatetags.blog_extras import (
12 | show_archives,
13 | show_categories,
14 | show_recent_posts,
15 | show_tags,
16 | )
17 |
18 |
19 | class BlogExtrasTestCase(TestCase):
20 | def setUp(self):
21 | apps.get_app_config("haystack").signal_processor.teardown()
22 | self.user = User.objects.create_superuser(
23 | username="admin", email="admin@hellogithub.com", password="admin"
24 | )
25 | self.cate = Category.objects.create(name="测试")
26 | self.ctx = Context()
27 |
28 | def test_show_recent_posts_without_any_post(self):
29 | template = Template("{% load blog_extras %}" "{% show_recent_posts %}")
30 | expected_html = template.render(self.ctx)
31 | self.assertInHTML('', expected_html)
32 | self.assertInHTML("暂无文章!", expected_html)
33 |
34 | def test_show_recent_posts_with_posts(self):
35 | post = Post.objects.create(
36 | title="测试标题", body="测试内容", category=self.cate, author=self.user,
37 | )
38 | context = Context(show_recent_posts(self.ctx))
39 | template = Template("{% load blog_extras %}" "{% show_recent_posts %}")
40 | expected_html = template.render(context)
41 | self.assertInHTML('', expected_html)
42 | self.assertInHTML(
43 | '{}'.format(post.get_absolute_url(), post.title),
44 | expected_html,
45 | )
46 |
47 | def test_show_recent_posts_nums_specified(self):
48 | post_list = []
49 | for i in range(7):
50 | post = Post.objects.create(
51 | title="测试标题-{}".format(i),
52 | body="测试内容",
53 | category=self.cate,
54 | author=self.user,
55 | )
56 | post_list.insert(0, post)
57 | context = Context(show_recent_posts(self.ctx, 3))
58 | template = Template("{% load blog_extras %}" "{% show_recent_posts %}")
59 | expected_html = template.render(context)
60 | self.assertInHTML('', expected_html)
61 | self.assertInHTML(
62 | '{}'.format(
63 | post_list[0].get_absolute_url(), post_list[0].title
64 | ),
65 | expected_html,
66 | )
67 | self.assertInHTML(
68 | '{}'.format(
69 | post_list[1].get_absolute_url(), post_list[1].title
70 | ),
71 | expected_html,
72 | )
73 | self.assertInHTML(
74 | '{}'.format(
75 | post_list[2].get_absolute_url(), post_list[2].title
76 | ),
77 | expected_html,
78 | )
79 |
80 | def test_show_categories_without_any_category(self):
81 | self.cate.delete()
82 | context = Context(show_categories(self.ctx))
83 | template = Template("{% load blog_extras %}" "{% show_categories %}")
84 | expected_html = template.render(context)
85 | self.assertInHTML('', expected_html)
86 | self.assertInHTML("暂无分类!", expected_html)
87 |
88 | def test_show_categories_with_categories(self):
89 | cate_with_posts = Category.objects.create(name="有文章的分类")
90 | Post.objects.create(
91 | title="测试标题-1", body="测试内容", category=cate_with_posts, author=self.user,
92 | )
93 | another_cate_with_posts = Category.objects.create(name="另一个有文章的分类")
94 | Post.objects.create(
95 | title="测试标题-2",
96 | body="测试内容",
97 | category=another_cate_with_posts,
98 | author=self.user,
99 | )
100 | context = Context(show_categories(self.ctx))
101 | template = Template("{% load blog_extras %}" "{% show_categories %}")
102 | expected_html = template.render(context)
103 | self.assertInHTML('', expected_html)
104 |
105 | url = reverse("blog:category", kwargs={"pk": cate_with_posts.pk})
106 | num_posts = cate_with_posts.post_set.count()
107 | frag = '{} ({})'.format(
108 | url, cate_with_posts.name, num_posts
109 | )
110 | self.assertInHTML(frag, expected_html)
111 |
112 | url = reverse("blog:category", kwargs={"pk": another_cate_with_posts.pk})
113 | num_posts = another_cate_with_posts.post_set.count()
114 | frag = '{} ({})'.format(
115 | url, another_cate_with_posts.name, num_posts
116 | )
117 | self.assertInHTML(frag, expected_html)
118 |
119 | def test_show_tags_without_any_tag(self):
120 | context = Context(show_tags(self.ctx))
121 | template = Template("{% load blog_extras %}" "{% show_tags %}")
122 | expected_html = template.render(context)
123 | self.assertInHTML('', expected_html)
124 | self.assertInHTML("暂无标签!", expected_html)
125 |
126 | def test_show_tags_with_tags(self):
127 | tag1 = Tag.objects.create(name="测试1")
128 | tag2 = Tag.objects.create(name="测试2")
129 | tag3 = Tag.objects.create(name="测试3")
130 | tag2_post = Post.objects.create(
131 | title="测试标题", body="测试内容", category=self.cate, author=self.user,
132 | )
133 | tag2_post.tags.add(tag2)
134 | tag2_post.save()
135 |
136 | another_tag2_post = Post.objects.create(
137 | title="测试标题", body="测试内容", category=self.cate, author=self.user,
138 | )
139 | another_tag2_post.tags.add(tag2)
140 | another_tag2_post.save()
141 |
142 | tag3_post = Post.objects.create(
143 | title="测试标题", body="测试内容", category=self.cate, author=self.user,
144 | )
145 | tag3_post.tags.add(tag3)
146 | tag3_post.save()
147 |
148 | context = Context(show_tags(self.ctx))
149 | template = Template("{% load blog_extras %}" "{% show_tags %}")
150 | expected_html = template.render(context)
151 | self.assertInHTML('', expected_html)
152 |
153 | tag2_url = reverse("blog:tag", kwargs={"pk": tag2.pk})
154 | tag2_num_posts = tag2.post_set.count()
155 | frag = '{} ({})'.format(
156 | tag2_url, tag2.name, tag2_num_posts
157 | )
158 | self.assertInHTML(frag, expected_html)
159 |
160 | tag3_url = reverse("blog:tag", kwargs={"pk": tag3.pk})
161 | tag3_num_posts = tag3.post_set.count()
162 | frag = '{} ({})'.format(
163 | tag3_url, tag3.name, tag3_num_posts
164 | )
165 | self.assertInHTML(frag, expected_html)
166 |
167 | def test_show_archives_without_any_post(self):
168 | context = Context(show_archives(self.ctx))
169 | template = Template("{% load blog_extras %}" "{% show_archives %}")
170 | expected_html = template.render(context)
171 | self.assertInHTML('', expected_html)
172 | self.assertInHTML("暂无归档!", expected_html)
173 |
174 | def test_show_archives_with_post(self):
175 | post1 = Post.objects.create(
176 | title="测试标题-1",
177 | body="测试内容",
178 | category=self.cate,
179 | author=self.user,
180 | created_time=timezone.now(),
181 | )
182 | post2 = Post.objects.create(
183 | title="测试标题-1",
184 | body="测试内容",
185 | category=self.cate,
186 | author=self.user,
187 | created_time=timezone.now() - timedelta(days=50),
188 | )
189 |
190 | context = Context(show_archives(self.ctx))
191 | template = Template("{% load blog_extras %}" "{% show_archives %}")
192 | expected_html = template.render(context)
193 | self.assertInHTML('', expected_html)
194 |
195 | created_time = post1.created_time
196 | url = reverse(
197 | "blog:archive",
198 | kwargs={"year": created_time.year, "month": created_time.month},
199 | )
200 | frag = '{} 年 {} 月'.format(
201 | url, created_time.year, created_time.month
202 | )
203 | self.assertInHTML(frag, expected_html)
204 |
205 | created_time = post2.created_time
206 | url = reverse(
207 | "blog:archive",
208 | kwargs={"year": created_time.year, "month": created_time.month},
209 | )
210 | frag = '{} 年 {} 月'.format(
211 | url, created_time.year, created_time.month
212 | )
213 | self.assertInHTML(frag, expected_html)
214 |
--------------------------------------------------------------------------------
/blog/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from ..utils import Highlighter
3 |
4 |
5 | class HighlighterTestCase(TestCase):
6 | def test_highlight(self):
7 | document = "这是一个比较长的标题,用于测试关键词高亮但不被截断。"
8 | highlighter = Highlighter("标题")
9 | expected = '这是一个比较长的标题,用于测试关键词高亮但不被截断。'
10 | self.assertEqual(highlighter.highlight(document), expected)
11 |
12 | highlighter = Highlighter("关键词高亮")
13 | expected = '这是一个比较长的标题,用于测试关键词高亮但不被截断。'
14 | self.assertEqual(highlighter.highlight(document), expected)
15 |
16 | highlighter = Highlighter("标题")
17 | document = "这是一个长度超过 200 的标题,应该被截断。" + "HelloDjangoTutorial" * 200
18 | self.assertTrue(
19 | highlighter.highlight(document).startswith(
20 | '...标题,应该被截断。'
21 | )
22 | )
23 |
--------------------------------------------------------------------------------
/blog/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from django.apps import apps
4 | from django.contrib.auth.models import User
5 | from django.test import TestCase
6 | from django.urls import reverse
7 | from django.utils import timezone
8 |
9 | from ..feeds import AllPostsRssFeed
10 | from ..models import Category, Post, Tag
11 |
12 |
13 | class BlogDataTestCase(TestCase):
14 | def setUp(self):
15 | apps.get_app_config("haystack").signal_processor.teardown()
16 |
17 | # User
18 | self.user = User.objects.create_superuser(
19 | username="admin", email="admin@hellogithub.com", password="admin"
20 | )
21 |
22 | # 分类
23 | self.cate1 = Category.objects.create(name="测试分类一")
24 | self.cate2 = Category.objects.create(name="测试分类二")
25 |
26 | # 标签
27 | self.tag1 = Tag.objects.create(name="测试标签一")
28 | self.tag2 = Tag.objects.create(name="测试标签二")
29 |
30 | # 文章
31 | self.post1 = Post.objects.create(
32 | title="测试标题一", body="测试内容一", category=self.cate1, author=self.user,
33 | )
34 | self.post1.tags.add(self.tag1)
35 | self.post1.save()
36 |
37 | self.post2 = Post.objects.create(
38 | title="测试标题二",
39 | body="测试内容二",
40 | category=self.cate2,
41 | author=self.user,
42 | created_time=timezone.now() - timedelta(days=100),
43 | )
44 |
45 |
46 | class IndexViewTestCase(BlogDataTestCase):
47 | def setUp(self):
48 | super().setUp()
49 | self.url = reverse("blog:index")
50 |
51 | def test_without_any_post(self):
52 | Post.objects.all().delete()
53 | response = self.client.get(self.url)
54 | self.assertEqual(response.status_code, 200)
55 | self.assertTemplateUsed("blog/index.html")
56 | self.assertContains(response, "暂时还没有发布的文章!")
57 |
58 | def test_with_posts(self):
59 | response = self.client.get(self.url)
60 | self.assertEqual(response.status_code, 200)
61 | self.assertTemplateUsed("blog/index.html")
62 | self.assertContains(response, self.post1.title)
63 | self.assertContains(response, self.post2.title)
64 | self.assertIn("post_list", response.context)
65 | self.assertIn("is_paginated", response.context)
66 | self.assertIn("page_obj", response.context)
67 |
68 | expected_qs = Post.objects.all().order_by("-created_time")
69 | self.assertQuerysetEqual(
70 | response.context["post_list"], [repr(p) for p in expected_qs]
71 | )
72 |
73 |
74 | class CategoryViewTestCase(BlogDataTestCase):
75 | def setUp(self):
76 | super().setUp()
77 | self.url = reverse("blog:category", kwargs={"pk": self.cate1.pk})
78 | self.url2 = reverse("blog:category", kwargs={"pk": self.cate2.pk})
79 |
80 | def test_visit_a_nonexistent_category(self):
81 | url = reverse("blog:category", kwargs={"pk": 100})
82 | response = self.client.get(url)
83 | self.assertEqual(response.status_code, 404)
84 |
85 | def test_without_any_post(self):
86 | Post.objects.all().delete()
87 | response = self.client.get(self.url2)
88 | self.assertEqual(response.status_code, 200)
89 | self.assertTemplateUsed("blog/index.html")
90 | self.assertContains(response, "暂时还没有发布的文章!")
91 |
92 | def test_with_posts(self):
93 | response = self.client.get(self.url)
94 | self.assertEqual(response.status_code, 200)
95 | self.assertTemplateUsed("blog/index.html")
96 | self.assertContains(response, self.post1.title)
97 | self.assertIn("post_list", response.context)
98 | self.assertIn("is_paginated", response.context)
99 | self.assertIn("page_obj", response.context)
100 | self.assertEqual(response.context["post_list"].count(), 1)
101 | expected_qs = self.cate1.post_set.all().order_by("-created_time")
102 | self.assertQuerysetEqual(
103 | response.context["post_list"], [repr(p) for p in expected_qs]
104 | )
105 |
106 |
107 | class ArchiveViewTestCase(BlogDataTestCase):
108 | def setUp(self):
109 | super().setUp()
110 | self.url = reverse(
111 | "blog:archive",
112 | kwargs={
113 | "year": self.post1.created_time.year,
114 | "month": self.post1.created_time.month,
115 | },
116 | )
117 |
118 | def test_without_any_post(self):
119 | Post.objects.all().delete()
120 |
121 | response = self.client.get(self.url)
122 | self.assertEqual(response.status_code, 200)
123 | self.assertTemplateUsed("blog/index.html")
124 | self.assertContains(response, "暂时还没有发布的文章!")
125 |
126 | def test_with_posts(self):
127 | response = self.client.get(self.url)
128 | self.assertEqual(response.status_code, 200)
129 | self.assertTemplateUsed("blog/index.html")
130 | self.assertContains(response, self.post1.title)
131 | self.assertIn("post_list", response.context)
132 | self.assertIn("is_paginated", response.context)
133 | self.assertIn("page_obj", response.context)
134 |
135 | self.assertEqual(response.context["post_list"].count(), 1)
136 | now = timezone.now()
137 | expected_qs = Post.objects.filter(
138 | created_time__year=now.year, created_time__month=now.month
139 | )
140 | self.assertQuerysetEqual(
141 | response.context["post_list"], [repr(p) for p in expected_qs]
142 | )
143 |
144 |
145 | class TagViewTestCase(BlogDataTestCase):
146 | def setUp(self):
147 | super().setUp()
148 | self.url1 = reverse("blog:tag", kwargs={"pk": self.tag1.pk})
149 | self.url2 = reverse("blog:tag", kwargs={"pk": self.tag2.pk})
150 |
151 | def test_visit_a_nonexistent_tag(self):
152 | url = reverse("blog:tag", kwargs={"pk": 100})
153 | response = self.client.get(url)
154 | self.assertEqual(response.status_code, 404)
155 |
156 | def test_without_any_post(self):
157 | response = self.client.get(self.url2)
158 | self.assertEqual(response.status_code, 200)
159 | self.assertTemplateUsed("blog/index.html")
160 | self.assertContains(response, "暂时还没有发布的文章!")
161 |
162 | def test_with_posts(self):
163 | response = self.client.get(self.url1)
164 | self.assertEqual(response.status_code, 200)
165 | self.assertTemplateUsed("blog/index.html")
166 | self.assertContains(response, self.post1.title)
167 | self.assertIn("post_list", response.context)
168 | self.assertIn("is_paginated", response.context)
169 | self.assertIn("page_obj", response.context)
170 |
171 | self.assertEqual(response.context["post_list"].count(), 1)
172 | expected_qs = self.tag1.post_set.all()
173 | self.assertQuerysetEqual(
174 | response.context["post_list"], [repr(p) for p in expected_qs]
175 | )
176 |
177 |
178 | class PostDetailViewTestCase(BlogDataTestCase):
179 | def setUp(self):
180 | super().setUp()
181 | self.md_post = Post.objects.create(
182 | title="Markdown 测试标题", body="# 标题", category=self.cate1, author=self.user,
183 | )
184 | self.url = reverse("blog:detail", kwargs={"pk": self.md_post.pk})
185 |
186 | def test_good_view(self):
187 | response = self.client.get(self.url)
188 | self.assertEqual(response.status_code, 200)
189 | self.assertTemplateUsed("blog/detail.html")
190 | self.assertContains(response, self.md_post.title)
191 | self.assertIn("post", response.context)
192 |
193 | def test_visit_a_nonexistent_post(self):
194 | url = reverse("blog:detail", kwargs={"pk": 100})
195 | response = self.client.get(url)
196 | self.assertEqual(response.status_code, 404)
197 |
198 | def test_increase_views(self):
199 | self.client.get(self.url)
200 | self.md_post.refresh_from_db()
201 | self.assertEqual(self.md_post.views, 1)
202 |
203 | self.client.get(self.url)
204 | self.md_post.refresh_from_db()
205 | self.assertEqual(self.md_post.views, 2)
206 |
207 | def test_markdownify_post_body_and_set_toc(self):
208 | response = self.client.get(self.url)
209 | self.assertContains(response, "文章目录")
210 | self.assertContains(response, self.md_post.title)
211 |
212 | post_template_var = response.context["post"]
213 | self.assertHTMLEqual(post_template_var.body_html, "标题
")
214 | self.assertHTMLEqual(post_template_var.toc, '标题')
215 |
216 |
217 | class AdminTestCase(BlogDataTestCase):
218 | def setUp(self):
219 | super().setUp()
220 | self.url = reverse("admin:blog_post_add")
221 |
222 | def test_set_author_after_publishing_the_post(self):
223 | data = {
224 | "title": "测试标题",
225 | "body": "测试内容",
226 | "category": self.cate1.pk,
227 | }
228 | self.client.login(username=self.user.username, password="admin")
229 | response = self.client.post(self.url, data=data)
230 | self.assertEqual(response.status_code, 302)
231 |
232 | post = Post.objects.all().latest("created_time")
233 | self.assertEqual(post.author, self.user)
234 | self.assertEqual(post.title, data.get("title"))
235 | self.assertEqual(post.category, self.cate1)
236 |
237 |
238 | class RSSTestCase(BlogDataTestCase):
239 | def setUp(self):
240 | super().setUp()
241 | self.url = reverse("rss")
242 |
243 | def test_rss_subscription_content(self):
244 | response = self.client.get(self.url)
245 | self.assertContains(response, AllPostsRssFeed.title)
246 | self.assertContains(response, AllPostsRssFeed.description)
247 | self.assertContains(response, self.post1.title)
248 | self.assertContains(response, self.post2.title)
249 | self.assertContains(
250 | response, "[%s] %s" % (self.post1.category, self.post1.title)
251 | )
252 | self.assertContains(
253 | response, "[%s] %s" % (self.post2.category, self.post2.title)
254 | )
255 | self.assertContains(response, self.post1.body)
256 | self.assertContains(response, self.post2.body)
257 |
--------------------------------------------------------------------------------
/blog/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | app_name = 'blog'
6 | urlpatterns = [
7 | path('', views.IndexView.as_view(), name='index'),
8 | path('posts//', views.PostDetailView.as_view(), name='detail'),
9 | path('archives///', views.ArchiveView.as_view(), name='archive'),
10 | path('categories//', views.CategoryView.as_view(), name='category'),
11 | path('tags//', views.TagView.as_view(), name='tag'),
12 | # path('search/', views.search, name='search'),
13 | ]
14 |
--------------------------------------------------------------------------------
/blog/utils.py:
--------------------------------------------------------------------------------
1 | from django.utils.html import strip_tags
2 | from haystack.utils import Highlighter as HaystackHighlighter
3 |
4 |
5 | class Highlighter(HaystackHighlighter):
6 | """
7 | 自定义关键词高亮器,不截断过短的文本(例如文章标题)
8 | """
9 |
10 | def highlight(self, text_block):
11 | self.text_block = strip_tags(text_block)
12 | highlight_locations = self.find_highlightable_words()
13 | start_offset, end_offset = self.find_window(highlight_locations)
14 | if len(text_block) < self.max_length:
15 | start_offset = 0
16 | return self.render_html(highlight_locations, start_offset, end_offset)
17 |
--------------------------------------------------------------------------------
/blog/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib import messages
2 | from django.db.models import Q
3 | from django.shortcuts import get_object_or_404, redirect, render
4 | from django.views.generic import DetailView, ListView
5 |
6 | from pure_pagination.mixins import PaginationMixin
7 |
8 | from .models import Category, Post, Tag
9 |
10 |
11 | class IndexView(PaginationMixin, ListView):
12 | model = Post
13 | template_name = "blog/index.html"
14 | context_object_name = "post_list"
15 | paginate_by = 10
16 |
17 |
18 | class CategoryView(IndexView):
19 | def get_queryset(self):
20 | cate = get_object_or_404(Category, pk=self.kwargs.get("pk"))
21 | return super().get_queryset().filter(category=cate)
22 |
23 |
24 | class ArchiveView(IndexView):
25 | def get_queryset(self):
26 | year = self.kwargs.get("year")
27 | month = self.kwargs.get("month")
28 | return (
29 | super()
30 | .get_queryset()
31 | .filter(created_time__year=year, created_time__month=month)
32 | )
33 |
34 |
35 | class TagView(IndexView):
36 | def get_queryset(self):
37 | t = get_object_or_404(Tag, pk=self.kwargs.get("pk"))
38 | return super().get_queryset().filter(tags=t)
39 |
40 |
41 | # 记得在顶部导入 DetailView
42 | class PostDetailView(DetailView):
43 | # 这些属性的含义和 ListView 是一样的
44 | model = Post
45 | template_name = "blog/detail.html"
46 | context_object_name = "post"
47 |
48 | def get(self, request, *args, **kwargs):
49 | # 覆写 get 方法的目的是因为每当文章被访问一次,就得将文章阅读量 +1
50 | # get 方法返回的是一个 HttpResponse 实例
51 | # 之所以需要先调用父类的 get 方法,是因为只有当 get 方法被调用后,
52 | # 才有 self.object 属性,其值为 Post 模型实例,即被访问的文章 post
53 | response = super().get(request, *args, **kwargs)
54 |
55 | # 将文章阅读量 +1
56 | # 注意 self.object 的值就是被访问的文章 post
57 | self.object.increase_views()
58 |
59 | # 视图必须返回一个 HttpResponse 对象
60 | return response
61 |
--------------------------------------------------------------------------------
/blogproject/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/blogproject/__init__.py
--------------------------------------------------------------------------------
/blogproject/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/blogproject/settings/__init__.py
--------------------------------------------------------------------------------
/blogproject/settings/common.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for blogproject project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.2.3.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.2/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.2/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | back = os.path.dirname
17 |
18 | BASE_DIR = back(back(back(os.path.abspath(__file__))))
19 |
20 | # Quick-start development settings - unsuitable for production
21 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
22 |
23 | # Application definition
24 |
25 | INSTALLED_APPS = [
26 | "django.contrib.admin",
27 | "django.contrib.auth",
28 | "django.contrib.contenttypes",
29 | "django.contrib.sessions",
30 | "django.contrib.messages",
31 | "django.contrib.staticfiles",
32 | "pure_pagination", # 分页
33 | "haystack", # 搜索
34 | "blog.apps.BlogConfig", # 注册 blog 应用
35 | "comments.apps.CommentsConfig", # 注册 comments 应用
36 | ]
37 |
38 | MIDDLEWARE = [
39 | "django.middleware.security.SecurityMiddleware",
40 | "django.contrib.sessions.middleware.SessionMiddleware",
41 | "django.middleware.common.CommonMiddleware",
42 | "django.middleware.csrf.CsrfViewMiddleware",
43 | "django.contrib.auth.middleware.AuthenticationMiddleware",
44 | "django.contrib.messages.middleware.MessageMiddleware",
45 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
46 | ]
47 |
48 | ROOT_URLCONF = "blogproject.urls"
49 |
50 | TEMPLATES = [
51 | {
52 | "BACKEND": "django.template.backends.django.DjangoTemplates",
53 | "DIRS": [os.path.join(BASE_DIR, "templates")],
54 | "APP_DIRS": True,
55 | "OPTIONS": {
56 | "context_processors": [
57 | "django.template.context_processors.debug",
58 | "django.template.context_processors.request",
59 | "django.contrib.auth.context_processors.auth",
60 | "django.contrib.messages.context_processors.messages",
61 | ],
62 | },
63 | },
64 | ]
65 |
66 | WSGI_APPLICATION = "blogproject.wsgi.application"
67 |
68 | # Database
69 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases
70 |
71 | DATABASES = {
72 | "default": {
73 | "ENGINE": "django.db.backends.sqlite3",
74 | "NAME": os.path.join(BASE_DIR, "database", "db.sqlite3"),
75 | }
76 | }
77 |
78 | # Password validation
79 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
80 |
81 | AUTH_PASSWORD_VALIDATORS = [
82 | {
83 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
84 | },
85 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
86 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
87 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
88 | ]
89 |
90 | # Internationalization
91 | # https://docs.djangoproject.com/en/2.2/topics/i18n/
92 |
93 | LANGUAGE_CODE = "zh-hans"
94 |
95 | TIME_ZONE = "Asia/Shanghai"
96 |
97 | USE_I18N = True
98 |
99 | USE_L10N = True
100 |
101 | USE_TZ = True
102 |
103 | # Static files (CSS, JavaScript, Images)
104 | # https://docs.djangoproject.com/en/2.2/howto/static-files/
105 |
106 | STATIC_URL = "/static/"
107 | STATIC_ROOT = os.path.join(BASE_DIR, "static")
108 |
109 | # 分页设置
110 | PAGINATION_SETTINGS = {
111 | "PAGE_RANGE_DISPLAYED": 4,
112 | "MARGIN_PAGES_DISPLAYED": 2,
113 | "SHOW_FIRST_PAGE_WHEN_INVALID": True,
114 | }
115 |
116 | # 搜索设置
117 | HAYSTACK_CONNECTIONS = {
118 | "default": {
119 | "ENGINE": "blog.elasticsearch2_ik_backend.Elasticsearch2IkSearchEngine",
120 | "URL": "",
121 | "INDEX_NAME": "hellodjango_blog_tutorial",
122 | },
123 | }
124 | HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
125 |
126 | enable = os.environ.get("ENABLE_HAYSTACK_REALTIME_SIGNAL_PROCESSOR", "yes")
127 | if enable in {"true", "True", "yes"}:
128 | HAYSTACK_SIGNAL_PROCESSOR = "haystack.signals.RealtimeSignalProcessor"
129 |
130 | HAYSTACK_CUSTOM_HIGHLIGHTER = "blog.utils.Highlighter"
131 | # HAYSTACK_DEFAULT_OPERATOR = 'AND'
132 | # HAYSTACK_FUZZY_MIN_SIM = 0.1
133 |
--------------------------------------------------------------------------------
/blogproject/settings/local.py:
--------------------------------------------------------------------------------
1 | from .common import *
2 |
3 | SECRET_KEY = 'development-secret-key'
4 |
5 | DEBUG = True
6 |
7 | ALLOWED_HOSTS = ['*']
8 |
9 | # 搜索设置
10 | HAYSTACK_CONNECTIONS['default']['URL'] = 'http://elasticsearch_local:9200/'
11 |
--------------------------------------------------------------------------------
/blogproject/settings/production.py:
--------------------------------------------------------------------------------
1 | from .common import *
2 |
3 | SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
4 |
5 | DEBUG = False
6 |
7 | ALLOWED_HOSTS = ['hellodjango-blog-tutorial-demo.zmrenwu.com']
8 | HAYSTACK_CONNECTIONS['default']['URL'] = 'http://hellodjango_blog_tutorial_elasticsearch:9200/'
9 |
--------------------------------------------------------------------------------
/blogproject/urls.py:
--------------------------------------------------------------------------------
1 | """blogproject URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.2/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: path('', 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: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path, include
18 |
19 | from blog.feeds import AllPostsRssFeed
20 |
21 | urlpatterns = [
22 | path('admin/', admin.site.urls),
23 | path('search/', include('haystack.urls')),
24 | path('', include('blog.urls')),
25 | path('', include('comments.urls')),
26 |
27 | # 记得在顶部引入 AllPostsRssFeed
28 | path('all/rss/', AllPostsRssFeed(), name='rss'),
29 | ]
30 |
--------------------------------------------------------------------------------
/blogproject/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for blogproject 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/2.2/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', 'blogproject.settings.production')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/comments/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/comments/__init__.py
--------------------------------------------------------------------------------
/comments/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Comment
3 |
4 |
5 | class CommentAdmin(admin.ModelAdmin):
6 | list_display = ['name', 'email', 'url', 'post', 'created_time']
7 | fields = ['name', 'email', 'url', 'text', 'post']
8 |
9 |
10 | admin.site.register(Comment, CommentAdmin)
11 |
--------------------------------------------------------------------------------
/comments/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CommentsConfig(AppConfig):
5 | name = 'comments'
6 | verbose_name = '评论'
7 |
--------------------------------------------------------------------------------
/comments/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 | from .models import Comment
4 |
5 |
6 | class CommentForm(forms.ModelForm):
7 | class Meta:
8 | model = Comment
9 | fields = ['name', 'email', 'url', 'text']
10 |
--------------------------------------------------------------------------------
/comments/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.3 on 2019-07-11 10:02
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import django.utils.timezone
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ('blog', '0002_auto_20190711_1802'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Comment',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('name', models.CharField(max_length=50, verbose_name='名字')),
22 | ('email', models.EmailField(max_length=254, verbose_name='邮箱')),
23 | ('url', models.URLField(blank=True, verbose_name='网址')),
24 | ('text', models.TextField(verbose_name='内容')),
25 | ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
26 | ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.Post', verbose_name='文章')),
27 | ],
28 | options={
29 | 'verbose_name': '评论',
30 | 'verbose_name_plural': '评论',
31 | },
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/comments/migrations/0002_auto_20191011_2326.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.5 on 2019-10-11 15:26
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('comments', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='comment',
15 | options={'ordering': ['-created_time'], 'verbose_name': '评论', 'verbose_name_plural': '评论'},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/comments/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/comments/migrations/__init__.py
--------------------------------------------------------------------------------
/comments/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils import timezone
3 |
4 |
5 | class Comment(models.Model):
6 | name = models.CharField('名字', max_length=50)
7 | email = models.EmailField('邮箱')
8 | url = models.URLField('网址', blank=True)
9 | text = models.TextField('内容')
10 | created_time = models.DateTimeField('创建时间', default=timezone.now)
11 | post = models.ForeignKey('blog.Post', verbose_name='文章', on_delete=models.CASCADE)
12 |
13 | class Meta:
14 | verbose_name = '评论'
15 | verbose_name_plural = verbose_name
16 | ordering = ['-created_time']
17 |
18 | def __str__(self):
19 | return '{}: {}'.format(self.name, self.text[:20])
20 |
--------------------------------------------------------------------------------
/comments/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/comments/templatetags/__init__.py
--------------------------------------------------------------------------------
/comments/templatetags/comments_extras.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from ..forms import CommentForm
3 |
4 | register = template.Library()
5 |
6 |
7 | @register.inclusion_tag('comments/inclusions/_form.html', takes_context=True)
8 | def show_comment_form(context, post, form=None):
9 | if form is None:
10 | form = CommentForm()
11 | return {
12 | 'form': form,
13 | 'post': post,
14 | }
15 |
16 |
17 | @register.inclusion_tag('comments/inclusions/_list.html', takes_context=True)
18 | def show_comments(context, post):
19 | comment_list = post.comment_set.all()
20 | comment_count = comment_list.count()
21 | return {
22 | 'comment_count': comment_count,
23 | 'comment_list': comment_list,
24 | }
25 |
--------------------------------------------------------------------------------
/comments/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/comments/tests/__init__.py
--------------------------------------------------------------------------------
/comments/tests/base.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.contrib.auth.models import User
3 | from django.test import TestCase
4 |
5 | from blog.models import Category, Post
6 |
7 |
8 | class CommentDataTestCase(TestCase):
9 | def setUp(self):
10 | apps.get_app_config("haystack").signal_processor.teardown()
11 | self.user = User.objects.create_superuser(
12 | username="admin", email="admin@hellogithub.com", password="admin"
13 | )
14 | self.cate = Category.objects.create(name="测试")
15 | self.post = Post.objects.create(
16 | title="测试标题", body="测试内容", category=self.cate, author=self.user,
17 | )
18 |
--------------------------------------------------------------------------------
/comments/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from .base import CommentDataTestCase
2 | from ..models import Comment
3 |
4 |
5 | class CommentModelTestCase(CommentDataTestCase):
6 | def setUp(self) -> None:
7 | super().setUp()
8 | self.comment = Comment.objects.create(
9 | name="评论者", email="a@a.com", text="评论内容", post=self.post,
10 | )
11 |
12 | def test_str_representation(self):
13 | self.assertEqual(self.comment.__str__(), "评论者: 评论内容")
14 |
--------------------------------------------------------------------------------
/comments/tests/test_templatetags.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from django.apps import apps
4 | from django.contrib.auth.models import User
5 | from django.template import Context, Template
6 | from django.utils import timezone
7 |
8 | from blog.models import Category, Post
9 | from .base import CommentDataTestCase
10 | from ..forms import CommentForm
11 | from ..models import Comment
12 | from ..templatetags.comments_extras import show_comment_form, show_comments
13 |
14 |
15 | class CommentExtraTestCase(CommentDataTestCase):
16 | def setUp(self) -> None:
17 | super().setUp()
18 | self.ctx = Context()
19 |
20 | def test_show_comment_form_with_empty_form(self):
21 | template = Template("{% load comments_extras %}" "{% show_comment_form post %}")
22 | form = CommentForm()
23 | context = Context(show_comment_form(self.ctx, self.post))
24 | expected_html = template.render(context)
25 | for field in form:
26 | label = ''.format(
27 | field.id_for_label, field.label
28 | )
29 | self.assertInHTML(label, expected_html)
30 | self.assertInHTML(str(field), expected_html)
31 |
32 | def test_show_comment_form_with_invalid_bound_form(self):
33 | template = Template(
34 | "{% load comments_extras %}" "{% show_comment_form post form %}"
35 | )
36 | invalid_data = {
37 | "email": "invalid_email",
38 | }
39 | form = CommentForm(data=invalid_data)
40 | self.assertFalse(form.is_valid())
41 | context = Context(show_comment_form(self.ctx, self.post, form=form))
42 | expected_html = template.render(context)
43 | for field in form:
44 | label = ''.format(
45 | field.id_for_label, field.label
46 | )
47 | self.assertInHTML(label, expected_html)
48 | self.assertInHTML(str(field), expected_html)
49 | self.assertInHTML(str(field.errors), expected_html)
50 |
51 | def test_show_comments_without_any_comment(self):
52 | template = Template("{% load comments_extras %}" "{% show_comments post %}")
53 | ctx_dict = show_comments(self.ctx, self.post)
54 | ctx_dict["post"] = self.post
55 | context = Context(ctx_dict)
56 | expected_html = template.render(context)
57 | self.assertInHTML("评论列表,共 0 条评论
", expected_html)
58 | self.assertInHTML("暂无评论", expected_html)
59 |
60 | def test_show_comments_with_comments(self):
61 | comment1 = Comment.objects.create(
62 | name="评论者1", email="a@a.com", text="评论内容1", post=self.post,
63 | )
64 | comment2 = Comment.objects.create(
65 | name="评论者2",
66 | email="a@a.com",
67 | text="评论内容2",
68 | post=self.post,
69 | created_time=timezone.now() - timedelta(days=1),
70 | )
71 | template = Template("{% load comments_extras %}" "{% show_comments post %}")
72 | ctx_dict = show_comments(self.ctx, self.post)
73 | ctx_dict["post"] = self.post
74 | context = Context(ctx_dict)
75 | expected_html = template.render(context)
76 | self.assertInHTML("评论列表,共 2 条评论
", expected_html)
77 | self.assertInHTML(comment1.name, expected_html)
78 | self.assertInHTML(comment1.text, expected_html)
79 | self.assertInHTML(comment2.name, expected_html)
80 | self.assertInHTML(comment2.text, expected_html)
81 | self.assertQuerysetEqual(
82 | ctx_dict["comment_list"], [repr(c) for c in [comment1, comment2]]
83 | )
84 |
--------------------------------------------------------------------------------
/comments/tests/test_views.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.contrib.auth.models import User
3 | from django.urls import reverse
4 |
5 | from blog.models import Category, Post
6 |
7 | from ..models import Comment
8 | from .base import CommentDataTestCase
9 |
10 |
11 | class CommentViewTestCase(CommentDataTestCase):
12 | def setUp(self) -> None:
13 | super().setUp()
14 | self.url = reverse("comments:comment", kwargs={"post_pk": self.post.pk})
15 |
16 | def test_comment_a_nonexistent_post(self):
17 | url = reverse("comments:comment", kwargs={"post_pk": 100})
18 | response = self.client.post(url, {})
19 | self.assertEqual(response.status_code, 404)
20 |
21 | def test_invalid_comment_data(self):
22 | invalid_data = {
23 | "email": "invalid_email",
24 | }
25 | response = self.client.post(self.url, invalid_data)
26 | self.assertTemplateUsed(response, "comments/preview.html")
27 | self.assertIn("post", response.context)
28 | self.assertIn("form", response.context)
29 | form = response.context["form"]
30 | for field_name, errors in form.errors.items():
31 | for err in errors:
32 | self.assertContains(response, err)
33 | self.assertContains(response, "评论发表失败!请修改表单中的错误后重新提交。")
34 |
35 | def test_valid_comment_data(self):
36 | valid_data = {
37 | "name": "评论者",
38 | "email": "a@a.com",
39 | "text": "评论内容",
40 | }
41 | response = self.client.post(self.url, valid_data, follow=True)
42 | self.assertRedirects(response, self.post.get_absolute_url())
43 | self.assertContains(response, "评论发表成功!")
44 | self.assertEqual(Comment.objects.count(), 1)
45 | comment = Comment.objects.first()
46 | self.assertEqual(comment.name, valid_data["name"])
47 | self.assertEqual(comment.text, valid_data["text"])
48 |
--------------------------------------------------------------------------------
/comments/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | app_name = 'comments'
6 | urlpatterns = [
7 | path('comment/', views.comment, name='comment'),
8 | ]
9 |
--------------------------------------------------------------------------------
/comments/views.py:
--------------------------------------------------------------------------------
1 | from blog.models import Post
2 | from django.shortcuts import get_object_or_404, redirect, render
3 | from django.views.decorators.http import require_POST
4 |
5 | from .forms import CommentForm
6 | from django.contrib import messages
7 |
8 |
9 | @require_POST
10 | def comment(request, post_pk):
11 | # 先获取被评论的文章,因为后面需要把评论和被评论的文章关联起来。
12 | # 这里我们使用了 Django 提供的一个快捷函数 get_object_or_404,
13 | # 这个函数的作用是当获取的文章(Post)存在时,则获取;否则返回 404 页面给用户。
14 | post = get_object_or_404(Post, pk=post_pk)
15 |
16 | # django 将用户提交的数据封装在 request.POST 中,这是一个类字典对象。
17 | # 我们利用这些数据构造了 CommentForm 的实例,这样就生成了一个绑定了用户提交数据的表单。
18 | form = CommentForm(request.POST)
19 |
20 | # 当调用 form.is_valid() 方法时,Django 自动帮我们检查表单的数据是否符合格式要求。
21 | if form.is_valid():
22 | # 检查到数据是合法的,调用表单的 save 方法保存数据到数据库,
23 | # commit=False 的作用是仅仅利用表单的数据生成 Comment 模型类的实例,但还不保存评论数据到数据库。
24 | comment = form.save(commit=False)
25 |
26 | # 将评论和被评论的文章关联起来。
27 | comment.post = post
28 |
29 | # 最终将评论数据保存进数据库,调用模型实例的 save 方法
30 | comment.save()
31 |
32 | messages.add_message(request, messages.SUCCESS, '评论发表成功!', extra_tags='success')
33 |
34 | # 重定向到 post 的详情页,实际上当 redirect 函数接收一个模型的实例时,它会调用这个模型实例的 get_absolute_url 方法,
35 | # 然后重定向到 get_absolute_url 方法返回的 URL。
36 | return redirect(post)
37 |
38 | # 检查到数据不合法,我们渲染一个预览页面,用于展示表单的错误。
39 | # 注意这里被评论的文章 post 也传给了模板,因为我们需要根据 post 来生成表单的提交地址。
40 | context = {
41 | 'post': post,
42 | 'form': form,
43 | }
44 | messages.add_message(request, messages.ERROR, '评论发表失败!请修改表单中的错误后重新提交。', extra_tags='danger')
45 |
46 | return render(request, 'comments/preview.html', context=context)
47 |
--------------------------------------------------------------------------------
/compose/local/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6-alpine
2 |
3 | ENV PYTHONUNBUFFERED 1
4 |
5 | # 替换为国内源
6 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
7 |
8 | RUN apk update \
9 | # Pillow dependencies
10 | && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev
11 |
12 | WORKDIR /app
13 |
14 | RUN pip install pipenv -i https://pypi.douban.com/simple
15 |
16 | COPY Pipfile /app/Pipfile
17 | COPY Pipfile.lock /app/Pipfile.lock
18 | RUN pipenv install --system --deploy --ignore-pipfile
19 |
20 | COPY ./compose/local/start.sh /start.sh
21 | RUN sed -i 's/\r//' /start.sh
22 | RUN chmod +x /start.sh
--------------------------------------------------------------------------------
/compose/local/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | python manage.py migrate
4 | python manage.py runserver 0.0.0.0:8000
--------------------------------------------------------------------------------
/compose/production/django/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6-alpine
2 |
3 | ENV PYTHONUNBUFFERED 1
4 |
5 | # 替换为国内源
6 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
7 |
8 | RUN apk update \
9 | # Pillow dependencies
10 | && apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev
11 |
12 | WORKDIR /app
13 |
14 | RUN pip install pipenv -i https://pypi.douban.com/simple
15 |
16 | COPY Pipfile /app/Pipfile
17 | COPY Pipfile.lock /app/Pipfile.lock
18 | RUN pipenv install --system --deploy --ignore-pipfile
19 |
20 | COPY . /app
21 |
22 | COPY ./compose/production/django/start.sh /start.sh
23 | RUN sed -i 's/\r//' /start.sh
24 | RUN chmod +x /start.sh
--------------------------------------------------------------------------------
/compose/production/django/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | python manage.py migrate
4 | python manage.py collectstatic --noinput
5 | gunicorn blogproject.wsgi:application -w 4 -k gthread -b 0.0.0.0:8000 --chdir=/app
--------------------------------------------------------------------------------
/compose/production/elasticsearch/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM elasticsearch:2.4.6-alpine
2 |
3 | COPY ./compose/production/elasticsearch/elasticsearch-analysis-ik-1.10.6.zip /usr/share/elasticsearch/plugins/
4 | RUN cd /usr/share/elasticsearch/plugins/ && mkdir ik && unzip elasticsearch-analysis-ik-1.10.6.zip -d ik/
5 | RUN rm /usr/share/elasticsearch/plugins/elasticsearch-analysis-ik-1.10.6.zip
6 |
7 | USER root
8 | COPY ./compose/production/elasticsearch/elasticsearch.yml /usr/share/elasticsearch/config/
9 | RUN chown elasticsearch:elasticsearch /usr/share/elasticsearch/config/elasticsearch.yml
10 |
11 | USER elasticsearch
12 |
13 |
--------------------------------------------------------------------------------
/compose/production/elasticsearch/elasticsearch-analysis-ik-1.10.6.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/compose/production/elasticsearch/elasticsearch-analysis-ik-1.10.6.zip
--------------------------------------------------------------------------------
/compose/production/elasticsearch/elasticsearch-analysis-ik-5.6.16.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/compose/production/elasticsearch/elasticsearch-analysis-ik-5.6.16.zip
--------------------------------------------------------------------------------
/compose/production/elasticsearch/elasticsearch.yml:
--------------------------------------------------------------------------------
1 | bootstrap.memory_lock: true
2 | network.host: 0.0.0.0
--------------------------------------------------------------------------------
/compose/production/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.17.1
2 |
3 | # 替换为国内源
4 | RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak
5 | COPY ./compose/production/nginx/sources.list /etc/apt/
6 | RUN apt-get update && apt-get install -y --allow-unauthenticated certbot python-certbot-nginx
7 |
8 | RUN rm /etc/nginx/conf.d/default.conf
9 | COPY ./compose/production/nginx/hellodjango-blog-tutorial.conf /etc/nginx/conf.d/hellodjango-blog-tutorial.conf
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/compose/production/nginx/hellodjango-blog-tutorial.conf-tmpl:
--------------------------------------------------------------------------------
1 | upstream hellodjango_blog_tutorial {
2 | server hellodjango_blog_tutorial:8000;
3 | }
4 |
5 | server {
6 | server_name hellodjango-blog-tutorial-demo.zmrenwu.com;
7 |
8 | location /static {
9 | alias /apps/hellodjango_blog_tutorial/static;
10 | }
11 |
12 | location / {
13 | proxy_set_header Host $host;
14 | proxy_set_header X-Real-IP $remote_addr;
15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
16 | proxy_set_header X-Forwarded-Proto $scheme;
17 |
18 | proxy_pass http://hellodjango_blog_tutorial;
19 | }
20 |
21 | listen 80;
22 | }
--------------------------------------------------------------------------------
/compose/production/nginx/sources.list:
--------------------------------------------------------------------------------
1 | deb-src http://archive.ubuntu.com/ubuntu xenial main restricted #Added by software-properties
2 | deb http://mirrors.aliyun.com/ubuntu/ xenial main restricted
3 | deb-src http://mirrors.aliyun.com/ubuntu/ xenial main restricted multiverse universe #Added by software-properties
4 | deb http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted
5 | deb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted multiverse universe #Added by software-properties
6 | deb http://mirrors.aliyun.com/ubuntu/ xenial universe
7 | deb http://mirrors.aliyun.com/ubuntu/ xenial-updates universe
8 | deb http://mirrors.aliyun.com/ubuntu/ xenial multiverse
9 | deb http://mirrors.aliyun.com/ubuntu/ xenial-updates multiverse
10 | deb http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse
11 | deb-src http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse #Added by software-properties
12 | deb http://archive.canonical.com/ubuntu xenial partner
13 | deb-src http://archive.canonical.com/ubuntu xenial partner
14 | deb http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted
15 | deb-src http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted multiverse universe #Added by software-properties
16 | deb http://mirrors.aliyun.com/ubuntu/ xenial-security universe
17 | deb http://mirrors.aliyun.com/ubuntu/ xenial-security multiverse
18 |
19 | deb http://mirrors.tencentyun.com/debian/ jessie main non-free contrib
20 | deb http://mirrors.tencentyun.com/debian/ jessie-updates main non-free contrib
21 | deb http://mirrors.tencentyun.com/debian/ jessie-backports main non-free contrib
22 | deb-src http://mirrors.tencentyun.com/debian/ jessie main non-free contrib
23 | deb-src http://mirrors.tencentyun.com/debian/ jessie-updates main non-free contrib
24 | deb-src http://mirrors.tencentyun.com/debian/ jessie-backports main non-free contrib
25 | deb http://mirrors.tencentyun.com/debian-security/ jessie/updates main non-free contrib
26 | deb-src http://mirrors.tencentyun.com/debian-security/ jessie/updates main non-free contrib
27 |
28 | deb http://deb.debian.org/debian stretch main
29 | deb http://security.debian.org/debian-security stretch/updates main
30 | deb http://deb.debian.org/debian stretch-updates main
31 |
--------------------------------------------------------------------------------
/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/cover.jpg
--------------------------------------------------------------------------------
/database/readme.md:
--------------------------------------------------------------------------------
1 | 为了兼容 Docker,默认的 sqlite 数据库生成在项目根目录的 database 目录下,因此在生成数据库之前需要确保项目根目录下 database 文件夹的存在。否则在生成数据库时会报错:
2 |
3 | ```
4 | django.db.utils.OperationalError: unable to open database file
5 | ```
6 |
7 | 如果使用 MySQL、PostgreSQL 等数据库引擎,则 database 文件夹可有可无。
--------------------------------------------------------------------------------
/fabfile.py:
--------------------------------------------------------------------------------
1 | from fabric import task
2 | from invoke import Responder
3 |
4 | from _credentials import github_password, github_username
5 |
6 |
7 | def _get_github_auth_responders():
8 | """
9 | 返回 GitHub 用户名密码自动填充器
10 | """
11 | username_responder = Responder(
12 | pattern="Username for 'https://github.com':",
13 | response='{}\n'.format(github_username)
14 | )
15 | password_responder = Responder(
16 | pattern="Password for 'https://{}@github.com':".format(github_username),
17 | response='{}\n'.format(github_password)
18 | )
19 | return [username_responder, password_responder]
20 |
21 |
22 | @task()
23 | def deploy(c):
24 | supervisor_conf_path = '~/etc/'
25 | supervisor_program_name = 'hellodjango-blog-tutorial'
26 |
27 | project_root_path = '~/apps/HelloDjango-blog-tutorial/'
28 |
29 | # 先停止应用
30 | with c.cd(supervisor_conf_path):
31 | cmd = 'supervisorctl stop {}'.format(supervisor_program_name)
32 | c.run(cmd)
33 |
34 | # 进入项目根目录,从 Git 拉取最新代码
35 | with c.cd(project_root_path):
36 | cmd = 'git pull'
37 | responders = _get_github_auth_responders()
38 | c.run(cmd, watchers=responders)
39 |
40 | # 重新启动应用
41 | with c.cd(supervisor_conf_path):
42 | cmd = 'supervisorctl start {}'.format(supervisor_program_name)
43 | c.run(cmd)
44 |
--------------------------------------------------------------------------------
/local.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | volumes:
4 | database_local:
5 | esdata_local:
6 |
7 | services:
8 | hellodjango_blog_tutorial_local:
9 | build:
10 | context: .
11 | dockerfile: ./compose/local/Dockerfile
12 | image: hellodjango_blog_tutorial_local
13 | container_name: hellodjango_blog_tutorial_local
14 | working_dir: /app
15 | volumes:
16 | - database_local:/app/database
17 | - .:/app
18 | ports:
19 | - "8000:8000"
20 | command: /start.sh
21 | depends_on:
22 | - elasticsearch_local
23 |
24 | elasticsearch_local:
25 | build:
26 | context: .
27 | dockerfile: ./compose/production/elasticsearch/Dockerfile
28 | image: hellodjango_blog_tutorial_elasticsearch_local
29 | container_name: hellodjango_blog_tutorial_elasticsearch_local
30 | volumes:
31 | - esdata_local:/usr/share/elasticsearch/data
32 | ports:
33 | - "9200:9200"
34 | environment:
35 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
36 | ulimits:
37 | memlock:
38 | soft: -1
39 | hard: -1
40 | nproc: 65536
41 | nofile:
42 | soft: 65536
43 | hard: 65536
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blogproject.settings.local')
9 | try:
10 | from django.core.management import execute_from_command_line
11 | except ImportError as exc:
12 | raise ImportError(
13 | "Couldn't import Django. Are you sure it's installed and "
14 | "available on your PYTHONPATH environment variable? Did you "
15 | "forget to activate a virtual environment?"
16 | ) from exc
17 | execute_from_command_line(sys.argv)
18 |
19 |
20 | if __name__ == '__main__':
21 | main()
22 |
--------------------------------------------------------------------------------
/production.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | volumes:
4 | static:
5 | database:
6 | esdata:
7 |
8 | services:
9 | hellodjango_blog_tutorial:
10 | build:
11 | context: .
12 | dockerfile: compose/production/django/Dockerfile
13 | image: hellodjango_blog_tutorial
14 | container_name: hellodjango_blog_tutorial
15 | working_dir: /app
16 | volumes:
17 | - database:/app/database
18 | - static:/app/static
19 | env_file:
20 | - .envs/.production
21 | ports:
22 | - "8000:8000"
23 | command: /start.sh
24 |
25 | nginx:
26 | build:
27 | context: .
28 | dockerfile: compose/production/nginx/Dockerfile
29 | image: hellodjango_blog_tutorial_nginx
30 | container_name: hellodjango_blog_tutorial_nginx
31 | volumes:
32 | - static:/apps/hellodjango_blog_tutorial/static
33 | ports:
34 | - "80:80"
35 | - "443:443"
36 |
37 | elasticsearch:
38 | build:
39 | context: .
40 | dockerfile: ./compose/production/elasticsearch/Dockerfile
41 | image: hellodjango_blog_tutorial_elasticsearch
42 | container_name: hellodjango_blog_tutorial_elasticsearch
43 | volumes:
44 | - esdata:/usr/share/elasticsearch/data
45 | ports:
46 | - "9200:9200"
47 | environment:
48 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
49 | ulimits:
50 | memlock:
51 | soft: -1
52 | hard: -1
53 | nproc: 65536
54 | nofile:
55 | soft: 65536
56 | hard: 65536
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | fabric
2 | coverage
3 | django~=2.2
4 | markdown
5 | gunicorn
6 | faker
7 | django-pure-pagination
8 | elasticsearch>=2,<3
9 | django-haystack
10 |
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HelloGitHub-Team/HelloDjango-blog-tutorial/ceb8c1edf9deed7177e07f5a3d4920c7602ebdad/scripts/__init__.py
--------------------------------------------------------------------------------
/scripts/fake.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pathlib
3 | import random
4 | import sys
5 | from datetime import timedelta
6 |
7 | import django
8 | from django.apps import apps
9 | from django.utils import timezone
10 |
11 | import faker
12 |
13 | # 将项目根目录添加到 Python 的模块搜索路径中
14 | back = os.path.dirname
15 | BASE_DIR = back(back(os.path.abspath(__file__)))
16 | sys.path.append(BASE_DIR)
17 |
18 | if __name__ == "__main__":
19 | # 启动 django
20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blogproject.settings.local")
21 | django.setup()
22 |
23 | from blog.models import Category, Post, Tag
24 | from comments.models import Comment
25 | from django.contrib.auth.models import User
26 |
27 | # 取消实时索引生成,因为本地运行 fake 脚本时可能并未启动 Elasticsearch 服务。
28 | apps.get_app_config("haystack").signal_processor.teardown()
29 |
30 | print("clean database")
31 | Post.objects.all().delete()
32 | Category.objects.all().delete()
33 | Tag.objects.all().delete()
34 | Comment.objects.all().delete()
35 | User.objects.all().delete()
36 |
37 | print("create a blog user")
38 | user = User.objects.create_superuser("admin", "admin@hellogithub.com", "admin")
39 |
40 | category_list = ["Python学习笔记", "开源项目", "工具资源", "程序员生活感悟", "test category"]
41 | tag_list = [
42 | "django",
43 | "Python",
44 | "Pipenv",
45 | "Docker",
46 | "Nginx",
47 | "Elasticsearch",
48 | "Gunicorn",
49 | "Supervisor",
50 | "test tag",
51 | ]
52 | a_year_ago = timezone.now() - timedelta(days=365)
53 |
54 | print("create categories and tags")
55 | for cate in category_list:
56 | Category.objects.create(name=cate)
57 |
58 | for tag in tag_list:
59 | Tag.objects.create(name=tag)
60 |
61 | print("create a markdown sample post")
62 | Post.objects.create(
63 | title="Markdown 与代码高亮测试",
64 | body=pathlib.Path(BASE_DIR)
65 | .joinpath("scripts", "md.sample")
66 | .read_text(encoding="utf-8"),
67 | category=Category.objects.create(name="Markdown测试"),
68 | author=user,
69 | )
70 |
71 | print("create some faked posts published within the past year")
72 | fake = faker.Faker() # English
73 | for _ in range(100):
74 | tags = Tag.objects.order_by("?")
75 | tag1 = tags.first()
76 | tag2 = tags.last()
77 | cate = Category.objects.order_by("?").first()
78 | created_time = fake.date_time_between(
79 | start_date="-1y", end_date="now", tzinfo=timezone.get_current_timezone()
80 | )
81 | post = Post.objects.create(
82 | title=fake.sentence().rstrip("."),
83 | body="\n\n".join(fake.paragraphs(10)),
84 | created_time=created_time,
85 | category=cate,
86 | author=user,
87 | )
88 | post.tags.add(tag1, tag2)
89 | post.save()
90 |
91 | fake = faker.Faker("zh_CN")
92 | for _ in range(100): # Chinese
93 | tags = Tag.objects.order_by("?")
94 | tag1 = tags.first()
95 | tag2 = tags.last()
96 | cate = Category.objects.order_by("?").first()
97 | created_time = fake.date_time_between(
98 | start_date="-1y", end_date="now", tzinfo=timezone.get_current_timezone()
99 | )
100 | post = Post.objects.create(
101 | title=fake.sentence().rstrip("."),
102 | body="\n\n".join(fake.paragraphs(10)),
103 | created_time=created_time,
104 | category=cate,
105 | author=user,
106 | )
107 | post.tags.add(tag1, tag2)
108 | post.save()
109 |
110 | print("create some comments")
111 | for post in Post.objects.all()[:20]:
112 | post_created_time = post.created_time
113 | delta_in_days = "-" + str((timezone.now() - post_created_time).days) + "d"
114 | for _ in range(random.randrange(3, 15)):
115 | Comment.objects.create(
116 | name=fake.name(),
117 | email=fake.email(),
118 | url=fake.uri(),
119 | text=fake.paragraph(),
120 | created_time=fake.date_time_between(
121 | start_date=delta_in_days,
122 | end_date="now",
123 | tzinfo=timezone.get_current_timezone(),
124 | ),
125 | post=post,
126 | )
127 |
128 | print("done!")
129 |
--------------------------------------------------------------------------------
/scripts/md.sample:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | # 欢迎使用马克飞象
5 |
6 | @(示例笔记本)[马克飞象|帮助|Markdown]
7 |
8 | **马克飞象**是一款专为印象笔记(Evernote)打造的Markdown编辑器,通过精心的设计与技术实现,配合印象笔记强大的存储和同步功能,带来前所未有的书写体验。特点概述:
9 |
10 | - **功能丰富** :支持高亮代码块、*LaTeX* 公式、流程图,本地图片以及附件上传,甚至截图粘贴,工作学习好帮手;
11 | - **得心应手** :简洁高效的编辑器,提供[桌面客户端][1]以及[离线Chrome App][2],支持移动端 Web;
12 | - **深度整合** :支持选择笔记本和添加标签,支持从印象笔记跳转编辑,轻松管理。
13 |
14 | -------------------
15 |
16 | [TOC]
17 |
18 | ## Markdown简介
19 |
20 | > Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式丰富的HTML页面。 —— [维基百科](https://zh.wikipedia.org/wiki/Markdown)
21 |
22 | 正如您在阅读的这份文档,它使用简单的符号标识不同的标题,将某些文字标记为**粗体**或者*斜体*,创建一个[链接](http://www.example.com)或一个脚注[^demo]。下面列举了几个高级功能,更多语法请按`Ctrl + /`查看帮助。
23 |
24 | ### 代码块
25 | ``` python
26 | @requires_authorization
27 | def somefunc(param1='', param2=0):
28 | '''A docstring'''
29 | if param1 > param2: # interesting
30 | print 'Greater'
31 | return (param2 - param1 + 1) or None
32 | class SomeClass:
33 | pass
34 | >>> message = '''interpreter
35 | ... prompt'''
36 | ```
37 | ### LaTeX 公式
38 |
39 | 可以创建行内公式,例如 $\Gamma(n) = (n-1)!\quad\forall n\in\mathbb N$。或者块级公式:
40 |
41 | $$ x = \dfrac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$
42 |
43 | ### 表格
44 | | Item | Value | Qty |
45 | | :-------- | --------:| :--: |
46 | | Computer | 1600 USD | 5 |
47 | | Phone | 12 USD | 12 |
48 | | Pipe | 1 USD | 234 |
49 |
50 | ### 流程图
51 | ```flow
52 | st=>start: Start
53 | e=>end
54 | op=>operation: My Operation
55 | cond=>condition: Yes or No?
56 |
57 | st->op->cond
58 | cond(yes)->e
59 | cond(no)->op
60 | ```
61 |
62 | 以及时序图:
63 |
64 | ```sequence
65 | Alice->Bob: Hello Bob, how are you?
66 | Note right of Bob: Bob thinks
67 | Bob-->Alice: I am good thanks!
68 | ```
69 |
70 | > **提示:**想了解更多,请查看**流程图**[语法][3]以及**时序图**[语法][4]。
71 |
72 | ### 复选框
73 |
74 | 使用 `- [ ]` 和 `- [x]` 语法可以创建复选框,实现 todo-list 等功能。例如:
75 |
76 | - [x] 已完成事项
77 | - [ ] 待办事项1
78 | - [ ] 待办事项2
79 |
80 | > **注意:**目前支持尚不完全,在印象笔记中勾选复选框是无效、不能同步的,所以必须在**马克飞象**中修改 Markdown 原文才可生效。下个版本将会全面支持。
81 |
82 |
83 | ## 印象笔记相关
84 |
85 | ### 笔记本和标签
86 | **马克飞象**增加了`@(笔记本)[标签A|标签B]`语法, 以选择笔记本和添加标签。 **绑定账号后**, 输入`(`自动会出现笔记本列表,请从中选择。
87 |
88 | ### 笔记标题
89 | **马克飞象**会自动使用文档内出现的第一个标题作为笔记标题。例如本文,就是第一行的 `欢迎使用马克飞象`。
90 |
91 | ### 快捷编辑
92 | 保存在印象笔记中的笔记,右上角会有一个红色的编辑按钮,点击后会回到**马克飞象**中打开并编辑该笔记。
93 | >**注意:**目前用户在印象笔记中单方面做的任何修改,马克飞象是无法自动感知和更新的。所以请务必回到马克飞象编辑。
94 |
95 | ### 数据同步
96 | **马克飞象**通过**将Markdown原文以隐藏内容保存在笔记中**的精妙设计,实现了对Markdown的存储和再次编辑。既解决了其他产品只是单向导出HTML的单薄,又规避了服务端存储Markdown带来的隐私安全问题。这样,服务端仅作为对印象笔记 API调用和数据转换之用。
97 |
98 | >**隐私声明:用户所有的笔记数据,均保存在印象笔记中。马克飞象不存储用户的任何笔记数据。**
99 |
100 | ### 离线存储
101 | **马克飞象**使用浏览器离线存储将内容实时保存在本地,不必担心网络断掉或浏览器崩溃。为了节省空间和避免冲突,已同步至印象笔记并且不再修改的笔记将删除部分本地缓存,不过依然可以随时通过`文档管理`打开。
102 |
103 | > **注意:**虽然浏览器存储大部分时候都比较可靠,但印象笔记作为专业云存储,更值得信赖。以防万一,**请务必经常及时同步到印象笔记**。
104 |
105 | ## 编辑器相关
106 | ### 设置
107 | 右侧系统菜单(快捷键`Ctrl + M`)的`设置`中,提供了界面字体、字号、自定义CSS、vim/emacs 键盘模式等高级选项。
108 |
109 | ### 快捷键
110 |
111 | 帮助 `Ctrl + /`
112 | 同步文档 `Ctrl + S`
113 | 创建文档 `Ctrl + Alt + N`
114 | 最大化编辑器 `Ctrl + Enter`
115 | 预览文档 `Ctrl + Alt + Enter`
116 | 文档管理 `Ctrl + O`
117 | 系统菜单 `Ctrl + M`
118 |
119 | 加粗 `Ctrl + B`
120 | 插入图片 `Ctrl + G`
121 | 插入链接 `Ctrl + L`
122 | 提升标题 `Ctrl + H`
123 |
124 | ## 关于收费
125 |
126 | **马克飞象**为新用户提供 10 天的试用期,试用期过后需要[续费](maxiang.info/vip.html)才能继续使用。未购买或者未及时续费,将不能同步新的笔记。之前保存过的笔记依然可以编辑。
127 |
128 |
129 | ## 反馈与建议
130 | - 微博:[@马克飞象](http://weibo.com/u/2788354117),[@GGock](http://weibo.com/ggock "开发者个人账号")
131 | - 邮箱:
132 |
133 | ---------
134 | 感谢阅读这份帮助文档。请点击右上角,绑定印象笔记账号,开启全新的记录与分享体验吧。
135 |
136 |
137 |
138 |
139 | [^demo]: 这是一个示例脚注。请查阅 [MultiMarkdown 文档](https://github.com/fletcher/MultiMarkdown/wiki/MultiMarkdown-Syntax-Guide#footnotes) 关于脚注的说明。 **限制:** 印象笔记的笔记内容使用 [ENML][5] 格式,基于 HTML,但是不支持某些标签和属性,例如id,这就导致`脚注`和`TOC`无法正常点击。
140 |
141 |
142 | [1]: http://maxiang.info/client_zh
143 | [2]: https://chrome.google.com/webstore/detail/kidnkfckhbdkfgbicccmdggmpgogehop
144 | [3]: http://adrai.github.io/flowchart.js/
145 | [4]: http://bramp.github.io/js-sequence-diagrams/
146 | [5]: https://dev.yinxiang.com/doc/articles/enml.php
147 |
148 |
149 | 欢迎使用马克飞象
150 | 示例笔记本 马克飞象 帮助 Markdown
151 |
152 |
153 | 马克飞象是一款专为印象笔记(Evernote)打造的Markdown编辑器,通过精心的设计与技术实现,配合印象笔记强大的存储和同步功能,带来前所未有的书写体验。特点概述:
154 |
155 | 功能丰富 :支持高亮代码块、LaTeX 公式、流程图,本地图片以及附件上传,甚至截图粘贴,工作学习好帮手;
156 |
157 | 得心应手 :简洁高效的编辑器,提供桌面客户端以及离线Chrome App,支持移动端 Web;
158 |
159 | 深度整合 :支持选择笔记本和添加标签,支持从印象笔记跳转编辑,轻松管理。
160 |
161 | 欢迎使用马克飞象
162 | Markdown简介
163 | 代码块
164 | LaTeX 公式
165 | 表格
166 | 流程图
167 | 复选框
168 | 印象笔记相关
169 | 笔记本和标签
170 | 笔记标题
171 | 快捷编辑
172 | 数据同步
173 | 离线存储
174 | 编辑器相关
175 | 设置
176 | 快捷键
177 | 关于收费
178 | 反馈与建议
179 | Markdown简介
180 | Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式丰富的HTML页面。 —— 维基百科
181 |
182 | 正如您在阅读的这份文档,它使用简单的符号标识不同的标题,将某些文字标记为粗体或者斜体,创建一个链接或一个脚注1。下面列举了几个高级功能,更多语法请按Ctrl + /查看帮助。
183 |
184 | 代码块
185 | @requires_authorization
186 | def somefunc(param1='', param2=0):
187 | '''A docstring'''
188 | if param1 > param2: # interesting
189 | print 'Greater'
190 | return (param2 - param1 + 1) or None
191 | class SomeClass:
192 | pass
193 | >>> message = '''interpreter
194 | ... prompt'''
195 | LaTeX 公式
196 | 可以创建行内公式,例如 。或者块级公式:
197 |
198 | 表格
199 | Item Value Qty
200 | Computer 1600 USD 5
201 | Phone 12 USD 12
202 | Pipe 1 USD 234
203 | 流程图
204 | Start
205 | My Operation
206 | Yes or No?
207 | End
208 | yes
209 | no
210 | 以及时序图:
211 |
212 | Alice
213 | Alice
214 | Bob
215 | Bob
216 | Hello Bob, how are you?
217 | Bob thinks
218 | I am good thanks!
219 | 提示:想了解更多,请查看流程图语法以及时序图语法。
220 |
221 | 复选框
222 | 使用 - [ ] 和 - [x] 语法可以创建复选框,实现 todo-list 等功能。例如:
223 |
224 | 已完成事项
225 | 待办事项1
226 | 待办事项2
227 | 注意:目前支持尚不完全,在印象笔记中勾选复选框是无效、不能同步的,所以必须在马克飞象中修改 Markdown 原文才可生效。下个版本将会全面支持。
228 |
229 | 印象笔记相关
230 | 笔记本和标签
231 | 马克飞象增加了@(笔记本)[标签A|标签B]语法, 以选择笔记本和添加标签。 绑定账号后, 输入(自动会出现笔记本列表,请从中选择。
232 |
233 | 笔记标题
234 | 马克飞象会自动使用文档内出现的第一个标题作为笔记标题。例如本文,就是第一行的 欢迎使用马克飞象。
235 |
236 | 快捷编辑
237 | 保存在印象笔记中的笔记,右上角会有一个红色的编辑按钮,点击后会回到马克飞象中打开并编辑该笔记。
238 |
239 | 注意:目前用户在印象笔记中单方面做的任何修改,马克飞象是无法自动感知和更新的。所以请务必回到马克飞象编辑。
240 |
241 | 数据同步
242 | 马克飞象通过将Markdown原文以隐藏内容保存在笔记中的精妙设计,实现了对Markdown的存储和再次编辑。既解决了其他产品只是单向导出HTML的单薄,又规避了服务端存储Markdown带来的隐私安全问题。这样,服务端仅作为对印象笔记 API调用和数据转换之用。
243 |
244 | 隐私声明:用户所有的笔记数据,均保存在印象笔记中。马克飞象不存储用户的任何笔记数据。
245 |
246 | 离线存储
247 | 马克飞象使用浏览器离线存储将内容实时保存在本地,不必担心网络断掉或浏览器崩溃。为了节省空间和避免冲突,已同步至印象笔记并且不再修改的笔记将删除部分本地缓存,不过依然可以随时通过文档管理打开。
248 |
249 | 注意:虽然浏览器存储大部分时候都比较可靠,但印象笔记作为专业云存储,更值得信赖。以防万一,请务必经常及时同步到印象笔记。
250 |
251 | 编辑器相关
252 | 设置
253 | 右侧系统菜单(快捷键Ctrl + M)的设置中,提供了界面字体、字号、自定义CSS、vim/emacs 键盘模式等高级选项。
254 |
255 | 快捷键
256 | 帮助 Ctrl + /
257 | 同步文档 Ctrl + S
258 | 创建文档 Ctrl + Alt + N
259 | 最大化编辑器 Ctrl + Enter
260 | 预览文档 Ctrl + Alt + Enter
261 | 文档管理 Ctrl + O
262 | 系统菜单 Ctrl + M
263 |
264 | 加粗 Ctrl + B
265 | 插入图片 Ctrl + G
266 | 插入链接 Ctrl + L
267 | 提升标题 Ctrl + H
268 |
269 | 关于收费
270 | 马克飞象为新用户提供 10 天的试用期,试用期过后需要续费才能继续使用。未购买或者未及时续费,将不能同步新的笔记。之前保存过的笔记依然可以编辑。
271 |
272 | 反馈与建议
273 | 微博:@马克飞象,@GGock
274 |
275 | 邮箱:hustgock@gmail.com
276 |
277 | 感谢阅读这份帮助文档。请点击右上角,绑定印象笔记账号,开启全新的记录与分享体验吧。
278 |
279 | 这是一个示例脚注。请查阅 MultiMarkdown 文档 关于脚注的说明。 限制: 印象笔记的笔记内容使用 ENML 格式,基于 HTML,但是不支持某些标签和属性,例如id,这就导致脚注和TOC无法正常点击。 ↩
280 | 绑定印象笔记账号
281 | 绑定 Evernote International 账号
282 | 当前文档
283 | 恢复至上次同步状态
284 | 删除文档
285 | 导出...
286 | 预览文档
287 | 分享链接
288 | 系统
289 | 设置
290 | 下载桌面客户端
291 | 下载离线Chrome App
292 | 使用说明
293 | 快捷帮助
294 | 常见问题
295 | 关于
296 |
297 | 搜索文件
298 | 强调
299 | *斜体* **粗体**
300 | CtrlI/B
301 | 链接
302 | [描述](http://example.com)
303 | CtrlL
304 | 图片
305 | 
306 | CtrlG
307 | 笔记本
308 | @(笔记本)[标签1,标签2,标签3]
309 | 标题
310 | 标题1 标题2
311 | ======== --------
312 |
313 | ## 标题2 ###### 标题6
314 | Ctrl1~5
315 | 列表
316 | 1. 有序列表 - 无序列表 - [ ] 复选框
317 | 2. 有序列表 - 无序列表 - [x] 复选框
318 | 引用
319 | > 这是引用的文字
320 | > 引用内可以嵌套标题、列表等
321 | 代码
322 | 这是一句行内代码 `var a=1` , 以下是代码区块:
323 | ```ruby
324 | print 'Hello world'
325 | ```
326 | CtrlK
327 | LaTex 公式
328 | 这是一句行内公式 $ y = x + 1 $ , 以下是整行公式:
329 | $$ a^2 + b^2 = c^2 $$
330 | 表格
331 | | Item | Value | Qty |
332 | | :-------- | --------:| :--: |
333 | | Computer | 1600 USD | 5 |
334 | CtrlAltT
335 | 文档管理 CtrlO
336 | 帮助 Ctrl/
337 | 最大化编辑器 CtrlEnter
338 | 预览文档 CtrlAltEnter
339 | 同步文档 CtrlS
340 | 创建文档 CtrlAltN
341 | 系统菜单 CtrlM
342 | 图片管道指令
343 | 
344 | @描述 @会将描述显示在图片下方
345 | left 左侧对齐
346 | right 右侧对齐
347 | center 居中
348 | 300x200 宽x高, 0代表自适应
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load blog_extras %}
3 |
4 |
5 |
6 | Black & White
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
101 |
102 | {% if messages %}
103 | {% for message in messages %}
104 |
105 |
107 | {{ message }}
108 |
109 | {% endfor %}
110 | {% endif %}
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {% block main %}
120 | {% endblock main %}
121 |
122 |
135 |
136 |
137 |
138 |
150 |
151 |
152 |
153 |
154 |
162 |
163 |
164 |
165 |
166 |
167 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/templates/blog/detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load comments_extras %}
3 |
4 | {% block main %}
5 |
6 |
17 |
18 | {{ post.body_html|safe }}
19 |
20 |
21 |
29 | {% endblock main %}
30 |
31 | {% block toc %}
32 | {% if post.toc %}
33 |
34 |
35 |
36 |
37 | {{ post.toc|safe }}
38 |
39 |
40 |
41 | {% endif %}
42 | {% endblock toc %}
43 |
--------------------------------------------------------------------------------
/templates/blog/inclusions/_archives.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/blog/inclusions/_categories.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/blog/inclusions/_recent_posts.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% for post in recent_post_list %}
5 | -
6 | {{ post.title }}
7 |
8 | {% empty %}
9 | 暂无文章!
10 | {% endfor %}
11 |
12 |
--------------------------------------------------------------------------------
/templates/blog/inclusions/_tags.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/blog/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block main %}
4 | {% for post in post_list %}
5 |
6 |
19 |
20 |
{{ post.excerpt }}...
21 |
24 |
25 |
26 | {% empty %}
27 | 暂时还没有发布的文章!
28 | {% endfor %}
29 |
30 | {% if is_paginated %}
31 | {{ page_obj.render }}
32 | {% endif %}
33 |
34 | {% endblock main %}
35 |
--------------------------------------------------------------------------------
/templates/comments/inclusions/_form.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/comments/inclusions/_list.html:
--------------------------------------------------------------------------------
1 | 评论列表,共 {{ comment_count }} 条评论
2 |
--------------------------------------------------------------------------------
/templates/comments/preview.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load comments_extras %}
3 |
4 | {% block main %}
5 | {% show_comment_form post form %}
6 | {% endblock main %}
--------------------------------------------------------------------------------
/templates/pure_pagination/pagination.html:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/templates/search/indexes/blog/post_text.txt:
--------------------------------------------------------------------------------
1 | {{ object.title }}
2 | {{ object.body }}
--------------------------------------------------------------------------------
/templates/search/search.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load highlight %}
3 |
4 | {% block main %}
5 | {% if query %}
6 | {% for result in page.object_list %}
7 |
8 |
27 |
28 |
{% highlight result.object.body with query %}
29 |
33 |
34 |
35 | {% empty %}
36 | 没有搜索到你想要的结果!
37 | {% endfor %}
38 |
39 | {% if page.has_previous or page.has_next %}
40 |
41 | {% if page.has_previous %}
42 |
{% endif %}« Previous
43 | {% if page.has_previous %}{% endif %}
44 |
|
45 | {% if page.has_next %}
{% endif %}Next
46 | »{% if page.has_next %}{% endif %}
47 |
48 | {% endif %}
49 | {% else %}
50 | 请输入搜索关键词,例如 django
51 | {% endif %}
52 | {% endblock main %}
--------------------------------------------------------------------------------
23 |
发表评论
24 | {% show_comment_form post %} 25 |