├── .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 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 31 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | WeiXin 13 | GitHub stars 14 | Sina Weibo 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'
\s*\s*
', 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('