├── .coveragerc ├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS ├── Makefile ├── README.md ├── TODO.md ├── admin_ui ├── LICENSE ├── __init__.py └── static │ └── admin │ ├── css │ ├── base.css │ ├── changelists.css │ ├── fonts.css │ ├── forms.css │ ├── login.css │ ├── rtl.css │ └── widgets.css │ ├── fonts │ ├── LICENSE.txt │ ├── README.txt │ ├── Roboto-Bold-webfont.woff │ ├── Roboto-Light-webfont.woff │ └── Roboto-Regular-webfont.woff │ └── img │ ├── changelist-bg.gif │ ├── changelist-bg_rtl.gif │ ├── default-bg-reverse.gif │ ├── default-bg.gif │ ├── deleted-overlay.gif │ ├── icon-no.gif │ ├── icon-unknown.gif │ ├── icon-yes.gif │ ├── icon_addlink.gif │ ├── icon_alert.gif │ ├── icon_calendar.gif │ ├── icon_changelink.gif │ ├── icon_clock.gif │ ├── icon_deletelink.gif │ ├── icon_error.gif │ ├── icon_searchbox.png │ ├── icon_success.gif │ ├── inline-delete-8bit.png │ ├── inline-delete.png │ ├── inline-restore-8bit.png │ ├── inline-restore.png │ ├── inline-splitter-bg.gif │ ├── nav-bg-grabber.gif │ ├── nav-bg-reverse.gif │ ├── nav-bg-selected.gif │ ├── nav-bg.gif │ ├── selector-icons.gif │ ├── selector-search.gif │ ├── sorting-icons.gif │ ├── tooltag-add.png │ └── tooltag-arrowright.png ├── api ├── __init__.py ├── blog_api.py ├── tests.py └── urls.py ├── blog ├── README.md ├── __init__.py ├── admin.py ├── feeds.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_event.py │ ├── 0003_auto_20150528_0036.py │ └── __init__.py ├── models.py ├── sitemaps.py ├── templatetags │ ├── __init__.py │ └── weblog.py ├── tests.py ├── urls.py └── views.py ├── conf ├── README.md ├── __init__.py ├── admin.py ├── context_processors.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_field_setting_site.py │ ├── 0003_update_site_setting.py │ ├── 0004_ssl_account_settings_rename.py │ └── __init__.py ├── models.py ├── static │ └── mezzanine │ │ └── css │ │ └── admin │ │ └── settings.css ├── templates │ └── admin │ │ └── conf │ │ └── setting │ │ └── change_list.html └── tests.py ├── core ├── __init__.py ├── defaults.py ├── middleware.py ├── models.py ├── request.py └── views.py ├── docs ├── architecture.dot └── architecture.jpg ├── echoes ├── __init__.py ├── local_settings.py ├── settings.py ├── urls.py └── wsgi.py ├── frontend ├── __init__.py ├── mustaches │ └── template.html ├── static │ ├── css │ │ ├── normalize.css │ │ └── skeleton.css │ ├── js │ │ └── navbar.js │ └── phodal │ │ ├── css │ │ ├── foundation.css │ │ └── main.css │ │ └── js │ │ ├── foundation.min.js │ │ ├── foundation │ │ ├── foundation.abide.js │ │ ├── foundation.accordion.js │ │ ├── foundation.alert.js │ │ ├── foundation.clearing.js │ │ ├── foundation.dropdown.js │ │ ├── foundation.equalizer.js │ │ ├── foundation.interchange.js │ │ ├── foundation.joyride.js │ │ ├── foundation.js │ │ ├── foundation.magellan.js │ │ ├── foundation.offcanvas.js │ │ ├── foundation.orbit.js │ │ ├── foundation.reveal.js │ │ ├── foundation.slider.js │ │ ├── foundation.tab.js │ │ ├── foundation.tooltip.js │ │ └── foundation.topbar.js │ │ ├── jquery-2.1.4.min.js │ │ └── jquery-2.1.4.min.map ├── templates │ ├── 400.html │ ├── 403.html │ ├── 404.html │ ├── 410.html │ ├── 500.html │ ├── base.html │ ├── base_error.html │ ├── base_weblog.html │ ├── blog │ │ ├── blog_pagination.html │ │ ├── entry_archive.html │ │ ├── entry_archive_day.html │ │ ├── entry_archive_month.html │ │ ├── entry_archive_year.html │ │ ├── entry_detail.html │ │ ├── entry_snippet.html │ │ ├── month_links_snippet.html │ │ ├── news_summary.html │ │ └── sidebar.html │ ├── flatpages │ │ └── default.html │ ├── includes │ │ ├── footer.html │ │ └── header.html │ ├── index.html │ └── mobile │ │ ├── index.html │ │ └── info.html ├── tests.py └── views.py ├── legacy ├── __init__.py ├── tests.py ├── urls.py └── views.py ├── manage.py ├── mobile └── __init__.py ├── mustache ├── __init__.py ├── shortcuts.py └── templatetags │ ├── __init__.py │ └── mustache.py ├── requirements-dev.txt ├── requirements.txt ├── tox.ini └── utils ├── __init__.py ├── cache.py ├── device.py ├── sites.py ├── urls.py └── views.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = 1 3 | 4 | [report] 5 | omit = .tox*,*tests*,*migrations* 6 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: DlWBtX7wBzL5SkpNeJoJS6MCT6DmIev0m -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.{html,json}] 12 | indent_style = space 13 | indent_size = 2 14 | trim_trailing_whitespace = false 15 | 16 | [*.{scss,js}] 17 | indent_style = tab 18 | indent_size = tab 19 | 20 | 21 | # Customizations for third party libraries 22 | 23 | [static/scss/{font-awesome/*.scss,_font-awesome.scss}] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | [static/js/lib/**] 28 | trim_trailing_whitespace = ignore 29 | insert_final_newline = ignore 30 | indent_style = ignore 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | *.db 4 | db 5 | .tox 6 | .coverage 7 | echoes/static/rest_framework 8 | echoes/static/admin 9 | echoes/static/mezzanine 10 | echoes/static/phodal -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: no 2 | addons: 3 | postgresql: "9.3" 4 | language: python 5 | env: 6 | - TOXENV=py27-tests 7 | - TOXENV=py27-isort 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install coveralls tox 11 | before_script: 12 | script: 13 | - tox 14 | after_success: coveralls 15 | notifications: 16 | email: false 17 | 18 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Phodal HUANG -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | STATIC = djangoproject/static 2 | SCSS = djangoproject/scss 3 | APP_LIST ?= admin_ui blog rest_framework frontend conf mustache mobile api legacy 4 | 5 | .PHONY: collectstatics compile-scss compile-scss-debug watch-scss run install test ci 6 | 7 | collectstatics: compile-scss 8 | ./manage.py collectstatic --noinput 9 | 10 | run: 11 | python manage.py runserver 0.0.0.0:8000 12 | 13 | install: 14 | pip install -r requirements/dev.txt 15 | 16 | test: 17 | @coverage run --source=. manage.py test -v2 $(APP_LIST) 18 | 19 | ci: test 20 | @coverage report 21 | 22 | isort: 23 | isort -rc $(APP_LIST) 24 | 25 | isort-check: 26 | isort -c -rc $(APP_LIST) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Echoes CMS 2 | 3 | > A Django CMS, follow trend. 4 | 5 | [![Build Status](https://travis-ci.org/phodal/echoes.svg?branch=master)](https://travis-ci.org/phodal/echoes) 6 | [![Coverage Status](https://coveralls.io/repos/phodal/echoes/badge.svg)](https://coveralls.io/r/phodal/echoes) 7 | 8 | ![Echoes CMS](./docs/architecture.jpg) 9 | 10 | ##Goals 11 | 12 | - CMS with Mustache 13 | - API for Frontend & Mobile 14 | - Responsive UI 15 | 16 | #Setup 17 | 18 | 1.Install 19 | 20 | pip install -r requirements.txt 21 | 22 | 2.Setup Database 23 | 24 | python manage.py syncdb 25 | python manage.py migrate 26 | 27 | 3.Run 28 | 29 | python manage.py runserver 30 | 31 | ##License 32 | 33 | © 2015 [Phodal Huang][phodal]. This code is distributed under the MIT license. 34 | [phodal]:http://www.phodal.com/ 35 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | ##Done 3 | 4 | - Admin UI 5 | - Mustache Template Support 6 | - Responsive Nav (Foundation) 7 | - Responsive Layout (Foundation) 8 | 9 | ###Publishing & Editing System 10 | 11 | - Markdown Support 12 | 13 | ##OnGoing 14 | 15 | ####More 16 | 17 | ##Weblog 18 | 19 | - Canonical Link For Weblog 20 | - Mustache Render Templates 21 | 22 | ###Page 23 | 24 | - Nav on Page (Mezzanine Like) 25 | 26 | ##TODO 27 | 28 | ###Basic 29 | 30 | - RESTful API 31 | - Mobile Interface (Browser) 32 | - Mobile API (Browser & APP) 33 | 34 | ###Publishing & Editing System 35 | 36 | - Editing on Page(onging) 37 | 38 | 39 | ###Others 40 | 41 | - add static link to other page -------------------------------------------------------------------------------- /admin_ui/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Alex Dergunov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the author nor the names of other contributors may 15 | be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /admin_ui/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.9.3' 2 | -------------------------------------------------------------------------------- /admin_ui/static/admin/css/changelists.css: -------------------------------------------------------------------------------- 1 | /* CHANGELISTS */ 2 | 3 | #changelist { 4 | position: relative; 5 | width: 100%; 6 | } 7 | 8 | #changelist table { 9 | width: 100%; 10 | } 11 | 12 | .change-list .hiddenfields { display:none; } 13 | 14 | .change-list .filtered table { 15 | border-right: none; 16 | } 17 | 18 | .change-list .filtered { 19 | min-height: 400px; 20 | } 21 | 22 | .change-list .filtered { 23 | background: #fff !important; 24 | } 25 | 26 | .change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { 27 | margin-right: 280px !important; 28 | width: auto !important; 29 | } 30 | 31 | .change-list .filtered table tbody th { 32 | padding-right: 1em; 33 | } 34 | 35 | #changelist-form .results { 36 | overflow-x: auto; 37 | } 38 | 39 | #changelist .toplinks { 40 | border-bottom: 1px solid #ccc !important; 41 | } 42 | 43 | #changelist .paginator { 44 | color: #666; 45 | border-bottom: 1px solid #eee; 46 | background: #fff; 47 | overflow: hidden; 48 | } 49 | 50 | /* CHANGELIST TABLES */ 51 | 52 | #changelist table thead th { 53 | padding: 0; 54 | white-space: nowrap; 55 | vertical-align: middle; 56 | } 57 | 58 | #changelist table thead th.action-checkbox-column { 59 | width: 1.5em; 60 | text-align: center; 61 | } 62 | 63 | #changelist table tbody td.action-checkbox { 64 | text-align:center; 65 | } 66 | 67 | #changelist table tfoot { 68 | color: #666; 69 | } 70 | 71 | /* TOOLBAR */ 72 | 73 | #changelist #toolbar { 74 | padding: 8px 10px; 75 | margin-bottom: 15px; 76 | border-top: 1px solid #eee; 77 | border-bottom: 1px solid #eee; 78 | background: #f8f8f8; 79 | color: #666; 80 | } 81 | 82 | #changelist #toolbar form input { 83 | border-radius: 4px; 84 | font-size: 14px; 85 | padding: 5px; 86 | color: #333; 87 | } 88 | 89 | #changelist #toolbar form #searchbar { 90 | height: 19px; 91 | border: 1px solid #ccc; 92 | padding: 2px 5px; 93 | margin: 0; 94 | vertical-align: top; 95 | font-size: 13px; 96 | } 97 | 98 | #changelist #toolbar form #searchbar:focus { 99 | outline: none; 100 | border-color: #999; 101 | } 102 | 103 | #changelist #toolbar form input[type="submit"] { 104 | border: 1px solid #ccc; 105 | padding: 2px 10px; 106 | margin: 0; 107 | vertical-align: -1px; 108 | background: #fff url(../img/nav-bg.gif) bottom repeat-x; 109 | cursor: pointer; 110 | color: #333; 111 | } 112 | 113 | #changelist #toolbar form input[type="submit"]:hover { 114 | border-color: #999; 115 | } 116 | 117 | #changelist #changelist-search img { 118 | vertical-align: middle; 119 | margin-right: 4px; 120 | } 121 | 122 | /* FILTER COLUMN */ 123 | 124 | #changelist-filter { 125 | position: absolute; 126 | top: 0; 127 | right: 0; 128 | z-index: 1000; 129 | width: 240px; 130 | background: #f8f8f8; 131 | border-left: none; 132 | margin: 0; 133 | } 134 | 135 | #changelist-filter h2 { 136 | font-size: 14px; 137 | text-transform: uppercase; 138 | letter-spacing: 0.5px; 139 | padding: 5px 15px; 140 | margin-bottom: 12px; 141 | border-bottom: none; 142 | } 143 | 144 | #changelist-filter h3 { 145 | font-weight: 400; 146 | font-size: 14px; 147 | padding: 0 15px; 148 | margin-bottom: 10px; 149 | } 150 | 151 | #changelist-filter ul { 152 | margin: 5px 0; 153 | padding: 0 15px 15px; 154 | border-bottom: 1px solid #eaeaea; 155 | } 156 | 157 | #changelist-filter ul:last-child { 158 | border-bottom: none; 159 | padding-bottom: none; 160 | } 161 | 162 | #changelist-filter li { 163 | list-style-type: none; 164 | margin-left: 0; 165 | padding-left: 0; 166 | } 167 | 168 | #changelist-filter a { 169 | display: block; 170 | color: #999; 171 | } 172 | 173 | #changelist-filter a:hover { 174 | color: #036 !important; 175 | } 176 | 177 | #changelist-filter li.selected { 178 | border-left: 5px solid #eaeaea; 179 | padding-left: 10px; 180 | margin-left: -15px; 181 | } 182 | 183 | #changelist-filter li.selected a { 184 | color: #5b80b2 !important; 185 | } 186 | 187 | 188 | /* DATE DRILLDOWN */ 189 | 190 | .change-list ul.toplinks { 191 | display: block; 192 | background: white url(../img/nav-bg-reverse.gif) 0 -10px repeat-x; 193 | border-top: 1px solid white; 194 | float: left; 195 | padding: 0 !important; 196 | margin: 0 !important; 197 | width: 100%; 198 | } 199 | 200 | .change-list ul.toplinks li { 201 | padding: 3px 6px; 202 | font-weight: bold; 203 | list-style-type: none; 204 | display: inline-block; 205 | } 206 | 207 | .change-list ul.toplinks .date-back a { 208 | color: #999; 209 | } 210 | 211 | .change-list ul.toplinks .date-back a:hover { 212 | color: #036; 213 | } 214 | 215 | /* PAGINATOR */ 216 | 217 | .paginator { 218 | font-size: 13px; 219 | padding-top: 10px; 220 | padding-bottom: 10px; 221 | line-height: 22px; 222 | margin: 0; 223 | border-top: 1px solid #ddd; 224 | } 225 | 226 | .paginator a:link, .paginator a:visited { 227 | padding: 2px 6px; 228 | background: #79aec8; 229 | text-decoration: none; 230 | color: #fff; 231 | } 232 | 233 | .paginator a.showall { 234 | padding: 0 !important; 235 | border: none !important; 236 | background: none !important; 237 | color: #5b80b2 !important; 238 | } 239 | 240 | .paginator a.showall:hover { 241 | color: #036 !important; 242 | background: none !important; 243 | } 244 | 245 | .paginator .end { 246 | border-width: 2px !important; 247 | margin-right: 6px; 248 | } 249 | 250 | .paginator .this-page { 251 | padding: 2px 6px; 252 | font-weight: bold; 253 | font-size: 13px; 254 | vertical-align: top; 255 | } 256 | 257 | .paginator a:hover { 258 | color: white; 259 | background: #036; 260 | } 261 | 262 | /* ACTIONS */ 263 | 264 | .filtered .actions { 265 | margin-right: 280px !important; 266 | border-right: none; 267 | } 268 | 269 | #changelist table input { 270 | margin: 0; 271 | vertical-align: baseline; 272 | } 273 | 274 | #changelist table tbody tr.selected { 275 | background-color: #FFFFCC; 276 | } 277 | 278 | #changelist .actions { 279 | padding: 10px; 280 | background: #fff; 281 | border-top: none; 282 | border-bottom: none; 283 | line-height: 24px; 284 | color: #999; 285 | } 286 | 287 | #changelist .actions.selected { 288 | background: #fffccf; 289 | border-top: 1px solid #fffee8; 290 | border-bottom: 1px solid #edecd6; 291 | } 292 | 293 | #changelist .actions span.all, 294 | #changelist .actions span.action-counter, 295 | #changelist .actions span.clear, 296 | #changelist .actions span.question { 297 | font-size: 13px; 298 | margin: 0 0.5em; 299 | display: none; 300 | } 301 | 302 | #changelist .actions:last-child { 303 | border-bottom: none; 304 | } 305 | 306 | #changelist .actions select { 307 | vertical-align: top; 308 | height: 24px; 309 | background: none; 310 | border: 1px solid #ccc; 311 | border-radius: 4px; 312 | font-size: 14px; 313 | padding: 0 0 0 4px; 314 | margin: 0; 315 | margin-left: 10px; 316 | } 317 | 318 | #changelist .actions label { 319 | display: inline-block; 320 | vertical-align: middle; 321 | font-size: 13px; 322 | } 323 | 324 | #changelist .actions .button { 325 | font-size: 13px; 326 | border: 1px solid #ccc; 327 | border-radius: 4px; 328 | background: #fff url(../img/nav-bg.gif) bottom repeat-x; 329 | cursor: pointer; 330 | height: 24px; 331 | line-height: 1; 332 | padding: 4px 8px; 333 | margin: 0; 334 | color: #333; 335 | } 336 | 337 | #changelist .actions .button:hover { 338 | border-color: #999; 339 | } 340 | -------------------------------------------------------------------------------- /admin_ui/static/admin/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | src: url('../fonts/Roboto-Bold-webfont.woff'); 4 | font-weight: 700; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto'; 10 | src: url('../fonts/Roboto-Regular-webfont.woff'); 11 | font-weight: 400; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Roboto'; 17 | src: url('../fonts/Roboto-Light-webfont.woff'); 18 | font-weight: 300; 19 | font-style: normal; 20 | } 21 | -------------------------------------------------------------------------------- /admin_ui/static/admin/css/forms.css: -------------------------------------------------------------------------------- 1 | @import url('widgets.css'); 2 | 3 | /* FORM ROWS */ 4 | 5 | .form-row { 6 | overflow: hidden; 7 | padding: 10px; 8 | font-size: 13px; 9 | border-bottom: 1px solid #eee; 10 | } 11 | 12 | .form-row img, .form-row input { 13 | vertical-align: middle; 14 | } 15 | 16 | .form-row label input[type="checkbox"] { 17 | margin-top: 0; 18 | vertical-align: 0; 19 | } 20 | 21 | form .form-row p { 22 | padding-left: 0; 23 | font-size: 11px !important; 24 | } 25 | 26 | .hidden { 27 | display: none; 28 | } 29 | 30 | /* FORM LABELS */ 31 | 32 | form h4 { 33 | margin: 0 !important; 34 | padding: 0 !important; 35 | border: none !important; 36 | } 37 | 38 | label { 39 | font-weight: normal !important; 40 | color: #666; 41 | font-size: 13px; 42 | } 43 | 44 | .required label, label.required { 45 | font-weight: bold !important; 46 | color: #333 !important; 47 | } 48 | 49 | /* RADIO BUTTONS */ 50 | 51 | form ul.radiolist li { 52 | list-style-type: none; 53 | } 54 | 55 | form ul.radiolist label { 56 | float: none; 57 | display: inline; 58 | } 59 | 60 | form ul.radiolist input[type="radio"] { 61 | margin: -2px 4px 0 0; 62 | padding: 0; 63 | outline: none; 64 | } 65 | 66 | form ul.inline { 67 | margin-left: 0; 68 | padding: 0; 69 | } 70 | 71 | form ul.inline li { 72 | float: left; 73 | padding-right: 7px; 74 | } 75 | 76 | /* ALIGNED FIELDSETS */ 77 | 78 | .aligned label { 79 | display: block; 80 | padding: 4px 10px 0 0; 81 | float: left; 82 | width: 160px; 83 | word-wrap: break-word; 84 | line-height: 1; 85 | } 86 | 87 | .aligned label:not(.vCheckboxLabel):after { 88 | content: ''; 89 | display: inline-block; 90 | vertical-align: middle; 91 | height: 26px; 92 | } 93 | 94 | .aligned label + p:not(.help) { 95 | font-size: 13px !important; 96 | padding: 6px 0; 97 | margin-top: 0; 98 | margin-bottom: 0; 99 | margin-left: 170px; 100 | } 101 | 102 | .aligned ul label { 103 | display: inline; 104 | float: none; 105 | width: auto; 106 | } 107 | 108 | .aligned .form-row input { 109 | margin-bottom: 0; 110 | } 111 | 112 | .colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { 113 | width: 350px; 114 | } 115 | 116 | form .aligned ul { 117 | margin-left: 160px; 118 | padding-left: 10px; 119 | } 120 | 121 | form .aligned ul.radiolist { 122 | display: inline-block; 123 | margin: 0; 124 | padding: 0; 125 | } 126 | 127 | form .aligned p.help { 128 | margin-top: 0; 129 | margin-left: 160px; 130 | padding-left: 10px; 131 | } 132 | 133 | form .aligned label + p.help { 134 | margin-left: 0; 135 | padding-left: 0; 136 | } 137 | 138 | form .aligned p.help:last-child { 139 | margin-bottom: 0; 140 | padding-bottom: 0; 141 | } 142 | 143 | form .aligned input + p.help, 144 | form .aligned textarea + p.help, 145 | form .aligned select + p.help { 146 | margin-left: 160px; 147 | padding-left: 10px; 148 | } 149 | 150 | form .aligned ul li { 151 | list-style: none; 152 | } 153 | 154 | form .aligned table p { 155 | margin-left: 0; 156 | padding-left: 0; 157 | } 158 | 159 | .aligned .vCheckboxLabel { 160 | float: none !important; 161 | display: inline-block; 162 | vertical-align: -3px; 163 | padding: 0 0 5px 5px; 164 | } 165 | 166 | .aligned .vCheckboxLabel + p.help { 167 | margin-top: -4px; 168 | } 169 | 170 | .colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField { 171 | width: 610px; 172 | } 173 | 174 | .checkbox-row p.help { 175 | margin-left: 0; 176 | padding-left: 0 !important; 177 | } 178 | 179 | fieldset .field-box { 180 | float: left; 181 | margin-right: 20px; 182 | } 183 | 184 | /* WIDE FIELDSETS */ 185 | 186 | .wide label { 187 | width: 200px !important; 188 | } 189 | 190 | form .wide p { 191 | margin-left: 200px !important; 192 | } 193 | 194 | form .wide p.help { 195 | padding-left: 38px; 196 | } 197 | 198 | .colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { 199 | width: 450px; 200 | } 201 | 202 | /* COLLAPSED FIELDSETS */ 203 | 204 | fieldset.collapsed * { 205 | display: none; 206 | } 207 | 208 | fieldset.collapsed h2, fieldset.collapsed { 209 | display: block !important; 210 | } 211 | 212 | fieldset.collapsed { 213 | border: 1px solid #eee; 214 | border-radius: 4px; 215 | overflow: hidden; 216 | } 217 | 218 | fieldset.collapsed h2 { 219 | background: #f8f8f8; 220 | color: #666; 221 | } 222 | 223 | fieldset .collapse-toggle { 224 | color: #fff; 225 | } 226 | 227 | fieldset.collapsed .collapse-toggle { 228 | background: transparent; 229 | display: inline !important; 230 | color: #447e9b; 231 | } 232 | 233 | /* MONOSPACE TEXTAREAS */ 234 | 235 | fieldset.monospace textarea { 236 | font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; 237 | } 238 | 239 | /* SUBMIT ROW */ 240 | 241 | .submit-row { 242 | padding: 12px 14px; 243 | margin: 0 0 20px; 244 | background: #f8f8f8; 245 | border: 1px solid #eee; 246 | border-radius: 4px; 247 | text-align: right; 248 | overflow: hidden; 249 | } 250 | 251 | body.popup .submit-row { 252 | overflow: auto; 253 | } 254 | 255 | .submit-row input { 256 | height: 35px; 257 | line-height: 15px; 258 | margin: 0 0 0 5px; 259 | } 260 | 261 | .submit-row input.default { 262 | margin: 0 0 0 8px; 263 | text-transform: uppercase; 264 | } 265 | 266 | .submit-row p { 267 | margin: 0.3em; 268 | } 269 | 270 | .submit-row p.deletelink-box { 271 | float: left; 272 | margin: 0; 273 | } 274 | 275 | .submit-row a.deletelink { 276 | display: block; 277 | background: #ba2121; 278 | border-radius: 4px; 279 | padding: 10px 15px; 280 | height: 15px; 281 | line-height: 15px; 282 | color: #fff; 283 | } 284 | 285 | .submit-row a.deletelink:hover, 286 | .submit-row a.deletelink:active { 287 | background: #a41515; 288 | } 289 | 290 | /* CUSTOM FORM FIELDS */ 291 | 292 | .vSelectMultipleField { 293 | vertical-align: top !important; 294 | } 295 | 296 | .vCheckboxField { 297 | border: none; 298 | } 299 | 300 | .vDateField, .vTimeField { 301 | margin-right: 2px; 302 | } 303 | 304 | .vDateField { 305 | min-width: 6.85em; 306 | } 307 | 308 | .vTimeField { 309 | min-width: 4.7em; 310 | } 311 | 312 | .vURLField { 313 | width: 30em; 314 | } 315 | 316 | .vLargeTextField, .vXMLLargeTextField { 317 | width: 48em; 318 | } 319 | 320 | .flatpages-flatpage #id_content { 321 | height: 40.2em; 322 | } 323 | 324 | .module table .vPositiveSmallIntegerField { 325 | width: 2.2em; 326 | } 327 | 328 | .vTextField { 329 | width: 20em; 330 | } 331 | 332 | .vIntegerField { 333 | width: 5em; 334 | } 335 | 336 | .vBigIntegerField { 337 | width: 10em; 338 | } 339 | 340 | .vForeignKeyRawIdAdminField { 341 | width: 5em; 342 | } 343 | 344 | /* INLINES */ 345 | 346 | .inline-group { 347 | padding: 0; 348 | margin: 0 0 30px; 349 | } 350 | 351 | .inline-group thead th { 352 | padding: 8px 10px; 353 | } 354 | 355 | .inline-group .aligned label { 356 | width: 160px; 357 | } 358 | 359 | .inline-related { 360 | position: relative; 361 | } 362 | 363 | .inline-related h3 { 364 | margin: 0; 365 | color: #666; 366 | padding: 5px; 367 | font-size: 13px; 368 | background: #f8f8f8; 369 | border-top: 1px solid #eee; 370 | border-bottom: 1px solid #eee; 371 | } 372 | 373 | .inline-related h3 span.delete { 374 | float: right; 375 | } 376 | 377 | .inline-related h3 span.delete label { 378 | margin-left: 2px; 379 | font-size: 11px; 380 | } 381 | 382 | .inline-related fieldset { 383 | margin: 0; 384 | background: #fff; 385 | border: none; 386 | width: 100%; 387 | } 388 | 389 | .inline-related fieldset.module h3 { 390 | margin: 0; 391 | padding: 2px 5px 3px 5px; 392 | font-size: 11px; 393 | text-align: left; 394 | font-weight: bold; 395 | background: #bcd; 396 | color: #fff; 397 | } 398 | 399 | .inline-group .tabular fieldset.module { 400 | border: none; 401 | } 402 | 403 | .inline-related.tabular fieldset.module table { 404 | width: 100%; 405 | } 406 | 407 | .last-related fieldset { 408 | border: none; 409 | } 410 | 411 | .inline-group .tabular tr.has_original td { 412 | padding-top: 2em; 413 | } 414 | 415 | .inline-group .tabular tr td.original { 416 | padding: 2px 0 0 0; 417 | width: 0; 418 | _position: relative; 419 | } 420 | 421 | .inline-group .tabular th.original { 422 | width: 0px; 423 | padding: 0; 424 | } 425 | 426 | .inline-group .tabular td.original p { 427 | position: absolute; 428 | left: 0; 429 | height: 1.1em; 430 | padding: 2px 9px; 431 | overflow: hidden; 432 | font-size: 9px; 433 | font-weight: bold; 434 | color: #666; 435 | _width: 700px; 436 | } 437 | 438 | .inline-group ul.tools { 439 | padding: 0; 440 | margin: 0; 441 | list-style: none; 442 | } 443 | 444 | .inline-group ul.tools li { 445 | display: inline; 446 | padding: 0 5px; 447 | } 448 | 449 | .inline-group div.add-row, 450 | .inline-group .tabular tr.add-row td { 451 | color: #666; 452 | background: #f8f8f8; 453 | padding: 8px 10px; 454 | border-bottom: 1px solid #eee; 455 | } 456 | 457 | .inline-group .tabular tr.add-row td { 458 | padding: 8px 10px; 459 | border-bottom: 1px solid #eee; 460 | } 461 | 462 | .inline-group ul.tools a.add, 463 | .inline-group div.add-row a, 464 | .inline-group .tabular tr.add-row td a { 465 | background: url(../img/icon_addlink.gif) 0 50% no-repeat; 466 | padding-left: 14px; 467 | font-size: 12px; 468 | outline: 0; /* Remove dotted border around link */ 469 | } 470 | 471 | .empty-form { 472 | display: none; 473 | } 474 | 475 | /* RELATED FIELD ADD ONE / LOOKUP */ 476 | 477 | .add-another, .related-lookup { 478 | margin-left: 5px; 479 | display: inline-block; 480 | } 481 | 482 | .add-another { 483 | width: 10px; 484 | height: 10px; 485 | background-image: url(../img/icon_addlink.gif); 486 | } 487 | 488 | .related-lookup { 489 | width: 16px; 490 | height: 16px; 491 | background-image: url(../img/selector-search.gif); 492 | background-repeat: no-repeat; 493 | background-position: 0 3px; 494 | vertical-align: middle; 495 | margin-top: -4px; 496 | } 497 | 498 | form .related-widget-wrapper ul { 499 | display: inline-block; 500 | margin-left: 0; 501 | padding-left: 0; 502 | } 503 | 504 | .clearable-file-input input { 505 | margin-top: 0; 506 | } 507 | -------------------------------------------------------------------------------- /admin_ui/static/admin/css/login.css: -------------------------------------------------------------------------------- 1 | /* LOGIN FORM */ 2 | 3 | body.login { 4 | background: #f8f8f8; 5 | } 6 | 7 | .login #header { 8 | height: auto; 9 | padding: 5px 16px; 10 | } 11 | 12 | .login #header h1 { 13 | font-size: 18px; 14 | } 15 | 16 | .login #header h1 a { 17 | color: #fff; 18 | } 19 | 20 | .login #content { 21 | padding: 20px 20px 0; 22 | } 23 | 24 | .login #container { 25 | background: #fff; 26 | border: 1px solid #eaeaea; 27 | border-radius: 4px; 28 | overflow: hidden; 29 | width: 28em; 30 | min-width: 300px; 31 | margin: 100px auto; 32 | } 33 | 34 | .login #content-main { 35 | width: 100%; 36 | } 37 | 38 | .login .form-row { 39 | padding: 4px 0; 40 | float: left; 41 | width: 100%; 42 | border-bottom: none; 43 | } 44 | 45 | .login .form-row label { 46 | padding-right: 0.5em; 47 | line-height: 2em; 48 | font-size: 1em; 49 | clear: both; 50 | color: #333; 51 | } 52 | 53 | .login .form-row #id_username, .login .form-row #id_password { 54 | clear: both; 55 | padding: 8px; 56 | width: 100%; 57 | -webkit-box-sizing: border-box; 58 | -moz-box-sizing: border-box; 59 | box-sizing: border-box; 60 | } 61 | 62 | .login span.help { 63 | font-size: 10px; 64 | display: block; 65 | } 66 | 67 | .login .submit-row { 68 | clear: both; 69 | padding: 1em 0 0 9.4em; 70 | margin: 0; 71 | border: none; 72 | background: none; 73 | text-align: left; 74 | } 75 | 76 | .login .password-reset-link { 77 | text-align: center; 78 | } -------------------------------------------------------------------------------- /admin_ui/static/admin/css/rtl.css: -------------------------------------------------------------------------------- 1 | body { 2 | direction: rtl; 3 | } 4 | 5 | /* LOGIN */ 6 | 7 | .login .form-row { 8 | float: right; 9 | } 10 | 11 | .login .form-row label { 12 | float: right; 13 | padding-left: 0.5em; 14 | padding-right: 0; 15 | text-align: left; 16 | } 17 | 18 | .login .submit-row { 19 | clear: both; 20 | padding: 1em 9.4em 0 0; 21 | } 22 | 23 | /* GLOBAL */ 24 | 25 | th { 26 | text-align: right; 27 | } 28 | 29 | .module h2, .module caption { 30 | text-align: right; 31 | } 32 | 33 | .addlink, .changelink { 34 | padding-left: 0px; 35 | padding-right: 12px; 36 | background-position: 100% 0.2em; 37 | } 38 | 39 | .deletelink { 40 | padding-left: 0px; 41 | padding-right: 12px; 42 | background-position: 100% 0.25em; 43 | } 44 | 45 | .object-tools { 46 | float: left; 47 | } 48 | 49 | thead th:first-child, 50 | tfoot td:first-child { 51 | border-left: none; 52 | } 53 | 54 | /* LAYOUT */ 55 | 56 | #user-tools { 57 | right: auto; 58 | left: 0; 59 | text-align: left; 60 | } 61 | 62 | div.breadcrumbs { 63 | text-align: right; 64 | } 65 | 66 | #content-main { 67 | float: right; 68 | } 69 | 70 | #content-related { 71 | float: left; 72 | margin-left: -19em; 73 | margin-right: auto; 74 | } 75 | 76 | .colMS { 77 | margin-left: 20em !important; 78 | margin-right: 10px !important; 79 | } 80 | 81 | /* SORTABLE TABLES */ 82 | 83 | table thead th.sorted .sortoptions { 84 | float: left; 85 | } 86 | 87 | thead th.sorted .text { 88 | padding-right: 0; 89 | padding-left: 42px; 90 | } 91 | 92 | /* dashboard styles */ 93 | 94 | .dashboard .module table td a { 95 | padding-left: .6em; 96 | padding-right: 12px; 97 | } 98 | 99 | /* changelists styles */ 100 | 101 | .change-list .filtered { 102 | background: #fff !important; 103 | } 104 | 105 | .change-list .filtered table { 106 | border-left: none; 107 | border-right: 0px none; 108 | } 109 | 110 | #changelist-filter { 111 | right: auto; 112 | left: 0; 113 | border-left: none; 114 | border-right: none; 115 | } 116 | 117 | .change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { 118 | margin-right: 0px !important; 119 | margin-left: 280px !important; 120 | } 121 | 122 | #changelist-filter li.selected { 123 | border-left: none; 124 | padding-left: 10px; 125 | margin-left: 0; 126 | border-right: 5px solid #eaeaea; 127 | padding-right: 10px; 128 | margin-right: -15px; 129 | } 130 | 131 | .filtered .actions { 132 | border-left:1px solid #DDDDDD; 133 | margin-left:160px !important; 134 | border-right: 0 none; 135 | margin-right:0 !important; 136 | } 137 | 138 | #changelist table tbody td:first-child, #changelist table tbody th:first-child { 139 | border-right: none; 140 | border-left: none; 141 | } 142 | 143 | /* FORMS */ 144 | 145 | .aligned label { 146 | padding: 0 0 3px 1em; 147 | float: right; 148 | } 149 | 150 | .submit-row { 151 | text-align: left 152 | } 153 | 154 | .submit-row p.deletelink-box { 155 | float: right; 156 | } 157 | 158 | .submit-row .deletelink { 159 | background: url(../img/icon_deletelink.gif) 0 50% no-repeat; 160 | padding-right: 14px; 161 | } 162 | 163 | .submit-row input.default { 164 | margin-left: 0; 165 | } 166 | 167 | .vDateField, .vTimeField { 168 | margin-left: 2px; 169 | } 170 | 171 | .aligned .form-row input { 172 | margin-left: 5px; 173 | } 174 | 175 | form ul.inline li { 176 | float: right; 177 | padding-right: 0; 178 | padding-left: 7px; 179 | } 180 | 181 | input[type=submit].default, .submit-row input.default { 182 | float: left; 183 | } 184 | 185 | fieldset .field-box { 186 | float: right; 187 | margin-left: 20px; 188 | margin-right: 0; 189 | } 190 | 191 | .errorlist li { 192 | background-position: 100% 12px; 193 | padding: 0; 194 | } 195 | 196 | .errornote { 197 | background-position: 100% 12px; 198 | padding: 10px 12px; 199 | } 200 | 201 | /* WIDGETS */ 202 | 203 | .calendarnav-previous { 204 | top: 0; 205 | left: auto; 206 | right: 10px; 207 | } 208 | 209 | .calendarnav-next { 210 | top: 0; 211 | right: auto; 212 | left: 10px; 213 | } 214 | 215 | .calendar caption, .calendarbox h2 { 216 | text-align: center; 217 | } 218 | 219 | .selector { 220 | float: right; 221 | } 222 | 223 | .selector .selector-filter { 224 | text-align: right; 225 | } 226 | 227 | .inline-deletelink { 228 | float: left; 229 | } 230 | 231 | form .form-row p.datetime { 232 | overflow: hidden; 233 | } 234 | 235 | /* MISC */ 236 | 237 | .inline-related h2, .inline-group h2 { 238 | text-align: right 239 | } 240 | 241 | .inline-related h3 span.delete { 242 | padding-right: 20px; 243 | padding-left: inherit; 244 | left: 10px; 245 | right: inherit; 246 | float:left; 247 | } 248 | 249 | .inline-related h3 span.delete label { 250 | margin-left: inherit; 251 | margin-right: 2px; 252 | } 253 | 254 | /* IE7 specific bug fixes */ 255 | 256 | div.colM { 257 | position: relative; 258 | } 259 | 260 | .submit-row input { 261 | float: left; 262 | } -------------------------------------------------------------------------------- /admin_ui/static/admin/fonts/README.txt: -------------------------------------------------------------------------------- 1 | Roboto webfont source: https://code.google.com/p/roboto-webfont/ 2 | Weights used in this project: Light (300), Regular (400), Bold (700) 3 | -------------------------------------------------------------------------------- /admin_ui/static/admin/fonts/Roboto-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/fonts/Roboto-Bold-webfont.woff -------------------------------------------------------------------------------- /admin_ui/static/admin/fonts/Roboto-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/fonts/Roboto-Light-webfont.woff -------------------------------------------------------------------------------- /admin_ui/static/admin/fonts/Roboto-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/fonts/Roboto-Regular-webfont.woff -------------------------------------------------------------------------------- /admin_ui/static/admin/img/changelist-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/changelist-bg.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/changelist-bg_rtl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/changelist-bg_rtl.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/default-bg-reverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/default-bg-reverse.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/default-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/default-bg.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/deleted-overlay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/deleted-overlay.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon-no.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon-no.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon-unknown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon-unknown.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon-yes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon-yes.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_addlink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_addlink.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_alert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_alert.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_calendar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_calendar.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_changelink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_changelink.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_clock.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_clock.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_deletelink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_deletelink.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_error.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_error.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_searchbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_searchbox.png -------------------------------------------------------------------------------- /admin_ui/static/admin/img/icon_success.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/icon_success.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/inline-delete-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/inline-delete-8bit.png -------------------------------------------------------------------------------- /admin_ui/static/admin/img/inline-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/inline-delete.png -------------------------------------------------------------------------------- /admin_ui/static/admin/img/inline-restore-8bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/inline-restore-8bit.png -------------------------------------------------------------------------------- /admin_ui/static/admin/img/inline-restore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/inline-restore.png -------------------------------------------------------------------------------- /admin_ui/static/admin/img/inline-splitter-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/inline-splitter-bg.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/nav-bg-grabber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/nav-bg-grabber.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/nav-bg-reverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/nav-bg-reverse.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/nav-bg-selected.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/nav-bg-selected.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/nav-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/nav-bg.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/selector-icons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/selector-icons.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/selector-search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/selector-search.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/sorting-icons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/sorting-icons.gif -------------------------------------------------------------------------------- /admin_ui/static/admin/img/tooltag-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/tooltag-add.png -------------------------------------------------------------------------------- /admin_ui/static/admin/img/tooltag-arrowright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/admin_ui/static/admin/img/tooltag-arrowright.png -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fdhuang' 2 | -------------------------------------------------------------------------------- /api/blog_api.py: -------------------------------------------------------------------------------- 1 | from rest_framework import filters 2 | from rest_framework import serializers, viewsets 3 | from rest_framework.decorators import detail_route 4 | from rest_framework.permissions import IsAuthenticatedOrReadOnly 5 | from rest_framework.response import Response 6 | from rest_framework import renderers 7 | from blog.models import Entry 8 | 9 | 10 | class BlogpostDetailSerializer(serializers.HyperlinkedModelSerializer): 11 | class Meta: 12 | model = Entry 13 | fields = ('headline', 'slug', 'author', 'body') 14 | 15 | 16 | class BlogPostDetailSet(viewsets.ReadOnlyModelViewSet): 17 | queryset = Entry.objects.filter(is_active=True) 18 | serializer_class = BlogpostDetailSerializer 19 | permission_classes = (IsAuthenticatedOrReadOnly,) 20 | filter_backends = (filters.SearchFilter,) 21 | search_fields = ('headline', 'slug', 'author', 'body') 22 | 23 | @detail_route(renderer_classes=(renderers.StaticHTMLRenderer,)) 24 | def highlight(self, request, *args, **kwargs): 25 | snippet = self.get_object() 26 | return Response(snippet.highlighted) 27 | 28 | 29 | class BlogpostListSerializer(serializers.HyperlinkedModelSerializer): 30 | class Meta: 31 | model = Entry 32 | fields = ('headline', 'slug', 'author') 33 | 34 | 35 | class BlogPostListSet(viewsets.ReadOnlyModelViewSet): 36 | queryset = Entry.objects.filter(is_active=True) 37 | serializer_class = BlogpostListSerializer 38 | permission_classes = (IsAuthenticatedOrReadOnly,) 39 | filter_backends = (filters.SearchFilter,) 40 | search_fields = ('headline', 'slug', 'author') 41 | 42 | @detail_route(renderer_classes=(renderers.StaticHTMLRenderer,)) 43 | def highlight(self, request, *args, **kwargs): 44 | snippet = self.get_object() 45 | return Response(snippet.highlighted) -------------------------------------------------------------------------------- /api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class ApiTests(TestCase): 5 | """ 6 | Tests for views that are instances of TemplateView. 7 | """ 8 | 9 | def test_blog_list_api(self): 10 | response = self.client.get('/api/blog_list/') 11 | self.assertEqual(response.status_code, 200) 12 | 13 | def test_blog_detail_api(self): 14 | response = self.client.get('/api/blog_detail/') 15 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /api/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf.urls import patterns, include, url 3 | from rest_framework import routers 4 | from api.blog_api import BlogPostListSet, BlogPostDetailSet 5 | 6 | router = routers.DefaultRouter() 7 | router.register(r'blog_list', BlogPostListSet) 8 | router.register(r'blog_detail', BlogPostDetailSet) 9 | 10 | urlpatterns = patterns('api.views', 11 | url(r'^', include(router.urls)), 12 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), 13 | ) -------------------------------------------------------------------------------- /blog/README.md: -------------------------------------------------------------------------------- 1 | basis on [https://github.com/django/djangoproject.com](https://github.com/django/djangoproject.com) -------------------------------------------------------------------------------- /blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/blog/__init__.py -------------------------------------------------------------------------------- /blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Entry, Event 4 | 5 | 6 | class EntryAdmin(admin.ModelAdmin): 7 | list_display = ('headline', 'pub_date', 'is_active', 'is_published', 'author') 8 | list_filter = ('is_active',) 9 | exclude = ('summary_html', 'body_html') 10 | prepopulated_fields = {"slug": ("headline",)} 11 | 12 | def formfield_for_dbfield(self, db_field, **kwargs): 13 | formfield = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs) 14 | if db_field.name == 'body': 15 | formfield.widget.attrs['rows'] = 25 16 | return formfield 17 | 18 | 19 | class EventAdmin(admin.ModelAdmin): 20 | list_display = ('headline', 'external_url', 'date', 'location', 'pub_date', 'is_active', 'is_published') 21 | list_filter = ('is_active',) 22 | 23 | admin.site.register(Entry, EntryAdmin) 24 | admin.site.register(Event, EventAdmin) 25 | -------------------------------------------------------------------------------- /blog/feeds.py: -------------------------------------------------------------------------------- 1 | from django.contrib.syndication.views import Feed 2 | 3 | from .models import Entry 4 | 5 | 6 | class WeblogEntryFeed(Feed): 7 | title = "The Django weblog" 8 | link = "https://www.djangoproject.com/weblog/" 9 | description = "Latest news about Django, the Python Web framework." 10 | 11 | def items(self): 12 | return Entry.objects.published()[:10] 13 | 14 | def item_pubdate(self, item): 15 | return item.pub_date 16 | 17 | def item_author_name(self, item): 18 | return item.author 19 | 20 | def item_description(self, item): 21 | return item.body_html 22 | -------------------------------------------------------------------------------- /blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Entry', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('headline', models.CharField(max_length=200)), 18 | ('slug', models.SlugField(unique_for_date='pub_date')), 19 | ('is_active', models.BooleanField(default=False, help_text="Tick to make this entry live (see also the publication date). Note that administrators (like yourself) are allowed to preview inactive entries whereas the general public aren't.")), 20 | ('pub_date', models.DateTimeField(help_text='For an entry to be published, it must be active and its publication date must be in the past.', verbose_name='Publication date')), 21 | ('content_format', models.CharField(max_length=50, choices=[('reST', 'reStructuredText'), ('html', 'Raw HTML')])), 22 | ('summary', models.TextField()), 23 | ('summary_html', models.TextField()), 24 | ('body', models.TextField()), 25 | ('body_html', models.TextField()), 26 | ('author', models.CharField(max_length=100)), 27 | ], 28 | options={ 29 | 'ordering': ('-pub_date',), 30 | 'db_table': 'blog_entries', 31 | 'verbose_name_plural': 'entries', 32 | 'get_latest_by': 'pub_date', 33 | }, 34 | bases=(models.Model,), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /blog/migrations/0002_event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('blog', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Event', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('headline', models.CharField(max_length=200)), 19 | ('external_url', models.URLField()), 20 | ('date', models.DateField()), 21 | ('location', models.CharField(max_length=100)), 22 | ('is_active', models.BooleanField(default=False, help_text="Tick to make this event live (see also the publication date). Note that administrators (like yourself) are allowed to preview inactive events whereas the general public aren't.")), 23 | ('pub_date', models.DateTimeField(help_text='For an event to be published, it must be active and its publication date must be in the past.', verbose_name='Publication date')), 24 | ], 25 | options={ 26 | 'ordering': ('-pub_date',), 27 | 'get_latest_by': 'pub_date', 28 | }, 29 | bases=(models.Model,), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /blog/migrations/0003_auto_20150528_0036.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('blog', '0002_event'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='entry', 16 | options={'ordering': ('-pub_date',), 'get_latest_by': 'pub_date', 'verbose_name_plural': 'Blog'}, 17 | ), 18 | migrations.AlterField( 19 | model_name='entry', 20 | name='content_format', 21 | field=models.CharField(max_length=50, choices=[(b'reST', b'reStructuredText'), (b'html', b'Raw HTML'), (b'md', b'Markdown')]), 22 | ), 23 | migrations.AlterField( 24 | model_name='event', 25 | name='date', 26 | field=models.DateTimeField(), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/blog/migrations/__init__.py -------------------------------------------------------------------------------- /blog/models.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | 3 | from django.conf import settings 4 | from django.core.urlresolvers import reverse 5 | from django.db import models 6 | from django.utils.encoding import smart_str 7 | from django.utils.timezone import now 8 | from django.utils.translation import ugettext_lazy as _ 9 | from docutils.core import publish_parts 10 | from uuslug import slugify 11 | import markdown2 12 | 13 | BLOG_DOCUTILS_SETTINGS = { 14 | 'doctitle_xform': False, 15 | 'initial_header_level': 3, 16 | 'id_prefix': 's-', 17 | 'raw_enabled': False, 18 | 'file_insertion_enabled': False, 19 | } 20 | BLOG_DOCUTILS_SETTINGS.update(getattr(settings, 'BLOG_DOCUTILS_SETTINGS', {})) 21 | 22 | 23 | class EntryQuerySet(models.QuerySet): 24 | def published(self): 25 | return self.active().filter(pub_date__lte=now()) 26 | 27 | def active(self): 28 | return self.filter(is_active=True) 29 | 30 | 31 | CONTENT_FORMAT_CHOICES = ( 32 | ('reST', 'reStructuredText'), 33 | ('html', 'Raw HTML'), 34 | ('md', 'Markdown') 35 | ) 36 | 37 | 38 | class Entry(models.Model): 39 | headline = models.CharField(max_length=200) 40 | slug = models.SlugField(unique_for_date='pub_date') 41 | is_active = models.BooleanField( 42 | help_text=_( 43 | "Tick to make this entry live (see also the publication date). " 44 | "Note that administrators (like yourself) are allowed to preview " 45 | "inactive entries whereas the general public aren't." 46 | ), 47 | default=False, 48 | ) 49 | pub_date = models.DateTimeField( 50 | verbose_name=_("Publication date"), 51 | help_text=_( 52 | "For an entry to be published, it must be active and its " 53 | "publication date must be in the past." 54 | ), 55 | ) 56 | content_format = models.CharField(choices=CONTENT_FORMAT_CHOICES, max_length=50) 57 | summary = models.TextField() 58 | summary_html = models.TextField() 59 | body = models.TextField() 60 | body_html = models.TextField() 61 | author = models.CharField(max_length=100) 62 | 63 | objects = EntryQuerySet.as_manager() 64 | 65 | class Meta: 66 | db_table = 'blog_entries' 67 | verbose_name_plural = 'Blog' 68 | ordering = ('-pub_date',) 69 | get_latest_by = 'pub_date' 70 | 71 | def __str__(self): 72 | return slugify(self.headline) 73 | 74 | def get_absolute_url(self): 75 | kwargs = { 76 | 'year': self.pub_date.year, 77 | 'month': self.pub_date.strftime('%b').lower(), 78 | 'day': self.pub_date.strftime('%d').lower(), 79 | 'slug': self.slug, 80 | } 81 | return reverse('weblog:entry', kwargs=kwargs) 82 | 83 | def is_published(self): 84 | """ 85 | Return True if the entry is publicly accessible. 86 | """ 87 | return self.is_active and self.pub_date <= now() 88 | is_published.boolean = True 89 | 90 | def save(self, *args, **kwargs): 91 | if self.content_format == 'html': 92 | self.summary_html = self.summary 93 | self.body_html = self.body 94 | elif self.content_format == 'reST': 95 | self.summary_html = publish_parts(source=smart_str(self.summary), 96 | writer_name="html", 97 | settings_overrides=BLOG_DOCUTILS_SETTINGS)['fragment'] 98 | self.body_html = publish_parts(source=smart_str(self.body), 99 | writer_name="html", 100 | settings_overrides=BLOG_DOCUTILS_SETTINGS)['fragment'] 101 | elif self.content_format == 'md': 102 | self.summary_html = markdown2.markdown(self.summary) 103 | self.body_html = markdown2.markdown(self.body) 104 | 105 | super(Entry, self).save(*args, **kwargs) 106 | 107 | 108 | class EventQuerySet(EntryQuerySet): 109 | def past(self): 110 | return self.filter(date__lte=now()).order_by('-date') 111 | 112 | def future(self): 113 | return self.filter(date__gte=now()).order_by('date') 114 | 115 | 116 | class Event(models.Model): 117 | headline = models.CharField(max_length=200, null=False) 118 | external_url = models.URLField() 119 | date = models.DateTimeField() 120 | location = models.CharField(max_length=100) 121 | is_active = models.BooleanField( 122 | help_text=_( 123 | "Tick to make this event live (see also the publication date). " 124 | "Note that administrators (like yourself) are allowed to preview " 125 | "inactive events whereas the general public aren't." 126 | ), 127 | default=False, 128 | ) 129 | pub_date = models.DateTimeField( 130 | verbose_name=_("Publication date"), 131 | help_text=_( 132 | "For an event to be published, it must be active and its " 133 | "publication date must be in the past." 134 | ), 135 | ) 136 | 137 | objects = EventQuerySet.as_manager() 138 | 139 | class Meta: 140 | ordering = ('-pub_date',) 141 | get_latest_by = 'pub_date' 142 | 143 | def is_published(self): 144 | """ 145 | Return True if the event is publicly accessible. 146 | """ 147 | return self.is_active and self.pub_date <= now() 148 | is_published.boolean = True 149 | -------------------------------------------------------------------------------- /blog/sitemaps.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sitemaps import Sitemap 2 | 3 | from .models import Entry 4 | 5 | 6 | class WeblogSitemap(Sitemap): 7 | changefreq = 'never' 8 | priority = 0.4 9 | 10 | def items(self): 11 | return Entry.objects.published() 12 | 13 | # lastmod wasn't implemented, because weblog pages used to contain comments. 14 | -------------------------------------------------------------------------------- /blog/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/blog/templatetags/__init__.py -------------------------------------------------------------------------------- /blog/templatetags/weblog.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from ..models import Entry 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.inclusion_tag('blog/entry_snippet.html') 9 | def render_latest_blog_entries(num, summary_first=False, hide_readmore=False, header_tag=''): 10 | entries = Entry.objects.published()[:num] 11 | return { 12 | 'entries': entries, 13 | 'summary_first': summary_first, 14 | 'header_tag': header_tag, 15 | 'hide_readmore': hide_readmore, 16 | } 17 | 18 | 19 | @register.inclusion_tag('blog/month_links_snippet.html') 20 | def render_month_links(): 21 | return { 22 | 'dates': Entry.objects.published().datetimes('pub_date', 'month', order='DESC'), 23 | } 24 | -------------------------------------------------------------------------------- /blog/tests.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | # from test.support import captured_stderr 3 | 4 | from django.core.urlresolvers import reverse 5 | from django.test import TestCase 6 | from django.utils import timezone 7 | 8 | from .models import Entry, Event 9 | 10 | 11 | class DateTimeMixin(object): 12 | def setUp(self): 13 | self.now = timezone.now() 14 | self.yesterday = self.now - timedelta(days=1) 15 | self.tomorrow = self.now + timedelta(days=1) 16 | 17 | 18 | class EntryTestCase(DateTimeMixin, TestCase): 19 | def test_manager_active(self): 20 | """ 21 | Make sure that the Entry manager's `active` method works 22 | """ 23 | Entry.objects.create(pub_date=self.now, is_active=False, headline='inactive') 24 | Entry.objects.create(pub_date=self.now, is_active=True, headline='active') 25 | 26 | self.assertQuerysetEqual(Entry.objects.published(), ['active'], transform=lambda entry: entry.headline) 27 | 28 | def test_manager_published(self): 29 | """ 30 | Make sure that the Entry manager's `published` method works 31 | """ 32 | Entry.objects.create(pub_date=self.yesterday, is_active=False, headline='past inactive') 33 | Entry.objects.create(pub_date=self.yesterday, is_active=True, headline='past active') 34 | Entry.objects.create(pub_date=self.tomorrow, is_active=False, headline='future inactive') 35 | Entry.objects.create(pub_date=self.tomorrow, is_active=True, headline='future active') 36 | 37 | self.assertQuerysetEqual(Entry.objects.published(), ['past active'], transform=lambda entry: entry.headline) 38 | 39 | # def test_docutils_safe(self): 40 | # """ 41 | # Make sure docutils' file inclusion directives are disabled by default. 42 | # """ 43 | # with captured_stderr() as self.docutils_stderr: 44 | # entry = Entry.objects.create( 45 | # pub_date=self.now, is_active=True, headline='active', content_format='reST', 46 | # body='.. raw:: html\n :file: somefile\n' 47 | # ) 48 | # self.assertIn('

