├── .gitignore ├── LICENSE ├── README.md ├── cookiecutter.json ├── image credits.md ├── readme_resources ├── app-screenshot.png ├── course.png ├── project-structure.png ├── python-for-entrepreneurs-c.png └── template-execution-trimmed.png └── {{cookiecutter.project_slug}} ├── .coveragerc ├── CHANGES.txt ├── MANIFEST.in ├── README.txt ├── development.ini ├── production.ini ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── {{cookiecutter.project_slug}} ├── __init__.py ├── bin └── readme.md ├── controllers ├── account_controller.py ├── base_controller.py ├── cms_controller.py ├── home_controller.py ├── newsletter_controller.py └── readme.md ├── data ├── account.py ├── cms_page.py ├── dbsession.py ├── modelbase.py ├── passwordreset.py └── readme.md ├── db └── readme.md ├── email ├── readme.md ├── template_paser.py └── templates │ ├── password_reset.html │ └── welcome.html ├── infrastructure ├── cookie_auth.py ├── readme.md └── static_cache.py ├── services ├── account_service.py ├── cms_service.py ├── email_service.py ├── log_service.py ├── mailinglist_service.py └── readme.md ├── static ├── bower_components │ ├── bootstrap-css │ │ ├── .bower.json │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bower.json │ │ ├── css │ │ │ ├── bootstrap-theme.css │ │ │ ├── bootstrap-theme.css.map │ │ │ ├── bootstrap-theme.min.css │ │ │ ├── bootstrap-theme.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.min.js │ │ │ └── npm.js │ ├── font-awesome │ │ ├── .bower.json │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── HELP-US-OUT.txt │ │ ├── bower.json │ │ ├── css │ │ │ ├── font-awesome.css │ │ │ ├── font-awesome.css.map │ │ │ └── font-awesome.min.css │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── less │ │ │ ├── animated.less │ │ │ ├── bordered-pulled.less │ │ │ ├── core.less │ │ │ ├── fixed-width.less │ │ │ ├── font-awesome.less │ │ │ ├── icons.less │ │ │ ├── larger.less │ │ │ ├── list.less │ │ │ ├── mixins.less │ │ │ ├── path.less │ │ │ ├── rotated-flipped.less │ │ │ ├── screen-reader.less │ │ │ ├── stacked.less │ │ │ └── variables.less │ │ └── scss │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _screen-reader.scss │ │ │ ├── _stacked.scss │ │ │ ├── _variables.scss │ │ │ └── font-awesome.scss │ ├── html5shiv │ │ ├── .bower.json │ │ ├── Gruntfile.js │ │ ├── MIT and GPL2 licenses.md │ │ ├── bower.json │ │ ├── package.json │ │ └── readme.md │ ├── jquery-dist │ │ ├── .bower.json │ │ ├── bower.json │ │ └── jquery.min.js │ └── respond │ │ ├── .bower.json │ │ ├── Gruntfile.js │ │ ├── LICENSE-MIT │ │ ├── README.md │ │ ├── bower.json │ │ ├── cross-domain │ │ ├── example.html │ │ ├── respond-proxy.html │ │ ├── respond.proxy.gif │ │ └── respond.proxy.js │ │ ├── dest │ │ ├── respond.matchmedia.addListener.min.js │ │ ├── respond.matchmedia.addListener.src.js │ │ ├── respond.min.js │ │ └── respond.src.js │ │ ├── package.json │ │ └── src │ │ ├── matchmedia.addListener.js │ │ ├── matchmedia.polyfill.js │ │ └── respond.js ├── css │ ├── home.css │ ├── landing-page.css │ ├── nav.css │ ├── site.css │ └── theme.css ├── img │ ├── book-us.jpg │ ├── cc-comm-attr.png │ ├── event.jpg │ ├── fav.png │ ├── hero.jpg │ ├── logo.png │ └── members │ │ ├── drummer.jpg │ │ ├── guitarist.jpg │ │ └── singer.jpg ├── js │ └── placeholder.txt └── readme.md ├── templates ├── account │ ├── forgot_password.pt │ ├── index.pt │ ├── register.pt │ ├── reset_password.pt │ ├── reset_sent.pt │ └── signin.pt ├── cms │ └── page.pt ├── home │ ├── about.pt │ ├── bookus.pt │ ├── contact.pt │ ├── image_credits.pt │ ├── index.pt │ └── not_implemented.pt ├── newsletter │ ├── failed.pt │ └── subscribed.pt ├── readme.md └── shared │ └── _layout.pt ├── tests.py └── viewmodels ├── forgotpassword_viewmodel.py ├── readme.md ├── register_viewmodel.py ├── resetpassword_viewmodel.py ├── signin_viewmodel.py └── viewmodelbase.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | .DS_Store 91 | .idea 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Kennedy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Talk Python's Pyramid Web App Starter 2 | 3 | An opinionated **_Cookiecutter_** template for creating Pyramid web applications starting way further down the development chain. This cookiecutter template will create a new Pyramid web application with the following benefits and more: 4 | 5 | * **Factored and organized**: This code was generalized out of a large, professional web application 6 | * **Master layout template**: Comes pre-configured with a master layout template. Navigation, CSS, JS, etc is factored into a single template file and is reused across all views 7 | * **Chameleon language**: This template uses the chameleon template language (the cleanest template language for Python - we did say opinionated right?) 8 | * **Pyramid Handlers**: Code is factored into handler / controller classes. You have the full power of object-oriented programming immediately available 9 | * **Secure user management**: The app comes with full user management. Users can register, log in and out, and reset passwords. We use the [passlib](https://passlib.readthedocs.io/en/stable/) package for secure user storage using best practices 10 | **SQLAlchemy data access**: Comes with [SQLAlchemy ORM](https://www.sqlalchemy.org/) preconfigured using **sqlite** 11 | * **Bootstrap and modern design**: As you can see from the screenshot below, the app comes with [bootstrap](https://getbootstrap.com/) and [fontawesome](http://fontawesome.io/). It uses a free, [open-source theme](https://startbootstrap.com/template-overviews/landing-page/) and images used under [CC-Attribution](https://creativecommons.org/licenses/by-sa/2.0/). 12 | * **Logging with [LogBook](https://logbook.readthedocs.io/en/stable/)**: A logging system for Python that replaces the standard library’s logging module. It was designed with both complex and simple applications in mind and the idea to make logging fun 13 | * **Runtime error monitoring with [Rollbar](https://rollbar.com)**: Rollbar adds runtime notifications and detailed error tracking and it comes pre-configured in this template 14 | * **Mailing list integration**: Comes with [Mailchimp](https://mailchimp.com/) integration. Just enter your API key and list ID to start collecting and managing users for your mailing list 15 | * **Outbound email with templates**: The app has a set of static HTML files with placeholders that are loaded by the outbound email system and populated with user data 16 | * **Bower static resource management**: Most templates are based on out-of-date files (css templates, js, etc.). This template uses [bower](https://bower.io/) for it's static files. This means a single CLI command will get you the latest everything. 17 | * **Fast pages that are never stale**: Every static resource is referenced with our own cache busting system. This means you can use extremely aggressive caching for performance on static files yet they immediately invalidate upon changes 18 | * **Built-in CMS**: The site supports loading landing pages and other static content from the database while still supporting the common look and feel. 19 | * **Comes with an entire online course**: This template is built from the final project in [Python for Entrepreneurs](https://training.talkpython.fm/courses/explore_entrepreneurs/python-for-entrepreneurs-build-and-launch-your-online-business), a 20 hour course on building professional web apps in Python and Pyramid from [Talk Python Training](https://training.talkpython.fm/) 20 | 21 | ## What you get 22 | 23 | Here's a screenshot of the home page. You also get much more as you navigate other sections of the web app. 24 | 25 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/readme_resources/app-screenshot.png) 26 | 27 | Here's the project folder structure: 28 | 29 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/readme_resources/project-structure.png) 30 | 31 | ## Usage 32 | 33 | To use this template you just run the single command: 34 | 35 | ``` 36 | cookiecutter https://github.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter 37 | ``` 38 | 39 | Answer the cookiecutter prompts (notice how smart defaults are suggested anywhere possible): 40 | 41 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/readme_resources/template-execution-trimmed.png) 42 | 43 | *[Note: Once you have run cookiecutter with the full URL, it is cached locally and can be run with just the template name.]* 44 | 45 | You will need to install Cookiecutter first: 46 | 47 | ``` 48 | pip3 install --user cookiecutter 49 | ``` 50 | 51 | (Use pip command for Python 3 on Windows) 52 | 53 | ## Compare to Pyramid's standard templates 54 | 55 | The Pyramid web framework offers a handful of nice starter templates. They have recently moved to [Cookiecutter](https://cookiecutter.readthedocs.io) as the primary way to [scaffold new Pyramid web applications](http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/project.html#pyramid-cookiecutters). 56 | 57 | There are a few existing templates but they are built to be bare bones and less opinionated so they are broadly applicable. They are: 58 | 59 | `pyramid-cookiecutter-starter` 60 | 61 | URL dispatch for routing and either Jinja2, Chameleon, or Mako for templating 62 | 63 | `pyramid-cookiecutter-alchemy` 64 | 65 | SQLite for persistent storage, SQLAlchemy for an ORM, URL dispatch for routing, and Jinja2 for templating. 66 | 67 | `pyramid-cookiecutter-zodb` 68 | 69 | ZODB for persistent storage, traversal for routing, and Chameleon for templating 70 | 71 | Pyramid cookiecutters released under the Pylons Project differ from each other on a number of axes: 72 | 73 | * the persistence mechanism they offer (no persistence mechanism, SQLAlchemy with SQLite, or ZODB) 74 | * the mechanism they use to map URLs to code (URL dispatch or traversal) 75 | * templating libraries (Jinja2, Chameleon, or Mako) 76 | 77 | ## Want to go deeper? Take the course 78 | 79 | [![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/readme_resources/course.png)](https://training.talkpython.fm/courses/explore_entrepreneurs/python-for-entrepreneurs-build-and-launch-your-online-business) 80 | 81 | If this web app is interesting to you and you'd like to dig way deeper into all of these topics, we have a 20-hour online course that goes into detail in each area of the app and more. 82 | 83 | [Visit the course page](https://training.talkpython.fm/courses/explore_entrepreneurs/python-for-entrepreneurs-build-and-launch-your-online-business) 84 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "required", 3 | "project_slug": "{{cookiecutter.project_name.lower().strip().replace(' ', '_').replace(':', '_').replace('-', '_').replace('!', '_')}}", 4 | "contact_name": "Your company or your name", 5 | "domain_name": "yourcompany.com", 6 | "contact_email": "contact@{{cookiecutter.domain_name.lower().strip()}}", 7 | "description": "", 8 | "integrations": "The following integrations are optional [enter to continue]", 9 | "mailchimp_api": "", 10 | "mailchimp_list_id": "", 11 | "outbound_smtp_username": "", 12 | "outbound_smtp_password": "", 13 | "outbound_smtp_server": "", 14 | "outbound_smtp_port": "587", 15 | "rollbar_access_token": "" 16 | } 17 | 18 | -------------------------------------------------------------------------------- /image credits.md: -------------------------------------------------------------------------------- 1 | # Image credits 2 | 3 | Thank you to all of the artists who made their work available via [flickr](https://www.flickr.com/). 4 | 5 | I have used the following images under the creative commons commercial license with attribution: 6 | 7 | **[Attribution-ShareAlike 2.0 Generic (CC BY-SA 2.0)](https://creativecommons.org/licenses/by-sa/2.0/)** 8 | 9 | The following list of images fulfills the attribution section of the license. Thank you to each artist / photographer. 10 | 11 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/static/img/hero.jpg) 12 | 13 | Photographer: The Zender Agenda 14 | Alterations: cropped 15 | 16 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/static/img/event.jpg) 17 | 18 | Photographer: Jason Kasper 19 | Alterations: cropped 20 | 21 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/static/img/book-us.jpg) 22 | 23 | Photographer: Photo Cindy 24 | Alterations: cropped 25 | 26 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/static/img/members/drummer.jpg) 27 | 28 | Photographer: James Mackintosh 29 | Alterations: cropped 30 | 31 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/static/img/members/guitarist.jpg) 32 | 33 | Photographer: Feast of Music 34 | Alterations: cropped 35 | 36 | ![](https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/static/img/members/singer.jpg) 37 | 38 | Photographer: Vincent van der Heijden 39 | Alterations: cropped -------------------------------------------------------------------------------- /readme_resources/app-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/readme_resources/app-screenshot.png -------------------------------------------------------------------------------- /readme_resources/course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/readme_resources/course.png -------------------------------------------------------------------------------- /readme_resources/project-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/readme_resources/project-structure.png -------------------------------------------------------------------------------- /readme_resources/python-for-entrepreneurs-c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/readme_resources/python-for-entrepreneurs-c.png -------------------------------------------------------------------------------- /readme_resources/template-execution-trimmed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/readme_resources/template-execution-trimmed.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = {{cookiecutter.project_slug}} 3 | omit = {{cookiecutter.project_slug}}/test* 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/CHANGES.txt: -------------------------------------------------------------------------------- 1 | 0.0 2 | --- 3 | 4 | - Initial version 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.ini *.cfg *.rst 2 | recursive-include {{cookiecutter.project_slug}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml 3 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/README.txt: -------------------------------------------------------------------------------- 1 | {{cookiecutter.project_slug}} README 2 | ================== 3 | 4 | Getting Started 5 | --------------- 6 | 7 | - cd 8 | 9 | - $VENV/bin/pip install -e . 10 | 11 | - $VENV/bin/pserve development.ini 12 | 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/development.ini: -------------------------------------------------------------------------------- 1 | ### 2 | # app configuration 3 | # http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html 4 | ### 5 | 6 | # [app:main] 7 | [pipeline:main] 8 | pipeline = 9 | rollbar 10 | {{cookiecutter.project_slug}} 11 | 12 | [app:{{cookiecutter.project_slug}}] 13 | use = egg:{{cookiecutter.project_slug}} 14 | 15 | pyramid.reload_templates = true 16 | pyramid.debug_authorization = false 17 | pyramid.debug_notfound = false 18 | pyramid.debug_routematch = false 19 | pyramid.default_locale_name = en 20 | pyramid.includes = pyramid_debugtoolbar 21 | # 22 | # Development mode (dev or prod) 23 | # 24 | mode = dev 25 | # 26 | # Mailchimp settings 27 | # 28 | mailchimp_api = {{cookiecutter.mailchimp_api}} 29 | mailchimp_list_id = {{cookiecutter.mailchimp_list_id}} 30 | # 31 | # Mail server settings 32 | # 33 | smtp_username = {{cookiecutter.outbound_smtp_username}} 34 | smtp_password = {{cookiecutter.outbound_smtp_password}} 35 | smtp_server = {{cookiecutter.outbound_smtp_server}} 36 | smtp_port = {{cookiecutter.outbound_smtp_port}} 37 | 38 | # 39 | # Logging settings 40 | # 41 | log_level = INFO 42 | log_filename = 43 | # 44 | # Rollbar settings 45 | # 46 | rollbar.access_token = {{cookiecutter.rollbar_access_token}} 47 | rollbar.environment = dev 48 | rollbar.branch = master 49 | rollbar.root = %(here)s 50 | 51 | 52 | 53 | [filter:rollbar] 54 | use = egg:rollbar#pyramid 55 | access_token = {{cookiecutter.rollbar_access_token}} 56 | environment = dev 57 | branch = master 58 | root = %(here)s 59 | 60 | 61 | # By default, the toolbar only appears for clients from IP addresses 62 | # '127.0.0.1' and '::1'. 63 | # debugtoolbar.hosts = 127.0.0.1 ::1 64 | 65 | ### 66 | # wsgi server configuration 67 | ### 68 | 69 | [server:main] 70 | use = egg:waitress#main 71 | host = 127.0.0.1 72 | port = 6544 73 | 74 | ### 75 | # logging configuration 76 | # http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html 77 | ### 78 | 79 | [loggers] 80 | keys = root, {{cookiecutter.project_slug}} 81 | 82 | [handlers] 83 | keys = console 84 | 85 | [formatters] 86 | keys = generic 87 | 88 | [logger_root] 89 | level = INFO 90 | handlers = console 91 | 92 | [logger_{{cookiecutter.project_slug}}] 93 | level = DEBUG 94 | handlers = 95 | qualname = {{cookiecutter.project_slug}} 96 | 97 | [handler_console] 98 | class = StreamHandler 99 | args = (sys.stderr,) 100 | level = NOTSET 101 | formatter = generic 102 | 103 | [formatter_generic] 104 | format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s 105 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/production.ini: -------------------------------------------------------------------------------- 1 | ### 2 | # app configuration 3 | # http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/environment.html 4 | ### 5 | 6 | [pipeline:main] 7 | pipeline = 8 | rollbar 9 | {{cookiecutter.project_slug}} 10 | 11 | [app:{{cookiecutter.project_slug}}] 12 | use = egg:{{cookiecutter.project_slug}} 13 | 14 | pyramid.reload_templates = false 15 | pyramid.debug_authorization = false 16 | pyramid.debug_notfound = false 17 | pyramid.debug_routematch = false 18 | pyramid.default_locale_name = en 19 | # 20 | # Development mode (dev or prod) 21 | # 22 | mode = prod 23 | # 24 | # Mailchimp settings 25 | # 26 | mailchimp_api = {{cookiecutter.mailchimp_api}} 27 | mailchimp_list_id = {{cookiecutter.mailchimp_list_id}} 28 | # 29 | # Mail server settings 30 | # 31 | smtp_username = {{cookiecutter.outbound_smtp_username}} 32 | smtp_password = {{cookiecutter.outbound_smtp_password}} 33 | smtp_server = {{cookiecutter.outbound_smtp_server}} 34 | smtp_port = {{cookiecutter.outbound_smtp_port}} 35 | 36 | # 37 | # Logging settings 38 | # 39 | log_level = NOTICE 40 | log_filename = ./app_log.txt 41 | # 42 | # Rollbar settings 43 | # 44 | rollbar.access_token = {{cookiecutter.rollbar_access_token}} 45 | rollbar.environment = production 46 | rollbar.branch = master 47 | rollbar.root = %(here)s 48 | 49 | 50 | 51 | 52 | [filter:rollbar] 53 | use = egg:rollbar#pyramid 54 | access_token = {{cookiecutter.rollbar_access_token}} 55 | environment = production 56 | branch = master 57 | root = %(here)s 58 | 59 | 60 | 61 | 62 | ### 63 | # wsgi server configuration 64 | ### 65 | 66 | [server:main] 67 | use = egg:waitress#main 68 | host = 0.0.0.0 69 | port = 6544 70 | 71 | ### 72 | # logging configuration 73 | # http://docs.pylonsproject.org/projects/pyramid/en/1.7-branch/narr/logging.html 74 | ### 75 | 76 | [loggers] 77 | keys = root, {{cookiecutter.project_slug}} 78 | 79 | [handlers] 80 | keys = console 81 | 82 | [formatters] 83 | keys = generic 84 | 85 | [logger_root] 86 | level = WARN 87 | handlers = console 88 | 89 | [logger_{{cookiecutter.project_slug}}] 90 | level = WARN 91 | handlers = 92 | qualname = {{cookiecutter.project_slug}} 93 | 94 | [handler_console] 95 | class = StreamHandler 96 | args = (sys.stderr,) 97 | level = NOTSET 98 | formatter = generic 99 | 100 | [formatter_generic] 101 | format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s 102 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = {{cookiecutter.project_slug}} 3 | python_files = *.py 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # core dependencies 3 | # 4 | -r requirements.txt 5 | 6 | 7 | ###################################### 8 | # primary dependencies 9 | # 10 | WebTest >= 1.3.1 11 | waitress 12 | pyramid-debugtoolbar 13 | pipdeptree 14 | nose 15 | pytest 16 | pytest-cov 17 | 18 | 19 | ###################################### 20 | # secondary dependencies 21 | # 22 | Mako 23 | MarkupSafe 24 | Pygments 25 | beautifulsoup4 26 | coverage 27 | nose 28 | pipdeptree 29 | py 30 | pyramid_mako 31 | 32 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/requirements.txt: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # primary dependencies 3 | # 4 | # 5 | html2text 6 | logbook 7 | mailchimp 8 | mailer 9 | passlib 10 | pyramid 11 | pyramid_chameleon 12 | pyramid_handlers 13 | rollbar 14 | sqlalchemy 15 | 16 | 17 | ###################################### 18 | # secondary dependencies 19 | # 20 | # 21 | Chameleon 22 | PasteDeploy 23 | WebOb 24 | docopt==0.4.0 25 | hupper 26 | repoze.lru 27 | requests 28 | six 29 | translationstring 30 | venusian 31 | zope.deprecation 32 | zope.interface 33 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | here = os.path.abspath(os.path.dirname(__file__)) 6 | with open(os.path.join(here, 'README.txt')) as f: 7 | README = f.read() 8 | with open(os.path.join(here, 'CHANGES.txt')) as f: 9 | CHANGES = f.read() 10 | 11 | requires = [ 12 | 'html2text', 13 | 'logbook', 14 | 'mailchimp', 15 | 'mailer', 16 | 'passlib', 17 | 'pyramid', 18 | 'pyramid_chameleon', 19 | # 'pyramid_debugtoolbar', # installed via pip3 install -r requirements-dev.txt 20 | 'pyramid_handlers', 21 | 'rollbar', 22 | 'sqlalchemy', 23 | # 'waitress', # installed via pip3 install -r requirements-dev.txt 24 | ] 25 | 26 | tests_require = [ 27 | 'WebTest >= 1.3.1', # py3 compatibility 28 | 'pytest', # includes virtualenv 29 | 'pytest-cov', 30 | ] 31 | 32 | setup(name='{{cookiecutter.project_slug}}', 33 | version='0.0', 34 | description='Site with CMS', 35 | long_description=README + '\n\n' + CHANGES, 36 | classifiers=[ 37 | "Programming Language :: Python", 38 | "Framework :: Pyramid", 39 | "Topic :: Internet :: WWW/HTTP", 40 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 41 | ], 42 | author='Talk Python', 43 | author_email='contact@talkpython.fm', 44 | url='', 45 | keywords='web pyramid pylons', 46 | packages=find_packages(), 47 | include_package_data=True, 48 | zip_safe=False, 49 | extras_require={ 50 | 'testing': tests_require, 51 | }, 52 | install_requires=requires, 53 | entry_points="""\ 54 | [paste.app_factory] 55 | main = {{cookiecutter.project_slug}}:main 56 | """, 57 | ) 58 | 59 | print() 60 | print() 61 | print('**************************************************************') 62 | print('*') 63 | print("* Be sure to run 'pip3 install -r requirements-dev.txt'") 64 | print("* if this is your dev install. Otherwise the dev server") 65 | print("* and debug toolbar will be absent.") 66 | print('*') 67 | print('**************************************************************') 68 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pkg_resources 3 | import os 4 | import sys 5 | from pyramid.config import Configurator 6 | # noinspection PyUnresolvedReferences 7 | import {{cookiecutter.project_slug}} 8 | import {{cookiecutter.project_slug}}.controllers.home_controller as home 9 | import {{cookiecutter.project_slug}}.controllers.account_controller as account 10 | import {{cookiecutter.project_slug}}.controllers.newsletter_controller as news 11 | import {{cookiecutter.project_slug}}.controllers.cms_controller as cms 12 | from {{cookiecutter.project_slug}}.data.dbsession import DbSessionFactory 13 | from {{cookiecutter.project_slug}}.email.template_paser import EmailTemplateParser 14 | from {{cookiecutter.project_slug}}.services.email_service import EmailService 15 | from {{cookiecutter.project_slug}}.services.cms_service import CmsService 16 | from {{cookiecutter.project_slug}}.services.log_service import LogService 17 | from {{cookiecutter.project_slug}}.services.mailinglist_service import MailingListService 18 | 19 | dev_mode = False 20 | 21 | 22 | def main(_, **settings): 23 | config = Configurator(settings=settings) 24 | 25 | init_logging(config) # log setup must run first 26 | init_mode(config) # mode must go next 27 | init_includes(config) # includes must go next 28 | init_routing(config) # it's pretty much flexible from here on down 29 | init_db(config) 30 | init_mailing_list(config) 31 | init_smtp_mail(config) 32 | init_email_templates(config) 33 | 34 | return config.make_wsgi_app() 35 | 36 | 37 | def init_logging(config): 38 | settings = config.get_settings() 39 | log_level = settings.get('log_level') 40 | log_filename = settings.get('log_filename') 41 | 42 | LogService.global_init(log_level, log_filename) 43 | 44 | log_package_versions() 45 | 46 | 47 | def init_email_templates(_): 48 | EmailTemplateParser.global_init() 49 | 50 | 51 | def init_smtp_mail(config): 52 | global dev_mode 53 | unset = 'YOUR_VALUE' 54 | 55 | settings = config.get_settings() 56 | smtp_username = settings.get('smtp_username') 57 | smtp_password = settings.get('smtp_password') 58 | smtp_server = settings.get('smtp_server') 59 | smtp_port = settings.get('smtp_port') 60 | 61 | local_dev_mode = dev_mode 62 | 63 | if smtp_username == unset: 64 | log = LogService.get_startup_log() 65 | log.warn("SMTP server values not set in config file. " 66 | "Outbound email will not work.") 67 | local_dev_mode = True # turn off email if the system has no server. 68 | 69 | EmailService.global_init(smtp_username, smtp_password, smtp_server, smtp_port, local_dev_mode) 70 | 71 | 72 | def init_db(_): 73 | global dev_mode 74 | 75 | top_folder = os.path.dirname({{cookiecutter.project_slug}}.__file__) 76 | rel_file = os.path.join('db', '{{cookiecutter.project_slug}}.sqlite') 77 | if dev_mode: 78 | rel_file = rel_file.replace('.sqlite', '_dev.sqlite') 79 | else: 80 | rel_file = rel_file.replace('.sqlite', '_prod.sqlite') 81 | 82 | db_file = os.path.join(top_folder, rel_file) 83 | DbSessionFactory.global_init(db_file) 84 | 85 | CmsService.init_test_data() 86 | 87 | 88 | def init_mode(config): 89 | global dev_mode 90 | settings = config.get_settings() 91 | dev_mode = settings.get('mode') == 'dev' 92 | log = LogService.get_startup_log() 93 | log.notice('Running in {} mode.'.format('dev' if dev_mode else 'prod')) 94 | 95 | 96 | def init_mailing_list(config): 97 | unset = 'ADD_YOUR_API_KEY' 98 | 99 | settings = config.get_settings() 100 | mailchimp_api = settings.get('mailchimp_api') 101 | mailchimp_list_id = settings.get('mailchimp_list_id') 102 | 103 | if mailchimp_api == unset: 104 | log = LogService.get_startup_log() 105 | log.warn("Mailchimp API values not set in config file. " 106 | "Mailing list subscriptions will not work.") 107 | 108 | MailingListService.global_init(mailchimp_api, mailchimp_list_id) 109 | 110 | 111 | def init_routing(config): 112 | config.add_static_view('static', 'static', cache_max_age=3600) 113 | 114 | config.add_handler('root', '/', handler=home.HomeController, action='index') 115 | 116 | add_controller_routes(config, home.HomeController, 'home') 117 | add_controller_routes(config, account.AccountController, 'account') 118 | add_controller_routes(config, news.NewsletterController, 'newsletter') 119 | 120 | # Add a CMS 'catch-all' route. See CmsController / CmsService for more info. 121 | # This entry must go at the very end of all routes 122 | config.add_handler('cms_route', '/*sub_path', handler=cms.CMSController, action='page') 123 | 124 | config.scan() 125 | 126 | 127 | def add_controller_routes(config, ctrl, prefix): 128 | config.add_handler(prefix + 'ctrl_index', '/' + prefix, handler=ctrl, action='index') 129 | config.add_handler(prefix + 'ctrl_index/', '/' + prefix + '/', handler=ctrl, action='index') 130 | config.add_handler(prefix + 'ctrl', '/' + prefix + '/{action}', handler=ctrl) 131 | config.add_handler(prefix + 'ctrl/', '/' + prefix + '/{action}/', handler=ctrl) 132 | config.add_handler(prefix + 'ctrl_id', '/' + prefix + '/{action}/{id}', handler=ctrl) 133 | 134 | 135 | def init_includes(config): 136 | config.include('pyramid_chameleon') 137 | config.include('pyramid_handlers') 138 | # config.include('rollbar.contrib.pyramid') 139 | 140 | 141 | def log_package_versions(): 142 | startup_log = LogService.get_startup_log() 143 | 144 | # TODO: UPDATE WITH OUR DEPENDENCIES 145 | 146 | # update from setup.py when changed! 147 | # This list is the closure of all dependencies, 148 | # taken from: pip list --format json 149 | requires = [{"name": "{{cookiecutter.project_slug}}", "version": "0.0"}, {"name": "appdirs", "version": "1.4.3"}, 150 | {"name": "Chameleon", "version": "3.1"}, {"name": "docopt", "version": "0.4.0"}, 151 | {"name": "html2text", "version": "2016.9.19"}, {"name": "hupper", "version": "0.4.4"}, 152 | {"name": "Logbook", "version": "1.0.0"}, {"name": "mailchimp", "version": "2.0.9"}, 153 | {"name": "mailer", "version": "0.8.1"}, {"name": "Mako", "version": "1.0.6"}, 154 | {"name": "MarkupSafe", "version": "1.0"}, {"name": "packaging", "version": "16.8"}, 155 | {"name": "passlib", "version": "1.7.1"}, {"name": "PasteDeploy", "version": "1.5.2"}, 156 | {"name": "pip", "version": "9.0.1"}, {"name": "Pygments", "version": "2.2.0"}, 157 | {"name": "pyparsing", "version": "2.2.0"}, {"name": "pyramid", "version": "1.8.3"}, 158 | {"name": "pyramid-chameleon", "version": "0.3"}, {"name": "pyramid-debugtoolbar", "version": "3.0.5"}, 159 | {"name": "pyramid-handlers", "version": "0.5"}, {"name": "pyramid-mako", "version": "1.0.2"}, 160 | {"name": "repoze.lru", "version": "0.6"}, {"name": "requests", "version": "2.13.0"}, 161 | {"name": "rollbar", "version": "0.13.11"}, {"name": "setuptools", "version": "34.3.2"}, 162 | {"name": "six", "version": "1.10.0"}, {"name": "SQLAlchemy", "version": "1.1.6"}, 163 | {"name": "translationstring", "version": "1.3"}, {"name": "venusian", "version": "1.0"}, 164 | {"name": "waitress", "version": "1.0.2"}, {"name": "WebOb", "version": "1.7.2"}, 165 | {"name": "zope.deprecation", "version": "4.2.0"}, {"name": "zope.interface", "version": "4.3.3"}] 166 | 167 | requires.sort(key=lambda d: d['name'].lower()) 168 | t0 = datetime.datetime.now() 169 | startup_log.notice('---------- Python version info ------------------') 170 | startup_log.notice(sys.version.replace('\n', ' ').replace(' ', ' ')) 171 | startup_log.notice('---------- package version info ------------------') 172 | for rec in requires: 173 | try: 174 | version = pkg_resources.get_distribution(rec['name']).version 175 | if version: 176 | startup_log.notice('{} v{}'.format(rec['name'], version)) 177 | else: 178 | startup_log.notice("WHERE IS IT? {}.".format(rec['name'])) 179 | except Exception as x: 180 | startup_log.notice('{} UNKNOWN VERSION ({})'.format(rec['name'], x)) 181 | 182 | dt = datetime.datetime.now() - t0 183 | 184 | startup_log.notice('Package info gathered in {} sec'.format(dt.total_seconds())) 185 | startup_log.notice('--------------------------------------------------') 186 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/bin/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/bin 2 | 3 | The bin folder is where you add your Python scripts that manage your web application. 4 | Do you need to migrate or cleanup data? How about a `{{cookiecutter.project_slug}}/bin/import_data.py` script? 5 | Need a backup script? Maybe a `{{cookiecutter.project_slug}}/bin/backup_to_zip.py` script is handy. 6 | 7 | Hope you get the idea. That's the goal with `bin`. Just keep in mind you're running inside 8 | a package named `{{cookiecutter.project_slug}}` so you'll need fully qualify any imports from your project in those scripts. 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/controllers/account_controller.py: -------------------------------------------------------------------------------- 1 | import pyramid_handlers 2 | from {{cookiecutter.project_slug}}.controllers.base_controller import BaseController 3 | from {{cookiecutter.project_slug}}.services.account_service import AccountService 4 | from {{cookiecutter.project_slug}}.services.email_service import EmailService 5 | from {{cookiecutter.project_slug}}.viewmodels.forgotpassword_viewmodel import ForgotPasswordViewModel 6 | from {{cookiecutter.project_slug}}.viewmodels.register_viewmodel import RegisterViewModel 7 | from {{cookiecutter.project_slug}}.viewmodels.resetpassword_viewmodel import ResetPasswordViewModel 8 | from {{cookiecutter.project_slug}}.viewmodels.signin_viewmodel import SigninViewModel 9 | import {{cookiecutter.project_slug}}.infrastructure.cookie_auth as cookie_auth 10 | 11 | 12 | class AccountController(BaseController): 13 | @pyramid_handlers.action(renderer='templates/account/index.pt') 14 | def index(self): 15 | if not self.logged_in_user_id: 16 | print("Cannot view account page, must login") 17 | self.redirect('/account/signin') 18 | 19 | return {} 20 | 21 | @pyramid_handlers.action(renderer='templates/account/signin.pt', 22 | request_method='GET', 23 | name='signin') 24 | def signin_get(self): 25 | return SigninViewModel().to_dict() 26 | 27 | @pyramid_handlers.action(renderer='templates/account/signin.pt', 28 | request_method='POST', 29 | name='signin') 30 | def signin_post(self): 31 | vm = SigninViewModel() 32 | vm.from_dict(self.merged_dicts) 33 | 34 | account = AccountService.get_authenticated_account(vm.email, vm.password) 35 | if not account: 36 | vm.error = "Email address or password are incorrect." 37 | self.log.notice("Failed login attempt: " + vm.error) 38 | return vm.to_dict() 39 | 40 | cookie_auth.set_auth(self.request, account.id) 41 | self.log.notice("User successfully logged in: " + vm.email) 42 | 43 | return self.redirect('/account') 44 | 45 | @pyramid_handlers.action() 46 | def logout(self): 47 | cookie_auth.logout(self.request) 48 | self.redirect('/') 49 | 50 | @pyramid_handlers.action(renderer='templates/account/register.pt', 51 | request_method='GET', 52 | name='register') 53 | def register_get(self): 54 | vm = RegisterViewModel() 55 | return vm.to_dict() 56 | 57 | @pyramid_handlers.action(renderer='templates/account/register.pt', 58 | request_method='POST', 59 | name='register') 60 | def register_post(self): 61 | vm = RegisterViewModel() 62 | vm.from_dict(self.request.POST) 63 | 64 | vm.validate() 65 | if vm.error: 66 | return vm.to_dict() 67 | 68 | account = AccountService.find_account_by_email(vm.email) 69 | if account: 70 | vm.error = "An account with this email already exists. " \ 71 | "Please log in instead." 72 | return vm.to_dict() 73 | 74 | account = AccountService.create_account(vm.email, vm.password) 75 | print("Registered new user: " + account.email) 76 | cookie_auth.set_auth(self.request, account.id) 77 | 78 | # send welcome email 79 | EmailService.send_welcome_email(account.email) 80 | 81 | # redirect 82 | print("Redirecting to account index page...") 83 | self.redirect('/account') 84 | 85 | # Form to generate reset code, trigger email (get) 86 | @pyramid_handlers.action(renderer='templates/account/forgot_password.pt', 87 | request_method='GET', 88 | name='forgot_password') 89 | def forgot_password_get(self): 90 | vm = ForgotPasswordViewModel() 91 | return vm.to_dict() 92 | 93 | # Form to generate reset code, trigger email (post) 94 | @pyramid_handlers.action(renderer='templates/account/forgot_password.pt', 95 | request_method='POST', 96 | name='forgot_password') 97 | def forgot_password_post(self): 98 | vm = ForgotPasswordViewModel() 99 | vm.from_dict(self.merged_dicts) 100 | 101 | vm.validate() 102 | if vm.error: 103 | return vm.to_dict() 104 | 105 | reset = AccountService.create_reset_code(vm.email) 106 | if not reset: 107 | vm.error = 'Cannot find the account with that email.' 108 | return vm.to_dict() 109 | 110 | EmailService.send_password_reset_email(vm.email, reset.id) 111 | print("Would email the code {} to {}".format( 112 | reset.id, vm.email 113 | )) 114 | 115 | self.redirect('/account/reset_sent') 116 | 117 | # Form to actually enter the new password based on reset code (get) 118 | @pyramid_handlers.action(renderer='templates/account/reset_password.pt', 119 | request_method='GET', 120 | name='reset_password') 121 | def reset_password_get(self): 122 | vm = ResetPasswordViewModel() 123 | vm.from_dict(self.merged_dicts) 124 | vm.validate() 125 | return vm.to_dict() 126 | 127 | # Form to actually enter the new password based on reset code (post) 128 | @pyramid_handlers.action(renderer='templates/account/reset_password.pt', 129 | request_method='POST', 130 | name='reset_password') 131 | def reset_password_post(self): 132 | vm = ResetPasswordViewModel() 133 | vm.from_dict(self.merged_dicts) 134 | vm.is_get = False 135 | 136 | vm.validate() 137 | if vm.error_msg: 138 | return vm.to_dict() 139 | 140 | AccountService.use_reset_code(vm.reset_code, self.request.remote_addr) 141 | account = AccountService.find_account_by_id(vm.reset.user_id) 142 | AccountService.set_password(vm.password, account.id) 143 | 144 | vm.message = 'Your password has been reset, please login.' 145 | return vm.to_dict() 146 | 147 | # A reset has been sent via email 148 | @pyramid_handlers.action(renderer='templates/account/reset_sent.pt') 149 | def reset_sent(self): 150 | return {} 151 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/controllers/base_controller.py: -------------------------------------------------------------------------------- 1 | import logbook 2 | 3 | import {{cookiecutter.project_slug}}.infrastructure.static_cache as static_cache 4 | import pyramid.httpexceptions as exc 5 | 6 | import {{cookiecutter.project_slug}}.infrastructure.cookie_auth as cookie_auth 7 | from {{cookiecutter.project_slug}}.services.account_service import AccountService 8 | 9 | 10 | class BaseController: 11 | __autoexpose__ = None 12 | 13 | def __init__(self, request): 14 | self.request = request 15 | self.build_cache_id = static_cache.build_cache_id 16 | 17 | log_name = 'Ctrls/' + type(self).__name__.replace("Controller", "") 18 | self.log = logbook.Logger(log_name) 19 | 20 | @property 21 | def is_logged_in(self): 22 | return cookie_auth.get_user_id_via_auth_cookie(self.request) is not None 23 | 24 | # noinspection PyMethodMayBeStatic 25 | def redirect(self, to_url, permanent=False): 26 | if permanent: 27 | raise exc.HTTPMovedPermanently(to_url) 28 | raise exc.HTTPFound(to_url) 29 | 30 | @property 31 | def merged_dicts(self): 32 | data = dict() 33 | data.update(self.request.GET) 34 | data.update(self.request.POST) 35 | data.update(self.request.matchdict) 36 | 37 | return data 38 | 39 | @property 40 | def logged_in_user_id(self): 41 | user_id = cookie_auth.get_user_id_via_auth_cookie(self.request) 42 | return user_id 43 | 44 | @property 45 | def logged_in_user(self): 46 | uid = self.logged_in_user_id 47 | if not uid: 48 | return None 49 | 50 | return AccountService.find_account_by_id(uid) 51 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/controllers/cms_controller.py: -------------------------------------------------------------------------------- 1 | import pyramid_handlers 2 | from pyramid.response import Response 3 | 4 | from {{cookiecutter.project_slug}}.controllers.base_controller import BaseController 5 | from {{cookiecutter.project_slug}}.services.cms_service import CmsService 6 | 7 | 8 | class CMSController(BaseController): 9 | @pyramid_handlers.action(renderer='templates/cms/page.pt') 10 | def page(self): 11 | url = self.request.path_url.replace(self.request.host_url, '') 12 | page = CmsService.get_page_by_url(url) 13 | 14 | if not page: 15 | return Response(status=404, body="Not found.") 16 | 17 | if page.is_redirect: 18 | return self.redirect(page.redirect_url) 19 | 20 | return {'html': page.html} 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/controllers/home_controller.py: -------------------------------------------------------------------------------- 1 | import pyramid_handlers 2 | from {{cookiecutter.project_slug}}.controllers.base_controller import BaseController 3 | 4 | 5 | class HomeController(BaseController): 6 | alternate_mode = False 7 | 8 | @pyramid_handlers.action(renderer='templates/home/index.pt') 9 | def index(self): 10 | return {'value': 'HOME'} 11 | 12 | @pyramid_handlers.action(renderer='templates/home/about.pt') 13 | def about(self): 14 | return {'value': 'ABOUT'} 15 | 16 | @pyramid_handlers.action(renderer='templates/home/bookus.pt') 17 | def bookus(self): 18 | return {} 19 | 20 | @pyramid_handlers.action(renderer='templates/home/contact.pt') 21 | def contact(self): 22 | return {'value': 'CONTACT'} 23 | 24 | @pyramid_handlers.action(renderer='templates/home/not_implemented.pt') 25 | def not_implemented(self): 26 | return {} 27 | 28 | @pyramid_handlers.action(renderer='templates/home/image_credits.pt') 29 | def image_credits(self): 30 | return {} 31 | 32 | def alternate_row_style(self): 33 | alt = self.alternate_mode 34 | self.alternate_mode = not self.alternate_mode 35 | 36 | if alt: 37 | return "alternate" 38 | else: 39 | return "" 40 | 41 | 42 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/controllers/newsletter_controller.py: -------------------------------------------------------------------------------- 1 | import pyramid_handlers 2 | from {{cookiecutter.project_slug}}.controllers.base_controller import BaseController 3 | from {{cookiecutter.project_slug}}.services.mailinglist_service import MailingListService 4 | 5 | 6 | class NewsletterController(BaseController): 7 | # POST /newsletter/add_subscriber 8 | @pyramid_handlers.action(request_method='POST') 9 | def add_subscriber(self): 10 | email = self.merged_dicts.get('email') 11 | 12 | if MailingListService.add_subscriber(email): 13 | self.redirect('/newsletter/subscribed') 14 | 15 | self.redirect('/newsletter/failed') 16 | 17 | @pyramid_handlers.action(renderer='templates/newsletter/subscribed.pt') 18 | def subscribed(self): 19 | return {} 20 | 21 | @pyramid_handlers.action(renderer='templates/newsletter/failed.pt') 22 | def failed(self): 23 | error = None 24 | if not MailingListService.get_is_initialized(): 25 | error = 'Your mailing list service is not initialized (keys missing in *.ini files)' 26 | 27 | return { 28 | 'error': error 29 | } 30 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/controllers/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/controllers 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/data/account.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from sqlalchemy import Column, String, Boolean, DateTime 4 | 5 | from {{cookiecutter.project_slug}}.data.modelbase import SqlAlchemyBase 6 | 7 | 8 | class Account(SqlAlchemyBase): 9 | __tablename__ = 'Account' 10 | 11 | id = Column(String, primary_key=True, 12 | default=lambda: str(uuid.uuid4()).replace('-', '')) 13 | 14 | email = Column(String, index=True, unique=True, nullable=False) 15 | password_hash = Column(String) 16 | created = Column(DateTime, default=datetime.datetime.now) 17 | email_confirmed = Column(Boolean, nullable=False, default=False) 18 | is_super_user = Column(Boolean, nullable=False, default=False) 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/data/cms_page.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | 4 | import sqlalchemy 5 | 6 | from {{cookiecutter.project_slug}}.data.modelbase import SqlAlchemyBase 7 | 8 | 9 | class CmsPage(SqlAlchemyBase): 10 | __tablename__ = 'CMSPage' 11 | 12 | url = sqlalchemy.Column(sqlalchemy.String, primary_key=True) 13 | created_date = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now) 14 | html = sqlalchemy.Column(sqlalchemy.String) 15 | is_redirect = sqlalchemy.Column(sqlalchemy.Boolean, default=False) 16 | redirect_url = sqlalchemy.Column(sqlalchemy.String) 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/data/dbsession.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy 2 | import sqlalchemy.orm 3 | from {{cookiecutter.project_slug}}.data.modelbase import SqlAlchemyBase 4 | # noinspection PyUnresolvedReferences 5 | import {{cookiecutter.project_slug}}.data.account 6 | # noinspection PyUnresolvedReferences 7 | import {{cookiecutter.project_slug}}.data.passwordreset 8 | # noinspection PyUnresolvedReferences 9 | import {{cookiecutter.project_slug}}.data.cms_page 10 | 11 | 12 | class DbSessionFactory: 13 | factory = None 14 | 15 | @staticmethod 16 | def global_init(db_file): 17 | if DbSessionFactory.factory: 18 | return 19 | 20 | if not db_file or not db_file.strip(): 21 | raise Exception("You must specify a data file.") 22 | 23 | conn_str = 'sqlite:///' + db_file 24 | print("Connecting to db with conn string: {}".format(conn_str)) 25 | 26 | engine = sqlalchemy.create_engine(conn_str, echo=False) 27 | SqlAlchemyBase.metadata.create_all(engine) 28 | DbSessionFactory.factory = sqlalchemy.orm.sessionmaker(bind=engine) 29 | 30 | @staticmethod 31 | def create_session(): 32 | return DbSessionFactory.factory() 33 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/data/modelbase.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy.ext.declarative as dec 2 | 3 | SqlAlchemyBase = dec.declarative_base() 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/data/passwordreset.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | 4 | import sqlalchemy 5 | 6 | from {{cookiecutter.project_slug}}.data.modelbase import SqlAlchemyBase 7 | 8 | 9 | class PasswordReset(SqlAlchemyBase): 10 | __tablename__ = 'PasswordReset' 11 | 12 | id = sqlalchemy.Column(sqlalchemy.String, primary_key=True, 13 | default=lambda: str(uuid.uuid4()).replace('-', '')) 14 | created_date = sqlalchemy.Column(sqlalchemy.DateTime, default=datetime.datetime.now) 15 | used_date = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True) 16 | was_used = sqlalchemy.Column(sqlalchemy.Boolean, default=False) 17 | used_ip_address = sqlalchemy.Column(sqlalchemy.String, nullable=True) 18 | user_id = sqlalchemy.Column(sqlalchemy.String, sqlalchemy.ForeignKey('Account.id')) 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/data/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/data 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/db/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/db 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/email/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/email 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/email/template_paser.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class EmailTemplateParser: 5 | welcome = None 6 | reset_password = None 7 | 8 | @staticmethod 9 | def global_init(): 10 | templates = os.path.join( 11 | os.path.dirname(__file__), 12 | 'templates' 13 | ) 14 | welcome_file = os.path.join(templates, 'welcome.html') 15 | with open(welcome_file) as fin: 16 | EmailTemplateParser.welcome = fin.read() 17 | 18 | welcome_file = os.path.join(templates, 'password_reset.html') 19 | with open(welcome_file) as fin: 20 | EmailTemplateParser.reset_password = fin.read() 21 | 22 | @staticmethod 23 | def expand(template, data): 24 | 25 | final_text = template 26 | for k in data: 27 | key = '{' + k + '}' 28 | final_text = final_text.replace(key, data[k]) 29 | 30 | return final_text 31 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/email/templates/password_reset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{cookiecutter.project_name}}: Password reset 6 | 7 | 8 | 9 |
10 | 11 |
16 | 17 |
18 |
19 |
20 | So we heard that you might have forgotten your password at 21 | {{cookiecutter.project_name}}. Don't worry, you can use this link below to reset it.
22 |
23 |
24 | Reset your password 27 |
28 |
29 | If that link didn't work, just copy and paste this into your browser:
30 |
31 |
32 | http://localhost:6544/account/reset_password/{reset_code}
33 |
34 |
35 | If you didn't request this reset, you should be able to safely ignore it. 36 | If you have questions, just reply to get back to us. 37 |
38 |
39 |
40 |
41 | Thanks for using {{cookiecutter.project_name}},
42 | {{cookiecutter.contact_name}} 43 |
44 |
45 |
46 | 47 |
48 |
49 | 50 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/email/templates/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to {{cookiecutter.project_name}} 6 | 7 | 8 | 9 |
10 | 11 |
16 | 17 |
18 |
19 | 20 |
21 | Congratulations {email}, thank you for registering with our site! 22 |
23 |
24 |
25 |
26 | Visit the site 29 |
30 |
31 |
32 |
33 |
34 | Thanks for using {{cookiecutter.project_name}},
35 | {{cookiecutter.contact_name}} 36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/infrastructure/cookie_auth.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from datetime import timedelta 3 | 4 | auth_cookie_name = '{{cookiecutter.project_slug}}_user' 5 | 6 | 7 | def set_auth(request, user_id): 8 | hash_val = __hash_text(user_id) 9 | val = "{}:{}".format(user_id, hash_val) 10 | 11 | request.add_response_callback(lambda req, resp: __add_cookie_callback( 12 | req, resp, auth_cookie_name, val 13 | )) 14 | 15 | 16 | def __hash_text(text): 17 | text = 'saltiness_' + text + '_for_the_text' 18 | return hashlib.sha512(text.encode('utf-8')).hexdigest() 19 | 20 | 21 | def __add_cookie_callback(_, response, name, value): 22 | response.set_cookie(name, value, max_age=timedelta(days=30)) 23 | 24 | 25 | def get_user_id_via_auth_cookie(request): 26 | if auth_cookie_name not in request.cookies: 27 | return None 28 | 29 | val = request.cookies[auth_cookie_name] 30 | parts = val.split(':') 31 | if len(parts) != 2: 32 | return None 33 | 34 | user_id = parts[0] 35 | hash_val = parts[1] 36 | hash_val_check = __hash_text(user_id) 37 | if hash_val != hash_val_check: 38 | print("Warning: Hash mismatch, invalid cookie value") 39 | return None 40 | 41 | return user_id 42 | 43 | 44 | def logout(request): 45 | request.add_response_callback(lambda req, resp: __delete_cookie_callback( 46 | resp, auth_cookie_name 47 | )) 48 | 49 | 50 | def __delete_cookie_callback(response, name): 51 | response.delete_cookie(name) 52 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/infrastructure/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/infrastructure 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/infrastructure/static_cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use this cache utility to compute the hash of files server as static resources 3 | on your website. As written, it must be in your '/' home folder, but you can 4 | adjust the computation of *fullname* to allow you to put it in subfolders. 5 | 6 | Set recompute_caches_every_request based on use case / deployment: 7 | recompute_caches_every_request = False in production 8 | recompute_caches_every_request = True in development 9 | 10 | Use in your templates as: 11 | 12 | 15 | """ 16 | 17 | import hashlib 18 | import os 19 | import {{cookiecutter.project_slug}} 20 | 21 | __full_path = os.path.dirname(os.path.abspath({{cookiecutter.project_slug}}.__file__)) 22 | __hash_lookup = dict() 23 | 24 | # Set this to False in production, True in development 25 | recompute_caches_every_request = True 26 | enable_tracing = False 27 | 28 | 29 | def build_cache_id(relative_file_url: str): 30 | if not relative_file_url: 31 | return "ERROR_NO_FILE_SPECIFIED" 32 | 33 | # Can we use a precomputed version? 34 | use_hash = not recompute_caches_every_request 35 | key = relative_file_url 36 | 37 | if use_hash and key in __hash_lookup: 38 | __trace("Using cached lookup for {} -> {}".format(key, __hash_lookup[key])) 39 | return __hash_lookup[key] 40 | 41 | fullname = os.path.abspath(os.path.join( 42 | __full_path, relative_file_url.lstrip('/'))) 43 | 44 | if not os.path.exists(fullname): 45 | return "ERROR_MISSING_FILE" 46 | 47 | digest_value = __get_file_hash(fullname) 48 | __hash_lookup[key] = digest_value 49 | 50 | __trace("Computed digest for {} -> {}".format(key, __hash_lookup[key])) 51 | return digest_value 52 | 53 | 54 | def __get_file_hash(filename): 55 | md5 = hashlib.md5() 56 | 57 | with open(filename, 'rb') as fin: 58 | data = fin.read() 59 | md5.update(data) 60 | 61 | return md5.hexdigest() 62 | 63 | 64 | def __trace(text): 65 | # This is really for just seeing things in action. 66 | # You might want real logging... 67 | if not enable_tracing: 68 | return 69 | 70 | print(text) 71 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/services/account_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from passlib.handlers.sha2_crypt import sha512_crypt 4 | from {{cookiecutter.project_slug}}.data.account import Account 5 | from {{cookiecutter.project_slug}}.data.dbsession import DbSessionFactory 6 | from {{cookiecutter.project_slug}}.data.passwordreset import PasswordReset 7 | 8 | 9 | class AccountService: 10 | @staticmethod 11 | def create_account(email, plain_text_password): 12 | session = DbSessionFactory.create_session() 13 | 14 | account = Account() 15 | account.email = email 16 | account.password_hash = AccountService.hash_text(plain_text_password) 17 | 18 | session.add(account) 19 | session.commit() 20 | 21 | return account 22 | 23 | @classmethod 24 | def find_account_by_email(cls, email): 25 | 26 | if not email or not email.strip(): 27 | return None 28 | 29 | email = email.lower().strip() 30 | 31 | session = DbSessionFactory.create_session() 32 | 33 | account = session.query(Account) \ 34 | .filter(Account.email == email) \ 35 | .first() 36 | 37 | return account 38 | 39 | @staticmethod 40 | def hash_text(plain_text_password): 41 | hashed_text = sha512_crypt.encrypt(plain_text_password, rounds=200000) 42 | return hashed_text 43 | 44 | @classmethod 45 | def get_authenticated_account(cls, email, plain_text_password): 46 | account = AccountService.find_account_by_email(email) 47 | if not account: 48 | return None 49 | 50 | if not sha512_crypt.verify(plain_text_password, account.password_hash): 51 | return None 52 | 53 | return account 54 | 55 | @classmethod 56 | def find_account_by_id(cls, user_id): 57 | if not user_id: 58 | return None 59 | 60 | user_id = user_id.strip() 61 | 62 | session = DbSessionFactory.create_session() 63 | 64 | account = session.query(Account) \ 65 | .filter(Account.id == user_id) \ 66 | .first() 67 | 68 | return account 69 | 70 | @staticmethod 71 | def create_reset_code(email): 72 | 73 | account = AccountService.find_account_by_email(email) 74 | if not account: 75 | return None 76 | 77 | session = DbSessionFactory.create_session() 78 | 79 | reset = PasswordReset() 80 | reset.used_ip_address = '1.2.3.4' # set for real 81 | reset.user_id = account.id 82 | 83 | session.add(reset) 84 | session.commit() 85 | 86 | return reset 87 | 88 | @classmethod 89 | def find_reset_code(cls, code): 90 | 91 | if not code or not code.strip(): 92 | return None 93 | 94 | session = DbSessionFactory.create_session() 95 | reset = session.query(PasswordReset).\ 96 | filter(PasswordReset.id == code).\ 97 | first() 98 | 99 | return reset 100 | 101 | @classmethod 102 | def use_reset_code(cls, reset_code, user_ip): 103 | session = DbSessionFactory.create_session() 104 | 105 | reset = session.query(PasswordReset). \ 106 | filter(PasswordReset.id == reset_code). \ 107 | first() 108 | 109 | if not reset: 110 | return 111 | 112 | reset.used_ip_address = user_ip 113 | reset.was_used = True 114 | reset.used_date = datetime.datetime.now() 115 | 116 | session.commit() 117 | 118 | @classmethod 119 | def set_password(cls, plain_text_password, account_id): 120 | print('Resetting password for user {}'.format(account_id)) 121 | session = DbSessionFactory.create_session() 122 | 123 | account = session.query(Account). \ 124 | filter(Account.id == account_id). \ 125 | first() 126 | 127 | if not account: 128 | print("Warning: Cannot reset password, no account found.") 129 | return 130 | 131 | print("New password set.") 132 | account.password_hash = AccountService.hash_text(plain_text_password) 133 | session.commit() 134 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/services/cms_service.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_slug}}.data.cms_page import CmsPage 2 | from {{cookiecutter.project_slug}}.data.dbsession import DbSessionFactory 3 | 4 | 5 | class CmsService: 6 | @classmethod 7 | def get_page_by_url(cls, url): 8 | if not url: 9 | return None 10 | 11 | url = url.lower().strip() 12 | session = DbSessionFactory.create_session() 13 | 14 | page = session.query(CmsPage) \ 15 | .filter(CmsPage.url == url) \ 16 | .first() 17 | 18 | session.close() 19 | 20 | return page 21 | 22 | @classmethod 23 | def add_page(cls, url, html, is_redirect=False, redirect_url=None): 24 | if not url or not url.strip(): 25 | raise ValueError('url cannot be empty') 26 | 27 | url = url.lower().strip() 28 | session = DbSessionFactory.create_session() 29 | 30 | page = CmsPage() 31 | page.url = url 32 | page.html = html 33 | page.is_redirect = is_redirect 34 | page.redirect_url = redirect_url 35 | 36 | session.add(page) 37 | session.commit() 38 | 39 | return page 40 | 41 | @classmethod 42 | def init_test_data(cls): 43 | url = '/landing_pages/a_dynamic_cms_page' 44 | 45 | if cls.get_page_by_url(url) is not None: 46 | return 47 | 48 | cls.add_page( 49 | url, 50 | '

This is a CMS page

\n' + 51 | '\n' + 52 | '

\n' + 53 | 'You can create them in the DB and any URL can be mapped.
\n' + 54 | 'See CmsController / CmsService for more info.\n' + 55 | '

\n' 56 | ) 57 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/services/email_service.py: -------------------------------------------------------------------------------- 1 | import mailer 2 | import html2text 3 | 4 | from {{cookiecutter.project_slug}}.email.template_paser import EmailTemplateParser 5 | 6 | 7 | class EmailService: 8 | __smtp_username = None 9 | __smtp_password = None 10 | __smtp_server = None 11 | __smtp_port = None 12 | __is_debug_mode = False 13 | __from_address = '{{cookiecutter.contact_name}} <{{cookiecutter.contact_email}}>' 14 | 15 | @staticmethod 16 | def global_init(username, password, server, port, is_debug): 17 | EmailService.__is_debug_mode = is_debug 18 | EmailService.__smtp_username = username 19 | EmailService.__smtp_password = password 20 | EmailService.__smtp_port = int(port) 21 | EmailService.__smtp_server = server 22 | 23 | @staticmethod 24 | def send_email(to_address, subject, html_body): 25 | try: 26 | smtp = EmailService.create_smtp_server() 27 | message = mailer.Message( 28 | From=EmailService.__from_address, 29 | To=to_address, 30 | charset='utf-8') 31 | message.Subject = subject 32 | message.Html = html_body 33 | message.Body = html2text.html2text(html_body) 34 | 35 | if not EmailService.__is_debug_mode: 36 | print("Sending message (live!)") 37 | smtp.send(message) 38 | else: 39 | print("Skipping send, email is in dev mode.") 40 | except Exception as x: 41 | print("Error sending mail: {}".format(x)) 42 | 43 | @staticmethod 44 | def create_smtp_server(): 45 | smtp = mailer.Mailer( 46 | host=EmailService.__smtp_server, 47 | port=EmailService.__smtp_port, 48 | usr=EmailService.__smtp_username, 49 | pwd=EmailService.__smtp_password, 50 | use_tls=True 51 | ) 52 | 53 | return smtp 54 | 55 | @classmethod 56 | def send_welcome_email(cls, email): 57 | html_body = EmailTemplateParser.expand( 58 | EmailTemplateParser.welcome, 59 | {'email': email} 60 | ) 61 | EmailService.send_email(email, 'Welcome to {{cookiecutter.project_name}}', html_body) 62 | 63 | @classmethod 64 | def send_password_reset_email(cls, email, reset_id): 65 | 66 | html_body = EmailTemplateParser.expand( 67 | EmailTemplateParser.reset_password, 68 | {'reset_code': reset_id} 69 | ) 70 | EmailService.send_email(email, 'Reset your password at {{cookiecutter.project_name}}', html_body) 71 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/services/log_service.py: -------------------------------------------------------------------------------- 1 | import logbook 2 | import sys 3 | 4 | 5 | class LogService: 6 | @staticmethod 7 | def global_init(log_level_text, filename): 8 | level = LogService.__get_logbook_logging_level(log_level_text) 9 | 10 | if not filename: 11 | logbook.StreamHandler(sys.stdout, level=level).push_application() 12 | else: 13 | logbook.TimedRotatingFileHandler( 14 | filename, level=level, 15 | date_format="%Y-%m-%d").push_application() 16 | 17 | msg = 'Logging initialized, level: {}, mode: {}'.format( 18 | log_level_text, 19 | "stdout mode" if not filename else 'file mode: ' + filename 20 | ) 21 | 22 | LogService.get_startup_log().notice(msg) 23 | 24 | @staticmethod 25 | def get_startup_log(): 26 | return logbook.Logger("App") 27 | 28 | @staticmethod 29 | def __get_logbook_logging_level(level_str): 30 | # logbook levels: 31 | # CRITICAL = 15 32 | # ERROR = 14 33 | # WARNING = 13 34 | # NOTICE = 12 35 | # INFO = 11 36 | # DEBUG = 10 37 | # TRACE = 9 38 | # NOTSET = 0 39 | 40 | level_str = level_str.upper().strip() 41 | 42 | if level_str == 'CRITICAL': 43 | return logbook.CRITICAL 44 | elif level_str == 'ERROR': 45 | return logbook.ERROR 46 | elif level_str == 'WARNING': 47 | return logbook.WARNING 48 | elif level_str == 'NOTICE': 49 | return logbook.NOTICE 50 | elif level_str == 'INFO': 51 | return logbook.INFO 52 | elif level_str == 'DEBUG': 53 | return logbook.DEBUG 54 | elif level_str == 'TRACE': 55 | return logbook.TRACE 56 | elif level_str == 'NOTSET': 57 | return logbook.NOTSET 58 | else: 59 | raise ValueError("Unknown logbook log level: {}".format(level_str)) 60 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/services/mailinglist_service.py: -------------------------------------------------------------------------------- 1 | import logbook 2 | import mailchimp 3 | 4 | 5 | class MailingListService: 6 | mailchimp_api = None 7 | mailchimp_list_id = None 8 | __log = logbook.Logger('Mailing List Service') 9 | 10 | @staticmethod 11 | def global_init(api_key, list_id): 12 | MailingListService.mailchimp_api = api_key 13 | MailingListService.mailchimp_list_id = list_id 14 | 15 | @staticmethod 16 | def add_subscriber(email): 17 | if not MailingListService.get_is_initialized(): 18 | MailingListService.__log.warn("CANNOT USE MAILCHIMP API. KEYS NOT SET") 19 | MailingListService.__log.warn("Set your API keys in development.ini / production.ini") 20 | return False 21 | 22 | api = mailchimp.Mailchimp(apikey=MailingListService.mailchimp_api) 23 | 24 | if not email or not email.strip(): 25 | return False 26 | 27 | try: 28 | api.lists.subscribe( 29 | MailingListService.mailchimp_list_id, 30 | {'email': email.strip().lower()}, 31 | double_optin=False, 32 | update_existing=True, 33 | replace_interests=False) 34 | MailingListService.__log.notice("Successfully added {} to the mailchimp list.".format(email)) 35 | return True 36 | except Exception as x: 37 | MailingListService.__log.error("Error during mailing list signup: {}".format(x)) 38 | return False 39 | 40 | @staticmethod 41 | def get_is_initialized(): 42 | return MailingListService.mailchimp_api != 'ADD_YOUR_API_KEY' and \ 43 | MailingListService.mailchimp_list_id != 'ADD_YOUR_LIST_ID' 44 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/services/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/services 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-css", 3 | "version": "3.3.7", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web", 13 | "bootstrap", 14 | "twitter-bootstrap", 15 | "bootstrap.js" 16 | ], 17 | "main": [ 18 | "css/bootstrap.min.css", 19 | "js/bootstrap.min.js" 20 | ], 21 | "homepage": "http://getbootstrap.com/", 22 | "author": { 23 | "name": "Twitter Inc." 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/jozefizso/bower-bootstrap-css.git" 28 | }, 29 | "license": "MIT", 30 | "_release": "3.3.7", 31 | "_resolution": { 32 | "type": "version", 33 | "tag": "v3.3.7", 34 | "commit": "250e1b5392a9dd3f5cf9774d39e25ca2b9e5c3b4" 35 | }, 36 | "_source": "https://github.com/jozefizso/bower-bootstrap-css.git", 37 | "_target": "^3.3.6", 38 | "_originalSource": "bootstrap-css" 39 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap 2 | 3 | 4 | This is a Bower component for [Bootstrap 3](http://getbootstrap.com/) CSS library (v3.3.7). 5 | 6 | ## Installation: 7 | 8 | `bower install bootstrap-css` 9 | 10 | ## Bootstrap 3.0 versions 11 | 12 | ``` 13 | bower install bootstrap-css#3.3.7 14 | bower install bootstrap-css#3.3.6 15 | bower install bootstrap-css#3.3.5 16 | bower install bootstrap-css#3.3.4 17 | bower install bootstrap-css#3.3.2 18 | bower install bootstrap-css#3.2.0 19 | bower install bootstrap-css#3.1.1 20 | bower install bootstrap-css#3.1.0 21 | bower install bootstrap-css#3.0.0 22 | ``` 23 | 24 | 25 | ## Bootstrap 2.3 versions 26 | 27 | ``` 28 | bower install bootstrap-css#2.3.2 29 | bower install bootstrap-css#2.3.1 30 | bower install bootstrap-css#2.3.0 31 | ``` 32 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-css", 3 | "version": "3.3.7", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web", 13 | "bootstrap", 14 | "twitter-bootstrap", 15 | "bootstrap.js" 16 | ], 17 | "main": [ 18 | "css/bootstrap.min.css", 19 | "js/bootstrap.min.js" 20 | ], 21 | "homepage": "http://getbootstrap.com/", 22 | "author": { 23 | "name": "Twitter Inc." 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/jozefizso/bower-bootstrap-css.git" 28 | }, 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/bootstrap-css/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "font-awesome", 3 | "description": "Font Awesome", 4 | "keywords": [], 5 | "homepage": "http://fontawesome.io", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "license": [ 9 | "OFL-1.1", 10 | "MIT", 11 | "CC-BY-3.0" 12 | ], 13 | "main": [ 14 | "less/font-awesome.less", 15 | "scss/font-awesome.scss" 16 | ], 17 | "ignore": [ 18 | "*/.*", 19 | "*.json", 20 | "src", 21 | "*.yml", 22 | "Gemfile", 23 | "Gemfile.lock", 24 | "*.md" 25 | ], 26 | "version": "4.7.0", 27 | "_release": "4.7.0", 28 | "_resolution": { 29 | "type": "version", 30 | "tag": "v4.7.0", 31 | "commit": "a3fe90fa5f6fac55d197f9cbd18e3f57dafb716c" 32 | }, 33 | "_source": "https://github.com/FortAwesome/Font-Awesome.git", 34 | "_target": "^4.7.0", 35 | "_originalSource": "font-awesome", 36 | "_direct": true 37 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.db 4 | *.db.old 5 | *.swp 6 | *.db-journal 7 | 8 | .coverage 9 | .DS_Store 10 | .installed.cfg 11 | _gh_pages/* 12 | 13 | .idea/* 14 | .svn/* 15 | src/website/static/* 16 | src/website/media/* 17 | 18 | bin 19 | cfcache 20 | develop-eggs 21 | dist 22 | downloads 23 | eggs 24 | parts 25 | tmp 26 | .sass-cache 27 | node_modules 28 | 29 | src/website/settingslocal.py 30 | stunnel.log 31 | 32 | .ruby-version 33 | .bundle 34 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/.npmignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.db 4 | *.db.old 5 | *.swp 6 | *.db-journal 7 | 8 | .coverage 9 | .DS_Store 10 | .installed.cfg 11 | _gh_pages/* 12 | 13 | .idea/* 14 | .svn/* 15 | src/website/static/* 16 | src/website/media/* 17 | 18 | bin 19 | cfcache 20 | develop-eggs 21 | dist 22 | downloads 23 | eggs 24 | parts 25 | tmp 26 | .sass-cache 27 | node_modules 28 | 29 | src/website/settingslocal.py 30 | stunnel.log 31 | 32 | .ruby-version 33 | 34 | # don't need these in the npm package. 35 | src/ 36 | _config.yml 37 | bower.json 38 | component.json 39 | composer.json 40 | CONTRIBUTING.md 41 | Gemfile 42 | Gemfile.lock 43 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "font-awesome", 3 | "description": "Font Awesome", 4 | "keywords": [], 5 | "homepage": "http://fontawesome.io", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "license": ["OFL-1.1", "MIT", "CC-BY-3.0"], 9 | "main": [ 10 | "less/font-awesome.less", 11 | "scss/font-awesome.scss" 12 | ], 13 | "ignore": [ 14 | "*/.*", 15 | "*.json", 16 | "src", 17 | "*.yml", 18 | "Gemfile", 19 | "Gemfile.lock", 20 | "*.md" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/html5shiv/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html5shiv", 3 | "version": "3.7.3", 4 | "main": [ 5 | "dist/html5shiv.js" 6 | ], 7 | "ignore": [ 8 | "**/.*", 9 | "composer.json", 10 | "test", 11 | "build", 12 | "src", 13 | "build.xml" 14 | ], 15 | "homepage": "https://github.com/aFarkas/html5shiv", 16 | "_release": "3.7.3", 17 | "_resolution": { 18 | "type": "version", 19 | "tag": "3.7.3", 20 | "commit": "ed28c56c071bddfe7d635ad88995674957a016be" 21 | }, 22 | "_source": "https://github.com/aFarkas/html5shiv.git", 23 | "_target": "^3.7.3", 24 | "_originalSource": "html5shiv", 25 | "_direct": true 26 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/html5shiv/Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt){ 3 | 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | pkg: grunt.file.readJSON('package.json'), 8 | bower: grunt.file.readJSON('bower.json'), 9 | copy: { 10 | demo: { 11 | files: [ 12 | {expand: true, src: ['src/*'], dest: 'dist/', filter: 'isFile', flatten: true} 13 | ] 14 | } 15 | }, 16 | 17 | uglify: { 18 | options: { 19 | beautify: { 20 | ascii_only : true 21 | }, 22 | preserveComments: 'some' 23 | }, 24 | html5shiv: { 25 | files: [{ 26 | expand: true, // Enable dynamic expansion. 27 | cwd: 'src/', // Src matches are relative to this path. 28 | src: ['**/*.js'], // Actual pattern(s) to match. 29 | dest: 'dist/', // Destination path prefix. 30 | ext: '.min.js' 31 | }] 32 | } 33 | }, 34 | watch: { 35 | js: { 36 | files: ['src/**/*.js'], 37 | tasks: ['copy', 'uglify', 'bytesize'] 38 | } 39 | }, 40 | bytesize: { 41 | all: { 42 | src: [ 43 | 'dist/**.min.js' 44 | ] 45 | } 46 | } 47 | }); 48 | 49 | 50 | // Default task. 51 | 52 | 53 | 54 | grunt.loadNpmTasks('grunt-contrib-copy'); 55 | grunt.loadNpmTasks('grunt-contrib-uglify'); 56 | grunt.loadNpmTasks('grunt-contrib-watch'); 57 | grunt.loadNpmTasks('grunt-bytesize'); 58 | 59 | grunt.registerTask('default', ['copy', 'uglify', 'bytesize', 'watch']); 60 | 61 | }; 62 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/html5shiv/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html5shiv", 3 | "version": "3.7.3", 4 | "main": [ 5 | "dist/html5shiv.js" 6 | ], 7 | "ignore": [ 8 | "**/.*", 9 | "composer.json", 10 | "test", 11 | "build", 12 | "src", 13 | "build.xml" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/html5shiv/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html5shiv", 3 | "version": "3.7.3", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/aFarkas/html5shiv.git" 7 | }, 8 | "main": "dist/html5shiv.js", 9 | "devDependencies": { 10 | "grunt-bytesize": ">=0.1.0", 11 | "grunt-contrib-watch": ">=0.3.0", 12 | "grunt-contrib-copy": ">=0.4.0", 13 | "grunt-contrib-uglify": ">=0.2.7", 14 | "grunt": ">=0.4.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/html5shiv/readme.md: -------------------------------------------------------------------------------- 1 | # The HTML5 Shiv 2 | 3 | The HTML5 Shiv enables use of HTML5 sectioning elements in legacy Internet Explorer and provides basic HTML5 styling for Internet Explorer 6-9, Safari 4.x (and iPhone 3.x), and Firefox 3.x. 4 | 5 | ### What do these files do? 6 | 7 | #### `html5shiv.js` 8 | * This includes the basic `createElement()` shiv technique, along with monkeypatches for `document.createElement` and `document.createDocumentFragment` for IE6-8. It also applies [basic styling](https://github.com/aFarkas/html5shiv/blob/51da98dabd3c537891b7fe6114633fb10de52473/src/html5shiv.js#L216-220) for HTML5 elements for IE6-9, Safari 4.x and FF 3.x. 9 | 10 | ####`html5shiv-printshiv.js` 11 | * This includes all of the above, as well as a mechanism allowing HTML5 elements to be styled and contain children while being printed in IE 6-8. 12 | 13 | ### Who can I get mad at now? 14 | 15 | HTML5 Shiv is maintained by [Alexander Farkas](https://github.com/aFarkas/), [Jonathan Neal](https://twitter.com/jon_neal) and [Paul Irish](https://twitter.com/paul_irish), with many contributions from [John-David Dalton](https://twitter.com/jdalton). It is also distributed with [Modernizr](http://modernizr.com/). 16 | 17 | If you have any issues in these implementations, you can report them here! :) 18 | 19 | For the full story of HTML5 Shiv and all of the people involved in making it, read [The Story of the HTML5 Shiv](http://paulirish.com/2011/the-history-of-the-html5-shiv/). 20 | 21 | ## Installation 22 | 23 | ###Using [Bower](http://bower.io/) 24 | 25 | `bower install html5shiv --save-dev` 26 | 27 | This will clone the latest version of the HTML5 shiv into the `bower_components` directory at the root of your project and also create or update the file `bower.json` which specifies your projects dependencies. 28 | 29 | Include the HTML5 shiv in the `` of your page in a conditional comment and after any stylesheets. 30 | 31 | ```html 32 | 35 | ``` 36 | 37 | ###Manual installation 38 | 39 | Download and extract the [latest zip package](https://github.com/aFarkas/html5shiv/archive/master.zip) from this repositiory and copy the two files `dist/html5shiv.js` and `dist/html5shiv-printshiv.js` into your project. Then include one of them into your `` as above. 40 | 41 | ## HTML5 Shiv API 42 | 43 | HTML5 Shiv works as a simple drop-in solution. In most cases there is no need to configure HTML5 Shiv or use methods provided by HTML5 Shiv. 44 | 45 | ### `html5.elements` option 46 | 47 | The `elements` option is a space separated string or array, which describes the **full** list of the elements to shiv. see also `addElements`. 48 | 49 | **Configuring `elements` before `html5shiv.js` is included.** 50 | 51 | ```js 52 | //create a global html5 options object 53 | window.html5 = { 54 | 'elements': 'mark section customelement' 55 | }; 56 | ``` 57 | **Configuring `elements` after `html5shiv.js` is included.** 58 | 59 | ```js 60 | //change the html5shiv options object 61 | window.html5.elements = 'mark section customelement'; 62 | //and re-invoke the `shivDocument` method 63 | html5.shivDocument(document); 64 | ``` 65 | 66 | ### `html5.shivCSS` 67 | 68 | If `shivCSS` is set to `true` HTML5 Shiv will add basic styles (mostly display: block) to sectioning elements (like section, article). In most cases a webpage author should include those basic styles in his normal stylesheet to ensure older browser support (i.e. Firefox 3.6) without JavaScript. 69 | 70 | The `shivCSS` is true by default and can be set false, only before html5shiv.js is included: 71 | 72 | ```js 73 | //create a global html5 options object 74 | window.html5 = { 75 | 'shivCSS': false 76 | }; 77 | ``` 78 | 79 | ### `html5.shivMethods` 80 | 81 | If the `shivMethods` option is set to `true` (by default) HTML5 Shiv will override `document.createElement`/`document.createDocumentFragment` in Internet Explorer 6-8 to allow dynamic DOM creation of HTML5 elements. 82 | 83 | Known issue: If an element is created using the overridden `createElement` method this element returns a document fragment as its `parentNode`, but should be normally `null`. If a script relies on this behavior, `shivMethods`should be set to `false`. 84 | Note: jQuery 1.7+ has implemented his own HTML5 DOM creation fix for Internet Explorer 6-8. If all your scripts (including Third party scripts) are using jQuery's manipulation and DOM creation methods, you might want to set this option to `false`. 85 | 86 | **Configuring `shivMethods` before `html5shiv.js` is included.** 87 | 88 | ```js 89 | //create a global html5 options object 90 | window.html5 = { 91 | 'shivMethods': false 92 | }; 93 | ``` 94 | **Configuring `elements` after `html5shiv.js` is included.** 95 | 96 | ```js 97 | //change the html5shiv options object 98 | window.html5.shivMethods = false; 99 | ``` 100 | 101 | ### `html5.addElements( newElements [, document] )` 102 | 103 | The `html5.addElements` method extends the list of elements to shiv. The newElements argument can be a whitespace separated list or an array. 104 | 105 | ```js 106 | //extend list of elements to shiv 107 | html5.addElements('element content'); 108 | ``` 109 | 110 | ### `html5.createElement( nodeName [, document] )` 111 | 112 | The `html5.createElement` method creates a shived element, even if `shivMethods` is set to false. 113 | 114 | ```js 115 | var container = html5.createElement('div'); 116 | //container is shived so we can add HTML5 elements using `innerHTML` 117 | container.innerHTML = '
This is a section
'; 118 | ``` 119 | 120 | ### `html5.createDocumentFragment( [document] )` 121 | 122 | The `html5.createDocumentFragment` method creates a shived document fragment, even if `shivMethods` is set to false. 123 | 124 | ```js 125 | var fragment = html5.createDocumentFragment(); 126 | var container = document.createElement('div'); 127 | fragment.appendChild(container); 128 | //fragment is shived so we can add HTML5 elements using `innerHTML` 129 | container.innerHTML = '
This is a section
'; 130 | ``` 131 | 132 | ## HTML5 Shiv Known Issues and Limitations 133 | 134 | - The `shivMethods` option (overriding `document.createElement`) and the `html5.createElement` method create elements, which are not disconnected and have a parentNode (see also issue #64) 135 | - The cloneNode problem is currently not addressed by HTML5 Shiv. HTML5 elements can be dynamically created, but can't be cloned in all cases. 136 | - The printshiv version of HTML5 Shiv has to alter the print styles and the whole DOM for printing. In case of complex websites and or a lot of print styles this might cause performance and/or styling issues. A possible solution could be the [htc-branch](https://github.com/aFarkas/html5shiv/tree/iepp-htc) of HTML5 Shiv, which uses another technique to implement print styles for Internet Explorer 6-8. 137 | 138 | ### What about the other HTML5 element projects? 139 | 140 | - The original conception and community collaboration story of the project is described at [The History of the HTML5 Shiv](http://paulirish.com/2011/the-history-of-the-html5-shiv/). 141 | - [IEPP](https://code.google.com/p/ie-print-protector), by Jon Neal, addressed the printing fault of the original `html5shiv`. It was merged into `html5shiv`. 142 | - **Shimprove**, in April 2010, patched `cloneNode` and `createElement` was later merged into `html5shiv` 143 | - **innerShiv**, introduced in August 2010 by JD Barlett, addressed dynamically adding new HTML5 elements into the DOM. [jQuery added support](http://blog.jquery.com/2011/11/03/jquery-1-7-released/) that made innerShiv redundant and `html5shiv` addressed the same issues as well, so the project was completed. 144 | - The **html5shim** and **html5shiv** sites on Google Code are maintained by Remy Sharp and are identical distribution points of this `html5shiv` project. 145 | - **Modernizr** is developed by the same people as `html5shiv` and can include the latest version in any custom builds created at modernizr.com 146 | - This `html5shiv` repo now contains tests for all the edge cases pursued by the above libraries and has been extensively tested, both in development and production. 147 | 148 | A [detailed changelog of html5shiv](https://github.com/aFarkas/html5shiv/wiki) is available. 149 | 150 | ### Why is it called a *shiv*? 151 | 152 | The term **shiv** [originates](http://ejohn.org/blog/html5-shiv/) from [John Resig](https://github.com/jeresig), who was thought to have used the word for its slang meaning, *a sharp object used as a knife-like weapon*, intended for Internet Explorer. Truth be known, John probably intended to use the word [shim](http://en.wikipedia.org/wiki/Shim_(computing)), which in computing means *an application compatibility workaround*. Rather than correct his mispelling, most developers familiar with Internet Explorer appreciated the visual imagery. And that, [kids](http://html5homi.es/), is [etymology](https://en.wikipedia.org/wiki/Etymology). 153 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/jquery-dist/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-dist", 3 | "version": "2.1.3", 4 | "authors": [ 5 | "Tyler Long " 6 | ], 7 | "description": "jquery.min.js", 8 | "main": "jquery.min.js", 9 | "keywords": [ 10 | "jquery" 11 | ], 12 | "license": "MIT", 13 | "homepage": "https://github.com/tylerlong/jquery-dist", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests", 20 | "README.md", 21 | "fabfile.py" 22 | ], 23 | "_release": "2.1.3", 24 | "_resolution": { 25 | "type": "version", 26 | "tag": "v2.1.3", 27 | "commit": "a25281ac05e35b52b6f339293053269e7de4bb74" 28 | }, 29 | "_source": "https://github.com/tylerlong/jquery-dist.git", 30 | "_target": "^2.1.3", 31 | "_originalSource": "jquery-dist", 32 | "_direct": true 33 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/jquery-dist/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-dist", 3 | "version": "2.1.3", 4 | "authors": [ 5 | "Tyler Long " 6 | ], 7 | "description": "jquery.min.js", 8 | "main": "jquery.min.js", 9 | "keywords": [ 10 | "jquery" 11 | ], 12 | "license": "MIT", 13 | "homepage": "https://github.com/tylerlong/jquery-dist", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests", 20 | "README.md", 21 | "fabfile.py" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "respond", 3 | "version": "1.4.2", 4 | "main": "dest/respond.src.js", 5 | "description": "Fast and lightweight polyfill for min/max-width CSS3 Media Queries (for IE 6-8, and more)", 6 | "ignore": [ 7 | "**/.*", 8 | "test" 9 | ], 10 | "homepage": "https://github.com/scottjehl/Respond", 11 | "_release": "1.4.2", 12 | "_resolution": { 13 | "type": "version", 14 | "tag": "1.4.2", 15 | "commit": "3eda118b4440825df56e3c12d054f9d316a5987a" 16 | }, 17 | "_source": "https://github.com/scottjehl/Respond.git", 18 | "_target": "^1.4.2", 19 | "_originalSource": "respond", 20 | "_direct": true 21 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | "use strict"; 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | banner: 8 | '/*! Respond.js v<%= pkg.version %>: <%= pkg.description %>' + 9 | ' * Copyright <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>\n' + 10 | ' * Licensed under <%= _.pluck(pkg.licenses, "url").join(", ") %>\n' + 11 | ' * <%= pkg.website %>' + 12 | ' */\n\n', 13 | uglify: { 14 | nonMinMatchMedia: { 15 | options: { 16 | mangle: false, 17 | compress: false, 18 | preserveComments: 'some', 19 | beautify: { 20 | beautify: true, 21 | indent_level: 2 22 | } 23 | }, 24 | files: { 25 | 'dest/respond.src.js': ['src/matchmedia.polyfill.js', 'src/respond.js'] 26 | } 27 | }, 28 | minMatchMedia: { 29 | options: { 30 | banner: '<%= banner %>' 31 | }, 32 | files: { 33 | 'dest/respond.min.js': ['src/matchmedia.polyfill.js', 'src/respond.js'] 34 | } 35 | }, 36 | nonMinMatchMediaListener: { 37 | options: { 38 | mangle: false, 39 | compress: false, 40 | preserveComments: 'some', 41 | beautify: { 42 | beautify: true, 43 | indent_level: 2 44 | } 45 | }, 46 | files: { 47 | 'dest/respond.matchmedia.addListener.src.js': ['src/matchmedia.polyfill.js', 'src/matchmedia.addListener.js', 'src/respond.js'] 48 | } 49 | }, 50 | minMatchMediaListener: { 51 | options: { 52 | banner: '<%= banner %>' 53 | }, 54 | files: { 55 | 'dest/respond.matchmedia.addListener.min.js': ['src/matchmedia.polyfill.js', 'src/matchmedia.addListener.js', 'src/respond.js'] 56 | } 57 | } 58 | }, 59 | jshint: { 60 | files: ['src/respond.js', 'src/matchmedia.polyfill.js'], 61 | options: { 62 | curly: true, 63 | eqeqeq: true, 64 | immed: true, 65 | latedef: false, 66 | newcap: true, 67 | noarg: true, 68 | sub: true, 69 | undef: true, 70 | boss: true, 71 | eqnull: true, 72 | smarttabs: true, 73 | node: true, 74 | es5: true, 75 | strict: false 76 | }, 77 | globals: { 78 | Image: true, 79 | window: true 80 | } 81 | } 82 | }); 83 | 84 | grunt.loadNpmTasks( 'grunt-contrib-jshint' ); 85 | grunt.loadNpmTasks( 'grunt-contrib-uglify' ); 86 | 87 | // Default task. 88 | grunt.registerTask('default', ['jshint', 'uglify']); 89 | 90 | }; 91 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Scott Jehl 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/README.md: -------------------------------------------------------------------------------- 1 | # Respond.js 2 | ### A fast & lightweight polyfill for min/max-width CSS3 Media Queries (for IE 6-8, and more) 3 | 4 | - Copyright 2011: Scott Jehl, scottjehl.com 5 | 6 | - Licensed under the MIT license. 7 | 8 | The goal of this script is to provide a fast and lightweight (3kb minified / 1kb gzipped) script to enable [responsive web designs](http://www.alistapart.com/articles/responsive-web-design/) in browsers that don't support CSS3 Media Queries - in particular, Internet Explorer 8 and under. It's written in such a way that it will probably patch support for other non-supporting browsers as well (more information on that soon). 9 | 10 | If you're unfamiliar with the concepts surrounding Responsive Web Design, you can read up [here](http://www.alistapart.com/articles/responsive-web-design/) and also [here](http://filamentgroup.com/examples/responsive-images/) 11 | 12 | [Demo page](https://rawgithub.com/scottjehl/Respond/master/test/test.html) (the colors change to show media queries working) 13 | 14 | 15 | Usage Instructions 16 | ====== 17 | 18 | 1. Craft your CSS with min/max-width media queries to adapt your layout from mobile (first) all the way up to desktop 19 | 20 | 21 |
 22 |     @media screen and (min-width: 480px){
 23 |         ...styles for 480px and up go here
 24 |     }
 25 | 
26 | 27 | 2. Reference the respond.min.js script (1kb min/gzipped) after all of your CSS (the earlier it runs, the greater chance IE users will not see a flash of un-media'd content) 28 | 29 | 3. Crack open Internet Explorer and pump fists in delight 30 | 31 | 32 | CDN/X-Domain Setup 33 | ====== 34 | 35 | Respond.js works by requesting a pristine copy of your CSS via AJAX, so if you host your stylesheets on a CDN (or a subdomain), you'll need to upload a proxy page to enable cross-domain communication. 36 | 37 | See `cross-domain/example.html` for a demo: 38 | 39 | - Upload `cross-domain/respond-proxy.html` to your external domain 40 | - Upload `cross-domain/respond.proxy.gif` to your origin domain 41 | - Reference the file(s) via `` element(s): 42 | 43 |
 44 | 	<!-- Respond.js proxy on external server -->
 45 | 	<link href="http://externalcdn.com/respond-proxy.html" id="respond-proxy" rel="respond-proxy" />
 46 | 
 47 | 	<!-- Respond.js redirect location on local server -->
 48 | 	<link href="/path/to/respond.proxy.gif" id="respond-redirect" rel="respond-redirect" />
 49 | 
 50 | 	<!-- Respond.js proxy script on local server -->
 51 | 	<script src="/path/to/respond.proxy.js"></script>
 52 | 
53 | 54 | If you are having problems with the cross-domain setup, make sure respond-proxy.html does not have a query string appended to it. 55 | 56 | Note: HUGE thanks to @doctyper for the contributions in the cross-domain proxy! 57 | 58 | 59 | Support & Caveats 60 | ====== 61 | 62 | Some notes to keep in mind: 63 | 64 | - This script's focus is purposely very narrow: only min-width and max-width media queries and all media types (screen, print, etc) are translated to non-supporting browsers. I wanted to keep things simple for filesize, maintenance, and performance, so I've intentionally limited support to queries that are essential to building a (mobile-first) responsive design. In the future, I may rework things a bit to include a hook for patching-in additional media query features - stay tuned! 65 | 66 | - Browsers that natively support CSS3 Media Queries are opted-out of running this script as quickly as possible. In testing for support, all other browsers are subjected to a quick test to determine whether they support media queries or not before proceeding to run the script. This test is now included separately at the top, and uses the window.matchMedia polyfill found here: https://github.com/paulirish/matchMedia.js . If you are already including this polyfill via Modernizr or otherwise, feel free to remove that part. 67 | 68 | - This script relies on no other scripts or frameworks (aside from the included matchMedia polyfill), and is optimized for mobile delivery (~1kb total filesize min/gzip) 69 | 70 | - As you might guess, this implementation is quite dumb in regards to CSS parsing rules. This is a good thing, because that allows it to run really fast, but its looseness may also cause unexpected behavior. For example: if you enclose a whole media query in a comment intending to disable its rules, you'll probably find that those rules will end up enabled in non-media-query-supporting browsers. 71 | 72 | - Respond.js doesn't parse CSS referenced via @import, nor does it work with media queries within style elements, as those styles can't be re-requested for parsing. 73 | 74 | - Due to security restrictions, some browsers may not allow this script to work on file:// urls (because it uses xmlHttpRequest). Run it on a web server. 75 | 76 | - If the request for the CSS file that includes MQ-specific styling is 77 | behind a redirect, Respond.js will fail silently. CSS files should 78 | respond with a 200 status. 79 | 80 | - Currently, media attributes on link elements are supported, but only if the linked stylesheet contains no media queries. If it does contain queries, the media attribute will be ignored and the internal queries will be parsed normally. In other words, @media statements in the CSS take priority. 81 | 82 | - Reportedly, if CSS files are encoded in UTF-8 with Byte-Order-Mark (BOM), they will not work with Respond.js in IE7 or IE8. Noted in issue #97 83 | 84 | - WARNING: Including @font-face rules inside a media query will cause IE7 and IE8 to hang during load. To work around this, place @font-face rules in the wide open, as a sibling to other media queries. 85 | 86 | - If you have more than 32 stylesheets referenced, IE will throw an error, `Invalid procedure call or argument`. Concatenate your CSS and the issue should go away. 87 | 88 | - Sass/SCSS source maps are not supported; `@media -sass-debug-info` will break respond.js. Noted in issue [#148](https://github.com/scottjehl/Respond/issues/148) 89 | 90 | - Internet Explorer 9 supports css3 media queries, but not within frames when the CSS containing the media query is in an external file (this appears to be a bug in IE9 — see http://stackoverflow.com/questions/10316247/media-queries-fail-inside-ie9-iframe). See this commit for a fix if you're having this problem. https://github.com/NewSignature/Respond/commit/1c86c66075f0a2099451eb426702fc3540d2e603 91 | 92 | - Nested Media Queries are not supported 93 | 94 | 95 | How's it work? 96 | ====== 97 | Basically, the script loops through the CSS referenced in the page and runs a regular expression or two on their contents to find media queries and their associated blocks of CSS. In Internet Explorer, the content of the stylesheet is impossible to retrieve in its pre-parsed state (which in IE 8-, means its media queries are removed from the text), so Respond.js re-requests the CSS files using Ajax and parses the text response from there. Be sure to configure your CSS files' caching properly so that this re-request doesn't actually go to the server, hitting your browser cache instead. 98 | 99 | From there, each media query block is appended to the head in order via style elements, and those style elements are enabled and disabled (read: appended and removed from the DOM) depending on how their min/max width compares with the browser width. The media attribute on the style elements will match that of the query in the CSS, so it could be "screen", "projector", or whatever you want. Any relative paths contained in the CSS will be prefixed by their stylesheet's href, so image paths will direct to their proper destination 100 | 101 | API Options? 102 | ====== 103 | Sure, a couple: 104 | 105 | - respond.update() : rerun the parser (helpful if you added a stylesheet to the page and it needs to be translated) 106 | - respond.mediaQueriesSupported: set to true if the browser natively supports media queries. 107 | - respond.getEmValue() : returns the pixel value of one em 108 | 109 | 110 | Alternatives to this script 111 | ====== 112 | This isn't the only CSS3 Media Query polyfill script out there; but it damn well may be the fastest. 113 | 114 | If you're looking for more robust CSS3 Media Query support, you might check out http://code.google.com/p/css3-mediaqueries-js/. In testing, I've found that script to be noticeably slow when rendering complex responsive designs (both in filesize and performance), but it really does support a lot more media query features than this script. Big hat tip to the authors! :) 115 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "respond", 3 | "version": "1.4.2", 4 | "main": "dest/respond.src.js", 5 | "description": "Fast and lightweight polyfill for min/max-width CSS3 Media Queries (for IE 6-8, and more)", 6 | "ignore": [ 7 | "**/.*", 8 | "test" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/cross-domain/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Respond JS Test Page 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

This is a visual test file for cross-domain proxy.

17 | 18 |

The media queries in the included CSS file simply change the body's background color depending on the browser width. If you see any colors aside from black, then the media queries are working in your browser. You can resize your browser window to see it change on the fly.

19 | 20 | 21 |

Media-attributes are working too! This should be visible above 600px.

22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/cross-domain/respond-proxy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Respond JS Proxy 7 | 8 | 9 | 95 | 96 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/cross-domain/respond.proxy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/cross-domain/respond.proxy.gif -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/cross-domain/respond.proxy.js: -------------------------------------------------------------------------------- 1 | /*! Respond.js: min/max-width media query polyfill. Remote proxy (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 2 | (function(win, doc, undefined){ 3 | var docElem = doc.documentElement, 4 | proxyURL = doc.getElementById("respond-proxy").href, 5 | redirectURL = (doc.getElementById("respond-redirect") || location).href, 6 | baseElem = doc.getElementsByTagName("base")[0], 7 | urls = [], 8 | refNode; 9 | 10 | function encode(url){ 11 | return win.encodeURIComponent(url); 12 | } 13 | 14 | function fakejax( url, callback ){ 15 | 16 | var iframe, 17 | AXO; 18 | 19 | // All hail Google http://j.mp/iKMI19 20 | // Behold, an iframe proxy without annoying clicky noises. 21 | if ( "ActiveXObject" in win ) { 22 | AXO = new ActiveXObject( "htmlfile" ); 23 | AXO.open(); 24 | AXO.write( '' ); 25 | AXO.close(); 26 | iframe = AXO.getElementById( "x" ); 27 | } else { 28 | iframe = doc.createElement( "iframe" ); 29 | iframe.style.cssText = "position:absolute;top:-99em"; 30 | docElem.insertBefore(iframe, docElem.firstElementChild || docElem.firstChild ); 31 | } 32 | 33 | iframe.src = checkBaseURL(proxyURL) + "?url=" + encode(redirectURL) + "&css=" + encode(checkBaseURL(url)); 34 | 35 | function checkFrameName() { 36 | var cssText; 37 | 38 | try { 39 | cssText = iframe.contentWindow.name; 40 | } 41 | catch (e) { } 42 | 43 | if (cssText) { 44 | // We've got what we need. Stop the iframe from loading further content. 45 | iframe.src = "about:blank"; 46 | iframe.parentNode.removeChild(iframe); 47 | iframe = null; 48 | 49 | 50 | // Per http://j.mp/kn9EPh, not taking any chances. Flushing the ActiveXObject 51 | if (AXO) { 52 | AXO = null; 53 | 54 | if (win.CollectGarbage) { 55 | win.CollectGarbage(); 56 | } 57 | } 58 | 59 | callback(cssText); 60 | } 61 | else{ 62 | win.setTimeout(checkFrameName, 100); 63 | } 64 | } 65 | 66 | win.setTimeout(checkFrameName, 500); 67 | } 68 | 69 | function checkBaseURL(href) { 70 | if (baseElem && href.indexOf(baseElem.href) === -1) { 71 | bref = (/\/$/).test(baseElem.href) ? baseElem.href : (baseElem.href + "/"); 72 | href = bref + href; 73 | } 74 | 75 | return href; 76 | } 77 | 78 | function checkRedirectURL() { 79 | // IE6 & IE7 don't build out absolute urls in attributes. 80 | // So respond.proxy.gif remains relative instead of http://example.com/respond.proxy.gif. 81 | // This trickery resolves that issue. 82 | if (~ !redirectURL.indexOf(location.host)) { 83 | 84 | var fakeLink = doc.createElement("div"); 85 | 86 | fakeLink.innerHTML = ''; 87 | docElem.insertBefore(fakeLink, docElem.firstElementChild || docElem.firstChild ); 88 | 89 | // Grab the parsed URL from that dummy object 90 | redirectURL = fakeLink.firstChild.href; 91 | 92 | // Clean up 93 | fakeLink.parentNode.removeChild(fakeLink); 94 | fakeLink = null; 95 | } 96 | } 97 | 98 | function buildUrls(){ 99 | var links = doc.getElementsByTagName( "link" ); 100 | 101 | for( var i = 0, linkl = links.length; i < linkl; i++ ){ 102 | 103 | var thislink = links[i], 104 | href = links[i].href, 105 | extreg = (/^([a-zA-Z:]*\/\/(www\.)?)/).test( href ), 106 | ext = (baseElem && !extreg) || extreg; 107 | 108 | //make sure it's an external stylesheet 109 | if( thislink.rel.indexOf( "stylesheet" ) >= 0 && ext ){ 110 | (function( link ){ 111 | fakejax( href, function( css ){ 112 | link.styleSheet.rawCssText = css; 113 | respond.update(); 114 | } ); 115 | })( thislink ); 116 | } 117 | } 118 | 119 | 120 | } 121 | 122 | if( !respond.mediaQueriesSupported ){ 123 | checkRedirectURL(); 124 | buildUrls(); 125 | } 126 | 127 | })( window, document ); 128 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/dest/respond.matchmedia.addListener.min.js: -------------------------------------------------------------------------------- 1 | /*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl 2 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT 3 | * */ 4 | 5 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";if(a.matchMedia&&a.matchMedia("all").addListener)return!1;var b=a.matchMedia,c=b("only all").matches,d=!1,e=0,f=[],g=function(){a.clearTimeout(e),e=a.setTimeout(function(){for(var c=0,d=f.length;d>c;c++){var e=f[c].mql,g=f[c].listeners||[],h=b(e.media).matches;if(h!==e.matches){e.matches=h;for(var i=0,j=g.length;j>i;i++)g[i].call(a,e)}}},30)};a.matchMedia=function(e){var h=b(e),i=[],j=0;return h.addListener=function(b){c&&(d||(d=!0,a.addEventListener("resize",g,!0)),0===j&&(j=f.push({mql:h,listeners:i})),i.push(b))},h.removeListener=function(a){for(var b=0,c=i.length;c>b;b++)i[b]===a&&i.splice(b,1)},h}}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b #mq-test-1 { width: 42px; }',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b #mq-test-1 { width: 42px; }'; 13 | docElem.insertBefore(fakeBody, refNode); 14 | bool = div.offsetWidth === 42; 15 | docElem.removeChild(fakeBody); 16 | return { 17 | matches: bool, 18 | media: q 19 | }; 20 | }; 21 | }(w.document); 22 | })(this); 23 | 24 | /*! Respond.js v1.4.0: min/max-width media query polyfill. (c) Scott Jehl. MIT Lic. j.mp/respondjs */ 25 | (function(w) { 26 | "use strict"; 27 | var respond = {}; 28 | w.respond = respond; 29 | respond.update = function() {}; 30 | var requestQueue = [], xmlHttp = function() { 31 | var xmlhttpmethod = false; 32 | try { 33 | xmlhttpmethod = new w.XMLHttpRequest(); 34 | } catch (e) { 35 | xmlhttpmethod = new w.ActiveXObject("Microsoft.XMLHTTP"); 36 | } 37 | return function() { 38 | return xmlhttpmethod; 39 | }; 40 | }(), ajax = function(url, callback) { 41 | var req = xmlHttp(); 42 | if (!req) { 43 | return; 44 | } 45 | req.open("GET", url, true); 46 | req.onreadystatechange = function() { 47 | if (req.readyState !== 4 || req.status !== 200 && req.status !== 304) { 48 | return; 49 | } 50 | callback(req.responseText); 51 | }; 52 | if (req.readyState === 4) { 53 | return; 54 | } 55 | req.send(null); 56 | }; 57 | respond.ajax = ajax; 58 | respond.queue = requestQueue; 59 | respond.regex = { 60 | media: /@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi, 61 | keyframes: /@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi, 62 | urls: /(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g, 63 | findStyles: /@media *([^\{]+)\{([\S\s]+?)$/, 64 | only: /(only\s+)?([a-zA-Z]+)\s?/, 65 | minw: /\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/, 66 | maxw: /\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/ 67 | }; 68 | respond.mediaQueriesSupported = w.matchMedia && w.matchMedia("only all") !== null && w.matchMedia("only all").matches; 69 | if (respond.mediaQueriesSupported) { 70 | return; 71 | } 72 | var doc = w.document, docElem = doc.documentElement, mediastyles = [], rules = [], appendedEls = [], parsedSheets = {}, resizeThrottle = 30, head = doc.getElementsByTagName("head")[0] || docElem, base = doc.getElementsByTagName("base")[0], links = head.getElementsByTagName("link"), lastCall, resizeDefer, eminpx, getEmValue = function() { 73 | var ret, div = doc.createElement("div"), body = doc.body, originalHTMLFontSize = docElem.style.fontSize, originalBodyFontSize = body && body.style.fontSize, fakeUsed = false; 74 | div.style.cssText = "position:absolute;font-size:1em;width:1em"; 75 | if (!body) { 76 | body = fakeUsed = doc.createElement("body"); 77 | body.style.background = "none"; 78 | } 79 | docElem.style.fontSize = "100%"; 80 | body.style.fontSize = "100%"; 81 | body.appendChild(div); 82 | if (fakeUsed) { 83 | docElem.insertBefore(body, docElem.firstChild); 84 | } 85 | ret = div.offsetWidth; 86 | if (fakeUsed) { 87 | docElem.removeChild(body); 88 | } else { 89 | body.removeChild(div); 90 | } 91 | docElem.style.fontSize = originalHTMLFontSize; 92 | if (originalBodyFontSize) { 93 | body.style.fontSize = originalBodyFontSize; 94 | } 95 | ret = eminpx = parseFloat(ret); 96 | return ret; 97 | }, applyMedia = function(fromResize) { 98 | var name = "clientWidth", docElemProp = docElem[name], currWidth = doc.compatMode === "CSS1Compat" && docElemProp || doc.body[name] || docElemProp, styleBlocks = {}, lastLink = links[links.length - 1], now = new Date().getTime(); 99 | if (fromResize && lastCall && now - lastCall < resizeThrottle) { 100 | w.clearTimeout(resizeDefer); 101 | resizeDefer = w.setTimeout(applyMedia, resizeThrottle); 102 | return; 103 | } else { 104 | lastCall = now; 105 | } 106 | for (var i in mediastyles) { 107 | if (mediastyles.hasOwnProperty(i)) { 108 | var thisstyle = mediastyles[i], min = thisstyle.minw, max = thisstyle.maxw, minnull = min === null, maxnull = max === null, em = "em"; 109 | if (!!min) { 110 | min = parseFloat(min) * (min.indexOf(em) > -1 ? eminpx || getEmValue() : 1); 111 | } 112 | if (!!max) { 113 | max = parseFloat(max) * (max.indexOf(em) > -1 ? eminpx || getEmValue() : 1); 114 | } 115 | if (!thisstyle.hasquery || (!minnull || !maxnull) && (minnull || currWidth >= min) && (maxnull || currWidth <= max)) { 116 | if (!styleBlocks[thisstyle.media]) { 117 | styleBlocks[thisstyle.media] = []; 118 | } 119 | styleBlocks[thisstyle.media].push(rules[thisstyle.rules]); 120 | } 121 | } 122 | } 123 | for (var j in appendedEls) { 124 | if (appendedEls.hasOwnProperty(j)) { 125 | if (appendedEls[j] && appendedEls[j].parentNode === head) { 126 | head.removeChild(appendedEls[j]); 127 | } 128 | } 129 | } 130 | appendedEls.length = 0; 131 | for (var k in styleBlocks) { 132 | if (styleBlocks.hasOwnProperty(k)) { 133 | var ss = doc.createElement("style"), css = styleBlocks[k].join("\n"); 134 | ss.type = "text/css"; 135 | ss.media = k; 136 | head.insertBefore(ss, lastLink.nextSibling); 137 | if (ss.styleSheet) { 138 | ss.styleSheet.cssText = css; 139 | } else { 140 | ss.appendChild(doc.createTextNode(css)); 141 | } 142 | appendedEls.push(ss); 143 | } 144 | } 145 | }, translate = function(styles, href, media) { 146 | var qs = styles.replace(respond.regex.keyframes, "").match(respond.regex.media), ql = qs && qs.length || 0; 147 | href = href.substring(0, href.lastIndexOf("/")); 148 | var repUrls = function(css) { 149 | return css.replace(respond.regex.urls, "$1" + href + "$2$3"); 150 | }, useMedia = !ql && media; 151 | if (href.length) { 152 | href += "/"; 153 | } 154 | if (useMedia) { 155 | ql = 1; 156 | } 157 | for (var i = 0; i < ql; i++) { 158 | var fullq, thisq, eachq, eql; 159 | if (useMedia) { 160 | fullq = media; 161 | rules.push(repUrls(styles)); 162 | } else { 163 | fullq = qs[i].match(respond.regex.findStyles) && RegExp.$1; 164 | rules.push(RegExp.$2 && repUrls(RegExp.$2)); 165 | } 166 | eachq = fullq.split(","); 167 | eql = eachq.length; 168 | for (var j = 0; j < eql; j++) { 169 | thisq = eachq[j]; 170 | mediastyles.push({ 171 | media: thisq.split("(")[0].match(respond.regex.only) && RegExp.$2 || "all", 172 | rules: rules.length - 1, 173 | hasquery: thisq.indexOf("(") > -1, 174 | minw: thisq.match(respond.regex.minw) && parseFloat(RegExp.$1) + (RegExp.$2 || ""), 175 | maxw: thisq.match(respond.regex.maxw) && parseFloat(RegExp.$1) + (RegExp.$2 || "") 176 | }); 177 | } 178 | } 179 | applyMedia(); 180 | }, makeRequests = function() { 181 | if (requestQueue.length) { 182 | var thisRequest = requestQueue.shift(); 183 | ajax(thisRequest.href, function(styles) { 184 | translate(styles, thisRequest.href, thisRequest.media); 185 | parsedSheets[thisRequest.href] = true; 186 | w.setTimeout(function() { 187 | makeRequests(); 188 | }, 0); 189 | }); 190 | } 191 | }, ripCSS = function() { 192 | for (var i = 0; i < links.length; i++) { 193 | var sheet = links[i], href = sheet.href, media = sheet.media, isCSS = sheet.rel && sheet.rel.toLowerCase() === "stylesheet"; 194 | if (!!href && isCSS && !parsedSheets[href]) { 195 | if (sheet.styleSheet && sheet.styleSheet.rawCssText) { 196 | translate(sheet.styleSheet.rawCssText, href, media); 197 | parsedSheets[href] = true; 198 | } else { 199 | if (!/^([a-zA-Z:]*\/\/)/.test(href) && !base || href.replace(RegExp.$1, "").split("/")[0] === w.location.host) { 200 | if (href.substring(0, 2) === "//") { 201 | href = w.location.protocol + href; 202 | } 203 | requestQueue.push({ 204 | href: href, 205 | media: media 206 | }); 207 | } 208 | } 209 | } 210 | } 211 | makeRequests(); 212 | }; 213 | ripCSS(); 214 | respond.update = ripCSS; 215 | respond.getEmValue = getEmValue; 216 | function callMedia() { 217 | applyMedia(true); 218 | } 219 | if (w.addEventListener) { 220 | w.addEventListener("resize", callMedia, false); 221 | } else if (w.attachEvent) { 222 | w.attachEvent("onresize", callMedia); 223 | } 224 | })(this); -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Respond.js", 3 | "description": "min/max-width media query polyfill", 4 | "version": "1.4.2", 5 | "homepage": "https://github.com/scottjehl/Respond", 6 | "author": { 7 | "name": "Scott Jehl", 8 | "email": "scott@filamentgroup.com", 9 | "url": "http://filamentgroup.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/scottjehl/Respond.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/scottjehl/Respond/issues" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "MIT", 21 | "url": "https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT" 22 | } 23 | ], 24 | "devDependencies": { 25 | "grunt-cli":"~0.1", 26 | "grunt": "~0.4.0", 27 | "grunt-contrib-jshint": "~0.2.0", 28 | "grunt-contrib-qunit": "~0.3.0", 29 | "grunt-contrib-uglify": "0.2.7" 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/src/matchmedia.addListener.js: -------------------------------------------------------------------------------- 1 | /*! matchMedia() polyfill addListener/removeListener extension. Author & copyright (c) 2012: Scott Jehl. Dual MIT/BSD license */ 2 | (function( w ){ 3 | "use strict"; 4 | // Bail out for browsers that have addListener support 5 | if (w.matchMedia && w.matchMedia('all').addListener) { 6 | return false; 7 | } 8 | 9 | var localMatchMedia = w.matchMedia, 10 | hasMediaQueries = localMatchMedia('only all').matches, 11 | isListening = false, 12 | timeoutID = 0, // setTimeout for debouncing 'handleChange' 13 | queries = [], // Contains each 'mql' and associated 'listeners' if 'addListener' is used 14 | handleChange = function(evt) { 15 | // Debounce 16 | w.clearTimeout(timeoutID); 17 | 18 | timeoutID = w.setTimeout(function() { 19 | for (var i = 0, il = queries.length; i < il; i++) { 20 | var mql = queries[i].mql, 21 | listeners = queries[i].listeners || [], 22 | matches = localMatchMedia(mql.media).matches; 23 | 24 | // Update mql.matches value and call listeners 25 | // Fire listeners only if transitioning to or from matched state 26 | if (matches !== mql.matches) { 27 | mql.matches = matches; 28 | 29 | for (var j = 0, jl = listeners.length; j < jl; j++) { 30 | listeners[j].call(w, mql); 31 | } 32 | } 33 | } 34 | }, 30); 35 | }; 36 | 37 | w.matchMedia = function(media) { 38 | var mql = localMatchMedia(media), 39 | listeners = [], 40 | index = 0; 41 | 42 | mql.addListener = function(listener) { 43 | // Changes would not occur to css media type so return now (Affects IE <= 8) 44 | if (!hasMediaQueries) { 45 | return; 46 | } 47 | 48 | // Set up 'resize' listener for browsers that support CSS3 media queries (Not for IE <= 8) 49 | // There should only ever be 1 resize listener running for performance 50 | if (!isListening) { 51 | isListening = true; 52 | w.addEventListener('resize', handleChange, true); 53 | } 54 | 55 | // Push object only if it has not been pushed already 56 | if (index === 0) { 57 | index = queries.push({ 58 | mql : mql, 59 | listeners : listeners 60 | }); 61 | } 62 | 63 | listeners.push(listener); 64 | }; 65 | 66 | mql.removeListener = function(listener) { 67 | for (var i = 0, il = listeners.length; i < il; i++){ 68 | if (listeners[i] === listener){ 69 | listeners.splice(i, 1); 70 | } 71 | } 72 | }; 73 | 74 | return mql; 75 | }; 76 | }( this )); 77 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/bower_components/respond/src/matchmedia.polyfill.js: -------------------------------------------------------------------------------- 1 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */ 2 | /*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */ 3 | 4 | (function(w){ 5 | "use strict"; 6 | w.matchMedia = w.matchMedia || (function( doc, undefined ) { 7 | 8 | var bool, 9 | docElem = doc.documentElement, 10 | refNode = docElem.firstElementChild || docElem.firstChild, 11 | // fakeBody required for 12 | fakeBody = doc.createElement( "body" ), 13 | div = doc.createElement( "div" ); 14 | 15 | div.id = "mq-test-1"; 16 | div.style.cssText = "position:absolute;top:-100em"; 17 | fakeBody.style.background = "none"; 18 | fakeBody.appendChild(div); 19 | 20 | return function(q){ 21 | 22 | div.innerHTML = "­"; 23 | 24 | docElem.insertBefore( fakeBody, refNode ); 25 | bool = div.offsetWidth === 42; 26 | docElem.removeChild( fakeBody ); 27 | 28 | return { 29 | matches: bool, 30 | media: q 31 | }; 32 | 33 | }; 34 | 35 | }( w.document )); 36 | }( this )); 37 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/css/home.css: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | background: #000 url("/static/img/hero.jpg") center center; 3 | /*width: 100%;*/ 4 | height: 500px; 5 | background-size: cover; 6 | border-bottom: 1px solid #444; 7 | margin-bottom: 0px; 8 | } 9 | 10 | .jumbotron #hero-message { 11 | padding-top: 85px; 12 | text-align: center; 13 | color: white; 14 | } 15 | 16 | .jumbotron #hero-message h1 { 17 | text-shadow: 4px 4px 2px rgba(0, 0, 0, 0.5); 18 | } 19 | 20 | .jumbotron .subtitle { 21 | text-shadow: 4px 4px 2px rgba(0, 0, 0, 0.5); 22 | /*font-weight: bold;*/ 23 | color: #ffdb3d; 24 | font-size: 26px; 25 | } 26 | 27 | 28 | 29 | form#newsletter { 30 | max-width: 500px; 31 | margin-left: auto; 32 | margin-right: auto; 33 | } 34 | 35 | form#newsletter .form-control { 36 | display: inline-block; 37 | } 38 | form#newsletter input.form-control { 39 | max-width: 350px; 40 | vertical-align: middle; 41 | } 42 | form#newsletter button.form-control { 43 | max-width: 120px; 44 | margin-left: 5px; 45 | } 46 | 47 | .band { 48 | text-align: center; 49 | } 50 | .band-members span { 51 | display: block; 52 | font-size: 18px; 53 | margin-top: 10px; 54 | } 55 | 56 | 57 | .bookus img, .upcoming img 58 | { 59 | border: 1px solid #444; 60 | border-radius: 5px; 61 | padding: 0px; 62 | } 63 | 64 | 65 | 66 | .content-section-a { 67 | background-color: #333; 68 | } 69 | 70 | .content-section-a.alternate { 71 | background-color: #222; 72 | padding: 30px 0; 73 | border-top: none; 74 | border-bottom: none; 75 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/css/landing-page.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Start Bootstrap - Landing Page (http://startbootstrap.com/) 3 | * Copyright 2013-2016 Start Bootstrap 4 | * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE) 5 | */ 6 | 7 | body, 8 | html { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | body, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6 { 20 | font-family: "Lato","Helvetica Neue",Helvetica,Arial,sans-serif; 21 | font-weight: 700; 22 | } 23 | 24 | .topnav { 25 | font-size: 14px; 26 | } 27 | 28 | .lead { 29 | font-size: 18px; 30 | font-weight: 400; 31 | } 32 | 33 | .intro-header { 34 | padding-top: 50px; /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */ 35 | padding-bottom: 50px; 36 | text-align: center; 37 | color: #f8f8f8; 38 | background: url(/static/img/hero.jpg) no-repeat center center; 39 | background-size: cover; 40 | } 41 | 42 | .intro-message { 43 | position: relative; 44 | padding-top: 20%; 45 | padding-bottom: 20%; 46 | } 47 | 48 | .intro-message > h1 { 49 | margin: 0; 50 | text-shadow: 2px 2px 3px rgba(0,0,0,0.6); 51 | font-size: 5em; 52 | } 53 | 54 | .intro-divider { 55 | width: 400px; 56 | border-top: 1px solid #f8f8f8; 57 | border-bottom: 1px solid rgba(0,0,0,0.2); 58 | } 59 | 60 | .intro-message > h3 { 61 | text-shadow: 2px 2px 3px rgba(0,0,0,0.6); 62 | } 63 | 64 | @media(max-width:767px) { 65 | .intro-message { 66 | padding-bottom: 15%; 67 | } 68 | 69 | .intro-message > h1 { 70 | font-size: 3em; 71 | } 72 | 73 | ul.intro-social-buttons > li { 74 | display: block; 75 | margin-bottom: 20px; 76 | padding: 0; 77 | } 78 | 79 | ul.intro-social-buttons > li:last-child { 80 | margin-bottom: 0; 81 | } 82 | 83 | .intro-divider { 84 | width: 100%; 85 | } 86 | } 87 | 88 | .network-name { 89 | text-transform: uppercase; 90 | font-size: 14px; 91 | font-weight: 400; 92 | letter-spacing: 2px; 93 | } 94 | 95 | .content-section-a { 96 | padding: 50px 0; 97 | background-color: #f8f8f8; 98 | } 99 | 100 | .content-section-b { 101 | padding: 50px 0; 102 | border-top: 1px solid #e7e7e7; 103 | border-bottom: 1px solid #e7e7e7; 104 | } 105 | 106 | .section-heading { 107 | margin-bottom: 30px; 108 | } 109 | 110 | .section-heading-spacer { 111 | float: left; 112 | width: 200px; 113 | border-top: 3px solid #e7e7e7; 114 | } 115 | 116 | .banner { 117 | padding: 100px 0; 118 | color: #f8f8f8; 119 | background: url(/static/img/theme/banner-bg.jpg) no-repeat center center; 120 | background-size: cover; 121 | } 122 | 123 | .banner h2 { 124 | margin: 0; 125 | text-shadow: 2px 2px 3px rgba(0,0,0,0.6); 126 | font-size: 3em; 127 | } 128 | 129 | .banner ul { 130 | margin-bottom: 0; 131 | } 132 | 133 | .banner-social-buttons { 134 | float: right; 135 | margin-top: 0; 136 | } 137 | 138 | @media(max-width:1199px) { 139 | ul.banner-social-buttons { 140 | float: left; 141 | margin-top: 15px; 142 | } 143 | } 144 | 145 | @media(max-width:767px) { 146 | .banner h2 { 147 | margin: 0; 148 | text-shadow: 2px 2px 3px rgba(0,0,0,0.6); 149 | font-size: 3em; 150 | } 151 | 152 | ul.banner-social-buttons > li { 153 | display: block; 154 | margin-bottom: 20px; 155 | padding: 0; 156 | } 157 | 158 | ul.banner-social-buttons > li:last-child { 159 | margin-bottom: 0; 160 | } 161 | } 162 | 163 | footer { 164 | padding: 50px 0; 165 | background-color: #f8f8f8; 166 | } 167 | 168 | p.copyright { 169 | margin: 15px 0 0; 170 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/css/nav.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | border-radius: 0px; 3 | } 4 | 5 | .navbar-brand img { 6 | height: 28px; 7 | display: inline-block; 8 | } 9 | 10 | a.navbar-brand img { 11 | vertical-align: middle; 12 | } 13 | a.navbar-brand { 14 | white-space: nowrap; 15 | font-size: 24px; 16 | } 17 | 18 | .navbar-inverse a.navbar-brand { 19 | color: white; 20 | } 21 | 22 | .navbar-inverse { 23 | background-color: black; 24 | } 25 | 26 | h1 { 27 | text-align: center; 28 | } 29 | 30 | .navbar-nav > li > a { 31 | padding-top: 8px; 32 | padding-bottom: 0px; 33 | 34 | } 35 | .nav>li>a { 36 | padding: 5px; 37 | } 38 | 39 | .navbar-brand, .nav { 40 | font-size: 18px; 41 | } 42 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #fafafa; 3 | background-color: #000; 4 | } 5 | 6 | .main_content { 7 | background-color: #222222; 8 | line-height: 1.5em; 9 | padding: 0px; 10 | } 11 | 12 | .nav { 13 | background-color: black; 14 | color: white; 15 | } 16 | 17 | .nav a { 18 | color: white; 19 | margin: 10px; 20 | } 21 | 22 | .nav a:visited, .nav a:active { 23 | text-decoration: none; 24 | } 25 | 26 | .navbar-inverse { 27 | background-color: #000; 28 | border-color: #080808; 29 | } 30 | 31 | .account-nav { 32 | float: right; 33 | } 34 | 35 | /* ********** ALBUM LIST ********* */ 36 | .album-in-list{ 37 | } 38 | 39 | .album-in-list ul { 40 | margin-top:10px; 41 | } 42 | .album-in-list .album-title { 43 | padding-top:10px; 44 | margin-bottom: 5px; 45 | } 46 | 47 | .album-in-list img { 48 | height: 150px; 49 | float: left; 50 | margin: 10px; 51 | margin-right: 50px; 52 | border-radius: 5px; 53 | } 54 | 55 | .album-in-list .album-title { 56 | font-size: 18px; 57 | } 58 | 59 | form.standard-form input{ 60 | color: black; 61 | } 62 | 63 | .standard-form input, 64 | .standard-form textarea 65 | { 66 | width: 350px; 67 | } 68 | .standard-form textarea { 69 | margin-left: 10px; 70 | } 71 | 72 | .standard-form input, .standard-form button { 73 | margin: 10px; 74 | } 75 | 76 | .error-msg { 77 | color: red; 78 | } 79 | 80 | .main_content { 81 | margin-top: 51px; 82 | } 83 | 84 | .yellow-foreground { 85 | color: #ffdb3d; 86 | } 87 | 88 | .blue-foreground { 89 | color: #326aa5; 90 | } 91 | 92 | footer { 93 | background-color: black; 94 | padding: 40px; 95 | margin-top: 0px; 96 | text-align: center; 97 | } 98 | 99 | footer { 100 | color: #555; 101 | } 102 | 103 | footer .row { 104 | padding: 0px; 105 | padding-bottom: 1em; 106 | margin: 0px; 107 | } 108 | 109 | footer a, footer .section-header { 110 | color: #999; 111 | } 112 | 113 | footer a:hover { 114 | color: #fff; 115 | } 116 | 117 | footer .mission { 118 | text-align: left; 119 | } 120 | 121 | footer .social { 122 | /*text-align: left;*/ 123 | line-height: 1.75em; 124 | } 125 | 126 | footer .social > div > a { 127 | width: 75px; 128 | text-align: left; 129 | display: inline-block; 130 | } 131 | 132 | .large-content-text { 133 | font-size: 18px; 134 | } 135 | 136 | .main-text, 137 | .large-content-text { 138 | font-weight: 300; 139 | line-height:1.75em; 140 | } 141 | 142 | .friends .twitter { 143 | font-size: 36px; 144 | margin-left: 100px; 145 | } 146 | .friends .large-content-text { 147 | margin-top: 30px; 148 | margin-bottom: 50px; 149 | } 150 | 151 | .account form { 152 | margin-top: 60px; 153 | margin-bottom: 80px; 154 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/css/theme.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); 2 | body { 3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-weight: 300; 5 | color: #ffffff; 6 | background: #bc2131; 7 | } 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6 { 14 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 15 | font-weight: 300; 16 | } 17 | p { 18 | font-weight: 300; 19 | } 20 | .font-normal { 21 | font-weight: 400; 22 | } 23 | .font-semi-bold { 24 | font-weight: 600; 25 | } 26 | .font-bold { 27 | font-weight: 700; 28 | } 29 | .starter-template { 30 | margin-top: 250px; 31 | } 32 | .starter-template .content { 33 | margin-left: 10px; 34 | } 35 | .starter-template .content h1 { 36 | margin-top: 10px; 37 | font-size: 60px; 38 | } 39 | .starter-template .content h1 .smaller { 40 | font-size: 40px; 41 | color: #f2b7bd; 42 | } 43 | .starter-template .content .lead { 44 | font-size: 25px; 45 | color: #f2b7bd; 46 | } 47 | .starter-template .content .lead .font-normal { 48 | color: #ffffff; 49 | } 50 | .starter-template .links { 51 | float: right; 52 | right: 0; 53 | margin-top: 125px; 54 | } 55 | .starter-template .links ul { 56 | display: block; 57 | padding: 0; 58 | margin: 0; 59 | } 60 | .starter-template .links ul li { 61 | list-style: none; 62 | display: inline; 63 | margin: 0 10px; 64 | } 65 | .starter-template .links ul li:first-child { 66 | margin-left: 0; 67 | } 68 | .starter-template .links ul li:last-child { 69 | margin-right: 0; 70 | } 71 | .starter-template .links ul li.current-version { 72 | color: #f2b7bd; 73 | font-weight: 400; 74 | } 75 | .starter-template .links ul li a { 76 | color: #ffffff; 77 | } 78 | .starter-template .links ul li a:hover { 79 | text-decoration: underline; 80 | } 81 | .starter-template .links ul li .icon-muted { 82 | color: #eb8b95; 83 | margin-right: 5px; 84 | } 85 | .starter-template .links ul li:hover .icon-muted { 86 | color: #ffffff; 87 | } 88 | .starter-template .copyright { 89 | margin-top: 10px; 90 | font-size: 0.9em; 91 | color: #f2b7bd; 92 | text-transform: lowercase; 93 | float: right; 94 | right: 0; 95 | } 96 | @media (max-width: 1199px) { 97 | .starter-template .content h1 { 98 | font-size: 45px; 99 | } 100 | .starter-template .content h1 .smaller { 101 | font-size: 30px; 102 | } 103 | .starter-template .content .lead { 104 | font-size: 20px; 105 | } 106 | } 107 | @media (max-width: 991px) { 108 | .starter-template { 109 | margin-top: 0; 110 | } 111 | .starter-template .logo { 112 | margin: 40px auto; 113 | } 114 | .starter-template .content { 115 | margin-left: 0; 116 | text-align: center; 117 | } 118 | .starter-template .content h1 { 119 | margin-bottom: 20px; 120 | } 121 | .starter-template .links { 122 | float: none; 123 | text-align: center; 124 | margin-top: 60px; 125 | } 126 | .starter-template .copyright { 127 | float: none; 128 | text-align: center; 129 | } 130 | } 131 | @media (max-width: 767px) { 132 | .starter-template .content h1 .smaller { 133 | font-size: 25px; 134 | display: block; 135 | } 136 | .starter-template .content .lead { 137 | font-size: 16px; 138 | } 139 | .starter-template .links { 140 | margin-top: 40px; 141 | } 142 | .starter-template .links ul li { 143 | display: block; 144 | margin: 0; 145 | } 146 | .starter-template .links ul li .icon-muted { 147 | display: none; 148 | } 149 | .starter-template .copyright { 150 | margin-top: 20px; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/book-us.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/book-us.jpg -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/cc-comm-attr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/cc-comm-attr.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/event.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/event.jpg -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/fav.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/hero.jpg -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/logo.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/members/drummer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/members/drummer.jpg -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/members/guitarist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/members/guitarist.jpg -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/members/singer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/img/members/singer.jpg -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/js/placeholder.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter/9d6664695d73717e30e0410e3635ef22d14fb217/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/js/placeholder.txt -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/static 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/forgot_password.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

Forgot your password?

7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 | Trouble logging in? No worries. We'll send you a reset 16 | link to your email address. Just enter the address you 17 | registered with and we'll get it right over there. 18 |
19 |
20 |
21 | Error: ${error} 22 |
23 | 24 |
25 | 29 | 30 | 31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 43 |
44 |
45 | 46 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/index.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 |
8 |
9 | 10 | 11 |

Your account

12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/register.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 32 |
33 |
34 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/reset_password.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

Reset your password

7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
18 | 24 | 25 | 26 |
27 | 28 |
29 | ${message} 30 |
31 |
32 | ${error_msg} 33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/reset_sent.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

Check your email,
your reset link has been sent

7 |
8 |
9 |
10 | 11 |
12 | Just click the link in your email to reset 13 | your password and log in. 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/signin.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/cms/page.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 12 | 13 |
14 |
15 |
16 |
17 | ${structure: html} 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/home/about.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 |

About page

9 |
The value from the controller is ${value}.
10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/home/bookus.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 |

Book us page

9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/home/contact.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |

Contact page

8 |
The value from the controller is ${value}.
9 |
10 |
11 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/home/image_credits.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 14 |
15 |
16 |
17 |
18 |

Image credits

19 |

Thank you to all of the artists who made their work available 20 | via flickr.
21 |
22 | I have used the following images under the creative commons 23 | commercial license with attribution:
24 | 27 |

28 |

29 | The following list of images fulfills the attribution section 30 | of the license. Thank you to each artist / photographer. 31 |

32 |

33 |
34 | Photographer: The Zender Agenda
35 | Alterations: cropped 36 |

37 |

38 |
39 | Photographer: Jason Kasper
40 | Alterations: cropped 41 |

42 |

43 |
44 | Photographer: Photo Cindy
45 | Alterations: cropped 46 |

47 |

48 |
49 | Photographer: James Mackintosh
50 | Alterations: cropped 51 |

52 |

53 |
54 | Photographer: Feast of Music
55 | Alterations: cropped 56 |

57 |

58 |
59 | Photographer: Vincent van der Heijden
60 | Alterations: cropped 61 |

62 |
63 |
64 |
65 |
66 |
67 |
68 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/home/index.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 | 8 |
9 |

{{cookiecutter.project_name}}

10 |

11 | Albums 12 | Upcoming Events 13 |

14 |

For those about to rock, we invoke you

15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 |
28 | 33 | 36 |
37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 |
52 |
53 |
54 | 56 |
57 |
58 |
59 |

Upcoming Events

60 |
    61 |
  • March 15th, The Bottleneck, Laurence KS
  • 62 |
  • March 17th, The Laughing Goat, Boulder CO
  • 63 |
  • March 20th, The State Room, Salt Lake City UT
  • 64 |
  • March 24th, Crystal Ballroom, Portland OR
  • 65 |
66 | 67 |
68 |
69 |
70 |
71 | 72 | 73 |
74 | 75 | 76 |
77 | 78 | 79 | 80 | 81 |
82 | 83 |
84 |
85 |

Band Members

86 |
87 |
88 | 90 | Dave Wilson 91 |
92 |
93 | 94 | Mark Spencer 95 |
96 |
97 | 98 | Rags 99 |
100 |
101 |
102 | 103 | 104 |
105 | 106 | 107 |
108 | 109 | 110 | 111 | 112 |
113 | 114 |
115 |
116 |
117 |
118 |
119 |

Book us

120 |

121 | We love to travel and thrive on live events. 122 | We've done huge concerts down to the most 123 | intimate events.
124 |
125 | Just get in touch and start the conversation. 126 |

127 | 128 |
129 |
130 |
131 | 133 |
134 |
135 |
136 | 137 | 138 |
139 | 140 | 141 |
142 | 143 | 144 | 145 | 146 |
147 | 148 |
149 | 150 |
151 |
152 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/home/not_implemented.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |

Missing page

8 |
9 |
This page is not yet implemented
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/newsletter/failed.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 |

Ooops

9 |
10 | Sorry but it seems something went wrong with that sign up.
11 |
12 |
13 | ${error}
14 |
15 |
16 | Try hitting that back button and verifying your email address was valid.
17 |
18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/newsletter/subscribed.pt: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 |

Awesome, you're on the list!

9 |
10 | Thanks so much for signing up to be notified of 11 | upcoming events and albums.
12 | Your email address is in good hands. We hate spam just as much as you do!
13 |
14 | If you haven't already, also follow us on twitter:
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/templates 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/shared/_layout.pt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | {{cookiecutter.project_name}} 14 | 15 | 16 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 92 | 93 | 94 |
95 |
96 |
97 | 98 | 99 |
100 |
101 |
102 |
103 |
104 |
Our story
105 | This site is not a real site. It's a just a real live demo 106 | from my online video course 107 | 108 | Python for Entrepreneurs.
109 |
110 | Blue / Yellow Rockets was built 111 | during the class to show how 112 | to create a website in Python, but also all the business related features 113 | no one sees (mailing lists, credit card payments, email, etc). 114 | 115 |
116 |
117 | 138 | 139 |
140 |
141 |
142 | 143 |
144 |
145 |
146 | 147 | 151 | 152 |
153 |
154 |
155 |
156 |
157 | 158 | 160 | 161 | 162 | 163 |
164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/tests.py: -------------------------------------------------------------------------------- 1 | # import unittest 2 | # 3 | # from pyramid import testing 4 | # 5 | # 6 | # class ViewTests(unittest.TestCase): 7 | # def setUp(self): 8 | # self.config = testing.setUp() 9 | # 10 | # def tearDown(self): 11 | # testing.tearDown() 12 | # 13 | # def test_my_view(self): 14 | # from .views import my_view 15 | # request = testing.DummyRequest() 16 | # info = my_view(request) 17 | # self.assertEqual(info['project'], '{{cookiecutter.project_slug}}') 18 | # 19 | # 20 | # class FunctionalTests(unittest.TestCase): 21 | # def setUp(self): 22 | # from {{cookiecutter.project_slug}} import main 23 | # app = main({}) 24 | # from webtest import TestApp 25 | # self.testapp = TestApp(app) 26 | # 27 | # def test_root(self): 28 | # res = self.testapp.get('/', status=200) 29 | # self.assertTrue(b'Pyramid' in res.body) 30 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/viewmodels/forgotpassword_viewmodel.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_slug}}.viewmodels.viewmodelbase import ViewModelBase 2 | 3 | 4 | class ForgotPasswordViewModel(ViewModelBase): 5 | def __init__(self): 6 | self.email = None 7 | self.error = None 8 | 9 | def from_dict(self, data_dict): 10 | self.email = data_dict.get('email') 11 | 12 | def validate(self): 13 | self.error = None 14 | if not self.email or not self.email.strip(): 15 | self.error = "You must specify an email address" 16 | return 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/viewmodels/readme.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_slug}}/viewmodels 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/viewmodels/register_viewmodel.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_slug}}.viewmodels.signin_viewmodel import SigninViewModel 2 | 3 | 4 | class RegisterViewModel(SigninViewModel): 5 | def __init__(self): 6 | super().__init__() 7 | self.confirm_password = None 8 | 9 | def from_dict(self, data_dict): 10 | super().from_dict(data_dict) 11 | self.confirm_password = data_dict.get('confirm_password') 12 | 13 | def validate(self): 14 | self.error = None 15 | if self.password != self.confirm_password: 16 | self.error = "The password and confirmation don't match" 17 | return 18 | 19 | if not self.password: 20 | self.error = "You must specify a password" 21 | return 22 | 23 | if not self.email: 24 | self.error = "You must specify an email address" 25 | return 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/viewmodels/resetpassword_viewmodel.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from {{cookiecutter.project_slug}}.services.account_service import AccountService 4 | from {{cookiecutter.project_slug}}.viewmodels.viewmodelbase import ViewModelBase 5 | 6 | 7 | class ResetPasswordViewModel(ViewModelBase): 8 | def __init__(self): 9 | self.reset_code = None 10 | self.reset = None 11 | self.error_msg = None 12 | self.message = None 13 | self.password = None 14 | self.is_get = True 15 | 16 | def from_dict(self, data_dict): 17 | # reset_code will be third part of URL: 18 | # /account/reset_password/f8489375729a 19 | # that is always id in our routing scheme 20 | self.reset_code = data_dict.get('id') 21 | 22 | self.password = data_dict.get('password') 23 | if self.reset_code: 24 | self.reset = AccountService.find_reset_code(self.reset_code) 25 | 26 | def validate(self): 27 | self.error_msg = None 28 | if not self.reset: 29 | self.error_msg = "Reset code not found" 30 | return 31 | 32 | if not self.is_get: 33 | if not (self.password or self.password.strip()): 34 | self.error_msg = 'You must enter a valid password' 35 | return 36 | if len(self.password) < 3: 37 | self.error_msg = 'You must enter a password with at least a few characters' 38 | return 39 | 40 | if self.reset.was_used: 41 | self.error_msg = 'This reset code has already been used.' 42 | return 43 | 44 | if self.reset.was_used: 45 | self.error_msg = 'This reset code has already been used.' 46 | return 47 | 48 | dt = datetime.datetime.now() - self.reset.created_date 49 | days = dt.total_seconds() / 60 / 60 / 24 50 | if days > 1: 51 | self.error_msg = 'This reset code has expired, generate a new one.' 52 | return 53 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/viewmodels/signin_viewmodel.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.project_slug}}.viewmodels.viewmodelbase import ViewModelBase 2 | 3 | 4 | class SigninViewModel(ViewModelBase): 5 | def __init__(self): 6 | self.email = None 7 | self.password = None 8 | self.error = None 9 | 10 | def from_dict(self, data_dict): 11 | self.email = data_dict.get('email') 12 | self.password = data_dict.get('password') 13 | 14 | # Just to test rollbar: 15 | # raise ValueError("Ka Boom!") 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/viewmodels/viewmodelbase.py: -------------------------------------------------------------------------------- 1 | class ViewModelBase: 2 | def to_dict(self): 3 | return self.__dict__ 4 | --------------------------------------------------------------------------------