"raw" directive disabled.

', entry.body_html) 49 | # self.assertIn('.. raw:: html\n :file: somefile', entry.body_html) 50 | 51 | 52 | class EventTestCase(DateTimeMixin, TestCase): 53 | def test_manager_past_future(self): 54 | """ 55 | Make sure that the Event manager's `past` and `future` methods works 56 | """ 57 | Event.objects.create(date=self.yesterday, pub_date=self.now, headline='past') 58 | Event.objects.create(date=self.tomorrow, pub_date=self.now, headline='future') 59 | 60 | self.assertQuerysetEqual(Event.objects.future(), ['future'], transform=lambda event: event.headline) 61 | self.assertQuerysetEqual(Event.objects.past(), ['past'], transform=lambda event: event.headline) 62 | 63 | def test_manager_past_future_include_today(self): 64 | """ 65 | Make sure that both .future() and .past() include today's events. 66 | """ 67 | Event.objects.create(date=self.now, pub_date=self.now, headline='today') 68 | 69 | # self.assertQuerysetEqual(Event.objects.future(), ['today'], transform=lambda event: event.headline) 70 | self.assertQuerysetEqual(Event.objects.past(), ['today'], transform=lambda event: event.headline) 71 | 72 | def test_past_future_ordering(self): 73 | """ 74 | Make sure the that .future() and .past() use the actual date for ordering 75 | (and not the pub_date). 76 | """ 77 | D = timedelta(days=1) 78 | Event.objects.create(date=self.yesterday - D, pub_date=self.yesterday - D, headline='a') 79 | Event.objects.create(date=self.yesterday, pub_date=self.yesterday, headline='b') 80 | 81 | Event.objects.create(date=self.tomorrow, pub_date=self.tomorrow, headline='c') 82 | Event.objects.create(date=self.tomorrow + D, pub_date=self.tomorrow + D, headline='d') 83 | 84 | self.assertQuerysetEqual(Event.objects.future(), ['c', 'd'], transform=lambda event: event.headline) 85 | self.assertQuerysetEqual(Event.objects.past(), ['b', 'a'], transform=lambda event: event.headline) 86 | # 87 | # 88 | # class ViewsTestCase(DateTimeMixin, TestCase): 89 | # def test_no_past_upcoming_events(self): 90 | # """ 91 | # Make sure there are no past event in the "upcoming events" sidebar (#399) 92 | # """ 93 | # # We need a published entry on the index page so that it doesn't return a 404 94 | # Entry.objects.create(pub_date=self.yesterday, is_active=True) 95 | # Event.objects.create(date=self.yesterday, pub_date=self.now, is_active=True, headline='Jezdezcon') 96 | # response = self.client.get(reverse('weblog:index')) 97 | # self.assertEqual(response.status_code, 200) 98 | # self.assertQuerysetEqual(response.context['events'], []) 99 | -------------------------------------------------------------------------------- /blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | url( 7 | r'^(?P\d{4})/(?P[a-z]{3})/(?P\w{1,2})/(?P[\w-]+)/$', 8 | views.BlogDateDetailView.as_view(), 9 | name="entry" 10 | ), 11 | url( 12 | r'^(?P\d{4})/(?P[a-z]{3})/(?P\w{1,2})/$', 13 | views.BlogDayArchiveView.as_view(), 14 | name="archive-day" 15 | ), 16 | url( 17 | r'^(?P\d{4})/(?P[a-z]{3})/$', 18 | views.BlogMonthArchiveView.as_view(), 19 | name="archive-month" 20 | ), 21 | url( 22 | r'^(?P\d{4})/$', 23 | views.BlogYearArchiveView.as_view(), 24 | name="archive-year" 25 | ), 26 | url( 27 | r'^/?$', 28 | views.BlogArchiveIndexView.as_view(), 29 | name="index" 30 | ), 31 | url( 32 | r'^(?P[\w-]+)/$', 33 | views.BlogDetailView.as_view(), 34 | name="entry" 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /blog/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import View, DetailView 2 | from django.views.generic.dates import ( 3 | ArchiveIndexView, DateDetailView, DayArchiveView, MonthArchiveView, 4 | YearArchiveView, 5 | ) 6 | 7 | from .models import Entry, Event 8 | 9 | 10 | class BlogViewMixin(object): 11 | date_field = 'pub_date' 12 | paginate_by = 10 13 | 14 | def get_allow_future(self): 15 | return self.request.user.is_staff 16 | 17 | def get_queryset(self): 18 | if self.request.user.is_staff: 19 | return Entry.objects.all() 20 | else: 21 | return Entry.objects.published() 22 | 23 | def get_context_data(self, **kwargs): 24 | context = super(BlogViewMixin, self).get_context_data(**kwargs) 25 | 26 | events_queryset = Event.objects.future() 27 | if not self.request.user.is_staff: 28 | events_queryset = events_queryset.published() 29 | 30 | context['events'] = events_queryset[:3] 31 | 32 | return context 33 | 34 | 35 | class BlogArchiveIndexView(BlogViewMixin, ArchiveIndexView): 36 | pass 37 | 38 | 39 | class BlogYearArchiveView(BlogViewMixin, YearArchiveView): 40 | make_object_list = True 41 | 42 | 43 | class BlogMonthArchiveView(BlogViewMixin, MonthArchiveView): 44 | pass 45 | 46 | 47 | class BlogDayArchiveView(BlogViewMixin, DayArchiveView): 48 | pass 49 | 50 | 51 | class BlogDateDetailView(BlogViewMixin, DateDetailView): 52 | pass 53 | 54 | 55 | class BlogDetailView(BlogViewMixin, DetailView): 56 | pass 57 | -------------------------------------------------------------------------------- /conf/README.md: -------------------------------------------------------------------------------- 1 | Inspired by [Mezzanine](https://github.com/stephenmcd/mezzanine) -------------------------------------------------------------------------------- /conf/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from functools import partial 4 | from warnings import warn 5 | 6 | from django.apps import AppConfig 7 | from future.builtins import bytes, str 8 | from django.conf import settings as django_settings 9 | from django.utils.functional import Promise 10 | from importlib import import_module 11 | 12 | 13 | registry = {} 14 | 15 | 16 | class ConfigureAppConfig(AppConfig): 17 | name = 'conf' 18 | verbose_name = '配置' 19 | 20 | 21 | default_app_config = 'conf.ConfigureAppConfig' 22 | 23 | 24 | def register_setting(name=None, label=None, editable=False, description=None, 25 | default=None, choices=None, append=False): 26 | """ 27 | Registers a setting that can be edited via the admin. This mostly 28 | equates to storing the given args as a dict in the ``registry`` 29 | dict by name. 30 | """ 31 | if name is None: 32 | raise TypeError("conf.register_setting requires the " 33 | "'name' keyword argument.") 34 | if editable and default is None: 35 | raise TypeError("conf.register_setting requires the " 36 | "'default' keyword argument when 'editable' is True.") 37 | 38 | # append is True when called from an app (typically external) 39 | # after the setting has already been registered, with the 40 | # intention of appending to its default value. 41 | if append and name in registry: 42 | registry[name]["default"] += default 43 | return 44 | 45 | # If an editable setting has a value defined in the 46 | # project's settings.py module, it can't be editable, since 47 | # these lead to a lot of confusion once its value gets 48 | # defined in the db. 49 | if hasattr(django_settings, name): 50 | editable = False 51 | if label is None: 52 | label = name.replace("_", " ").title() 53 | 54 | # Python 2/3 compatibility. isinstance() is overridden by future 55 | # on Python 2 to behave as Python 3 in conjunction with either 56 | # Python 2's native types or the future.builtins types. 57 | if isinstance(default, bool): 58 | # Prevent bools treated as ints 59 | setting_type = bool 60 | elif isinstance(default, int): 61 | # An int or long or subclass on Py2 62 | setting_type = int 63 | elif isinstance(default, (str, Promise)): 64 | # A unicode or subclass on Py2 65 | setting_type = str 66 | elif isinstance(default, bytes): 67 | # A byte-string or subclass on Py2 68 | setting_type = bytes 69 | else: 70 | setting_type = type(default) 71 | registry[name] = {"name": name, "label": label, "editable": editable, 72 | "description": description, "default": default, 73 | "choices": choices, "type": setting_type} 74 | 75 | 76 | class Settings(object): 77 | """ 78 | An object that provides settings via dynamic attribute access. 79 | 80 | Settings that are registered as editable will be stored in the 81 | database once the site settings form in the admin is first saved. 82 | When these values are accessed via this settings object, *all* 83 | database stored settings get retrieved from the database. 84 | 85 | When accessing uneditable settings their default values are used, 86 | unless they've been given a value in the project's settings.py 87 | module. 88 | 89 | The settings object also provides access to Django settings via 90 | ``django.conf.settings``, in order to provide a consistent method 91 | of access for all settings. 92 | """ 93 | 94 | # These functions map setting types to the functions that should be 95 | # used to convert them from the Unicode string stored in the database. 96 | # If a type doesn't appear in this map, the type itself will be used. 97 | TYPE_FUNCTIONS = { 98 | bool: lambda val: val != "False", 99 | bytes: partial(bytes, encoding='utf8') 100 | } 101 | 102 | def __init__(self): 103 | """ 104 | The ``_loaded`` attribute is a flag for defining whether 105 | editable settings have been loaded from the database. It 106 | defaults to ``True`` here to avoid errors when the DB table 107 | is first created. It's then set to ``False`` whenever the 108 | ``use_editable`` method is called, which should be called 109 | before using editable settings in the database. 110 | ``_editable_cache`` is the dict that stores the editable 111 | settings once they're loaded from the database, the first 112 | time an editable setting is accessed. 113 | """ 114 | self._loaded = True 115 | self._editable_cache = {} 116 | 117 | def use_editable(self): 118 | """ 119 | Empty the editable settings cache and set the loaded flag to 120 | ``False`` so that settings will be loaded from the DB on next 121 | access. If the conf app is not installed then set the loaded 122 | flag to ``True`` in order to bypass DB lookup entirely. 123 | """ 124 | self._loaded = __name__ not in getattr(self, "INSTALLED_APPS") 125 | self._editable_cache = {} 126 | 127 | def _load(self): 128 | """ 129 | Load settings from the database into cache. Delete any settings from 130 | the database that are no longer registered, and emit a warning if 131 | there are settings that are defined in settings.py and the database. 132 | """ 133 | from conf.models import Setting 134 | 135 | removed_settings = [] 136 | conflicting_settings = [] 137 | 138 | for setting_obj in Setting.objects.all(): 139 | 140 | try: 141 | registry[setting_obj.name] 142 | except KeyError: 143 | # Setting in DB isn't registered (removed from code), 144 | # so add to removal list and skip remaining handling. 145 | removed_settings.append(setting_obj.name) 146 | continue 147 | 148 | # Convert DB value to correct type. 149 | setting_type = registry[setting_obj.name]["type"] 150 | type_fn = self.TYPE_FUNCTIONS.get(setting_type, setting_type) 151 | try: 152 | setting_value = type_fn(setting_obj.value) 153 | except ValueError: 154 | # Shouldn't occur, but just a safeguard 155 | # for if the db value somehow ended up as 156 | # an invalid type. 157 | setting_value = registry[setting_obj.name]["default"] 158 | 159 | # Only use DB setting if it's not defined in settings.py 160 | # module, in which case add it to conflicting list for 161 | # warning. 162 | try: 163 | getattr(django_settings, setting_obj.name) 164 | except AttributeError: 165 | self._editable_cache[setting_obj.name] = setting_value 166 | else: 167 | if setting_value != registry[setting_obj.name]["default"]: 168 | conflicting_settings.append(setting_obj.name) 169 | 170 | if removed_settings: 171 | Setting.objects.filter(name__in=removed_settings).delete() 172 | if conflicting_settings: 173 | warn("These settings are defined in both settings.py and " 174 | "the database: %s. The settings.py values will be used." 175 | % ", ".join(conflicting_settings)) 176 | self._loaded = True 177 | 178 | def __getattr__(self, name): 179 | 180 | # Lookup name as a registered setting or a Django setting. 181 | try: 182 | setting = registry[name] 183 | except KeyError: 184 | return getattr(django_settings, name) 185 | 186 | # First access for an editable setting - load from DB into cache. 187 | if setting["editable"] and not self._loaded: 188 | self._load() 189 | 190 | # Use cached editable setting if found, otherwise use the 191 | # value defined in the project's settings.py module if it 192 | # exists, finally falling back to the default defined when 193 | # registered. 194 | try: 195 | return self._editable_cache[name] 196 | except KeyError: 197 | return getattr(django_settings, name, setting["default"]) 198 | 199 | # 200 | # echoes_first = lambda app: not app.startswith("") 201 | # print django_settings.INSTALLED_APPS 202 | # for app in sorted(django_settings.INSTALLED_APPS, key=echoes_first): 203 | # module = import_module(app) 204 | # try: 205 | # print app 206 | # import_module("%s.defaults" % app) 207 | # except: 208 | # if module_has_submodule(module, "defaults"): 209 | # raise 210 | import_module("core.defaults") 211 | settings = Settings() 212 | -------------------------------------------------------------------------------- /conf/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.contrib import admin 4 | from django.contrib.messages import info 5 | from django.http import HttpResponseRedirect 6 | from django.utils.translation import ugettext_lazy as _ 7 | try: 8 | from django.utils.encoding import force_text 9 | except ImportError: 10 | # Backward compatibility for Py2 and Django < 1.5 11 | from django.utils.encoding import force_unicode as force_text 12 | 13 | from conf.models import Setting 14 | from conf.forms import SettingsForm 15 | from utils.urls import admin_url 16 | 17 | 18 | class SettingsAdmin(admin.ModelAdmin): 19 | """ 20 | Admin class for settings model. Redirect add/change views to the list 21 | view where a single form is rendered for editing all settings. 22 | """ 23 | 24 | class Media: 25 | css = {"all": ("mezzanine/css/admin/settings.css",)} 26 | 27 | def changelist_redirect(self): 28 | changelist_url = admin_url(Setting, "changelist") 29 | return HttpResponseRedirect(changelist_url) 30 | 31 | def add_view(self, *args, **kwargs): 32 | return self.changelist_redirect() 33 | 34 | def change_view(self, *args, **kwargs): 35 | return self.changelist_redirect() 36 | 37 | def changelist_view(self, request, extra_context=None): 38 | if extra_context is None: 39 | extra_context = {} 40 | settings_form = SettingsForm(request.POST or None) 41 | if settings_form.is_valid(): 42 | settings_form.save() 43 | info(request, _("Settings were successfully updated.")) 44 | return self.changelist_redirect() 45 | extra_context["settings_form"] = settings_form 46 | extra_context["title"] = u"%s %s" % ( 47 | _("Change"), force_text(Setting._meta.verbose_name_plural)) 48 | return super(SettingsAdmin, self).changelist_view(request, 49 | extra_context) 50 | 51 | 52 | admin.site.register(Setting, SettingsAdmin) 53 | -------------------------------------------------------------------------------- /conf/context_processors.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from django.contrib.flatpages.models import FlatPage 3 | from utils.cache import (cache_key_prefix, cache_installed, 4 | cache_get, cache_set) 5 | 6 | 7 | # Deprecated settings and their defaults. 8 | DEPRECATED = { 9 | "PAGES_MENU_SHOW_ALL": True 10 | } 11 | 12 | 13 | class TemplateSettings(dict): 14 | """ 15 | Dict wrapper for template settings. This exists only to warn when 16 | deprecated settings are accessed in templates. 17 | """ 18 | 19 | def __getitem__(self, k): 20 | if k in DEPRECATED: 21 | from warnings import warn 22 | 23 | warn("%s is deprecated, please remove it from your templates" % k) 24 | return super(TemplateSettings, self).__getitem__(k) 25 | 26 | def __getattr__(self, name): 27 | try: 28 | return self.__getitem__(name) 29 | except KeyError: 30 | raise AttributeError 31 | 32 | 33 | def settings(request=None): 34 | """ 35 | Add the settings object to the template context. 36 | """ 37 | from conf import settings 38 | 39 | settings_dict = None 40 | cache_settings = request and cache_installed() 41 | if cache_settings: 42 | cache_key = cache_key_prefix(request) + "context-settings" 43 | settings_dict = cache_get(cache_key) 44 | if not settings_dict: 45 | settings.use_editable() 46 | settings_dict = TemplateSettings() 47 | for k in settings.TEMPLATE_ACCESSIBLE_SETTINGS: 48 | settings_dict[k] = getattr(settings, k, "") 49 | for k in DEPRECATED: 50 | settings_dict[k] = getattr(settings, k, DEPRECATED) 51 | if cache_settings: 52 | cache_set(cache_key, settings_dict) 53 | # This is basically the same as the old ADMIN_MEDIA_PREFIX setting, 54 | # we just use it in a few spots in the admin to optionally load a 55 | # file from either grappelli or Django admin if grappelli isn't 56 | # installed. We don't call it ADMIN_MEDIA_PREFIX in order to avoid 57 | # any confusion. 58 | # if settings.GRAPPELLI_INSTALLED: 59 | # settings_dict["MEZZANINE_ADMIN_PREFIX"] = "grappelli/" 60 | # else: 61 | # settings_dict["MEZZANINE_ADMIN_PREFIX"] = "admin/" 62 | return {"settings": settings_dict} 63 | 64 | 65 | def flatpage(request=None): 66 | """ 67 | Add the settings object to the template context. 68 | """ 69 | flatpages = FlatPage.objects.all() 70 | return {"flatpages": flatpages} 71 | 72 | -------------------------------------------------------------------------------- /conf/forms.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from future.builtins import int 4 | 5 | from collections import defaultdict 6 | 7 | from django import forms 8 | from django.utils.safestring import mark_safe 9 | from django.utils.translation import ugettext_lazy as _ 10 | from django.template.defaultfilters import urlize 11 | 12 | from conf import settings, registry 13 | from conf.models import Setting 14 | 15 | 16 | FIELD_TYPES = { 17 | bool: forms.BooleanField, 18 | int: forms.IntegerField, 19 | float: forms.FloatField, 20 | } 21 | 22 | 23 | class SettingsForm(forms.Form): 24 | """ 25 | Form for settings - creates a field for each setting in 26 | ``mezzanine.conf`` that is marked as editable. 27 | """ 28 | 29 | def __init__(self, *args, **kwargs): 30 | super(SettingsForm, self).__init__(*args, **kwargs) 31 | settings.use_editable() 32 | # Create a form field for each editable setting's from its type. 33 | for name in sorted(registry.keys()): 34 | setting = registry[name] 35 | if setting["editable"]: 36 | field_class = FIELD_TYPES.get(setting["type"], forms.CharField) 37 | kwargs = { 38 | "label": setting["label"] + ":", 39 | "required": setting["type"] in (int, float), 40 | "initial": getattr(settings, name), 41 | "help_text": self.format_help(setting["description"]), 42 | } 43 | if setting["choices"]: 44 | field_class = forms.ChoiceField 45 | kwargs["choices"] = setting["choices"] 46 | self.fields[name] = field_class(**kwargs) 47 | css_class = field_class.__name__.lower() 48 | self.fields[name].widget.attrs["class"] = css_class 49 | 50 | def __iter__(self): 51 | """ 52 | Calculate and apply a group heading to each field and order by the 53 | heading. 54 | """ 55 | fields = list(super(SettingsForm, self).__iter__()) 56 | group = lambda field: field.name.split("_", 1)[0].title() 57 | misc = _("杂项") 58 | groups = defaultdict(int) 59 | for field in fields: 60 | groups[group(field)] += 1 61 | for (i, field) in enumerate(fields): 62 | setattr(fields[i], "group", group(field)) 63 | if groups[fields[i].group] == 1: 64 | fields[i].group = misc 65 | return iter(sorted(fields, key=lambda x: (x.group == misc, x.group))) 66 | 67 | def save(self): 68 | """ 69 | Save each of the settings to the DB. 70 | """ 71 | for (name, value) in self.cleaned_data.items(): 72 | setting_obj, created = Setting.objects.get_or_create(name=name) 73 | setting_obj.value = value 74 | setting_obj.save() 75 | 76 | def format_help(self, description): 77 | """ 78 | Format the setting's description into HTML. 79 | """ 80 | for bold in ("``", "*"): 81 | parts = [] 82 | if description is None: 83 | description = "" 84 | for i, s in enumerate(description.split(bold)): 85 | parts.append(s if i % 2 == 0 else "%s" % s) 86 | description = "".join(parts) 87 | return mark_safe(urlize(description).replace("\n", "
")) 88 | -------------------------------------------------------------------------------- /conf/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Setting' 12 | db.create_table('conf_setting', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('value', self.gf('django.db.models.fields.CharField')(max_length=2000)), 15 | ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), 16 | )) 17 | db.send_create_signal('conf', ['Setting']) 18 | 19 | 20 | def backwards(self, orm): 21 | 22 | # Deleting model 'Setting' 23 | db.delete_table('conf_setting') 24 | 25 | 26 | models = { 27 | 'conf.setting': { 28 | 'Meta': {'object_name': 'Setting'}, 29 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 31 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '2000'}) 32 | } 33 | } 34 | 35 | complete_apps = ['conf'] 36 | -------------------------------------------------------------------------------- /conf/migrations/0002_auto__add_field_setting_site.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Setting.site' 12 | db.add_column('conf_setting', 'site', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['sites.Site']), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Setting.site' 18 | db.delete_column('conf_setting', 'site_id') 19 | 20 | 21 | models = { 22 | 'conf.setting': { 23 | 'Meta': {'object_name': 'Setting'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 26 | 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), 27 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '2000'}) 28 | }, 29 | 'sites.site': { 30 | 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, 31 | 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | } 35 | } 36 | 37 | complete_apps = ['conf'] 38 | -------------------------------------------------------------------------------- /conf/migrations/0003_update_site_setting.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import DataMigration 5 | from django.db import models 6 | 7 | class Migration(DataMigration): 8 | 9 | def forwards(self, orm): 10 | from django.contrib.sites.models import Site 11 | try: 12 | site = Site.objects.get_current() 13 | except Site.DoesNotExist: 14 | pass 15 | else: 16 | orm.Setting.objects.all().update(site=site) 17 | 18 | 19 | def backwards(self, orm): 20 | pass 21 | 22 | 23 | models = { 24 | 'conf.setting': { 25 | 'Meta': {'object_name': 'Setting'}, 26 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 28 | 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), 29 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '2000'}) 30 | }, 31 | 'sites.site': { 32 | 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, 33 | 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 34 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 35 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 36 | } 37 | } 38 | 39 | complete_apps = ['conf'] 40 | -------------------------------------------------------------------------------- /conf/migrations/0004_ssl_account_settings_rename.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import DataMigration 5 | from django.db import models 6 | 7 | 8 | SETTINGS_RENAMES = ( 9 | ("SHOP_SSL_ENABLED", "SSL_ENABLED"), 10 | ("SHOP_SSL_FORCE_HOST", "SSL_FORCE_HOST"), 11 | ) 12 | 13 | 14 | class Migration(DataMigration): 15 | 16 | def forwards(self, orm): 17 | "Write your forwards methods here." 18 | for name_from, name_to in SETTINGS_RENAMES: 19 | try: 20 | setting = orm.Setting.objects.get(name=name_from) 21 | except orm.Setting.DoesNotExist: 22 | pass 23 | else: 24 | setting.name = name_to 25 | setting.save() 26 | 27 | 28 | def backwards(self, orm): 29 | "Write your backwards methods here." 30 | for name_to, name_from in SETTINGS_RENAMES: 31 | try: 32 | setting = orm.Setting.objects.get(name=name_from) 33 | except orm.Setting.DoesNotExist: 34 | pass 35 | else: 36 | setting.name = name_to 37 | setting.save() 38 | 39 | models = { 40 | 'conf.setting': { 41 | 'Meta': {'object_name': 'Setting'}, 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 44 | 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}), 45 | 'value': ('django.db.models.fields.CharField', [], {'max_length': '2000'}) 46 | }, 47 | 'sites.site': { 48 | 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, 49 | 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 50 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 51 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 52 | } 53 | } 54 | 55 | complete_apps = ['conf'] 56 | -------------------------------------------------------------------------------- /conf/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/conf/migrations/__init__.py -------------------------------------------------------------------------------- /conf/models.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.utils.encoding import python_2_unicode_compatible 7 | 8 | from core.models import SiteRelated 9 | 10 | 11 | @python_2_unicode_compatible 12 | class Setting(SiteRelated): 13 | """ 14 | Stores values for ``mezzanine.conf`` that can be edited via the admin. 15 | """ 16 | 17 | name = models.CharField(max_length=50) 18 | value = models.CharField(max_length=2000) 19 | 20 | class Meta: 21 | verbose_name = _("Setting") 22 | verbose_name_plural = _("设置") 23 | 24 | def __str__(self): 25 | return "%s: %s" % (self.name, self.value) 26 | -------------------------------------------------------------------------------- /conf/static/mezzanine/css/admin/settings.css: -------------------------------------------------------------------------------- 1 | .module {margin:10px 0 20px 0; border-bottom:#fff; width:550px;} 2 | #content-main p {padding:5px 10px; margin:0; border-top:1px solid #fff; 3 | border-bottom:1px solid #ddd;} 4 | #content-main label {width:230px; float:left;} 5 | #content-main p input {float:left; margin:2px 10px 5px 0; width:auto !important;} 6 | .help {display:block; clear:left; padding-top:5px; margin-left:230px;} 7 | -------------------------------------------------------------------------------- /conf/templates/admin/conf/setting/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 | 7 | {% if request.POST %} 8 |

{% trans "Please correct the errors below." %}

9 | {{ settings_form.non_field_errors }} 10 | {% endif %} 11 | 12 |
13 | {% csrf_token %} 14 | {% for field in settings_form %} 15 | {% ifchanged field.group %} 16 | {% if not forloop.first %} 17 |
18 | {% endif %} 19 |
20 |

{% trans field.group %}

21 | {% endifchanged %} 22 |

23 | {{ field.label_tag }}{{ field }}{{ field.errors }} 24 | {{ field.help_text }} 25 |

26 | {% if forloop.last %} 27 |
28 | {% endif %} 29 | {% endfor %} 30 |
31 | 32 |
33 | 34 | 35 | 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /conf/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from future.builtins import bytes, str 3 | import sys 4 | 5 | from django.conf import settings as django_settings 6 | from django.utils.unittest import skipUnless 7 | 8 | from conf import settings, registry, register_setting 9 | from conf.models import Setting 10 | from django.test import TestCase 11 | 12 | 13 | class ConfTests(TestCase): 14 | 15 | @skipUnless(sys.version_info[0] == 2, 16 | "Randomly fails or succeeds under Python 3 as noted in " 17 | "GH #858 - please fix.") 18 | def test_settings(self): 19 | """ 20 | Test that an editable setting can be overridden with a DB 21 | value and that the data type is preserved when the value is 22 | returned back out of the DB. Also checks to ensure no 23 | unsupported types are defined for editable settings. 24 | """ 25 | # Find an editable setting for each supported type. 26 | names_by_type = {} 27 | for setting in registry.values(): 28 | if setting["editable"] and setting["type"] not in names_by_type: 29 | names_by_type[setting["type"]] = setting["name"] 30 | # Create a modified value for each setting and save it. 31 | values_by_name = {} 32 | for (setting_type, setting_name) in names_by_type.items(): 33 | setting_value = registry[setting_name]["default"] 34 | if setting_type in (int, float): 35 | setting_value += 1 36 | elif setting_type is bool: 37 | setting_value = not setting_value 38 | elif setting_type is str: 39 | setting_value += u"test" 40 | elif setting_type is bytes: 41 | setting_value += b"test" 42 | else: 43 | setting = "%s: %s" % (setting_name, setting_type) 44 | self.fail("Unsupported setting type for %s" % setting) 45 | values_by_name[setting_name] = setting_value 46 | Setting.objects.create(name=setting_name, value=setting_value) 47 | # Load the settings and make sure the DB values have persisted. 48 | settings.use_editable() 49 | for (name, value) in values_by_name.items(): 50 | self.assertEqual(getattr(settings, name), value) 51 | 52 | def test_editable_override(self): 53 | """ 54 | Test that an editable setting is always overridden by a settings.py 55 | setting of the same name. 56 | """ 57 | Setting.objects.all().delete() 58 | django_settings.FOO = "Set in settings.py" 59 | db_value = Setting(name="FOO", value="Set in database") 60 | db_value.save() 61 | settings.use_editable() 62 | first_value = settings.FOO 63 | settings.SITE_TITLE # Triggers access? 64 | second_value = settings.FOO 65 | self.assertEqual(first_value, second_value) 66 | 67 | def test_bytes_conversion(self): 68 | register_setting(name="BYTES_TEST_SETTING", editable=True, default=b"") 69 | Setting.objects.create(name="BYTES_TEST_SETTING", 70 | value="A unicode value") 71 | settings.use_editable() 72 | self.assertEqual(settings.BYTES_TEST_SETTING, b"A unicode value") 73 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fdhuang' 2 | -------------------------------------------------------------------------------- /core/defaults.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | from conf import register_setting 7 | 8 | register_setting( 9 | name="GOOGLE_ANALYTICS_ID", 10 | label=_("Google Analytics ID"), 11 | description=_("Google Analytics ID (http://www.google.com/analytics/)"), 12 | editable=True, 13 | default="", 14 | ) 15 | 16 | register_setting( 17 | name="SITE_TITLE", 18 | label=_("站点标题"), 19 | description=_(" ---------- "), 20 | editable=True, 21 | default="Echoes CMS", 22 | ) 23 | 24 | register_setting( 25 | name="SITE_TAGLINE", 26 | label=_("标语"), 27 | description=_("标语将显示在所有页面的最上方"), 28 | editable=True, 29 | default=_("另外一个开源内容管理平台"), 30 | ) 31 | 32 | register_setting( 33 | name="DEVICE_DEFAULT", 34 | description=_("Device specific template sub-directory to use as the " 35 | "default device."), 36 | editable=False, 37 | default="", 38 | ) 39 | 40 | register_setting( 41 | name="DEVICE_USER_AGENTS", 42 | description=_("Mapping of device specific template sub-directory names to " 43 | "the sequence of strings that may be found in their user agents."), 44 | editable=False, 45 | default=( 46 | ("mobile", ("2.0 MMP", "240x320", "400X240", "AvantGo", "BlackBerry", 47 | "Blazer", "Cellphone", "Danger", "DoCoMo", "Elaine/3.0", 48 | "EudoraWeb", "Googlebot-Mobile", "hiptop", "IEMobile", 49 | "KYOCERA/WX310K", "LG/U990", "MIDP-2.", "MMEF20", "MOT-V", 50 | "NetFront", "Newt", "Nintendo Wii", "Nitro", "Nokia", 51 | "Opera Mini", "Palm", "PlayStation Portable", "portalmmm", 52 | "Proxinet", "ProxiNet", "SHARP-TQ-GX10", "SHG-i900", 53 | "Small", "SonyEricsson", "Symbian OS", "SymbianOS", 54 | "TS21i-10", "UP.Browser", "UP.Link", "webOS", "Windows CE", 55 | "WinWAP", "YahooSeeker/M1A1-R2D2", "iPhone", "iPod", "Android", 56 | "BlackBerry9530", "LG-TU915 Obigo", "LGE VX", "webOS", 57 | "Nokia5800",) 58 | ), 59 | ), 60 | ) 61 | 62 | register_setting( 63 | name="TEMPLATE_ACCESSIBLE_SETTINGS", 64 | description=_("Sequence of setting names available within templates."), 65 | editable=False, 66 | default=( 67 | "ACCOUNTS_APPROVAL_REQUIRED", "ACCOUNTS_VERIFICATION_REQUIRED", 68 | "ADMIN_MENU_COLLAPSED", 69 | "BITLY_ACCESS_TOKEN", "BLOG_USE_FEATURED_IMAGE", 70 | "COMMENTS_DISQUS_SHORTNAME", "COMMENTS_NUM_LATEST", 71 | "COMMENTS_DISQUS_API_PUBLIC_KEY", "COMMENTS_DISQUS_API_SECRET_KEY", 72 | "COMMENTS_USE_RATINGS", "DEV_SERVER", "FORMS_USE_HTML5", 73 | "GRAPPELLI_INSTALLED", "GOOGLE_ANALYTICS_ID", "JQUERY_FILENAME", 74 | "LOGIN_URL", "LOGOUT_URL", "SITE_TITLE", "SITE_TAGLINE", "USE_L10N", 75 | ), 76 | ) -------------------------------------------------------------------------------- /core/middleware.py: -------------------------------------------------------------------------------- 1 | from django.template import Template 2 | from utils.device import templates_for_device 3 | 4 | 5 | class TemplateForDeviceMiddleware(object): 6 | """ 7 | Inserts device-specific templates to the template list. 8 | """ 9 | 10 | def process_template_response(self, request, response): 11 | if hasattr(response, "template_name"): 12 | if not isinstance(response.template_name, Template): 13 | templates = templates_for_device(request, response.template_name) 14 | response.template_name = templates 15 | return response 16 | 17 | -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sites.managers import CurrentSiteManager 2 | from django.db import models 3 | from utils.sites import current_site_id 4 | 5 | 6 | class SiteRelated(models.Model): 7 | """ 8 | Abstract model for all things site-related. Adds a foreignkey to 9 | Django's ``Site`` model, and filters by site with all querysets. 10 | See ``mezzanine.utils.sites.current_site_id`` for implementation 11 | details. 12 | """ 13 | 14 | objects = CurrentSiteManager() 15 | 16 | class Meta: 17 | abstract = True 18 | 19 | site = models.ForeignKey("sites.Site", editable=False) 20 | 21 | def save(self, update_site=False, *args, **kwargs): 22 | """ 23 | Set the site to the current site when the record is first 24 | created, or the ``update_site`` argument is explicitly set 25 | to ``True``. 26 | """ 27 | if update_site or not self.id: 28 | self.site_id = current_site_id() 29 | super(SiteRelated, self).save(*args, **kwargs) 30 | -------------------------------------------------------------------------------- /core/request.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import threading 4 | 5 | 6 | _thread_local = threading.local() 7 | 8 | 9 | def current_request(): 10 | """ 11 | Retrieves the request from the current thread. 12 | """ 13 | return getattr(_thread_local, "request", None) 14 | 15 | 16 | class CurrentRequestMiddleware(object): 17 | """ 18 | Stores the request in the current thread for global access. 19 | """ 20 | 21 | def process_request(self, request): 22 | _thread_local.request = request 23 | 24 | # def process_response(self, request, response): 25 | # try: 26 | # return response 27 | # finally: 28 | # _thread_local.request = None 29 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from utils.cache import add_cache_bypass 3 | from utils.urls import next_url 4 | from utils.views import set_cookie 5 | 6 | 7 | def set_device(request, device=""): 8 | """ 9 | Sets a device name in a cookie when a user explicitly wants to go 10 | to the site for a particular device (eg mobile). 11 | """ 12 | response = redirect(add_cache_bypass(next_url(request) or "/")) 13 | set_cookie(response, "echoes-device", device, 60 * 60 * 24 * 365) 14 | return response 15 | 16 | 17 | def direct_to_template(request, template, extra_context=None, **kwargs): 18 | """ 19 | Replacement for Django's ``direct_to_template`` that uses 20 | ``TemplateResponse`` via ``mezzanine.utils.views.render``. 21 | """ 22 | context = extra_context or {} 23 | context["params"] = kwargs 24 | for (key, value) in context.items(): 25 | if callable(value): 26 | context[key] = value() 27 | return render(request, template, context) -------------------------------------------------------------------------------- /docs/architecture.dot: -------------------------------------------------------------------------------- 1 | digraph tree 2 | { 3 | nodesep=0.5; 4 | rankdir=BT; 5 | charset="UTF-8"; 6 | fixedsize=true; 7 | compound=true; 8 | node [style="rounded,filled", width=0, height=0, shape=box, fillcolor="#E5E5E5", concentrate=true] 9 | 10 | subgraph cluster_0 { 11 | label = "Django"; 12 | "Admin UI" 13 | "RESTful API" 14 | subgraph cluster_4 { 15 | label="SEO" 16 | "SiteMap" 17 | "MicroData" 18 | "Custom SEO" 19 | } 20 | subgraph cluster_3 { 21 | label="Template" 22 | "Inside" 23 | "Mustache" 24 | } 25 | } 26 | 27 | subgraph cluster_1 { 28 | label = "Mobile"; 29 | "Mobile Version" [shape=box style="filled"] 30 | "Mobile APP"[shape=box style="filled"] 31 | } 32 | 33 | "Mustache" -> "Mobile Version" 34 | "RESTful API" -> "Mobile Version"[lhead=cluster_1] 35 | } 36 | -------------------------------------------------------------------------------- /docs/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/docs/architecture.jpg -------------------------------------------------------------------------------- /echoes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/echoes/__init__.py -------------------------------------------------------------------------------- /echoes/local_settings.py: -------------------------------------------------------------------------------- 1 | 2 | # SECURITY WARNING: keep the secret key used in production secret! 3 | SECRET_KEY = '3!-s8_8sxseixu%(bg6f_%)df9+y$6p8v+v!0^(k+9c9g3w$g7' 4 | 5 | # SECURITY WARNING: don't run with debug turned on in production! 6 | DEBUG = True 7 | 8 | DATABASES = { 9 | "default": { 10 | # Ends with "postgresql_psycopg2", "mysql", "sqlite3" or "oracle". 11 | "ENGINE": "django.db.backends.sqlite3", 12 | # DB name or path to database file if using sqlite3. 13 | "NAME": "db/db.sqlite3", 14 | # Not used with sqlite3. 15 | "USER": "", 16 | # Not used with sqlite3. 17 | "PASSWORD": "", 18 | # Set to empty string for localhost. Not used with sqlite3. 19 | "HOST": "", 20 | # Set to empty string for default. Not used with sqlite3. 21 | "PORT": "", 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /echoes/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for echoes project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | 15 | STATICFILES_FINDERS = ( 16 | "django.contrib.staticfiles.finders.FileSystemFinder", 17 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 18 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 19 | ) 20 | 21 | ######### 22 | # PATHS # 23 | ######### 24 | 25 | import os 26 | 27 | # Full filesystem path to the project. 28 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 29 | 30 | # Name of the directory for the project. 31 | PROJECT_DIRNAME = PROJECT_ROOT.split(os.sep)[-1] 32 | 33 | # Every cache key will get prefixed with this value - here we set it to 34 | # the name of the directory the project is in to try and use something 35 | # project specific. 36 | CACHE_MIDDLEWARE_KEY_PREFIX = PROJECT_DIRNAME 37 | 38 | # URL prefix for static files. 39 | # Example: "http://media.lawrence.com/static/" 40 | STATIC_URL = "/static/" 41 | 42 | # Absolute path to the directory static files should be collected to. 43 | # Don't put anything in this directory yourself; store your static files 44 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 45 | # Example: "/home/media/media.lawrence.com/static/" 46 | STATIC_ROOT = os.path.join(PROJECT_ROOT, STATIC_URL.strip("/")) 47 | 48 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 49 | # trailing slash. 50 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 51 | MEDIA_URL = STATIC_URL + "media/" 52 | 53 | # Absolute filesystem path to the directory that will hold user-uploaded files. 54 | # Example: "/home/media/media.lawrence.com/media/" 55 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, *MEDIA_URL.strip("/").split("/")) 56 | 57 | # Package/module name to import the root urlpatterns from for the project. 58 | ROOT_URLCONF = "%s.urls" % PROJECT_DIRNAME 59 | 60 | # Put strings here, like "/home/html/django_templates" 61 | # or "C:/www/django/templates". 62 | # Always use forward slashes, even on Windows. 63 | # Don't forget to use absolute paths, not relative paths. 64 | TEMPLATE_DIRS = (os.path.join(PROJECT_ROOT, "templates"),) 65 | 66 | # Quick-start development settings - unsuitable for production 67 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 68 | 69 | ALLOWED_HOSTS = [] 70 | 71 | 72 | # Application definition 73 | 74 | INSTALLED_APPS = ( 75 | 'admin_ui', 76 | 'django.contrib.sites', 77 | 'django.contrib.admin', 78 | 'django.contrib.auth', 79 | 'django.contrib.flatpages', 80 | 'django.contrib.contenttypes', 81 | 'django.contrib.humanize', 82 | 'django.contrib.sessions', 83 | 'django.contrib.messages', 84 | 'django.contrib.staticfiles', 85 | 'django.contrib.redirects', 86 | 'django.contrib.sitemaps', 87 | 'rest_framework', 88 | 'frontend', 89 | 'conf', 90 | 'blog', 91 | 'mustache', 92 | 'mobile', 93 | ) 94 | 95 | SITE_ID = 1 96 | 97 | PYSTACHE_APP_TEMPLATE_DIR = 'templates' 98 | 99 | MIDDLEWARE_CLASSES = ( 100 | 'django.contrib.sessions.middleware.SessionMiddleware', 101 | 'django.middleware.common.CommonMiddleware', 102 | 'django.middleware.csrf.CsrfViewMiddleware', 103 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 104 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 105 | 'django.contrib.messages.middleware.MessageMiddleware', 106 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 107 | 'django.middleware.security.SecurityMiddleware', 108 | 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 109 | 'django.contrib.redirects.middleware.RedirectFallbackMiddleware', 110 | "core.middleware.TemplateForDeviceMiddleware", 111 | ) 112 | 113 | TEMPLATES = [ 114 | { 115 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 116 | 'DIRS': [], 117 | 'OPTIONS': { 118 | 'context_processors': [ 119 | "django.contrib.auth.context_processors.auth", 120 | "django.contrib.messages.context_processors.messages", 121 | "django.template.context_processors.debug", 122 | "django.template.context_processors.i18n", 123 | "django.template.context_processors.media", 124 | "django.template.context_processors.static", 125 | "django.template.context_processors.tz", 126 | "conf.context_processors.settings", 127 | "conf.context_processors.flatpage", 128 | "django.template.context_processors.request", 129 | ], 130 | 'loaders': [ 131 | "django.template.loaders.filesystem.Loader", 132 | "django.template.loaders.app_directories.Loader", 133 | ] 134 | }, 135 | }, 136 | ] 137 | 138 | WSGI_APPLICATION = 'echoes.wsgi.application' 139 | 140 | # Internationalization 141 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 142 | 143 | LANGUAGE_CODE = 'zh-hans' 144 | 145 | TIME_ZONE = 'UTC' 146 | 147 | USE_I18N = True 148 | 149 | USE_L10N = True 150 | 151 | USE_TZ = True 152 | 153 | try: 154 | from local_settings import * 155 | except ImportError: 156 | pass 157 | -------------------------------------------------------------------------------- /echoes/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url, patterns 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.views.decorators.cache import cache_page 6 | from blog.feeds import WeblogEntryFeed 7 | from blog.sitemaps import WeblogSitemap 8 | from django.contrib.sitemaps import FlatPageSitemap, views as sitemap_views 9 | 10 | from conf import settings 11 | from django.contrib.flatpages import views as flat_views 12 | 13 | sitemaps = { 14 | 'weblog': WeblogSitemap, 15 | 'flatpages': FlatPageSitemap, 16 | } 17 | 18 | urlpatterns = patterns("frontend.views", 19 | url("^$", "homepage", name="home"), 20 | ) 21 | 22 | urlpatterns += patterns("core.views", 23 | url("^set_device/(?P.*)/$", "set_device", name="set_device"), 24 | url(r'^rss/weblog/$', WeblogEntryFeed(), name='weblog-feed'), 25 | url(r'^weblog/', include('blog.urls', namespace='weblog')), 26 | ) 27 | 28 | urlpatterns += [ 29 | url(r'^admin/', include(admin.site.urls)), 30 | url(r'^api/', include('api.urls')), 31 | url(r'^sitemap\.xml$', cache_page(60 * 60 * 6)(sitemap_views.sitemap), {'sitemaps': sitemaps}), 32 | url(r'', include('legacy.urls')), 33 | url(r'^(?P.*/)$', flat_views.flatpage), 34 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 35 | 36 | urlpatterns += staticfiles_urlpatterns() -------------------------------------------------------------------------------- /echoes/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for echoes project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/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", "echoes.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /frontend/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fdhuang' 2 | -------------------------------------------------------------------------------- /frontend/mustaches/template.html: -------------------------------------------------------------------------------- 1 |

{{ True }}

-------------------------------------------------------------------------------- /frontend/static/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /frontend/static/js/navbar.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Variables 3 | var $nav = $('.navbar'), 4 | $body = $('body'), 5 | $window = $(window), 6 | $popoverLink = $('[data-popover]'), 7 | navOffsetTop = $nav.offset().top, 8 | $document = $(document), 9 | entityMap = { 10 | "&": "&", 11 | "<": "<", 12 | ">": ">", 13 | '"': '"', 14 | "'": ''', 15 | "/": '/' 16 | } 17 | 18 | function init() { 19 | $window.on('scroll', onScroll) 20 | $window.on('resize', resize) 21 | $popoverLink.on('click', openPopover) 22 | $document.on('click', closePopover) 23 | $('a[href^="#"]').on('click', smoothScroll) 24 | } 25 | 26 | function smoothScroll(e) { 27 | e.preventDefault(); 28 | $(document).off("scroll"); 29 | var target = this.hash, 30 | menu = target; 31 | $target = $(target); 32 | $('html, body').stop().animate({ 33 | 'scrollTop': $target.offset().top-40 34 | }, 0, 'swing', function () { 35 | window.location.hash = target; 36 | $(document).on("scroll", onScroll); 37 | }); 38 | } 39 | 40 | function openPopover(e) { 41 | e.preventDefault() 42 | closePopover(); 43 | var popover = $($(this).data('popover')); 44 | popover.toggleClass('open') 45 | e.stopImmediatePropagation(); 46 | } 47 | 48 | function closePopover(e) { 49 | if($('.popover.open').length > 0) { 50 | $('.popover').removeClass('open') 51 | } 52 | } 53 | 54 | $("#button").click(function() { 55 | $('html, body').animate({ 56 | scrollTop: $("#elementtoScrollToID").offset().top 57 | }, 2000); 58 | }); 59 | 60 | function resize() { 61 | $body.removeClass('has-docked-nav') 62 | navOffsetTop = $nav.offset().top 63 | onScroll() 64 | } 65 | 66 | function onScroll() { 67 | if(navOffsetTop < $window.scrollTop() && !$body.hasClass('has-docked-nav')) { 68 | $body.addClass('has-docked-nav') 69 | } 70 | if(navOffsetTop > $window.scrollTop() && $body.hasClass('has-docked-nav')) { 71 | $body.removeClass('has-docked-nav') 72 | } 73 | } 74 | 75 | init(); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /frontend/static/phodal/css/main.css: -------------------------------------------------------------------------------- 1 | #footer { 2 | bottom: 0; 3 | width: 100%; 4 | height: 100px; /* choose any height */ 5 | } 6 | 7 | .footer-bottom { 8 | background: #0C4B33; 9 | padding: 40px 0 10px; 10 | } 11 | 12 | .footer-bottom ul.links { 13 | margin: 0; 14 | display: inline-block; 15 | padding-bottom: 0px; 16 | } 17 | 18 | .footer-bottom li { 19 | list-style: none; 20 | } 21 | 22 | .footer-bottom li a, 23 | .footer-bottom p.copyright a { 24 | color: #2B8C67; 25 | } 26 | 27 | .footer-bottom ul.links li { 28 | display: block; 29 | float: left; 30 | margin-left: 6px; 31 | text-transform: uppercase; 32 | font-weight: 700; 33 | font-size: 0.75rem; 34 | } 35 | 36 | .footer-bottom ul.links li a { 37 | color: #fff; 38 | padding: 3px 6px; 39 | } 40 | 41 | .footer-bottom ul.links li a:hover { 42 | color: #777; 43 | } 44 | 45 | .footer-bottom p.copyright { 46 | margin: 6px 0 0; 47 | font-size: 0.75rem; 48 | color: #2B8C67; 49 | } 50 | 51 | .footer-bottom span.mobile { 52 | margin: 6px 0 0; 53 | font-size: 0.75rem; 54 | color: #666; 55 | } -------------------------------------------------------------------------------- /frontend/static/phodal/js/foundation/foundation.accordion.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.accordion = { 5 | name : 'accordion', 6 | 7 | version : '5.5.2', 8 | 9 | settings : { 10 | content_class : 'content', 11 | active_class : 'active', 12 | multi_expand : false, 13 | toggleable : true, 14 | callback : function () {} 15 | }, 16 | 17 | init : function (scope, method, options) { 18 | this.bindings(method, options); 19 | }, 20 | 21 | events : function (instance) { 22 | var self = this; 23 | var S = this.S; 24 | self.create(this.S(instance)); 25 | 26 | S(this.scope) 27 | .off('.fndtn.accordion') 28 | .on('click.fndtn.accordion', '[' + this.attr_name() + '] > dd > a, [' + this.attr_name() + '] > li > a', function (e) { 29 | var accordion = S(this).closest('[' + self.attr_name() + ']'), 30 | groupSelector = self.attr_name() + '=' + accordion.attr(self.attr_name()), 31 | settings = accordion.data(self.attr_name(true) + '-init') || self.settings, 32 | target = S('#' + this.href.split('#')[1]), 33 | aunts = $('> dd, > li', accordion), 34 | siblings = aunts.children('.' + settings.content_class), 35 | active_content = siblings.filter('.' + settings.active_class); 36 | 37 | e.preventDefault(); 38 | 39 | if (accordion.attr(self.attr_name())) { 40 | siblings = siblings.add('[' + groupSelector + '] dd > ' + '.' + settings.content_class + ', [' + groupSelector + '] li > ' + '.' + settings.content_class); 41 | aunts = aunts.add('[' + groupSelector + '] dd, [' + groupSelector + '] li'); 42 | } 43 | 44 | if (settings.toggleable && target.is(active_content)) { 45 | target.parent('dd, li').toggleClass(settings.active_class, false); 46 | target.toggleClass(settings.active_class, false); 47 | S(this).attr('aria-expanded', function(i, attr){ 48 | return attr === 'true' ? 'false' : 'true'; 49 | }); 50 | settings.callback(target); 51 | target.triggerHandler('toggled', [accordion]); 52 | accordion.triggerHandler('toggled', [target]); 53 | return; 54 | } 55 | 56 | if (!settings.multi_expand) { 57 | siblings.removeClass(settings.active_class); 58 | aunts.removeClass(settings.active_class); 59 | aunts.children('a').attr('aria-expanded','false'); 60 | } 61 | 62 | target.addClass(settings.active_class).parent().addClass(settings.active_class); 63 | settings.callback(target); 64 | target.triggerHandler('toggled', [accordion]); 65 | accordion.triggerHandler('toggled', [target]); 66 | S(this).attr('aria-expanded','true'); 67 | }); 68 | }, 69 | 70 | create: function($instance) { 71 | var self = this, 72 | accordion = $instance, 73 | aunts = $('> .accordion-navigation', accordion), 74 | settings = accordion.data(self.attr_name(true) + '-init') || self.settings; 75 | 76 | aunts.children('a').attr('aria-expanded','false'); 77 | aunts.has('.' + settings.content_class + '.' + settings.active_class).children('a').attr('aria-expanded','true'); 78 | 79 | if (settings.multi_expand) { 80 | $instance.attr('aria-multiselectable','true'); 81 | } 82 | }, 83 | 84 | off : function () {}, 85 | 86 | reflow : function () {} 87 | }; 88 | }(jQuery, window, window.document)); 89 | -------------------------------------------------------------------------------- /frontend/static/phodal/js/foundation/foundation.alert.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.alert = { 5 | name : 'alert', 6 | 7 | version : '5.5.2', 8 | 9 | settings : { 10 | callback : function () {} 11 | }, 12 | 13 | init : function (scope, method, options) { 14 | this.bindings(method, options); 15 | }, 16 | 17 | events : function () { 18 | var self = this, 19 | S = this.S; 20 | 21 | $(this.scope).off('.alert').on('click.fndtn.alert', '[' + this.attr_name() + '] .close', function (e) { 22 | var alertBox = S(this).closest('[' + self.attr_name() + ']'), 23 | settings = alertBox.data(self.attr_name(true) + '-init') || self.settings; 24 | 25 | e.preventDefault(); 26 | if (Modernizr.csstransitions) { 27 | alertBox.addClass('alert-close'); 28 | alertBox.on('transitionend webkitTransitionEnd oTransitionEnd', function (e) { 29 | S(this).trigger('close.fndtn.alert').remove(); 30 | settings.callback(); 31 | }); 32 | } else { 33 | alertBox.fadeOut(300, function () { 34 | S(this).trigger('close.fndtn.alert').remove(); 35 | settings.callback(); 36 | }); 37 | } 38 | }); 39 | }, 40 | 41 | reflow : function () {} 42 | }; 43 | }(jQuery, window, window.document)); 44 | -------------------------------------------------------------------------------- /frontend/static/phodal/js/foundation/foundation.equalizer.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.equalizer = { 5 | name : 'equalizer', 6 | 7 | version : '5.5.2', 8 | 9 | settings : { 10 | use_tallest : true, 11 | before_height_change : $.noop, 12 | after_height_change : $.noop, 13 | equalize_on_stack : false, 14 | act_on_hidden_el: false 15 | }, 16 | 17 | init : function (scope, method, options) { 18 | Foundation.inherit(this, 'image_loaded'); 19 | this.bindings(method, options); 20 | this.reflow(); 21 | }, 22 | 23 | events : function () { 24 | this.S(window).off('.equalizer').on('resize.fndtn.equalizer', function (e) { 25 | this.reflow(); 26 | }.bind(this)); 27 | }, 28 | 29 | equalize : function (equalizer) { 30 | var isStacked = false, 31 | group = equalizer.data('equalizer'), 32 | settings = equalizer.data(this.attr_name(true)+'-init') || this.settings, 33 | vals, 34 | firstTopOffset; 35 | 36 | if (settings.act_on_hidden_el) { 37 | vals = group ? equalizer.find('['+this.attr_name()+'-watch="'+group+'"]') : equalizer.find('['+this.attr_name()+'-watch]'); 38 | } 39 | else { 40 | vals = group ? equalizer.find('['+this.attr_name()+'-watch="'+group+'"]:visible') : equalizer.find('['+this.attr_name()+'-watch]:visible'); 41 | } 42 | 43 | if (vals.length === 0) { 44 | return; 45 | } 46 | 47 | settings.before_height_change(); 48 | equalizer.trigger('before-height-change.fndth.equalizer'); 49 | vals.height('inherit'); 50 | 51 | if (settings.equalize_on_stack === false) { 52 | firstTopOffset = vals.first().offset().top; 53 | vals.each(function () { 54 | if ($(this).offset().top !== firstTopOffset) { 55 | isStacked = true; 56 | return false; 57 | } 58 | }); 59 | if (isStacked) { 60 | return; 61 | } 62 | } 63 | 64 | var heights = vals.map(function () { return $(this).outerHeight(false) }).get(); 65 | 66 | if (settings.use_tallest) { 67 | var max = Math.max.apply(null, heights); 68 | vals.css('height', max); 69 | } else { 70 | var min = Math.min.apply(null, heights); 71 | vals.css('height', min); 72 | } 73 | 74 | settings.after_height_change(); 75 | equalizer.trigger('after-height-change.fndtn.equalizer'); 76 | }, 77 | 78 | reflow : function () { 79 | var self = this; 80 | 81 | this.S('[' + this.attr_name() + ']', this.scope).each(function () { 82 | var $eq_target = $(this), 83 | media_query = $eq_target.data('equalizer-mq'), 84 | ignore_media_query = true; 85 | 86 | if (media_query) { 87 | media_query = 'is_' + media_query.replace(/-/g, '_'); 88 | if (Foundation.utils.hasOwnProperty(media_query)) { 89 | ignore_media_query = false; 90 | } 91 | } 92 | 93 | self.image_loaded(self.S('img', this), function () { 94 | if (ignore_media_query || Foundation.utils[media_query]()) { 95 | self.equalize($eq_target) 96 | } else { 97 | var vals = $eq_target.find('[' + self.attr_name() + '-watch]:visible'); 98 | vals.css('height', 'auto'); 99 | } 100 | }); 101 | }); 102 | } 103 | }; 104 | })(jQuery, window, window.document); 105 | -------------------------------------------------------------------------------- /frontend/static/phodal/js/foundation/foundation.magellan.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs['magellan-expedition'] = { 5 | name : 'magellan-expedition', 6 | 7 | version : '5.5.2', 8 | 9 | settings : { 10 | active_class : 'active', 11 | threshold : 0, // pixels from the top of the expedition for it to become fixes 12 | destination_threshold : 20, // pixels from the top of destination for it to be considered active 13 | throttle_delay : 30, // calculation throttling to increase framerate 14 | fixed_top : 0, // top distance in pixels assigend to the fixed element on scroll 15 | offset_by_height : true, // whether to offset the destination by the expedition height. Usually you want this to be true, unless your expedition is on the side. 16 | duration : 700, // animation duration time 17 | easing : 'swing' // animation easing 18 | }, 19 | 20 | init : function (scope, method, options) { 21 | Foundation.inherit(this, 'throttle'); 22 | this.bindings(method, options); 23 | }, 24 | 25 | events : function () { 26 | var self = this, 27 | S = self.S, 28 | settings = self.settings; 29 | 30 | // initialize expedition offset 31 | self.set_expedition_position(); 32 | 33 | S(self.scope) 34 | .off('.magellan') 35 | .on('click.fndtn.magellan', '[' + self.add_namespace('data-magellan-arrival') + '] a[href*=#]', function (e) { 36 | var sameHost = ((this.hostname === location.hostname) || !this.hostname), 37 | samePath = self.filterPathname(location.pathname) === self.filterPathname(this.pathname), 38 | testHash = this.hash.replace(/(:|\.|\/)/g, '\\$1'), 39 | anchor = this; 40 | 41 | if (sameHost && samePath && testHash) { 42 | e.preventDefault(); 43 | var expedition = $(this).closest('[' + self.attr_name() + ']'), 44 | settings = expedition.data('magellan-expedition-init'), 45 | hash = this.hash.split('#').join(''), 46 | target = $('a[name="' + hash + '"]'); 47 | 48 | if (target.length === 0) { 49 | target = $('#' + hash); 50 | 51 | } 52 | 53 | // Account for expedition height if fixed position 54 | var scroll_top = target.offset().top - settings.destination_threshold + 1; 55 | if (settings.offset_by_height) { 56 | scroll_top = scroll_top - expedition.outerHeight(); 57 | } 58 | $('html, body').stop().animate({ 59 | 'scrollTop' : scroll_top 60 | }, settings.duration, settings.easing, function () { 61 | if (history.pushState) { 62 | history.pushState(null, null, anchor.pathname + '#' + hash); 63 | } 64 | else { 65 | location.hash = anchor.pathname + '#' + hash; 66 | } 67 | }); 68 | } 69 | }) 70 | .on('scroll.fndtn.magellan', self.throttle(this.check_for_arrivals.bind(this), settings.throttle_delay)); 71 | }, 72 | 73 | check_for_arrivals : function () { 74 | var self = this; 75 | self.update_arrivals(); 76 | self.update_expedition_positions(); 77 | }, 78 | 79 | set_expedition_position : function () { 80 | var self = this; 81 | $('[' + this.attr_name() + '=fixed]', self.scope).each(function (idx, el) { 82 | var expedition = $(this), 83 | settings = expedition.data('magellan-expedition-init'), 84 | styles = expedition.attr('styles'), // save styles 85 | top_offset, fixed_top; 86 | 87 | expedition.attr('style', ''); 88 | top_offset = expedition.offset().top + settings.threshold; 89 | 90 | //set fixed-top by attribute 91 | fixed_top = parseInt(expedition.data('magellan-fixed-top')); 92 | if (!isNaN(fixed_top)) { 93 | self.settings.fixed_top = fixed_top; 94 | } 95 | 96 | expedition.data(self.data_attr('magellan-top-offset'), top_offset); 97 | expedition.attr('style', styles); 98 | }); 99 | }, 100 | 101 | update_expedition_positions : function () { 102 | var self = this, 103 | window_top_offset = $(window).scrollTop(); 104 | 105 | $('[' + this.attr_name() + '=fixed]', self.scope).each(function () { 106 | var expedition = $(this), 107 | settings = expedition.data('magellan-expedition-init'), 108 | styles = expedition.attr('style'), // save styles 109 | top_offset = expedition.data('magellan-top-offset'); 110 | 111 | //scroll to the top distance 112 | if (window_top_offset + self.settings.fixed_top >= top_offset) { 113 | // Placeholder allows height calculations to be consistent even when 114 | // appearing to switch between fixed/non-fixed placement 115 | var placeholder = expedition.prev('[' + self.add_namespace('data-magellan-expedition-clone') + ']'); 116 | if (placeholder.length === 0) { 117 | placeholder = expedition.clone(); 118 | placeholder.removeAttr(self.attr_name()); 119 | placeholder.attr(self.add_namespace('data-magellan-expedition-clone'), ''); 120 | expedition.before(placeholder); 121 | } 122 | expedition.css({position :'fixed', top : settings.fixed_top}).addClass('fixed'); 123 | } else { 124 | expedition.prev('[' + self.add_namespace('data-magellan-expedition-clone') + ']').remove(); 125 | expedition.attr('style', styles).css('position', '').css('top', '').removeClass('fixed'); 126 | } 127 | }); 128 | }, 129 | 130 | update_arrivals : function () { 131 | var self = this, 132 | window_top_offset = $(window).scrollTop(); 133 | 134 | $('[' + this.attr_name() + ']', self.scope).each(function () { 135 | var expedition = $(this), 136 | settings = expedition.data(self.attr_name(true) + '-init'), 137 | offsets = self.offsets(expedition, window_top_offset), 138 | arrivals = expedition.find('[' + self.add_namespace('data-magellan-arrival') + ']'), 139 | active_item = false; 140 | offsets.each(function (idx, item) { 141 | if (item.viewport_offset >= item.top_offset) { 142 | var arrivals = expedition.find('[' + self.add_namespace('data-magellan-arrival') + ']'); 143 | arrivals.not(item.arrival).removeClass(settings.active_class); 144 | item.arrival.addClass(settings.active_class); 145 | active_item = true; 146 | return true; 147 | } 148 | }); 149 | 150 | if (!active_item) { 151 | arrivals.removeClass(settings.active_class); 152 | } 153 | }); 154 | }, 155 | 156 | offsets : function (expedition, window_offset) { 157 | var self = this, 158 | settings = expedition.data(self.attr_name(true) + '-init'), 159 | viewport_offset = window_offset; 160 | 161 | return expedition.find('[' + self.add_namespace('data-magellan-arrival') + ']').map(function (idx, el) { 162 | var name = $(this).data(self.data_attr('magellan-arrival')), 163 | dest = $('[' + self.add_namespace('data-magellan-destination') + '=' + name + ']'); 164 | if (dest.length > 0) { 165 | var top_offset = dest.offset().top - settings.destination_threshold; 166 | if (settings.offset_by_height) { 167 | top_offset = top_offset - expedition.outerHeight(); 168 | } 169 | top_offset = Math.floor(top_offset); 170 | return { 171 | destination : dest, 172 | arrival : $(this), 173 | top_offset : top_offset, 174 | viewport_offset : viewport_offset 175 | } 176 | } 177 | }).sort(function (a, b) { 178 | if (a.top_offset < b.top_offset) { 179 | return -1; 180 | } 181 | if (a.top_offset > b.top_offset) { 182 | return 1; 183 | } 184 | return 0; 185 | }); 186 | }, 187 | 188 | data_attr : function (str) { 189 | if (this.namespace.length > 0) { 190 | return this.namespace + '-' + str; 191 | } 192 | 193 | return str; 194 | }, 195 | 196 | off : function () { 197 | this.S(this.scope).off('.magellan'); 198 | this.S(window).off('.magellan'); 199 | }, 200 | 201 | filterPathname : function (pathname) { 202 | pathname = pathname || ''; 203 | return pathname 204 | .replace(/^\//,'') 205 | .replace(/(?:index|default).[a-zA-Z]{3,4}$/,'') 206 | .replace(/\/$/,''); 207 | }, 208 | 209 | reflow : function () { 210 | var self = this; 211 | // remove placeholder expeditions used for height calculation purposes 212 | $('[' + self.add_namespace('data-magellan-expedition-clone') + ']', self.scope).remove(); 213 | } 214 | }; 215 | }(jQuery, window, window.document)); 216 | -------------------------------------------------------------------------------- /frontend/static/phodal/js/foundation/foundation.offcanvas.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.offcanvas = { 5 | name : 'offcanvas', 6 | 7 | version : '5.5.2', 8 | 9 | settings : { 10 | open_method : 'move', 11 | close_on_click : false 12 | }, 13 | 14 | init : function (scope, method, options) { 15 | this.bindings(method, options); 16 | }, 17 | 18 | events : function () { 19 | var self = this, 20 | S = self.S, 21 | move_class = '', 22 | right_postfix = '', 23 | left_postfix = ''; 24 | 25 | if (this.settings.open_method === 'move') { 26 | move_class = 'move-'; 27 | right_postfix = 'right'; 28 | left_postfix = 'left'; 29 | } else if (this.settings.open_method === 'overlap_single') { 30 | move_class = 'offcanvas-overlap-'; 31 | right_postfix = 'right'; 32 | left_postfix = 'left'; 33 | } else if (this.settings.open_method === 'overlap') { 34 | move_class = 'offcanvas-overlap'; 35 | } 36 | 37 | S(this.scope).off('.offcanvas') 38 | .on('click.fndtn.offcanvas', '.left-off-canvas-toggle', function (e) { 39 | self.click_toggle_class(e, move_class + right_postfix); 40 | if (self.settings.open_method !== 'overlap') { 41 | S('.left-submenu').removeClass(move_class + right_postfix); 42 | } 43 | $('.left-off-canvas-toggle').attr('aria-expanded', 'true'); 44 | }) 45 | .on('click.fndtn.offcanvas', '.left-off-canvas-menu a', function (e) { 46 | var settings = self.get_settings(e); 47 | var parent = S(this).parent(); 48 | 49 | if (settings.close_on_click && !parent.hasClass('has-submenu') && !parent.hasClass('back')) { 50 | self.hide.call(self, move_class + right_postfix, self.get_wrapper(e)); 51 | parent.parent().removeClass(move_class + right_postfix); 52 | } else if (S(this).parent().hasClass('has-submenu')) { 53 | e.preventDefault(); 54 | S(this).siblings('.left-submenu').toggleClass(move_class + right_postfix); 55 | } else if (parent.hasClass('back')) { 56 | e.preventDefault(); 57 | parent.parent().removeClass(move_class + right_postfix); 58 | } 59 | $('.left-off-canvas-toggle').attr('aria-expanded', 'true'); 60 | }) 61 | .on('click.fndtn.offcanvas', '.right-off-canvas-toggle', function (e) { 62 | self.click_toggle_class(e, move_class + left_postfix); 63 | if (self.settings.open_method !== 'overlap') { 64 | S('.right-submenu').removeClass(move_class + left_postfix); 65 | } 66 | $('.right-off-canvas-toggle').attr('aria-expanded', 'true'); 67 | }) 68 | .on('click.fndtn.offcanvas', '.right-off-canvas-menu a', function (e) { 69 | var settings = self.get_settings(e); 70 | var parent = S(this).parent(); 71 | 72 | if (settings.close_on_click && !parent.hasClass('has-submenu') && !parent.hasClass('back')) { 73 | self.hide.call(self, move_class + left_postfix, self.get_wrapper(e)); 74 | parent.parent().removeClass(move_class + left_postfix); 75 | } else if (S(this).parent().hasClass('has-submenu')) { 76 | e.preventDefault(); 77 | S(this).siblings('.right-submenu').toggleClass(move_class + left_postfix); 78 | } else if (parent.hasClass('back')) { 79 | e.preventDefault(); 80 | parent.parent().removeClass(move_class + left_postfix); 81 | } 82 | $('.right-off-canvas-toggle').attr('aria-expanded', 'true'); 83 | }) 84 | .on('click.fndtn.offcanvas', '.exit-off-canvas', function (e) { 85 | self.click_remove_class(e, move_class + left_postfix); 86 | S('.right-submenu').removeClass(move_class + left_postfix); 87 | if (right_postfix) { 88 | self.click_remove_class(e, move_class + right_postfix); 89 | S('.left-submenu').removeClass(move_class + left_postfix); 90 | } 91 | $('.right-off-canvas-toggle').attr('aria-expanded', 'true'); 92 | }) 93 | .on('click.fndtn.offcanvas', '.exit-off-canvas', function (e) { 94 | self.click_remove_class(e, move_class + left_postfix); 95 | $('.left-off-canvas-toggle').attr('aria-expanded', 'false'); 96 | if (right_postfix) { 97 | self.click_remove_class(e, move_class + right_postfix); 98 | $('.right-off-canvas-toggle').attr('aria-expanded', 'false'); 99 | } 100 | }); 101 | }, 102 | 103 | toggle : function (class_name, $off_canvas) { 104 | $off_canvas = $off_canvas || this.get_wrapper(); 105 | if ($off_canvas.is('.' + class_name)) { 106 | this.hide(class_name, $off_canvas); 107 | } else { 108 | this.show(class_name, $off_canvas); 109 | } 110 | }, 111 | 112 | show : function (class_name, $off_canvas) { 113 | $off_canvas = $off_canvas || this.get_wrapper(); 114 | $off_canvas.trigger('open.fndtn.offcanvas'); 115 | $off_canvas.addClass(class_name); 116 | }, 117 | 118 | hide : function (class_name, $off_canvas) { 119 | $off_canvas = $off_canvas || this.get_wrapper(); 120 | $off_canvas.trigger('close.fndtn.offcanvas'); 121 | $off_canvas.removeClass(class_name); 122 | }, 123 | 124 | click_toggle_class : function (e, class_name) { 125 | e.preventDefault(); 126 | var $off_canvas = this.get_wrapper(e); 127 | this.toggle(class_name, $off_canvas); 128 | }, 129 | 130 | click_remove_class : function (e, class_name) { 131 | e.preventDefault(); 132 | var $off_canvas = this.get_wrapper(e); 133 | this.hide(class_name, $off_canvas); 134 | }, 135 | 136 | get_settings : function (e) { 137 | var offcanvas = this.S(e.target).closest('[' + this.attr_name() + ']'); 138 | return offcanvas.data(this.attr_name(true) + '-init') || this.settings; 139 | }, 140 | 141 | get_wrapper : function (e) { 142 | var $off_canvas = this.S(e ? e.target : this.scope).closest('.off-canvas-wrap'); 143 | 144 | if ($off_canvas.length === 0) { 145 | $off_canvas = this.S('.off-canvas-wrap'); 146 | } 147 | return $off_canvas; 148 | }, 149 | 150 | reflow : function () {} 151 | }; 152 | }(jQuery, window, window.document)); 153 | -------------------------------------------------------------------------------- /frontend/static/phodal/js/foundation/foundation.slider.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.slider = { 5 | name : 'slider', 6 | 7 | version : '5.5.2', 8 | 9 | settings : { 10 | start : 0, 11 | end : 100, 12 | step : 1, 13 | precision : null, 14 | initial : null, 15 | display_selector : '', 16 | vertical : false, 17 | trigger_input_change : false, 18 | on_change : function () {} 19 | }, 20 | 21 | cache : {}, 22 | 23 | init : function (scope, method, options) { 24 | Foundation.inherit(this, 'throttle'); 25 | this.bindings(method, options); 26 | this.reflow(); 27 | }, 28 | 29 | events : function () { 30 | var self = this; 31 | 32 | $(this.scope) 33 | .off('.slider') 34 | .on('mousedown.fndtn.slider touchstart.fndtn.slider pointerdown.fndtn.slider', 35 | '[' + self.attr_name() + ']:not(.disabled, [disabled]) .range-slider-handle', function (e) { 36 | if (!self.cache.active) { 37 | e.preventDefault(); 38 | self.set_active_slider($(e.target)); 39 | } 40 | }) 41 | .on('mousemove.fndtn.slider touchmove.fndtn.slider pointermove.fndtn.slider', function (e) { 42 | if (!!self.cache.active) { 43 | e.preventDefault(); 44 | if ($.data(self.cache.active[0], 'settings').vertical) { 45 | var scroll_offset = 0; 46 | if (!e.pageY) { 47 | scroll_offset = window.scrollY; 48 | } 49 | self.calculate_position(self.cache.active, self.get_cursor_position(e, 'y') + scroll_offset); 50 | } else { 51 | self.calculate_position(self.cache.active, self.get_cursor_position(e, 'x')); 52 | } 53 | } 54 | }) 55 | .on('mouseup.fndtn.slider touchend.fndtn.slider pointerup.fndtn.slider', function (e) { 56 | self.remove_active_slider(); 57 | }) 58 | .on('change.fndtn.slider', function (e) { 59 | self.settings.on_change(); 60 | }); 61 | 62 | self.S(window) 63 | .on('resize.fndtn.slider', self.throttle(function (e) { 64 | self.reflow(); 65 | }, 300)); 66 | 67 | // update slider value as users change input value 68 | this.S('[' + this.attr_name() + ']').each(function () { 69 | var slider = $(this), 70 | handle = slider.children('.range-slider-handle')[0], 71 | settings = self.initialize_settings(handle); 72 | 73 | if (settings.display_selector != '') { 74 | $(settings.display_selector).each(function(){ 75 | if (this.hasOwnProperty('value')) { 76 | $(this).change(function(){ 77 | // is there a better way to do this? 78 | slider.foundation("slider", "set_value", $(this).val()); 79 | }); 80 | } 81 | }); 82 | } 83 | }); 84 | }, 85 | 86 | get_cursor_position : function (e, xy) { 87 | var pageXY = 'page' + xy.toUpperCase(), 88 | clientXY = 'client' + xy.toUpperCase(), 89 | position; 90 | 91 | if (typeof e[pageXY] !== 'undefined') { 92 | position = e[pageXY]; 93 | } else if (typeof e.originalEvent[clientXY] !== 'undefined') { 94 | position = e.originalEvent[clientXY]; 95 | } else if (e.originalEvent.touches && e.originalEvent.touches[0] && typeof e.originalEvent.touches[0][clientXY] !== 'undefined') { 96 | position = e.originalEvent.touches[0][clientXY]; 97 | } else if (e.currentPoint && typeof e.currentPoint[xy] !== 'undefined') { 98 | position = e.currentPoint[xy]; 99 | } 100 | 101 | return position; 102 | }, 103 | 104 | set_active_slider : function ($handle) { 105 | this.cache.active = $handle; 106 | }, 107 | 108 | remove_active_slider : function () { 109 | this.cache.active = null; 110 | }, 111 | 112 | calculate_position : function ($handle, cursor_x) { 113 | var self = this, 114 | settings = $.data($handle[0], 'settings'), 115 | handle_l = $.data($handle[0], 'handle_l'), 116 | handle_o = $.data($handle[0], 'handle_o'), 117 | bar_l = $.data($handle[0], 'bar_l'), 118 | bar_o = $.data($handle[0], 'bar_o'); 119 | 120 | requestAnimationFrame(function () { 121 | var pct; 122 | 123 | if (Foundation.rtl && !settings.vertical) { 124 | pct = self.limit_to(((bar_o + bar_l - cursor_x) / bar_l), 0, 1); 125 | } else { 126 | pct = self.limit_to(((cursor_x - bar_o) / bar_l), 0, 1); 127 | } 128 | 129 | pct = settings.vertical ? 1 - pct : pct; 130 | 131 | var norm = self.normalized_value(pct, settings.start, settings.end, settings.step, settings.precision); 132 | 133 | self.set_ui($handle, norm); 134 | }); 135 | }, 136 | 137 | set_ui : function ($handle, value) { 138 | var settings = $.data($handle[0], 'settings'), 139 | handle_l = $.data($handle[0], 'handle_l'), 140 | bar_l = $.data($handle[0], 'bar_l'), 141 | norm_pct = this.normalized_percentage(value, settings.start, settings.end), 142 | handle_offset = norm_pct * (bar_l - handle_l) - 1, 143 | progress_bar_length = norm_pct * 100, 144 | $handle_parent = $handle.parent(), 145 | $hidden_inputs = $handle.parent().children('input[type=hidden]'); 146 | 147 | if (Foundation.rtl && !settings.vertical) { 148 | handle_offset = -handle_offset; 149 | } 150 | 151 | handle_offset = settings.vertical ? -handle_offset + bar_l - handle_l + 1 : handle_offset; 152 | this.set_translate($handle, handle_offset, settings.vertical); 153 | 154 | if (settings.vertical) { 155 | $handle.siblings('.range-slider-active-segment').css('height', progress_bar_length + '%'); 156 | } else { 157 | $handle.siblings('.range-slider-active-segment').css('width', progress_bar_length + '%'); 158 | } 159 | 160 | $handle_parent.attr(this.attr_name(), value).trigger('change.fndtn.slider'); 161 | 162 | $hidden_inputs.val(value); 163 | if (settings.trigger_input_change) { 164 | $hidden_inputs.trigger('change.fndtn.slider'); 165 | } 166 | 167 | if (!$handle[0].hasAttribute('aria-valuemin')) { 168 | $handle.attr({ 169 | 'aria-valuemin' : settings.start, 170 | 'aria-valuemax' : settings.end 171 | }); 172 | } 173 | $handle.attr('aria-valuenow', value); 174 | 175 | if (settings.display_selector != '') { 176 | $(settings.display_selector).each(function () { 177 | if (this.hasAttribute('value')) { 178 | $(this).val(value); 179 | } else { 180 | $(this).text(value); 181 | } 182 | }); 183 | } 184 | 185 | }, 186 | 187 | normalized_percentage : function (val, start, end) { 188 | return Math.min(1, (val - start) / (end - start)); 189 | }, 190 | 191 | normalized_value : function (val, start, end, step, precision) { 192 | var range = end - start, 193 | point = val * range, 194 | mod = (point - (point % step)) / step, 195 | rem = point % step, 196 | round = ( rem >= step * 0.5 ? step : 0); 197 | return ((mod * step + round) + start).toFixed(precision); 198 | }, 199 | 200 | set_translate : function (ele, offset, vertical) { 201 | if (vertical) { 202 | $(ele) 203 | .css('-webkit-transform', 'translateY(' + offset + 'px)') 204 | .css('-moz-transform', 'translateY(' + offset + 'px)') 205 | .css('-ms-transform', 'translateY(' + offset + 'px)') 206 | .css('-o-transform', 'translateY(' + offset + 'px)') 207 | .css('transform', 'translateY(' + offset + 'px)'); 208 | } else { 209 | $(ele) 210 | .css('-webkit-transform', 'translateX(' + offset + 'px)') 211 | .css('-moz-transform', 'translateX(' + offset + 'px)') 212 | .css('-ms-transform', 'translateX(' + offset + 'px)') 213 | .css('-o-transform', 'translateX(' + offset + 'px)') 214 | .css('transform', 'translateX(' + offset + 'px)'); 215 | } 216 | }, 217 | 218 | limit_to : function (val, min, max) { 219 | return Math.min(Math.max(val, min), max); 220 | }, 221 | 222 | initialize_settings : function (handle) { 223 | var settings = $.extend({}, this.settings, this.data_options($(handle).parent())), 224 | decimal_places_match_result; 225 | 226 | if (settings.precision === null) { 227 | decimal_places_match_result = ('' + settings.step).match(/\.([\d]*)/); 228 | settings.precision = decimal_places_match_result && decimal_places_match_result[1] ? decimal_places_match_result[1].length : 0; 229 | } 230 | 231 | if (settings.vertical) { 232 | $.data(handle, 'bar_o', $(handle).parent().offset().top); 233 | $.data(handle, 'bar_l', $(handle).parent().outerHeight()); 234 | $.data(handle, 'handle_o', $(handle).offset().top); 235 | $.data(handle, 'handle_l', $(handle).outerHeight()); 236 | } else { 237 | $.data(handle, 'bar_o', $(handle).parent().offset().left); 238 | $.data(handle, 'bar_l', $(handle).parent().outerWidth()); 239 | $.data(handle, 'handle_o', $(handle).offset().left); 240 | $.data(handle, 'handle_l', $(handle).outerWidth()); 241 | } 242 | 243 | $.data(handle, 'bar', $(handle).parent()); 244 | return $.data(handle, 'settings', settings); 245 | }, 246 | 247 | set_initial_position : function ($ele) { 248 | var settings = $.data($ele.children('.range-slider-handle')[0], 'settings'), 249 | initial = ((typeof settings.initial == 'number' && !isNaN(settings.initial)) ? settings.initial : Math.floor((settings.end - settings.start) * 0.5 / settings.step) * settings.step + settings.start), 250 | $handle = $ele.children('.range-slider-handle'); 251 | this.set_ui($handle, initial); 252 | }, 253 | 254 | set_value : function (value) { 255 | var self = this; 256 | $('[' + self.attr_name() + ']', this.scope).each(function () { 257 | $(this).attr(self.attr_name(), value); 258 | }); 259 | if (!!$(this.scope).attr(self.attr_name())) { 260 | $(this.scope).attr(self.attr_name(), value); 261 | } 262 | self.reflow(); 263 | }, 264 | 265 | reflow : function () { 266 | var self = this; 267 | self.S('[' + this.attr_name() + ']').each(function () { 268 | var handle = $(this).children('.range-slider-handle')[0], 269 | val = $(this).attr(self.attr_name()); 270 | self.initialize_settings(handle); 271 | 272 | if (val) { 273 | self.set_ui($(handle), parseFloat(val)); 274 | } else { 275 | self.set_initial_position($(this)); 276 | } 277 | }); 278 | } 279 | }; 280 | 281 | }(jQuery, window, window.document)); 282 | -------------------------------------------------------------------------------- /frontend/static/phodal/js/foundation/foundation.tab.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.tab = { 5 | name : 'tab', 6 | 7 | version : '5.5.2', 8 | 9 | settings : { 10 | active_class : 'active', 11 | callback : function () {}, 12 | deep_linking : false, 13 | scroll_to_content : true, 14 | is_hover : false 15 | }, 16 | 17 | default_tab_hashes : [], 18 | 19 | init : function (scope, method, options) { 20 | var self = this, 21 | S = this.S; 22 | 23 | // Store the default active tabs which will be referenced when the 24 | // location hash is absent, as in the case of navigating the tabs and 25 | // returning to the first viewing via the browser Back button. 26 | S('[' + this.attr_name() + '] > .active > a', this.scope).each(function () { 27 | self.default_tab_hashes.push(this.hash); 28 | }); 29 | 30 | // store the initial href, which is used to allow correct behaviour of the 31 | // browser back button when deep linking is turned on. 32 | self.entry_location = window.location.href; 33 | 34 | this.bindings(method, options); 35 | this.handle_location_hash_change(); 36 | }, 37 | 38 | events : function () { 39 | var self = this, 40 | S = this.S; 41 | 42 | var usual_tab_behavior = function (e, target) { 43 | var settings = S(target).closest('[' + self.attr_name() + ']').data(self.attr_name(true) + '-init'); 44 | if (!settings.is_hover || Modernizr.touch) { 45 | e.preventDefault(); 46 | e.stopPropagation(); 47 | self.toggle_active_tab(S(target).parent()); 48 | } 49 | }; 50 | 51 | S(this.scope) 52 | .off('.tab') 53 | // Key event: focus/tab key 54 | .on('keydown.fndtn.tab', '[' + this.attr_name() + '] > * > a', function(e) { 55 | var el = this; 56 | var keyCode = e.keyCode || e.which; 57 | // if user pressed tab key 58 | if (keyCode == 9) { 59 | e.preventDefault(); 60 | // TODO: Change usual_tab_behavior into accessibility function? 61 | usual_tab_behavior(e, el); 62 | } 63 | }) 64 | // Click event: tab title 65 | .on('click.fndtn.tab', '[' + this.attr_name() + '] > * > a', function(e) { 66 | var el = this; 67 | usual_tab_behavior(e, el); 68 | }) 69 | // Hover event: tab title 70 | .on('mouseenter.fndtn.tab', '[' + this.attr_name() + '] > * > a', function (e) { 71 | var settings = S(this).closest('[' + self.attr_name() + ']').data(self.attr_name(true) + '-init'); 72 | if (settings.is_hover) { 73 | self.toggle_active_tab(S(this).parent()); 74 | } 75 | }); 76 | 77 | // Location hash change event 78 | S(window).on('hashchange.fndtn.tab', function (e) { 79 | e.preventDefault(); 80 | self.handle_location_hash_change(); 81 | }); 82 | }, 83 | 84 | handle_location_hash_change : function () { 85 | 86 | var self = this, 87 | S = this.S; 88 | 89 | S('[' + this.attr_name() + ']', this.scope).each(function () { 90 | var settings = S(this).data(self.attr_name(true) + '-init'); 91 | if (settings.deep_linking) { 92 | // Match the location hash to a label 93 | var hash; 94 | if (settings.scroll_to_content) { 95 | hash = self.scope.location.hash; 96 | } else { 97 | // prefix the hash to prevent anchor scrolling 98 | hash = self.scope.location.hash.replace('fndtn-', ''); 99 | } 100 | if (hash != '') { 101 | // Check whether the location hash references a tab content div or 102 | // another element on the page (inside or outside the tab content div) 103 | var hash_element = S(hash); 104 | if (hash_element.hasClass('content') && hash_element.parent().hasClass('tabs-content')) { 105 | // Tab content div 106 | self.toggle_active_tab($('[' + self.attr_name() + '] > * > a[href=' + hash + ']').parent()); 107 | } else { 108 | // Not the tab content div. If inside the tab content, find the 109 | // containing tab and toggle it as active. 110 | var hash_tab_container_id = hash_element.closest('.content').attr('id'); 111 | if (hash_tab_container_id != undefined) { 112 | self.toggle_active_tab($('[' + self.attr_name() + '] > * > a[href=#' + hash_tab_container_id + ']').parent(), hash); 113 | } 114 | } 115 | } else { 116 | // Reference the default tab hashes which were initialized in the init function 117 | for (var ind = 0; ind < self.default_tab_hashes.length; ind++) { 118 | self.toggle_active_tab($('[' + self.attr_name() + '] > * > a[href=' + self.default_tab_hashes[ind] + ']').parent()); 119 | } 120 | } 121 | } 122 | }); 123 | }, 124 | 125 | toggle_active_tab : function (tab, location_hash) { 126 | var self = this, 127 | S = self.S, 128 | tabs = tab.closest('[' + this.attr_name() + ']'), 129 | tab_link = tab.find('a'), 130 | anchor = tab.children('a').first(), 131 | target_hash = '#' + anchor.attr('href').split('#')[1], 132 | target = S(target_hash), 133 | siblings = tab.siblings(), 134 | settings = tabs.data(this.attr_name(true) + '-init'), 135 | interpret_keyup_action = function (e) { 136 | // Light modification of Heydon Pickering's Practical ARIA Examples: http://heydonworks.com/practical_aria_examples/js/a11y.js 137 | 138 | // define current, previous and next (possible) tabs 139 | 140 | var $original = $(this); 141 | var $prev = $(this).parents('li').prev().children('[role="tab"]'); 142 | var $next = $(this).parents('li').next().children('[role="tab"]'); 143 | var $target; 144 | 145 | // find the direction (prev or next) 146 | 147 | switch (e.keyCode) { 148 | case 37: 149 | $target = $prev; 150 | break; 151 | case 39: 152 | $target = $next; 153 | break; 154 | default: 155 | $target = false 156 | break; 157 | } 158 | 159 | if ($target.length) { 160 | $original.attr({ 161 | 'tabindex' : '-1', 162 | 'aria-selected' : null 163 | }); 164 | $target.attr({ 165 | 'tabindex' : '0', 166 | 'aria-selected' : true 167 | }).focus(); 168 | } 169 | 170 | // Hide panels 171 | 172 | $('[role="tabpanel"]') 173 | .attr('aria-hidden', 'true'); 174 | 175 | // Show panel which corresponds to target 176 | 177 | $('#' + $(document.activeElement).attr('href').substring(1)) 178 | .attr('aria-hidden', null); 179 | 180 | }, 181 | go_to_hash = function(hash) { 182 | // This function allows correct behaviour of the browser's back button when deep linking is enabled. Without it 183 | // the user would get continually redirected to the default hash. 184 | var is_entry_location = window.location.href === self.entry_location, 185 | default_hash = settings.scroll_to_content ? self.default_tab_hashes[0] : is_entry_location ? window.location.hash :'fndtn-' + self.default_tab_hashes[0].replace('#', '') 186 | 187 | if (!(is_entry_location && hash === default_hash)) { 188 | window.location.hash = hash; 189 | } 190 | }; 191 | 192 | // allow usage of data-tab-content attribute instead of href 193 | if (anchor.data('tab-content')) { 194 | target_hash = '#' + anchor.data('tab-content').split('#')[1]; 195 | target = S(target_hash); 196 | } 197 | 198 | if (settings.deep_linking) { 199 | 200 | if (settings.scroll_to_content) { 201 | 202 | // retain current hash to scroll to content 203 | go_to_hash(location_hash || target_hash); 204 | 205 | if (location_hash == undefined || location_hash == target_hash) { 206 | tab.parent()[0].scrollIntoView(); 207 | } else { 208 | S(target_hash)[0].scrollIntoView(); 209 | } 210 | } else { 211 | // prefix the hashes so that the browser doesn't scroll down 212 | if (location_hash != undefined) { 213 | go_to_hash('fndtn-' + location_hash.replace('#', '')); 214 | } else { 215 | go_to_hash('fndtn-' + target_hash.replace('#', '')); 216 | } 217 | } 218 | } 219 | 220 | // WARNING: The activation and deactivation of the tab content must 221 | // occur after the deep linking in order to properly refresh the browser 222 | // window (notably in Chrome). 223 | // Clean up multiple attr instances to done once 224 | tab.addClass(settings.active_class).triggerHandler('opened'); 225 | tab_link.attr({'aria-selected' : 'true', tabindex : 0}); 226 | siblings.removeClass(settings.active_class) 227 | siblings.find('a').attr({'aria-selected' : 'false', tabindex : -1}); 228 | target.siblings().removeClass(settings.active_class).attr({'aria-hidden' : 'true', tabindex : -1}); 229 | target.addClass(settings.active_class).attr('aria-hidden', 'false').removeAttr('tabindex'); 230 | settings.callback(tab); 231 | target.triggerHandler('toggled', [target]); 232 | tabs.triggerHandler('toggled', [tab]); 233 | 234 | tab_link.off('keydown').on('keydown', interpret_keyup_action ); 235 | }, 236 | 237 | data_attr : function (str) { 238 | if (this.namespace.length > 0) { 239 | return this.namespace + '-' + str; 240 | } 241 | 242 | return str; 243 | }, 244 | 245 | off : function () {}, 246 | 247 | reflow : function () {} 248 | }; 249 | }(jQuery, window, window.document)); 250 | -------------------------------------------------------------------------------- /frontend/templates/400.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_error.html' %} 2 | 3 | {% block title %}Bad request{% endblock %} 4 | 5 | {% block header %}

400

{% endblock %} 6 | 7 | {% block content %} 8 |

Bad request

9 | 10 |

Yikes, this was a bad request. Not sure why, but it sure was bad.

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /frontend/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_error.html' %} 2 | 3 | {% block title %}Permission denied{% endblock %} 4 | 5 | {% block header %}

403

{% endblock %} 6 | 7 | {% block content %} 8 |

Permission denied

9 | 10 |

Apologies, but it seems as if you're not allowed to access this page. We honestly hope this is just a mistake.

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /frontend/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_error.html' %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block header %}

404

{% endblock %} 6 | 7 | {% block content %} 8 |

Page not found

9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /frontend/templates/410.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_error.html' %} 2 | 3 | {% block title %}Page removed{% endblock %} 4 | 5 | {% block header %}

410

{% endblock %} 6 | 7 | {% block content %} 8 |

Page removed.

9 | 10 |

Sorry, we've removed some of parts of the site that were completely out 11 | of date. In most cases, that content has been moved into 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /frontend/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_error.html' %} 2 | 3 | {% block title %}Page unavailable{% endblock %} 4 | 5 | {% block header %}

500

{% endblock %} 6 | 7 | {% block content %} 8 |

Page unavailable

9 | 10 |

We're sorry, but the requested page is currently unavailable.

11 | 12 |

We're messing around with things internally, and the server had a bit of a hiccup.

13 | 14 |

Please try again later.

15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /frontend/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load i18n mustache staticfiles %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block extend_seo %}{% endblock %} 15 | {% block meta_title %}{% endblock %}{% if settings.SITE_TITLE %} | {{ settings.SITE_TITLE }}{% endif %} 16 | 17 | 18 | 19 | {% include "includes/header.html" %} 20 |
21 |
22 | {% block main %} 23 |
24 | {% block content %}{% endblock %} 25 | Back to Top 26 |
27 |
28 | {% block content-related %}{% endblock %} 29 | {% block content-extra %}{% endblock %} 30 |
31 | {% endblock %} 32 |
33 | 34 |
35 | 36 | {% include "includes/footer.html" %} 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/templates/base_error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block layout_class %}sidebar-right{% endblock %} 3 | {% block title %}Django Community{% endblock %} 4 | 5 | {% block header %} 6 |

Community

7 |

Django Community{% if community_stats %} 8 | {% endif %}

9 | {% endblock %} 10 | 11 | {% block content-related %} 12 |
13 |

Improve Echoes

14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /frontend/templates/base_weblog.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load weblog %} 3 | {% block layout_class %}sidebar-right{% endblock %} 4 | {% block title %}News & Events{% endblock %} 5 | 6 | {% block header %} 7 |

News & Events

8 | {% endblock %} 9 | 10 | {% block extrahead %} 11 | 12 | {% endblock %} 13 | 14 | {% block content-related %} 15 | {% include 'blog/sidebar.html' %} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /frontend/templates/blog/blog_pagination.html: -------------------------------------------------------------------------------- 1 | {% if is_paginated %} 2 | 21 | {% endif %} 22 | -------------------------------------------------------------------------------- /frontend/templates/blog/entry_archive.html: -------------------------------------------------------------------------------- 1 | {% extends "base_weblog.html" %} 2 | 3 | {% block content %} 4 | 5 | {% comment %} 6 | {# This is not implemented in backend yet #} 7 | 15 | {% endcomment %} 16 | 17 |
18 |
    19 | {% for object in latest %} 20 | {% include 'blog/news_summary.html' with object=object %} 21 | {% endfor %} 22 |
23 |
24 | 25 | {% include 'blog/blog_pagination.html' %} 26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /frontend/templates/blog/entry_archive_day.html: -------------------------------------------------------------------------------- 1 | {% extends "base_weblog.html" %} 2 | 3 | {% block meta_title %}{{ day|date:"F j" }} | Weblog {% endblock %} 4 | 5 | {% block content %} 6 | 7 |

{{ day|date:"F j" }} archive

8 | 9 |
10 |
    11 | {% for object in object_list %} 12 | {% include 'blog/news_summary.html' with object=object %} 13 | {% endfor %} 14 |
15 |
16 | 17 | {% include 'blog/blog_pagination.html' %} 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /frontend/templates/blog/entry_archive_month.html: -------------------------------------------------------------------------------- 1 | {% extends "base_weblog.html" %} 2 | 3 | {% block meta_title %}{{ month|date:"F" }} | Weblog {% endblock %} 4 | 5 | {% block content %} 6 | 7 |

{{ month|date:"F Y" }} archive

8 | 9 |
10 |
    11 | {% for object in object_list %} 12 | {% include 'blog/news_summary.html' with object=object %} 13 | {% endfor %} 14 |
15 |
16 | 17 | {% include 'blog/blog_pagination.html' %} 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /frontend/templates/blog/entry_archive_year.html: -------------------------------------------------------------------------------- 1 | {% extends "base_weblog.html" %} 2 | 3 | {% block meta_title %}{{ year|date:"Y" }} | Weblog {% endblock %} 4 | 5 | {% block content %} 6 | 7 |

{{ year|date:"Y" }} archive

8 | 9 |
10 |
    11 | {% for object in object_list %} 12 | {% include 'blog/news_summary.html' with object=object %} 13 | {% endfor %} 14 |
15 |
16 | 17 | {% include 'blog/blog_pagination.html' %} 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /frontend/templates/blog/entry_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base_weblog.html" %} 2 | 3 | {% block extend_seo %} 4 | 5 | {% endblock %} 6 | 7 | 8 | {% block meta_title %}{{ object.headline|escape }}{% endblock %} 9 | 10 | {% block content %} 11 |

{{ object.headline|safe }}

12 | 13 | Posted by {{ object.author }} on {{ object.pub_date|date:"F j, Y" }} {% comment %}in Release Announcements{% endcomment %} 14 | {{ object.body_html|safe }} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /frontend/templates/blog/entry_snippet.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 | {% for e in entries %} 4 | {% include 'blog/news_summary.html' with object=e %} 5 | {% endfor %} 6 |
7 |
8 | -------------------------------------------------------------------------------- /frontend/templates/blog/month_links_snippet.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% regroup dates by year as dates_by_year %} 3 | 4 |
    5 | {% for month in dates_by_year %} 6 |
  • 7 |

    {{ month.grouper }}

    8 |
    9 | 14 |

    15 |
    16 |
  • 17 | {% endfor %} 18 |
19 | -------------------------------------------------------------------------------- /frontend/templates/blog/news_summary.html: -------------------------------------------------------------------------------- 1 | 2 | <{{ header_tag|default:'h2' }}> 3 | {{ object.headline|safe }} 4 | 5 | {% if summary_first %} 6 | {{ object.summary_html|safe }} 7 | {% endif %} 8 | 9 | Posted by {{ object.author }} on {{ object.pub_date|date:"F j, Y" }} 10 | 11 | {% if not summary_first %} 12 | {{ object.summary_html|safe }} 13 | {% endif %} 14 | {% if not hide_readmore %} 15 | 阅读更多 16 | {% endif %} 17 | -------------------------------------------------------------------------------- /frontend/templates/blog/sidebar.html: -------------------------------------------------------------------------------- 1 | {% load weblog %} 2 |

附加信息

3 |
4 | {% if events %} 5 |

活动

6 |
    7 | {% for event in events %} 8 |
  • 9 | 10 | {{ event.date|date:"F j, Y" }} | 11 | {{ event.location }} 12 | 13 | 14 |
  • 15 | {% endfor %} 16 |
17 | {% endif %} 18 | 19 |

档案

20 | {% render_month_links %} 21 | 22 |

RSS

23 | 26 |
27 | -------------------------------------------------------------------------------- /frontend/templates/flatpages/default.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block layout_class %}full-width{% endblock %} 4 | 5 | {% block meta_title %}{{ flatpage.title }}{% endblock %} 6 | 7 | {% block content %} 8 |

{{ flatpage.title }}

9 | {{ flatpage.content }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /frontend/templates/includes/footer.html: -------------------------------------------------------------------------------- 1 | {% load i18n staticfiles %} 2 | 3 | 27 | 28 | 29 | 30 | 33 | {% if settings.GOOGLE_ANALYTICS_ID %} 34 | 47 | {% endif %} -------------------------------------------------------------------------------- /frontend/templates/includes/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load i18n mustache staticfiles %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% if settings.SITE_TITLE %} {{ settings.SITE_TITLE }}{% endif %} 15 | 16 | 17 | 18 | {% include "includes/header.html" %} 19 |
20 |
21 |

{% if settings.SITE_TAGLINE %} {{ settings.SITE_TAGLINE }}{% endif %}

22 |
23 |

Why Echoes?

24 | 25 |
26 |
    27 |
  • With Django, take Web applications from concept to launch in a matter of hours!
  • 28 |
  • With Mustache, quickly build mobile version website!
  • 29 |
  • With Foundation, use flat style responsive layout!
  • 30 |
  • With RESTful API, quickly develop mobile app!
  • 31 |
32 |
33 | 34 |
35 | 36 | {% include "includes/footer.html" %} 37 | 38 | 39 | -------------------------------------------------------------------------------- /frontend/templates/mobile/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load i18n mustache staticfiles %} 4 | 5 | 6 | 7 | 8 | {% if settings.SITE_TITLE %} {{ settings.SITE_TITLE }}{% endif %} 9 | 10 | 11 |
12 |

{% if settings.SITE_TAGLINE %} {{ settings.SITE_TAGLINE }}{% endif %}

13 | 38 |

39 | Lorem ipsum dolor sit amet, porta voluptate at, sodales non est tortor, lorem eleifend fringilla praesent, amet 40 | nulla fringilla leo, gravida tempor egestas bibendum mattis donec. Facilisis porta viverra. Congue nunc, vel 41 | turpis sociosqu arcu commodo nulla, voluptatem tellus ornare nulla, etiam consequat tortor amet porttitor, 42 | aliquet vel ligula vel. Ac tincidunt, lectus integer, et integer ac sem massa ornare, nostra elementum velit in 43 | tristique et, nibh vestibulum est magna vitae pellentesque. Praesent ut venenatis praesent turpis commodo urna. 44 | Vitae mauris enim turpis nec, montes ipsum id hymenaeos. Vivamus pellentesque sapien. Donec purus non asperiores 45 | elementum ornare praesent, tellus leo optio vivamus diam lacinia, tincidunt ut non cras scelerisque pretium. Est 46 | velit eu gravida, mus dignissim. Tempor suspendisse at inceptos, wisi in, sodales eros dolor curae sed, elit at 47 | amet nulla in quis ornare. Urna duis, lacus velit. Massa quam vehicula suspendisse, id vitae, quam ligula quis 48 | odio rhoncus.

49 | 50 |

Facilisi fusce, elit maecenas, nec dui sed a, vivamus in per non, orci elementum consectetuer leo pede. Maecenas 51 | malesuada dignissim, dictum augue turpis, tincidunt habitasse pellentesque ut sit ante volutpat, mauris mi velit 52 | non magna amet. Sodales elit sed ut vivamus, suspendisse ac mattis vitae adipiscing, non sagittis dictum in. 53 | Amet amet eleifend, natoque vel urna, lacus eu donec placerat at vitae rhoncus, augue orci volutpat duis 54 | rhoncus, nulla consectetuer suspendisse lacinia at nec. Ipsum nec eros quis. Vehicula nulla, neque phasellus 55 | scelerisque condimentum auctor condimentum, fermentum turpis habitant elit vel suspendisse, tortor phasellus, 56 | diam suscipit. Risus ac dui amet in in pulvinar, ut in, vestibulum et a, ornare libero, nec ligula a aliquet 57 | facilisi nulla proin. Sollicitudin convallis amet justo massa diam vestibulum, adipiscing aliquam eu, ut dolor 58 | at rutrum in dui, aliquam libero, magnis fusce habitasse lorem pellentesque feugiat. Ridiculus mollit risus mus 59 | phasellus suspendisse. Sapien ut pede, aenean risus. Penatibus id odio aspernatur hac, aenean eu. 60 |

61 | {% trans "桌面版" %} 62 |
63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /frontend/templates/mobile/info.html: -------------------------------------------------------------------------------- 1 |

hello

-------------------------------------------------------------------------------- /frontend/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | class TemplateViewTests(TestCase): 4 | """ 5 | Tests for views that are instances of TemplateView. 6 | """ 7 | def test_homepage(self): 8 | response = self.client.get('/') 9 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /frontend/views.py: -------------------------------------------------------------------------------- 1 | from django.template import RequestContext 2 | from django.template.response import TemplateResponse 3 | 4 | 5 | def render(request, templates, dictionary=None, context_instance=None, 6 | **kwargs): 7 | """ 8 | Mimics ``django.shortcuts.render`` but uses a TemplateResponse for 9 | ``mezzanine.core.middleware.TemplateForDeviceMiddleware`` 10 | """ 11 | dictionary = dictionary or {} 12 | if context_instance: 13 | context_instance.update(dictionary) 14 | else: 15 | context_instance = RequestContext(request, dictionary) 16 | return TemplateResponse(request, templates, context_instance, **kwargs) 17 | 18 | 19 | def homepage(request, **kwargs): 20 | context = {"params": kwargs} 21 | for (key, value) in context.items(): 22 | if callable(value): 23 | context[key] = value() 24 | return render(request, 'index.html', context) -------------------------------------------------------------------------------- /legacy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/echoes/afc7fd7e48e5db724cec66cee78dff002f9027d5/legacy/__init__.py -------------------------------------------------------------------------------- /legacy/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class LegacyTests(TestCase): 5 | urls = 'legacy.urls' 6 | 7 | # Just a smoke test to ensure the URLconf works 8 | 9 | def test_gone(self): 10 | response = self.client.get('/comments/') 11 | self.assertEqual(response.status_code, 410) 12 | -------------------------------------------------------------------------------- /legacy/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Legacy URLs for documentation pages. 3 | """ 4 | from django.conf.urls import url 5 | 6 | from .views import gone 7 | 8 | urlpatterns = [ 9 | url(r'^comments/', gone), 10 | url(r'^rss/comments/$', gone), 11 | url(r'^documentation', gone), 12 | 13 | url(r'^400/$', 'django.views.defaults.bad_request'), 14 | url(r'^403/$', 'django.views.defaults.permission_denied'), 15 | url(r'^404/$', 'django.views.defaults.page_not_found'), 16 | url(r'^410/$', gone), 17 | url(r'^500/$', 'django.views.defaults.server_error'), 18 | ] 19 | -------------------------------------------------------------------------------- /legacy/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def gone(request, *args, **kwargs): 5 | """ 6 | Display a nice 410 gone page. 7 | """ 8 | return render(request, '410.html', status=410) 9 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "echoes.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /mobile/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fdhuang' 2 | -------------------------------------------------------------------------------- /mustache/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fdhuang' 2 | -------------------------------------------------------------------------------- /mustache/shortcuts.py: -------------------------------------------------------------------------------- 1 | import pystache 2 | 3 | 4 | def render(self, context): 5 | flatcontext = {} 6 | self.renderer = pystache.Renderer(search_dirs=None, file_extension="mustache") 7 | for d in context.dicts: 8 | flatcontext.update(d) 9 | 10 | return self.renderer.render(self.parsed, flatcontext) 11 | -------------------------------------------------------------------------------- /mustache/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fdhuang' 2 | -------------------------------------------------------------------------------- /mustache/templatetags/mustache.py: -------------------------------------------------------------------------------- 1 | """ Templatetags for including pystash templates """ 2 | import os 3 | import sys 4 | 5 | from django import template 6 | from django.conf import settings 7 | from django.core.exceptions import ImproperlyConfigured 8 | from django.utils._os import safe_join 9 | from importlib import import_module 10 | 11 | import pystache 12 | 13 | register = template.Library() 14 | 15 | 16 | class PystacheTemplateDoesNotExist(Exception): 17 | pass 18 | 19 | 20 | def get_app_template_dirs(): 21 | fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() 22 | app_template_dirs = [] 23 | for app in settings.INSTALLED_APPS: 24 | try: 25 | mod = import_module(app) 26 | except ImportError, e: 27 | raise ImproperlyConfigured('ImportError %s: %s' % (app, e.args[0])) 28 | attr = getattr(settings, 'PYSTACHE_APP_TEMPLATE_DIR', 'templates') 29 | template_dir = os.path.join(os.path.dirname(mod.__file__), 30 | attr) 31 | if os.path.isdir(template_dir): 32 | app_template_dirs.append(template_dir.decode(fs_encoding)) 33 | return app_template_dirs 34 | 35 | 36 | def get_template_sources(template_name): 37 | for template_dir in get_app_template_dirs(): 38 | try: 39 | yield safe_join(template_dir, template_name) 40 | except UnicodeDecodeError: 41 | # The template dir name was a bytestring that wasn't valid UTF-8. 42 | raise 43 | except ValueError: 44 | # The joined path was located outside of template_dir. 45 | pass 46 | 47 | 48 | def load_template_source(template_name): 49 | for filepath in get_template_sources(template_name): 50 | try: 51 | file = open(filepath) 52 | try: 53 | return file.read().decode(settings.FILE_CHARSET), filepath 54 | finally: 55 | file.close() 56 | except IOError: 57 | pass 58 | raise PystacheTemplateDoesNotExist(template_name) 59 | 60 | 61 | @register.simple_tag(takes_context=True) 62 | def mustache(context, template_name): 63 | template, source = load_template_source(template_name) 64 | return pystache.render(template, context.dicts[0]) 65 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | coveralls==0.5 2 | coverage==3.7.1 3 | isort==3.9.6 4 | flake8==2.4.1 5 | tox -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==1.8 2 | django-grappelli==2.6.4 3 | djangorestframework==3.1.2 4 | markdown==2.6.2 5 | django-filter==0.10 6 | pystache==0.5.4 7 | docutils==0.12 8 | django-uuslug==1.0.2 9 | markdown2==2.3 10 | ujson==1.33 11 | future==0.14.3 -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist =py27-{tests,isort} 3 | skipsdist = true 4 | 5 | [testenv] 6 | whitelist_externals = make 7 | deps = 8 | tests: -r{toxinidir}/requirements.txt 9 | isort: isort 10 | commands = 11 | tests: make ci 12 | flake8: flake8 -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fdhuang' 2 | -------------------------------------------------------------------------------- /utils/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from hashlib import md5 4 | from time import time 5 | 6 | from django.core.cache import cache 7 | from django.utils.cache import _i18n_cache_key_suffix 8 | 9 | from conf import settings 10 | from utils.device import device_from_request 11 | from utils.sites import current_site_id 12 | 13 | 14 | def _hashed_key(key): 15 | """ 16 | Hash keys when talking directly to the cache API, to avoid 17 | keys longer than the backend supports (eg memcache limit is 255) 18 | """ 19 | return md5(key.encode("utf-8")).hexdigest() 20 | 21 | 22 | def cache_set(key, value, timeout=None, refreshed=False): 23 | """ 24 | Wrapper for ``cache.set``. Stores the cache entry packed with 25 | the desired cache expiry time. When the entry is retrieved from 26 | cache, the packed expiry time is also checked, and if past, 27 | the stale cache entry is stored again with an expiry that has 28 | ``CACHE_SET_DELAY_SECONDS`` added to it. In this case the entry 29 | is not returned, so that a cache miss occurs and the entry 30 | should be set by the caller, but all other callers will still get 31 | the stale entry, so no real cache misses ever occur. 32 | """ 33 | if timeout is None: 34 | timeout = settings.CACHE_MIDDLEWARE_SECONDS 35 | refresh_time = timeout + time() 36 | real_timeout = timeout + settings.CACHE_SET_DELAY_SECONDS 37 | packed = (value, refresh_time, refreshed) 38 | return cache.set(_hashed_key(key), packed, real_timeout) 39 | 40 | 41 | def cache_get(key): 42 | """ 43 | Wrapper for ``cache.get``. The expiry time for the cache entry 44 | is stored with the entry. If the expiry time has past, put the 45 | stale entry back into cache, and don't return it to trigger a 46 | fake cache miss. 47 | """ 48 | packed = cache.get(_hashed_key(key)) 49 | if packed is None: 50 | return None 51 | value, refresh_time, refreshed = packed 52 | if (time() > refresh_time) and not refreshed: 53 | cache_set(key, value, settings.CACHE_SET_DELAY_SECONDS, True) 54 | return None 55 | return value 56 | 57 | 58 | def cache_installed(): 59 | """ 60 | Returns ``True`` if a cache backend is configured, and the 61 | cache middlware classes are present. 62 | """ 63 | has_key = hasattr(settings, "NEVERCACHE_KEY") 64 | return has_key and settings.CACHES and not settings.TESTING and set(( 65 | "mezzanine.core.middleware.UpdateCacheMiddleware", 66 | "mezzanine.core.middleware.FetchFromCacheMiddleware", 67 | )).issubset(set(settings.MIDDLEWARE_CLASSES)) 68 | 69 | 70 | def cache_key_prefix(request): 71 | """ 72 | Cache key for Mezzanine's cache middleware. Adds the current 73 | device and site ID. 74 | """ 75 | cache_key = "%s.%s.%s." % ( 76 | settings.CACHE_MIDDLEWARE_KEY_PREFIX, 77 | current_site_id(), 78 | device_from_request(request) or "default", 79 | ) 80 | return _i18n_cache_key_suffix(request, cache_key) 81 | 82 | 83 | def nevercache_token(): 84 | """ 85 | Returns the secret token that delimits content wrapped in 86 | the ``nevercache`` template tag. 87 | """ 88 | return "nevercache." + settings.NEVERCACHE_KEY 89 | 90 | 91 | def add_cache_bypass(url): 92 | """ 93 | Adds the current time to the querystring of the URL to force a 94 | cache reload. Used for when a form post redirects back to a 95 | page that should display updated content, such as new comments or 96 | ratings. 97 | """ 98 | if not cache_installed(): 99 | return url 100 | hash_str = "" 101 | if "#" in url: 102 | url, hash_str = url.split("#", 1) 103 | hash_str = "#" + hash_str 104 | url += "?" if "?" not in url else "&" 105 | return url + "t=" + str(time()).replace(".", "") + hash_str 106 | -------------------------------------------------------------------------------- /utils/device.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | 4 | def device_from_request(request): 5 | """ 6 | Determine's the device name from the request by first looking for an 7 | overridding cookie, and if not found then matching the user agent. 8 | Used at both the template level for choosing the template to load and 9 | also at the cache level as a cache key prefix. 10 | """ 11 | from conf import settings 12 | try: 13 | # If a device was set via cookie, match available devices. 14 | for (device, _) in settings.DEVICE_USER_AGENTS: 15 | if device == request.COOKIES["echoes-device"]: 16 | return device 17 | except KeyError: 18 | # If a device wasn't set via cookie, match user agent. 19 | try: 20 | user_agent = request.META["HTTP_USER_AGENT"].lower() 21 | except KeyError: 22 | pass 23 | else: 24 | try: 25 | user_agent = user_agent.decode("utf-8") 26 | except AttributeError: 27 | pass 28 | for (device, ua_strings) in settings.DEVICE_USER_AGENTS: 29 | for ua_string in ua_strings: 30 | if ua_string.lower() in user_agent: 31 | return device 32 | return "" 33 | 34 | 35 | def templates_for_device(request, templates): 36 | """ 37 | Given a template name (or list of them), returns the template names 38 | as a list, with each name prefixed with the device directory 39 | inserted before it's associate default in the list. 40 | """ 41 | from conf import settings 42 | if not isinstance(templates, (list, tuple)): 43 | templates = [templates] 44 | device = device_from_request(request) 45 | device_templates = [] 46 | for template in templates: 47 | if device: 48 | device_templates.append("%s/%s" % (device, template)) 49 | if settings.DEVICE_DEFAULT and settings.DEVICE_DEFAULT != device: 50 | default = "%s/%s" % (settings.DEVICE_DEFAULT, template) 51 | device_templates.append(default) 52 | device_templates.append(template) 53 | return device_templates 54 | -------------------------------------------------------------------------------- /utils/sites.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | import sys 5 | 6 | from django.contrib.sites.models import Site 7 | 8 | from conf import settings 9 | from core.request import current_request 10 | 11 | 12 | def current_site_id(): 13 | """ 14 | Responsible for determining the current ``Site`` instance to use 15 | when retrieving data for any ``SiteRelated`` models. If a request 16 | is available, and the site can be determined from it, we store the 17 | site against the request for subsequent retrievals. Otherwise the 18 | order of checks is as follows: 19 | 20 | - ``site_id`` in session. Used in the admin so that admin users 21 | can switch sites and stay on the same domain for the admin. 22 | - host for the current request matched to the domain of the site 23 | instance. 24 | - ``MEZZANINE_SITE_ID`` environment variable, so management 25 | commands or anything else outside of a request can specify a 26 | site. 27 | - ``SITE_ID`` setting. 28 | 29 | """ 30 | from utils.cache import cache_installed, cache_get, cache_set 31 | request = current_request() 32 | site_id = getattr(request, "site_id", None) 33 | if request and not site_id: 34 | site_id = request.session.get("site_id", None) 35 | if not site_id: 36 | domain = request.get_host().lower() 37 | if cache_installed(): 38 | # Don't use Mezzanine's cache_key_prefix here, since it 39 | # uses this very function we're in right now to create a 40 | # per-site cache key. 41 | bits = (settings.CACHE_MIDDLEWARE_KEY_PREFIX, domain) 42 | cache_key = "%s.site_id.%s" % bits 43 | site_id = cache_get(cache_key) 44 | if not site_id: 45 | try: 46 | site = Site.objects.get(domain__iexact=domain) 47 | except Site.DoesNotExist: 48 | pass 49 | else: 50 | site_id = site.id 51 | if cache_installed(): 52 | cache_set(cache_key, site_id) 53 | if request and site_id: 54 | request.site_id = site_id 55 | if not site_id: 56 | site_id = os.environ.get("MEZZANINE_SITE_ID", settings.SITE_ID) 57 | return site_id 58 | 59 | 60 | def has_site_permission(user): 61 | """ 62 | Checks if a staff user has staff-level access for the current site. 63 | The actual permission lookup occurs in ``SitePermissionMiddleware`` 64 | which then marks the request with the ``has_site_permission`` flag, 65 | so that we only query the db once per request, so this function 66 | serves as the entry point for everything else to check access. We 67 | also fall back to an ``is_staff`` check if the middleware is not 68 | installed, to ease migration. 69 | """ 70 | mw = "mezzanine.core.middleware.SitePermissionMiddleware" 71 | if mw not in settings.MIDDLEWARE_CLASSES: 72 | from warnings import warn 73 | warn(mw + " missing from settings.MIDDLEWARE_CLASSES - per site" 74 | "permissions not applied") 75 | return user.is_staff and user.is_active 76 | return getattr(user, "has_site_permission", False) 77 | 78 | 79 | def host_theme_path(request): 80 | """ 81 | Returns the directory of the theme associated with the given host. 82 | """ 83 | for (host, theme) in settings.HOST_THEMES: 84 | if host.lower() == request.get_host().split(":")[0].lower(): 85 | try: 86 | __import__(theme) 87 | module = sys.modules[theme] 88 | except ImportError: 89 | pass 90 | else: 91 | return os.path.dirname(os.path.abspath(module.__file__)) 92 | return "" 93 | 94 | 95 | def templates_for_host(request, templates): 96 | """ 97 | Given a template name (or list of them), returns the template names 98 | as a list, with each name prefixed with the device directory 99 | inserted into the front of the list. 100 | """ 101 | if not isinstance(templates, (list, tuple)): 102 | templates = [templates] 103 | theme_dir = host_theme_path(request) 104 | host_templates = [] 105 | if theme_dir: 106 | for template in templates: 107 | host_templates.append("%s/templates/%s" % (theme_dir, template)) 108 | host_templates.append(template) 109 | return host_templates 110 | return templates 111 | -------------------------------------------------------------------------------- /utils/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.core.urlresolvers import (reverse) 4 | from django.utils.http import is_safe_url 5 | 6 | try: 7 | from django.utils.encoding import smart_text 8 | except ImportError: 9 | # Backward compatibility for Py2 and Django < 1.5 10 | from django.utils.encoding import smart_unicode as smart_text 11 | 12 | def next_url(request): 13 | """ 14 | Returns URL to redirect to from the ``next`` param in the request. 15 | """ 16 | next = request.REQUEST.get("next", "") 17 | host = request.get_host() 18 | return next if next and is_safe_url(next, host=host) else None 19 | 20 | def admin_url(model, url, object_id=None): 21 | """ 22 | Returns the URL for the given model and admin url name. 23 | """ 24 | opts = model._meta 25 | url = "admin:%s_%s_%s" % (opts.app_label, opts.object_name.lower(), url) 26 | args = () 27 | if object_id is not None: 28 | args = (object_id,) 29 | return reverse(url, args=args) 30 | -------------------------------------------------------------------------------- /utils/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, unicode_literals 2 | 3 | from datetime import datetime, timedelta 4 | 5 | try: 6 | from urllib.parse import urlencode 7 | except ImportError: # Python 2 8 | from urllib import urlencode 9 | try: 10 | from urllib.request import Request, urlopen 11 | except ImportError: # Python 2 12 | from urllib2 import Request, urlopen 13 | 14 | 15 | def set_cookie(response, name, value, expiry_seconds=None, secure=False): 16 | """ 17 | Set cookie wrapper that allows number of seconds to be given as the 18 | expiry time, and ensures values are correctly encoded. 19 | """ 20 | if expiry_seconds is None: 21 | expiry_seconds = 90 * 24 * 60 * 60 # Default to 90 days. 22 | expires = datetime.strftime(datetime.utcnow() + 23 | timedelta(seconds=expiry_seconds), 24 | "%a, %d-%b-%Y %H:%M:%S GMT") 25 | # Django doesn't seem to support unicode cookie keys correctly on 26 | # Python 2. Work around by encoding it. See 27 | # https://code.djangoproject.com/ticket/19802 28 | try: 29 | response.set_cookie(name, value, expires=expires, secure=secure) 30 | except (KeyError, TypeError): 31 | response.set_cookie(name.encode('utf-8'), value, expires=expires, 32 | secure=secure) 33 | --------------------------------------------------------------------------------