Browse Source

Adding project to github

Vince Salvino 6 years ago
commit
46dda3eed2
100 changed files with 6214 additions and 0 deletions
  1. 15 0
      .gitattributes
  2. 15 0
      .gitignore
  3. 160 0
      DOCS.md
  4. 27 0
      LICENSE
  5. 4 0
      MANIFEST.in
  6. 99 0
      README.md
  7. 0 0
      coderedcms/__init__.py
  8. 10 0
      coderedcms/admin_urls.py
  9. 0 0
      coderedcms/bin/__init__.py
  10. 148 0
      coderedcms/bin/coderedcms.py
  11. 62 0
      coderedcms/blocks/__init__.py
  12. 237 0
      coderedcms/blocks/base_blocks.py
  13. 297 0
      coderedcms/blocks/content_blocks.py
  14. 257 0
      coderedcms/blocks/html_blocks.py
  15. 110 0
      coderedcms/blocks/layout_blocks.py
  16. 88 0
      coderedcms/blocks/metadata_blocks.py
  17. 158 0
      coderedcms/forms.py
  18. 69 0
      coderedcms/migrations/0001_initial.py
  19. 0 0
      coderedcms/migrations/__init__.py
  20. 9 0
      coderedcms/models/__init__.py
  21. 1023 0
      coderedcms/models/page_models.py
  22. 247 0
      coderedcms/models/snippet_models.py
  23. 370 0
      coderedcms/models/wagtailsettings_models.py
  24. 18 0
      coderedcms/project_template/.gitattributes
  25. 21 0
      coderedcms/project_template/Dockerfile
  26. 10 0
      coderedcms/project_template/manage.py
  27. 0 0
      coderedcms/project_template/project_name/__init__.py
  28. 0 0
      coderedcms/project_template/project_name/settings/__init__.py
  29. 188 0
      coderedcms/project_template/project_name/settings/base.py
  30. 18 0
      coderedcms/project_template/project_name/settings/dev.py
  31. 75 0
      coderedcms/project_template/project_name/settings/prod.py
  32. 37 0
      coderedcms/project_template/project_name/urls.py
  33. 16 0
      coderedcms/project_template/project_name/wsgi.prod.py
  34. 16 0
      coderedcms/project_template/project_name/wsgi.py
  35. 8 0
      coderedcms/project_template/requirements.txt
  36. 30 0
      coderedcms/project_template/website/migrations/0001_initial.py
  37. 56 0
      coderedcms/project_template/website/migrations/0002_initial_data.py
  38. 0 0
      coderedcms/project_template/website/migrations/__init__.py
  39. 82 0
      coderedcms/project_template/website/models.py
  40. 0 0
      coderedcms/project_template/website/static/css/custom.css
  41. 0 0
      coderedcms/project_template/website/static/js/custom.js
  42. 165 0
      coderedcms/schema.py
  43. 8 0
      coderedcms/search_urls.py
  44. 184 0
      coderedcms/settings.py
  45. 354 0
      coderedcms/static/css/codered-admin.css
  46. 240 0
      coderedcms/static/css/codered-editor.css
  47. 330 0
      coderedcms/static/css/codered-front.css
  48. BIN
      coderedcms/static/img/codered.png
  49. 31 0
      coderedcms/static/js/codered-editor.js
  50. 103 0
      coderedcms/static/js/codered-front.js
  51. 12 0
      coderedcms/templates/404.html
  52. 27 0
      coderedcms/templates/500.html
  53. 17 0
      coderedcms/templates/coderedcms/blocks/article_block_card.html
  54. 6 0
      coderedcms/templates/coderedcms/blocks/base_block.html
  55. 45 0
      coderedcms/templates/coderedcms/blocks/base_link_block.html
  56. 13 0
      coderedcms/templates/coderedcms/blocks/button_block.html
  57. 17 0
      coderedcms/templates/coderedcms/blocks/card_block.html
  58. 19 0
      coderedcms/templates/coderedcms/blocks/card_blurb.html
  59. 19 0
      coderedcms/templates/coderedcms/blocks/card_foot.html
  60. 17 0
      coderedcms/templates/coderedcms/blocks/card_head.html
  61. 19 0
      coderedcms/templates/coderedcms/blocks/card_head_foot.html
  62. 19 0
      coderedcms/templates/coderedcms/blocks/card_img.html
  63. 10 0
      coderedcms/templates/coderedcms/blocks/cardgrid_columns.html
  64. 10 0
      coderedcms/templates/coderedcms/blocks/cardgrid_deck.html
  65. 10 0
      coderedcms/templates/coderedcms/blocks/cardgrid_group.html
  66. 47 0
      coderedcms/templates/coderedcms/blocks/carousel_block.html
  67. 12 0
      coderedcms/templates/coderedcms/blocks/code_block.html
  68. 9 0
      coderedcms/templates/coderedcms/blocks/column_block.html
  69. 7 0
      coderedcms/templates/coderedcms/blocks/document_link_block.html
  70. 26 0
      coderedcms/templates/coderedcms/blocks/download_block.html
  71. 12 0
      coderedcms/templates/coderedcms/blocks/embed_video_block.html
  72. 5 0
      coderedcms/templates/coderedcms/blocks/external_link_block.html
  73. 8 0
      coderedcms/templates/coderedcms/blocks/google_map.html
  74. 14 0
      coderedcms/templates/coderedcms/blocks/grid_block.html
  75. 4 0
      coderedcms/templates/coderedcms/blocks/h1_block.html
  76. 4 0
      coderedcms/templates/coderedcms/blocks/h2_block.html
  77. 4 0
      coderedcms/templates/coderedcms/blocks/h3_block.html
  78. 20 0
      coderedcms/templates/coderedcms/blocks/hero_block.html
  79. 20 0
      coderedcms/templates/coderedcms/blocks/image_block.html
  80. 34 0
      coderedcms/templates/coderedcms/blocks/image_gallery_block.html
  81. 11 0
      coderedcms/templates/coderedcms/blocks/image_link_block.html
  82. 34 0
      coderedcms/templates/coderedcms/blocks/modal_block.html
  83. 8 0
      coderedcms/templates/coderedcms/blocks/page_link_block.html
  84. 14 0
      coderedcms/templates/coderedcms/blocks/pagelist_article_card_columns.html
  85. 14 0
      coderedcms/templates/coderedcms/blocks/pagelist_article_card_deck.html
  86. 14 0
      coderedcms/templates/coderedcms/blocks/pagelist_article_card_group.html
  87. 25 0
      coderedcms/templates/coderedcms/blocks/pagelist_article_media.html
  88. 16 0
      coderedcms/templates/coderedcms/blocks/pagelist_block.html
  89. 21 0
      coderedcms/templates/coderedcms/blocks/pagelist_list_group.html
  90. 9 0
      coderedcms/templates/coderedcms/blocks/pricelist_block.html
  91. 16 0
      coderedcms/templates/coderedcms/blocks/pricelistitem_block.html
  92. 7 0
      coderedcms/templates/coderedcms/blocks/quote_block.html
  93. 22 0
      coderedcms/templates/coderedcms/blocks/struct_data_action.json
  94. 6 0
      coderedcms/templates/coderedcms/blocks/struct_data_hours.json
  95. 29 0
      coderedcms/templates/coderedcms/blocks/table_block.html
  96. 4 0
      coderedcms/templates/coderedcms/formfields/date.html
  97. 7 0
      coderedcms/templates/coderedcms/formfields/datetime.html
  98. 4 0
      coderedcms/templates/coderedcms/formfields/time.html
  99. 22 0
      coderedcms/templates/coderedcms/includes/pagination.html
  100. 50 0
      coderedcms/templates/coderedcms/includes/struct_data_article.json

+ 15 - 0
.gitattributes

@@ -0,0 +1,15 @@
+# Set the default line ending behavior.
+* text eol=lf
+
+# Explicitly declare text files you want to always be normalized and converted
+# to native line endings on checkout.
+*.py text
+*.html text
+*.js text
+*.css text
+*.json text
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
+*.gif binary

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+*.pyc
+*Thumbs.db
+*~
+*.sqlite3
+*.txz
+*.tgz
+media/
+build/
+dist/
+__pycache__
+codered_cms.egg-info/
+coderedcms.egg-info/
+.vscode/
+testapp/
+testproject/

+ 160 - 0
DOCS.md

@@ -0,0 +1,160 @@
+# Documentation
+
+[< Back to README](README.md)
+
+Table of Contents:
+* [Quick Start](#quick-start)
+* [Customizing your website](#customizing-your-website)
+* [Searching](#searching)
+* [Hooks](#hooks)
+* [Settings](#codered-cms-settings)
+* [Developing coderedcms](#developing-and-testing-codered-cms)
+
+
+
+
+## Quick start
+1. Run `pip install coderedcms`
+
+2. Run `coderedcms start mysite`
+
+3. Run `python manage.py migrate` to create the core models.
+
+4. Run `python manage.py createsuperuser` to create the initial admin user.
+
+5. Run `python manage.py runserver` to launch the development server, and go to `http://localhost:8000` in your browser, or `http://localhost:8000/admin/` to log in with your admin account.
+
+
+
+## Customizing your website
+After following the quickstart, you are greeted by a barebones website. There are a few settings you will want to change to add your own branding.
+
+### Site name
+This is shown by default in the navbar, and also added to the title attribute of each page. This can be changed in Settings > Sites > localhost. Hostname and port only need to be changed when running in  production.
+
+### Site settings
+Under Settings > Sites in the Wagtail Admin, you will want to make sure this setting is up to date with the proper Hostname and Port. Failure to do so can cause the Preview button on pages to return a 500 error.
+
+### Logo & icon
+The logo that appears in the navbar, the wagtail admin, and your favicon can be set in Settings > Layout. Here you can also change navbar settings (based on Bootstrap CSS framework).
+
+### Navigation bars
+Navbars are top navigation elements that create a "main menu" experience. Navbars are managed as snippets. They render from top down based on the order they were created in.
+
+### Footers
+Similar to Navbars, footers are also managed as snippets and also render top down based on the order they were created in.
+
+### Custom CSS
+A django app called `website` has been created to hold your custom changes. In website/static/ there are custom.css and custom.js files that get loaded on every page by default. Adding anything to these files will automatically populate on the site and override any default styles. By default, Bootstrap 4 and jQuery are already included on the site.
+
+### Custom templates
+The templates directory inside the `website` app is empty by default. Any templates you put in here will override the default coderedcms templates if they follow the same name and diretory structure. This uses the standard Django template rendering engine. For example, to change the formatting of the article page, copy `coderedcms/templates/coderedcms/pages/article_page.html` to `website/templates/coderedcms/pages/article_page.html` and modify it.
+
+### Custom models
+The django app `website` has been created with default models based on pre-built abstract CodeRed CMS models. You can use these as-is, override existing fields and function, and add custom fields to these models. After making a change to any of these models, be sure to run `python manage.py makemigrations` and `python manage.py migrate` to apply the database changes.
+
+
+## Searching
+A search page is available by default at the `/search/` URL, which can be customized in the `urls.py` file in your project. To enable a search bar in the navigation bar, check Settings > Layout > Search box. Search results are paginated; to specify the number of results per page, edit the value in Settings > General > Search Settings.
+
+### Search result formatting
+Each search result is rendered using the template at `coderedcms/pages/search_result.html`. The template can be overriden per model with the `search_template` attribute.
+
+### Search result filtering
+To enable additional filtering by page type, add `search_filterable = True` to the page model. The `search_name` and `search_name_plural` fields are then used to display the labels for these filters (defaults to `verbose_name` and `verbose_name_plural` if not specified). For example, to enable search filtering by Blog or by Products in addition to All Results:
+```
+class BlogPage(CoderedArticlePage):
+    search_filterable = True
+    search_name = 'Blog Post'
+    search_name_plural = 'Blog'
+
+class Product(CoderedWebPage):
+    search_filterable = True
+    search_name = 'Product'
+    search_name_plural = 'Products'
+```
+Would enable the following filter options on the search page: All Results, Blog, Products.
+
+### Search fields
+If using the Wagtail DatabaseSearch backend (default), only page Title and Search Description fields are searched upon. This is due to a limitation in the DatabaseSearch backend; other backends such as PostgreSQL and Elasticsearch will search on additional specific fields such as body, article captions, etc. To enable more specific searching while still using the database backend, the specific models can be flagged for inclusion in search by setting `search_db_include = True` on the page model. Note that this must be set on every type of page model you wish to include in search. When setting this flag, search is performed independently on each page type, and the results are combined. So you may want to also specify `search_db_boost` (int) to control the order in which the pages are searched. Pages with a higher `search_db_boost` are searched first, and results are shown higher in the list. For example:
+```
+class Article(CoderedArticlePage):
+    search_db_include = True
+    search_db_boost = 10
+    ...
+
+class WebPage(CoderedWebPage):
+    search_db_include = True
+    search_db_boost = 9
+    ...
+
+class FormPage(CoderedFormPage):
+    ...
+```
+In this example, Article search results will be shown before WebPage results when using the DatabaseSearch backend. FormPage results will not be shown at all, due to the absence `search_db_include`. If no models have `search_db_include = True`, All CoderedPages will be searched by title and description. When using any search backend other than database, `search_db_*` variables are ignored.
+
+
+
+## Hooks
+Building on the concept of wagtail hooks, there are some additional hooks in CodeRed CMS
+
+### `is_request_cacheable`
+The callable passed into this hook should take a `request` argument, and return a `bool` indicating whether or not the response to this request should be cached (served from the cache if it is already cached). Not returning, or returning anything other than a bool will not affect the caching decision. For example:
+```
+from wagtail.core import hooks
+
+@hooks.register('is_request_cacheable')
+def nocache_in_query(request):
+    # if the querystring contains a "nocache" key, return False to forcibly not cache.
+    # otherwise, do not return to let the CMS decide how to cache.
+    if 'nocache' in request.GET:
+        return False
+```
+
+
+
+## CodeRed CMS Settings
+Default settings are loaded from coderedcms/settings.py. Available settings for CodeRed CMS:
+
+### CODERED_CACHE_PAGES
+Boolean on whether or not to load the page caching machinery and enable cache settings in the wagtail admin.
+
+### CODERED_CACHE_BACKEND
+The name of the django cache backend to use for CodeRed CMS. Defaults to `'default'` which is required by Django when using the cache.
+
+### CODERED_PROTECTED_MEDIA_ROOT
+The directory where files from File Upload fields on Form Pages are saved. These files are served through django using `PROTECTED_MEDIA_URL` and require login to access. Defaults to `protected/` in your project directory.
+
+### CODERED_PROTECTED_MEDIA_URL
+The url for protected media files from form file uploads. Defaults to '/protected/'
+
+### CODERED_PROTECTED_MEDIA_UPLOAD_WHITELIST
+The allowed filetypes for media upload in the form of a list of file type extensions. Default is blank. For example, to only allow documents and images: `['.pdf', '.doc', '.docx', '.txt', '.rtf', '.jpg', '.jpeg', '.png', '.gif']`
+
+### CODERED_PROTECTED_MEDIA_UPLOAD_BLACKLIST
+The disallowed filetypes for media upload in the form of a list of file type extensions. Defaults to `['.sh', '.exe', '.bat', '.app', '.jar', '.py', '.php']`
+
+### CODERED_FRONTEND_\*
+Various frontend settings to specify defaults and choices used in the wagtail admin related to rendering blocks, pages, and templates. By default, all CODERED_FRONTEND_\* settings are designed to work with Bootstrap 4 CSS framework, but these can be customized if using a different CSS framework or theme variant.
+
+
+
+## Developing and testing codered-cms
+To create a test project locally before committing your changes:
+
+1. Run `pip install -e ./` from the codered-cms directory. The -e flag makes the install editable, which is relevant when running makemigrations in test project to actually generate the migration files in the codered-cms pip package.
+
+2. Follow steps 3 through 5 in the quickstart above. Use "testproject" or "testapp" for your project name to ensure it is ignored by git.
+
+3. When making model or block changes whithin coderedcms, run `makemigrations coderedcms` in the test project to generate the relevant migration files for the pip package. ALWAYS follow steps 3 and 4 in the quickstart above with a fresh database before making migrations.
+
+4. When model or block changes affect the local test project (i.e. the "website" app), run `makemigrations website` in the test project to generate the relevant migration files locally. Apply and test the migrations. When satisfied, copy the new migration files to the `project_template/website/migrations/` directory.
+
+When making changes that are potentially destructive or backwards incompatible, increment the minor version number until coderedcms reaches a stable `1.0` release. Each production project that uses coderedcms should specify the appropriate version in its requirements.txt to prevent breakage.
+
+### Building pip packages
+To build a publicly consumable pip package, run:
+
+    python setup.py sdist bdist_wheel
+
+which will build a source distribution and a wheel in the `dist/` directory.

+ 27 - 0
LICENSE

@@ -0,0 +1,27 @@
+Copyright (c) CodeRed LLC and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+    3. Neither the name of CodeRed nor the names of its contributors may be used
+       to endorse or promote products derived from this software without
+       specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 4 - 0
MANIFEST.in

@@ -0,0 +1,4 @@
+include LICENSE *.md *.rst *.txt
+graft coderedcms
+global-exclude __pycache__
+global-exclude *.py[co]

+ 99 - 0
README.md

@@ -0,0 +1,99 @@
+# CodeRed CMS
+
+CodeRed CMS is a content management system built with [Wagtail](https://wagtail.io/) specifically geared towards marketing websites as a professionally-managed WordPress replacement.
+
+[Documentation](DOCS.md) | [CodeRed](https://www.coderedcorp.com/) - makers of CodeRed CMS.
+
+
+
+## Quick start
+1. Run `pip install coderedcms`
+
+2. Run `coderedcms start mysite`
+
+3. Run `python manage.py migrate` to create the core models.
+
+4. Run `python manage.py createsuperuser` to create the initial admin user.
+
+5. Run `python manage.py runserver` to launch the development server, and go to `http://localhost:8000` in your browser, or `http://localhost:8000/admin/` to log in with your admin account.
+
+See the [documentation](DOCS.md) for next steps and customizing your new site.
+
+
+
+## Why use CodeRed CMS?
+In addition to the numerous benefits of [Wagtail](https://wagtail.io/features/), CodeRed has features that are *great* for marketing websites:
+
+* **The entire editing experience is tailored for marketing content.** Create flashy hero units, callouts, and forms using a beautiful editing interface. Full SEO attributes and detailed Google Analytics tracking is enabled out of the box. Editors and Designers can easily update content, designs, and layout without fear of breakage and without the need to consult developers. Developers can fully customize the site without relying on plugins or undocumented hacks.
+
+* **Built-in SEO** including Opengraph, structured data, and many other meta tags automatically applied. A sitemap and robots.txt are also present and automatically updated. Articles and blog posts support Google’s preferred AMP format.
+
+* **Responsive design** out of the box! Editors can build content using [Bootstrap 4](https://getbootstrap.com/) components including navbars, hero units, carousels, cards, modals, and the powerful grid system.
+
+* **Fast load times** made possible by a built-in page cache. The cache automatically refreshes whenever a page is published, or by the click of a button. Cached pages load as quickly as static HTML files.
+
+* **Full Google Analytics tracking** can be turned on by adding your UA- tag. Detailed event tracking can be turned on globally and fine-tuned for each clickable element such as links, buttons, and images.
+
+* **Professionally-backed support**. Both CodeRed CMS and Wagtail (the technology powering CodeRed CMS) are produced by software companies who offer professional support and services. This is built on proven technology that successfully serves small businesses and large enterprises around the world every day. [Find a developer near you](https://madewithwagtail.org/developers/).
+
+
+
+## Roadmap
+Officially, CodeRed CMS is in a beta stage. That being said, it is currently in use on production sites. However there are still many activities that are needed before hitting a 1.0 “stable” status.
+
+Work already in progress before 1.0 release:
+
+* Higher test coverage.
+
+* Full documentation via sphynx/readthedocs.
+
+* Usability feedback and testing regarding the admin/editor experience.
+
+* Improved accessibility of the CodeRed-provided HTML templates (excluding the admin interface... this is an issue wagtail is dealing with upstream).
+
+Other future plans:
+
+* Continue adding commonly used abstract page types and blocks available out of the box(e.g. calendar/events, product page, store locator, etc.)
+
+* Continue updating and enhancing SEO/meta attributes as standards evolve.
+
+* Built-in SSO with major identity providers such as Google and Office 365.
+
+* ADA compliance enforcement features and workflows in the admin.
+
+* Light e-commerce functionality, or at least a smooth integration with an existing e-commerce framework.
+
+
+
+## Inspiration and Design Philosophy
+
+### Inspiration from WordPress
+We the creators of CodeRed CMS deal with WordPress sites on a daily basis. While WordPress is fantastic for blogs and do-it-yourself websites, we feel it is very frustrustrating for use in a professional environment where the site needs to be actively enhanced, maintained, and secured on a daily basis. We designed CodeRed CMS as a marketing-focused WordPress replacement, *not* a WordPress clone. The intended audience is an agency, technology firm, business, or non-profit who has at least one full stack web developer managing the website.
+
+WordPress users will feel comfortable with CodeRed CMS, as many of the editing and design paradigms are similar such as:
+
+* Global site and branding settings.
+
+* Main menu builder is familiar.
+
+* Editors can change the template used by each page.
+
+PLUS many aspects are greatly enhanced:
+
+* Visual content blocks eliminate need for cryptic short-codes.
+
+* Content blocks can each be customized with CSS classes and selectable templates.
+
+* Developers can easily customize the editing interface and page types without 3rd party plugins or themes.
+
+* The site can be professionally managed with better control over 3rd party plugins to prevent unplanned breakage (if you've ever managed a large WordPress site - you know exactly how painful this is).
+
+### As an Extension of Wagtail
+CodeRed CMS is a pip package that essentially wraps Wagtail and provides marketing-specific features that are ready to be used out of the box. Everything that can be done with Wagtail can be done with CodeRed.
+
+One major point of difference between between CodeRed and stock Wagtail is the approach to design and content. Wagtail being more of a CMS framework, is focused on a clear separation between design (UX) and content. We agree with this approach for larger informational sites. But as is usually the case with marketing sites, design and information are more tightly coupled. Developers shouldn’t *need* to create a new page type or a new block just to handle a design deviation that is used in one place on the site. Designers and editors shouldn’t *need* to engage the developer for every minor design-related change such as changing a CSS class. For this reason, CodeRed blurs the lines of design and content by enabling editors to specify templates on a per-page and per-block basis, CSS classes per-block, and many other logo, layout, and branding settings. We realize this is not the right approach for every site - but we do believe it adds a lot of value for marketing sites.
+
+
+
+## Contact
+We would love to hear your questions, comments, and feedback. Contact us on github or at info@coderedcorp.com.

+ 0 - 0
coderedcms/__init__.py


+ 10 - 0
coderedcms/admin_urls.py

@@ -0,0 +1,10 @@
+from django.conf.urls import include, url
+from wagtail.admin import urls as wagtailadmin_urls
+from coderedcms.views import clear_cache
+from coderedcms.settings import cr_settings
+
+
+urlpatterns = [
+    url(r'^codered/clearcache$', clear_cache, name="clear_cache"),
+    url(r'', include(wagtailadmin_urls)),
+]

+ 0 - 0
coderedcms/bin/__init__.py


+ 148 - 0
coderedcms/bin/coderedcms.py

@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+import os
+import sys
+from argparse import ArgumentParser
+
+from django.core.management import ManagementUtility
+
+
+CURRENT_PYTHON = sys.version_info[:2]
+REQUIRED_PYTHON = (3, 4)
+
+if CURRENT_PYTHON < REQUIRED_PYTHON:
+    sys.stderr.write("This version of Wagtail requires Python {}.{} or above - you are running {}.{}\n".format(*(REQUIRED_PYTHON + CURRENT_PYTHON)))
+    sys.exit(1)
+
+
+class Command:
+    description = None
+
+    def create_parser(self, command_name=None):
+        if command_name is None:
+            prog = None
+        else:
+            # hack the prog name as reported to ArgumentParser to include the command
+            prog = "%s %s" % (prog_name(), command_name)
+
+        parser = ArgumentParser(
+            description=getattr(self, 'description', None), add_help=False, prog=prog
+        )
+        self.add_arguments(parser)
+        return parser
+
+    def add_arguments(self, parser):
+        pass
+
+    def print_help(self, command_name):
+        parser = self.create_parser(command_name=command_name)
+        parser.print_help()
+
+    def execute(self, argv):
+        parser = self.create_parser()
+        options = parser.parse_args(sys.argv[2:])
+        options_dict = vars(options)
+        self.run(**options_dict)
+
+
+class CreateProject(Command):
+    description = "Creates the directory structure for a new CodeRed CMS project."
+
+    def add_arguments(self, parser):
+        parser.add_argument('project_name', help="Name for your CodeRed CMS project")
+        parser.add_argument('dest_dir', nargs='?', help="Destination directory inside which to create the project")
+
+    def run(self, project_name=None, dest_dir=None):
+        # Make sure given name is not already in use by another python package/module.
+        try:
+            __import__(project_name)
+        except ImportError:
+            pass
+        else:
+            sys.exit("'%s' conflicts with the name of an existing "
+                     "Python module and cannot be used as a project "
+                     "name. Please try another name." % project_name)
+
+        print("Creating a CodeRed CMS project called %(project_name)s" % {'project_name': project_name})  # noqa
+
+        # Create the project from the Wagtail template using startapp
+
+        # First find the path to Wagtail
+        import coderedcms
+        codered_path = os.path.dirname(coderedcms.__file__)
+        template_path = os.path.join(codered_path, 'project_template')
+
+        # Call django-admin startproject
+        utility_args = ['django-admin.py',
+                        'startproject',
+                        '--template=' + template_path,
+                        '--ext=html,rst',
+                        '--name=Dockerfile',
+                        project_name]
+
+        if dest_dir:
+            utility_args.append(dest_dir)
+
+        utility = ManagementUtility(utility_args)
+        utility.execute()
+
+        print("Success! %(project_name)s has been created" % {'project_name': project_name})  # noqa
+
+
+
+
+COMMANDS = {
+    'start': CreateProject(),
+}
+
+
+def prog_name():
+    return os.path.basename(sys.argv[0])
+
+
+def help_index():
+    print("Type '%s help <subcommand>' for help on a specific subcommand.\n" % prog_name())  # NOQA
+    print("Available subcommands:\n")  # NOQA
+    for name, cmd in sorted(COMMANDS.items()):
+        print("    %s%s" % (name.ljust(20), cmd.description))  # NOQA
+
+
+def unknown_command(command):
+    print("Unknown command: '%s'" % command)  # NOQA
+    print("Type '%s help' for usage." % prog_name())  # NOQA
+    sys.exit(1)
+
+
+def main():
+    try:
+        command_name = sys.argv[1]
+    except IndexError:
+        help_index()
+        return
+
+    if command_name == 'help':
+        try:
+            help_command_name = sys.argv[2]
+        except IndexError:
+            help_index()
+            return
+
+        try:
+            command = COMMANDS[help_command_name]
+        except KeyError:
+            unknown_command(help_command_name)
+            return
+
+        command.print_help(help_command_name)
+        return
+
+    try:
+        command = COMMANDS[command_name]
+    except KeyError:
+        unknown_command(command_name)
+        return
+
+    command.execute(sys.argv)
+
+
+if __name__ == "__main__":
+    main()

+ 62 - 0
coderedcms/blocks/__init__.py

@@ -0,0 +1,62 @@
+"""
+Blocks module entry point. Used to cleanly organize blocks into
+individual files based on purpose, but provide them all as a
+single `blocks` module.
+"""
+
+from django.utils.translation import ugettext_lazy as _
+
+from .base_blocks import * #noqa
+from .html_blocks import * #noqa
+from .metadata_blocks import * #noqa
+from .content_blocks import * #noqa
+from .layout_blocks import * #noqa
+
+
+# Collections of blocks commonly used together.
+
+HTML_STREAMBLOCKS = [
+    ('text', blocks.RichTextBlock(icon='fa-file-text-o')),
+    ('button', ButtonBlock()),
+    ('image', ImageBlock()),
+    ('image_link', ImageLinkBlock()),
+    ('html', blocks.RawHTMLBlock(icon='code', classname='monospace', label=_('HTML'))),
+    ('download', DownloadBlock()),
+    ('embed_video', EmbedVideoBlock()),
+    ('quote', QuoteBlock()),
+    ('table', TableBlock()),
+    ('google_map', EmbedGoogleMapBlock()),
+    ('code', CodeBlock()),
+]
+
+CONTENT_STREAMBLOCKS = HTML_STREAMBLOCKS + [
+    ('card', CardBlock()),
+    ('carousel', CarouselBlock()),
+    ('image_gallery', ImageGalleryBlock()),
+    ('page_list', PageListBlock()),
+    ('modal', ModalBlock(HTML_STREAMBLOCKS)),
+    ('pricelist', PriceListBlock()),
+]
+
+NAVIGATION_STREAMBLOCKS = [
+    ('page_link', NavPageLinkWithSubLinkBlock()),
+    ('external_link', NavExternalLinkWithSubLinkBlock()),
+    ('document_link', NavDocumentLinkWithSubLinkBlock()),
+]
+
+BASIC_LAYOUT_STREAMBLOCKS = [
+    ('row', GridBlock(HTML_STREAMBLOCKS)),
+    ('html', blocks.RawHTMLBlock(icon='code', classname='monospace', label=_('HTML'))),
+]
+
+LAYOUT_STREAMBLOCKS = [
+    ('row', GridBlock(CONTENT_STREAMBLOCKS)),
+    ('cardgrid', CardGridBlock([
+        ('card', CardBlock()),])
+    ),
+    ('hero', HeroBlock([
+        ('row', GridBlock(CONTENT_STREAMBLOCKS)),
+        ('html', blocks.RawHTMLBlock(icon='code', classname='monospace', label=_('HTML'))),])
+    ),
+    ('html', blocks.RawHTMLBlock(icon='code', classname='monospace', label=_('HTML'))),
+]

+ 237 - 0
coderedcms/blocks/base_blocks.py

@@ -0,0 +1,237 @@
+"""
+Bases, mixins, and utilites for blocks.
+"""
+
+from django import forms
+from django.template.loader import render_to_string
+from django.utils.encoding import force_text
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+from wagtail.core import blocks
+from wagtail.core.models import Collection
+from wagtail.documents.blocks import DocumentChooserBlock
+
+from coderedcms.settings import cr_settings
+
+
+class MultiSelectBlock(blocks.FieldBlock):
+    """
+    Renders as MultipleChoiceField, used for adding checkboxes,
+    radios, or multiselect inputs in the streamfield.
+    """
+    def __init__(self, required=True, help_text=None, choices=None, widget=None, **kwargs):
+        self.field = forms.MultipleChoiceField(
+            required=required,
+            help_text=help_text,
+            choices=choices,
+            widget=widget,
+        )
+        super().__init__(**kwargs)
+
+    def get_searchable_content(self, value):
+        return [force_text(value)]
+
+
+class CollectionChooserBlock(blocks.ChooserBlock):
+    """
+    Enables choosing a wagtail Collection in the streamfield.
+    """
+    target_model = Collection
+    widget = forms.Select
+
+    def value_for_form(self, value):
+        if isinstance(value, self.target_model):
+            return value.pk
+        return value
+
+
+class ButtonMixin(blocks.StructBlock):
+    """
+    Standard style and size options for buttons.
+    """
+    button_title = blocks.CharBlock(
+        max_length=255,
+        required=True,
+        label=_('Button Title'),
+    )
+    button_style = blocks.ChoiceBlock(
+        choices=cr_settings['FRONTEND_BTN_STYLE_CHOICES'],
+        default=cr_settings['FRONTEND_BTN_STYLE_DEFAULT'],
+        required=False,
+        label=_('Button Style'),
+    )
+    button_size = blocks.ChoiceBlock(
+        choices=cr_settings['FRONTEND_BTN_SIZE_CHOICES'],
+        default=cr_settings['FRONTEND_BTN_SIZE_DEFAULT'],
+        required=False,
+        label=_('Button Size'),
+    )
+
+
+class CoderedAdvSettings(blocks.StructBlock):
+    """
+    Common fields each block should have,
+    which are hidden under the block's "Advanced Settings" dropdown.
+    """
+    # placeholder, real value get set in __init__()
+    custom_template = blocks.Block()
+
+    custom_css_class = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Custom CSS Class'),
+    )
+    custom_id = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Custom ID'),
+    )
+
+    class Meta:
+        form_template = 'wagtailadmin/block_forms/base_block_settings_struct.html'
+        label = _('Advanced Settings')
+
+    def __init__(self, local_blocks=None, template_choices=None, **kwargs):
+        if not local_blocks:
+            local_blocks = ()
+
+        local_blocks += (
+            (
+                'custom_template',
+                blocks.ChoiceBlock(
+                    choices=template_choices,
+                    default=None,
+                    required=False,
+                    label=_('Template'))
+            ),
+        )
+
+        super().__init__(local_blocks, **kwargs)
+
+
+class CoderedAdvTrackingSettings(CoderedAdvSettings):
+    """
+    CoderedAdvSettings plus additional tracking fields.
+    """
+    ga_tracking_event_category = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Tracking Event Category'),
+    )
+    ga_tracking_event_label = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Tracking Event Label'),
+    )
+
+
+class CoderedAdvColumnSettings(CoderedAdvSettings):
+    """
+    BaseBlockSettings plus additional column fields.
+    """
+    column_breakpoint = blocks.ChoiceBlock(
+        choices=cr_settings['FRONTEND_COL_BREAK_CHOICES'],
+        default=cr_settings['FRONTEND_COL_BREAK_DEFAULT'],
+        required=False,
+        verbose_name=_('Column Breakpoint'),
+        help_text=_('Screen size at which the column will expand horizontally or stack vertically.'),
+    )
+
+
+class BaseBlock(blocks.StructBlock):
+    """
+    Common attributes for all blocks used in CodeRed CMS.
+    """
+    # subclasses can override this to determine the advanced settings class
+    advsettings_class = CoderedAdvSettings
+
+    # placeholder, real value get set in __init__() from advsettings_class
+    settings = blocks.Block()
+
+    def __init__(self, local_blocks=None, **kwargs):
+        """
+        Construct and inject settings block, then initialize normally.
+        """
+        klassname = self.__class__.__name__.lower()
+        choices = cr_settings['FRONTEND_TEMPLATES_BLOCKS'].get('*', ()) + \
+                  cr_settings['FRONTEND_TEMPLATES_BLOCKS'].get(klassname, ())
+
+        if not local_blocks:
+            local_blocks = ()
+
+        local_blocks += (('settings', self.advsettings_class(template_choices=choices)),)
+
+        super().__init__(local_blocks, **kwargs)
+
+    def render(self, value, context=None):
+        template = value['settings']['custom_template']
+
+        if not template:
+            template = self.get_template(context=context)
+            if not template:
+                return self.render_basic(value, context=context)
+
+        if context is None:
+            new_context = self.get_context(value)
+        else:
+            new_context = self.get_context(value, parent_context=dict(context))
+
+        return mark_safe(render_to_string(template, new_context))
+
+
+class BaseLayoutBlock(BaseBlock):
+    """
+    Common attributes for all blocks used in CodeRed CMS.
+    """
+    # Subclasses can override this to provide a default list of blocks for the content.
+    content_streamblocks = []
+
+    def __init__(self, local_blocks=None, **kwargs):
+        if not local_blocks and self.content_streamblocks:
+            local_blocks = self.content_streamblocks
+
+        if local_blocks:
+            local_blocks = (('content', blocks.StreamBlock(local_blocks, label=_('Content'))),)
+
+        super().__init__(local_blocks, **kwargs)
+
+
+class LinkStructValue(blocks.StructValue):
+    """
+    Generates a URL for blocks with multiple link choices.
+    """
+    @property
+    def url(self):
+        page = self.get('page_link')
+        doc = self.get('doc_link')
+        ext = self.get('other_link')
+        if page:
+            return page.url
+        elif doc:
+            return doc.url
+        else:
+            return ext
+
+class BaseLinkBlock(BaseBlock):
+    """
+    Common attributes for creating a link within the CMS.
+    """
+    page_link = blocks.PageChooserBlock(
+        required=False,
+        label=_('Page link'),
+    )
+    doc_link = DocumentChooserBlock(
+        required=False,
+        label=_('Document link'),
+    )
+    other_link = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Other link'),
+    )
+
+    advsettings_class = CoderedAdvTrackingSettings
+
+    class Meta:
+        value_class = LinkStructValue
+

+ 297 - 0
coderedcms/blocks/content_blocks.py

@@ -0,0 +1,297 @@
+"""
+Content blocks are for building complex, nested HTML structures that usually
+contain sub-blocks, and may require javascript to function properly.
+"""
+
+from django.utils.translation import ugettext_lazy as _
+from wagtail.core import blocks
+from wagtail.core.models import Page
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.snippets.blocks import SnippetChooserBlock
+
+from .base_blocks import BaseBlock, BaseLayoutBlock, ButtonMixin, CollectionChooserBlock
+from .html_blocks import ButtonBlock
+
+
+class CardBlock(BaseBlock):
+    """
+    A component of information with image, text, and buttons.
+    """
+    image = ImageChooserBlock(
+        required=False,
+        max_length=255,
+        label=_('Image'),
+    )
+    title = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Title'),
+    )
+    subtitle = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Subtitle'),
+    )
+    description = blocks.RichTextBlock(
+        features=['bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link'],
+        label=_('Body'),
+    )
+    links = blocks.StreamBlock(
+        [('Links', ButtonBlock())],
+        blank=True,
+        required=False,
+        label=_('Links'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/card_foot.html'
+        icon = 'fa-list-alt'
+        label = _('Card')
+
+
+class CarouselBlock(BaseBlock):
+    """
+    Enables choosing a Carousel snippet.
+    """
+    carousel = SnippetChooserBlock('coderedcms.Carousel')
+
+    class Meta:
+        icon = 'image'
+        label = _('Carousel')
+        template = 'coderedcms/blocks/carousel_block.html'
+
+
+class ImageGalleryBlock(BaseBlock):
+    """
+    Show a collection of images with interactive previews that expand to
+    full size images in a modal.
+    """
+    collection = CollectionChooserBlock(
+        label=_('Image Collection'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/image_gallery_block.html'
+        icon = 'image'
+        label = _('Image Gallery')
+
+
+class ModalBlock(ButtonMixin, BaseLayoutBlock):
+    """
+    Renders a button that then opens a popup/modal with content.
+    """
+    header = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Modal heading'),
+    )
+    content = blocks.StreamBlock(
+        [],
+        label=_('Modal content'),
+    )
+    footer = blocks.StreamBlock(
+        [
+            ('text', blocks.CharBlock(icon='fa-file-text-o', max_length=255, label=_('Simple Text'))),
+            ('button', ButtonBlock()),
+        ],
+        label=_('Modal footer'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/modal_block.html'
+        icon = 'fa-window-maximize'
+        label = _('Modal')
+
+
+class NavBaseLinkBlock(BaseBlock):
+    display_text = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Display text'),
+    )
+    image = ImageChooserBlock(
+        required=False,
+        label=_('Image'),
+    )
+
+
+class NavExternalLinkBlock(NavBaseLinkBlock):
+    """
+    External link.
+    """
+    link = blocks.CharBlock(
+        required=False,
+        label=_('URL'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/external_link_block.html'
+        label = _('External Link')
+
+
+class NavPageLinkBlock(NavBaseLinkBlock):
+    """
+    Page link.
+    """
+    page = blocks.PageChooserBlock(
+        label=_('Page'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/page_link_block.html'
+        label = _('Page Link')
+
+
+class NavDocumentLinkBlock(NavBaseLinkBlock):
+    """
+    Document link.
+    """
+    document = DocumentChooserBlock(
+        label=_('Document'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/document_link_block.html'
+        label = _('Document Link')
+
+
+class NavSubLinkBlock(BaseBlock):
+    """
+    Streamblock for rendering nested sub-links.
+    """
+    sub_links = blocks.StreamBlock(
+        [
+            ('page_link', NavPageLinkBlock()),
+            ('external_link', NavExternalLinkBlock()),
+            ('document_link', NavDocumentLinkBlock()),
+        ],
+        required=False,
+        label=_('Sub-links'),
+    )
+
+
+class NavExternalLinkWithSubLinkBlock(NavSubLinkBlock, NavExternalLinkBlock):
+    """
+    Extermal link with option for sub-links.
+    """
+    class Meta:
+        label = _('External link with sub-links')
+
+
+class NavPageLinkWithSubLinkBlock(NavSubLinkBlock, NavPageLinkBlock):
+    """
+    Page link with option for sub-links or showing child pages.
+    """
+    show_child_links = blocks.BooleanBlock(
+        required=False,
+        default=False,
+        label=_('Show child pages'),
+        help_text=_('Automatically show a link to the Page’s child pages as a dropdown menu.'),
+    )
+
+    class Meta:
+        label = _('Page link with sub-links')
+
+
+class NavDocumentLinkWithSubLinkBlock(NavSubLinkBlock, NavDocumentLinkBlock):
+    """
+    Document link with option for sub-links.
+    """
+    class Meta:
+        label = _('Document link with sub-links')
+
+
+class PageListBlock(BaseBlock):
+    """
+    Renders a preview of selected pages.
+    """
+    show_preview = blocks.BooleanBlock(
+        required=False,
+        default=False,
+        label=_('Show body preview'),
+    )
+    num_posts = blocks.IntegerBlock(
+        default=3,
+        label=_('Number of pages to show'),
+    )
+    indexed_by = blocks.PageChooserBlock(
+        required=False,
+        label=_('Limit to'),
+        help_text=_('Only show pages that are children of the selected page. Uses the subpage sorting as specified in the page’s LAYOUT tab.'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/pagelist_block.html'
+        icon = 'list-ul'
+        label = _('Latest Pages')
+
+    def get_context(self, value, parent_context=None):
+
+        context = super().get_context(value, parent_context=parent_context)
+
+        if value['indexed_by']:
+            indexer = value['indexed_by'].specific
+            # try to use the CoderedPage `get_index_children()`,
+            # but fall back to get_children if this is a non-CoderedPage
+            try:
+                pages = indexer.get_index_children()
+            except AttributeError:
+                pages = indexer.get_children().live()
+        else:
+            pages = Page.objects.live().order_by('-first_published_at')
+
+        context['pages'] = pages[:value['num_posts']]
+        return context
+
+
+class PriceListItemBlock(BaseBlock):
+    """
+    Represents one item in a PriceListBlock, such as an entree in a restaurant menu.
+    """
+    image = ImageChooserBlock(
+        required=False,
+        label=_('Image'),
+    )
+    name = blocks.CharBlock(
+        requred=True,
+        max_length=255,
+        label=_('Name'),
+    )
+    description = blocks.TextBlock(
+        required=False,
+        rows=4,
+        label=_('Description'),
+    )
+    price = blocks.CharBlock(
+        required=True,
+        label=_('Price'),
+        help_text=_('Any text here. Include currency sign if desired.'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/pricelistitem_block.html'
+        icon = 'fa-usd'
+        label = _('Price List Item')
+
+
+class PriceListBlock(BaseBlock):
+    """
+    A price list, such as a menu for a restaurant.
+    """
+    heading = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Heading'),
+    )
+    items = blocks.StreamBlock(
+        [
+            ('item', PriceListItemBlock()),
+        ],
+        label=_('Items'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/pricelist_block.html'
+        icon = 'fa-usd'
+        label = _('Price List')

+ 257 - 0
coderedcms/blocks/html_blocks.py

@@ -0,0 +1,257 @@
+"""
+HTML blocks are simple blocks used to represent common HTML elements,
+with additional styling and attributes.
+"""
+
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+from pygments import highlight
+from pygments.lexers import get_all_lexers, get_lexer_by_name
+from pygments.formatters import HtmlFormatter
+from wagtail.contrib.table_block.blocks import TableBlock as WagtailTableBlock
+from wagtail.core import blocks
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+
+from .base_blocks import BaseBlock, BaseLinkBlock, ButtonMixin, CoderedAdvTrackingSettings, LinkStructValue
+
+
+class ButtonBlock(ButtonMixin, BaseLinkBlock):
+    """
+    A link styled as a button.
+    """
+    class Meta:
+        template = 'coderedcms/blocks/button_block.html'
+        icon = 'fa-hand-pointer-o'
+        label = _('Button Link')
+        value_class = LinkStructValue
+
+
+class CodeBlock(BaseBlock):
+    """
+    Source code with syntax highlighting in a <pre> tag.
+    """
+    LANGUAGE_CHOICES = []
+
+    for lex in get_all_lexers():
+        LANGUAGE_CHOICES.append((lex[1][0], lex[0]))
+
+    language = blocks.ChoiceBlock(
+        required=False,
+        choices=LANGUAGE_CHOICES,
+        label=_('Syntax highlighting'),
+    )
+    title = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Title'),
+    )
+    code = blocks.TextBlock(
+        classname='monospace',
+        rows=8,
+        label=('Code'),
+        help_text=_('Code is rendered in a <pre> tag.'),
+    )
+
+    def get_context(self, value, parent_context=None):
+        ctx = super(CodeBlock, self).get_context(value, parent_context)
+
+        if value['language']:
+            src = value['code'].strip('\n')
+            lexer = get_lexer_by_name(value['language'])
+            code_html = mark_safe(highlight(src, lexer, HtmlFormatter()))
+        else:
+            code_html = format_html('<pre>{}</pre>', value['code'])
+
+        ctx.update({
+            'code_html': code_html,
+        })
+
+        return ctx
+
+    class Meta:
+        template = 'coderedcms/blocks/code_block.html'
+        icon = 'fa-file-code-o'
+        label = _('Formatted Code')
+
+
+class DownloadBlock(ButtonMixin, BaseBlock):
+    """
+    Link to a file that can be downloaded.
+    """
+    automatic_download = blocks.BooleanBlock(
+        required=False,
+        label=_('Auto download'),
+    )
+    downloadable_file = DocumentChooserBlock(
+        required=False,
+        label=_('Document link'),
+    )
+
+    advsettings_class = CoderedAdvTrackingSettings
+
+    class Meta:
+        template = 'coderedcms/blocks/download_block.html'
+        icon = 'download'
+        label = _('Download')
+
+
+class EmbedGoogleMapBlock(BaseBlock):
+    """
+    An embedded Google map in an <iframe>.
+    """
+    search = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Search query'),
+        help_text=_('Address or search term used to find your location on the map.'),
+    )
+    api_key = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('API key'),
+        help_text=_('Optional. Only required to use place ID and zoom features.')
+    )
+    place_id = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Google place ID'),
+        help_text=_('Requires API key to use place ID.')
+    )
+    map_zoom_level = blocks.IntegerBlock(
+        required=False,
+        default=14,
+        label=_('Map zoom level'),
+        help_text=_('Requires API key to use zoom. 1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings')
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/google_map.html'
+        icon = 'fa-map'
+        label = _('Google Map')
+
+
+class EmbedVideoBlock(BaseBlock):
+    """
+    An embedded video on the page in an <iframe>. Currently supports youtube and vimeo.
+    """
+    url = blocks.URLBlock(
+        required=True,
+        label=_('URL'),
+        help_text=_('Link to a YouTube or Vimeo video.'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/embed_video_block.html'
+        icon = 'media'
+        label = _('Embed Video')
+
+
+class H1Block(BaseBlock):
+    """
+    An <h1> heading.
+    """
+    text = blocks.CharBlock(
+        max_length=255,
+        label=_('Text'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/h1_block.html'
+        icon = 'fa-header'
+        label = _('Heading 1')
+
+
+class H2Block(BaseBlock):
+    """
+    An <h2> heading.
+    """
+    text = blocks.CharBlock(
+        max_length=255,
+        label=_('Text'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/h2_block.html'
+        icon = 'fa-header'
+        label = _('Heading 2')
+
+
+class H3Block(BaseBlock):
+    """
+    An <h3> heading.
+    """
+    text = blocks.CharBlock(
+        max_length=255,
+        label=_('Text'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/h3_block.html'
+        icon = 'fa-header'
+        label = _('Heading 3')
+
+
+class TableBlock(BaseBlock):
+    table = WagtailTableBlock()
+
+    class Meta:
+        template = 'coderedcms/blocks/table_block.html'
+        icon = 'fa-table'
+        label = 'Table'
+
+
+class ImageBlock(BaseBlock):
+    """
+    An <img>, by default styled responsively to fill its container.
+    """
+    image = ImageChooserBlock(
+        label=_('Image'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/image_block.html'
+        icon = 'image'
+        label = _('Image')
+
+
+class ImageLinkBlock(BaseLinkBlock):
+    """
+    An <a> with an image inside it, instead of text.
+    """
+    image = ImageChooserBlock(
+        label=_('Image'),
+    )
+    alt_text = blocks.CharBlock(
+        max_length=255,
+        required=True,
+        help_text=_('Alternate text to show if the image doesn’t load'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/image_link_block.html'
+        icon = 'image'
+        label = _('Image Link')
+        value_class = LinkStructValue
+
+
+class QuoteBlock(BaseBlock):
+    """
+    A <blockquote>.
+    """
+    text = blocks.TextBlock(
+        required=True,
+        rows=4,
+        label=_('Quote Text'),
+    )
+    author = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Author'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/quote_block.html'
+        icon = 'openquote'
+        label = _('Quote')

+ 110 - 0
coderedcms/blocks/layout_blocks.py

@@ -0,0 +1,110 @@
+"""
+Layout blocks are essentially a wrapper around content.
+e.g. rows, columns, hero units, etc.
+"""
+
+from django.utils.translation import ugettext_lazy as _
+from wagtail.core import blocks
+from wagtail.images.blocks import ImageChooserBlock
+
+from coderedcms.settings import cr_settings
+
+from .base_blocks import BaseBlock, BaseLayoutBlock, CoderedAdvColumnSettings
+
+
+### Level 1 layout blocks
+
+
+class ColumnBlock(BaseLayoutBlock):
+    """
+    Renders content in a column.
+    """
+    column_size = blocks.ChoiceBlock(
+        choices=cr_settings['FRONTEND_COL_SIZE_CHOICES'],
+        default=cr_settings['FRONTEND_COL_SIZE_DEFAULT'],
+        required=False,
+        label=_('Column size'),
+    )
+
+    advsettings_class = CoderedAdvColumnSettings
+
+    class Meta:
+        template = 'coderedcms/blocks/column_block.html'
+        icon = 'placeholder'
+        label = 'Column'
+
+
+class GridBlock(BaseLayoutBlock):
+    """
+    Renders a row of columns.
+    """
+    fluid = blocks.BooleanBlock(
+        required=False,
+        label=_('Full width'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/grid_block.html'
+        icon = 'fa-columns'
+        label = _('Responsive Grid Row')
+
+    def __init__(self, local_blocks=None, **kwargs):
+        super().__init__(
+            local_blocks = [
+                ('content', ColumnBlock(local_blocks))
+            ]
+        )
+
+class CardGridBlock(BaseLayoutBlock):
+    """
+    Renders a row of cards.
+    """
+    fluid = blocks.BooleanBlock(
+        required=False,
+        label=_('Full width'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/cardgrid_deck.html'
+        icon = 'fa-th-large'
+        label = _('Card Grid')
+
+
+class HeroBlock(BaseLayoutBlock):
+    """
+    Wrapper with color and image background options.
+    """
+
+    fluid = blocks.BooleanBlock(
+        required=False,
+        default=True,
+        label=_('Full width'),
+    )
+    is_parallax = blocks.BooleanBlock(
+        required=False,
+        label=_('Parallax Effect'),
+        help_text=_('Background images scroll slower than foreground images, creating an illusion of depth.'),
+    )
+    background_image = ImageChooserBlock(required=False)
+    tile_image = blocks.BooleanBlock(
+        required=False,
+        default=False,
+        label=_('Tile background image'),
+    )
+    background_color = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Background color'),
+        help_text=_('Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)'),
+    )
+    foreground_color = blocks.CharBlock(
+        required=False,
+        max_length=255,
+        label=_('Text color'),
+        help_text=_('Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)'),
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/hero_block.html'
+        icon = 'fa-newspaper-o'
+        label = 'Hero Unit'

+ 88 - 0
coderedcms/blocks/metadata_blocks.py

@@ -0,0 +1,88 @@
+"""
+JSON and meta-data blocks, primarily used for SEO purposes.
+"""
+
+import json
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from wagtail.core import blocks
+
+from coderedcms import schema
+
+from .base_blocks import MultiSelectBlock
+
+
+class OpenHoursValue(blocks.StructValue):
+    """
+    Renders selected days as a json list.
+    """
+    @property
+    def days_json(self):
+        """
+        Custom property to return days as json list instead of default python list.
+        """
+        return json.dumps(self['days'])
+
+class OpenHoursBlock(blocks.StructBlock):
+    """
+    Holds day and time combination for business open hours.
+    """
+    days = MultiSelectBlock(
+        required=True,
+        verbose_name=_('Days'),
+        help_text=_('For late night hours past 23:59, define each day in a separate block.'),
+        widget=forms.CheckboxSelectMultiple,
+        choices=(
+            ('Monday', _('Monday')),
+            ('Tuesday', _('Tuesday')),
+            ('Wednesday', _('Wednesday')),
+            ('Thursday', _('Thursday')),
+            ('Friday', _('Friday')),
+            ('Saturday', _('Saturday')),
+            ('Sunday', _('Sunday')),
+        ))
+    start_time = blocks.TimeBlock(verbose_name=_('Opening time'))
+    end_time = blocks.TimeBlock(verbose_name=_('Closing time'))
+
+    class Meta:
+        template = 'coderedcms/blocks/struct_data_hours.json'
+        label = _('Open Hours')
+        value_class = OpenHoursValue
+
+
+class StructuredDataActionBlock(blocks.StructBlock):
+    """
+    Action object from schema.org
+    """
+    action_type = blocks.ChoiceBlock(
+        verbose_name=_('Action Type'),
+        required=True,
+        choices=schema.SCHEMA_ACTION_CHOICES
+    )
+    target = blocks.URLBlock(verbose_name=_('Target URL'))
+    language = blocks.CharBlock(
+        verbose_name=_('Language'),
+        help_text=_('If the action is offered in multiple languages, create separate actions for each language.'),
+        default='en-US'
+    )
+    result_type = blocks.ChoiceBlock(
+        required=False,
+        verbose_name=_('Result Type'),
+        help_text=_('Leave blank for OrderAction'),
+        choices=schema.SCHEMA_RESULT_CHOICES
+    )
+    result_name = blocks.CharBlock(
+        required=False,
+        verbose_name=_('Result Name'),
+        help_text=_('Example: "Reserve a table", "Book an appointment", etc.')
+    )
+    extra_json = blocks.RawHTMLBlock(
+        required=False,
+        verbose_name=_('Additional action markup'),
+        classname='monospace',
+        help_text=_('Additional JSON-LD inserted into the Action dictionary. Must be properties of https://schema.org/Action.')
+    )
+
+    class Meta:
+        template = 'coderedcms/blocks/struct_data_action.json'
+        label = _('Action')

+ 158 - 0
coderedcms/forms.py

@@ -0,0 +1,158 @@
+"""
+Enhancements to wagtail.contrib.forms.
+"""
+import csv
+import os
+from django import forms
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.shortcuts import render
+from wagtail.contrib.forms.views import SubmissionsListView as WagtailSubmissionsListView
+from wagtail.contrib.forms.forms import FormBuilder
+from wagtail.contrib.forms.models import AbstractFormField
+
+from coderedcms.settings import cr_settings
+
+
+FORM_FIELD_CHOICES = (
+    (_('Text'), (
+        ('singleline', _('Single line text')),
+        ('multiline', _('Multi-line text')),
+        ('email', _('Email')),
+        ('number', _('Number - only allows integers')),
+        ('url', _('URL')),
+    ),),
+    (_('Choice'), (
+        ('checkboxes', _('Checkboxes')),
+        ('dropdown', _('Drop down')),
+        ('radio', _('Radio buttons')),
+        ('multiselect', _('Multiple select')),
+        ('checkbox', _('Single checkbox')),
+    ),),
+    (_('Date & Time'), (
+        ('date', _('Date')),
+        ('time', _('Time')),
+        ('datetime', _('Date and time')),
+    ),),
+    (_('File Upload'), (
+        ('file', _('Secure File - login required to access uploaded files')),
+    ),),
+    (_('Other'), (
+        ('hidden', _('Hidden field')),
+    ),),
+)
+
+
+### Files
+
+class SecureFileField(forms.FileField):
+    custom_error_messages = {
+        'blacklist_file': _('Submitted file is not allowed.'),
+        'whitelist_file': _('Submitted file is not allowed.')
+    }
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.error_messages.update(self.custom_error_messages)
+
+    def validate(self, value):
+        super(SecureFileField, self).validate(value)
+        if value:
+            self._check_whitelist(value)
+            self._check_blacklist(value)
+
+    def _check_whitelist(self, value):
+        if cr_settings['PROTECTED_MEDIA_UPLOAD_WHITELIST']:
+            if os.path.splitext(value.name)[1].lower() not in cr_settings['PROTECTED_MEDIA_UPLOAD_WHITELIST']:
+                raise ValidationError(self.error_messages['whitelist_file'])
+
+    def _check_blacklist(self, value):
+        if cr_settings['PROTECTED_MEDIA_UPLOAD_BLACKLIST']:
+            if os.path.splitext(value.name)[1].lower() in cr_settings['PROTECTED_MEDIA_UPLOAD_BLACKLIST']:
+                raise ValidationError(self.error_messages['blacklist_file'])
+
+
+### Date
+
+class CoderedDateInput(forms.DateInput):
+    template_name = 'coderedcms/formfields/date.html'
+
+class CoderedDateField(forms.DateField):
+    widget = CoderedDateInput()
+
+
+### Datetime
+
+class CoderedDateTimeInput(forms.DateTimeInput):
+    template_name = 'coderedcms/formfields/datetime.html'
+
+class CoderedDateTimeField(forms.DateTimeField):
+    widget = CoderedDateTimeInput()
+    input_formats = ['%Y-%m-%dT%H:%M', '%m/%d/%Y %I:%M %p', '%m/%d/%Y %I:%M%p', '%m/%d/%Y %H:%M']
+
+
+### Time
+
+class CoderedTimeInput(forms.TimeInput):
+    template_name = 'coderedcms/formfields/time.html'
+
+class CoderedTimeField(forms.TimeField):
+    widget = CoderedTimeInput()
+    input_formats = ['%H:%M', '%I:%M %p', '%I:%M%p']
+
+
+class CoderedFormBuilder(FormBuilder):
+    """
+    Enhance wagtail FormBuilder with additional custom fields.
+    """
+
+    def create_file_field(self, field, options):
+        return SecureFileField(**options)
+
+    def create_date_field(self, field, options):
+        return CoderedDateField(**options)
+
+    def create_datetime_field(self, field, options):
+        return CoderedDateTimeField(**options)
+
+    def create_time_field(self, field, options):
+        return CoderedTimeField(**options)
+
+
+class CoderedSubmissionsListView(WagtailSubmissionsListView):
+    def get_csv_response(self, context):
+        filename = self.get_csv_filename()
+        response = HttpResponse(content_type='text/csv; charset=utf-8')
+        response['Content-Disposition'] = 'attachment;filename={}'.format(filename)
+
+        writer = csv.writer(response)
+        writer.writerow(context['data_headings'])
+        for data_row in context['data_rows']:
+            modified_data_row = []
+            for cell in data_row:
+                modified_cell = utils.attempt_protected_media_value_conversion(self.request, cell)
+                modified_data_row.append(modified_cell)
+
+            writer.writerow(modified_data_row)
+        return response
+
+
+class CoderedFormField(AbstractFormField):
+    class Meta:
+        abstract = True
+
+    field_type = models.CharField(verbose_name=_('field type'), max_length=16, choices=FORM_FIELD_CHOICES, blank=True)
+
+
+class SearchForm(forms.Form):
+    s = forms.CharField(
+        max_length=255,
+        required=False,
+        label=_('Search'),
+    )
+    t = forms.CharField(
+        widget=forms.HiddenInput,
+        max_length=255,
+        required=False,
+        label=_('Page type'),
+    )

File diff suppressed because it is too large
+ 69 - 0
coderedcms/migrations/0001_initial.py


+ 0 - 0
coderedcms/migrations/__init__.py


+ 9 - 0
coderedcms/models/__init__.py

@@ -0,0 +1,9 @@
+"""
+Models module entry point. Used to cleanly organize various models
+into files based on their purpose, but provide them all via
+a single `models` module.
+"""
+
+from .page_models import * #noqa
+from .snippet_models import * #noqa
+from .wagtailsettings_models import * #noqa

+ 1023 - 0
coderedcms/models/page_models.py

@@ -0,0 +1,1023 @@
+"""
+Base and abstract pages used in CodeRed CMS.
+"""
+
+import json
+import os
+from django.conf import settings
+from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
+from django.core.files.storage import FileSystemStorage
+from django.core.mail import send_mail, EmailMessage
+from django.core.paginator import Paginator
+from django.core.serializers.json import DjangoJSONEncoder
+from django.db import models
+from django.shortcuts import render, redirect
+from django.template import Context, Template
+from django.utils import timezone
+from django.utils.html import strip_tags
+from django.utils.translation import ugettext_lazy as _
+from wagtail.admin.edit_handlers import (
+    HelpPanel,
+    FieldPanel,
+    FieldRowPanel,
+    InlinePanel,
+    MultiFieldPanel,
+    ObjectList,
+    PageChooserPanel,
+    StreamFieldPanel,
+    TabbedInterface)
+from wagtail.core.fields import StreamField
+from wagtail.core.models import PageBase, Page, Site
+from wagtail.core.utils import resolve_model_string
+from wagtail.contrib.forms.edit_handlers import FormSubmissionsPanel
+from wagtail.contrib.forms.forms import WagtailAdminFormPageForm
+from wagtail.images.edit_handlers import ImageChooserPanel
+from wagtail.contrib.forms.models import FormSubmission
+from wagtail.search import index
+
+from coderedcms import schema, utils
+from coderedcms.blocks import (
+    CONTENT_STREAMBLOCKS,
+    LAYOUT_STREAMBLOCKS,
+    OpenHoursBlock,
+    StructuredDataActionBlock)
+from coderedcms.forms import CoderedFormBuilder, CoderedSubmissionsListView
+from coderedcms.models.wagtailsettings_models import GeneralSettings, LayoutSettings, SeoSettings
+from coderedcms.settings import cr_settings
+
+
+CODERED_PAGE_MODELS = []
+
+
+def get_page_models():
+    return CODERED_PAGE_MODELS
+
+
+class CoderedPageMeta(PageBase):
+    def __init__(cls, name, bases, dct):
+        super().__init__(name, bases, dct)
+        if 'amp_template' not in dct:
+            cls.amp_template = None
+        if 'search_db_include' not in dct:
+            cls.search_db_include = False
+        if 'search_db_boost' not in dct:
+            cls.search_db_boost = 0
+        if 'search_filterable' not in dct:
+            cls.search_filterable = False
+        if 'search_name' not in dct:
+            cls.search_name = cls._meta.verbose_name
+        if 'search_name_plural' not in dct:
+            cls.search_name_plural = cls._meta.verbose_name_plural
+        if 'search_template' not in dct:
+            cls.search_template = 'coderedcms/pages/search_result.html'
+        if not cls._meta.abstract:
+            CODERED_PAGE_MODELS.append(cls)
+
+
+class CoderedPage(Page, metaclass=CoderedPageMeta):
+    """
+    General use page with caching, templating, and SEO functionality.
+    All pages should inherit from this.
+    """
+    class Meta:
+        verbose_name = _('CodeRed Page')
+
+    # Do not allow this page type to be created in wagtail admin
+    is_creatable = False
+
+    # Templates
+    # The page will render the following templates under certain conditions:
+    #
+    # template = ''
+    # amp_template = ''
+    # ajax_template = ''
+    # search_template = ''
+
+
+    ###############
+    # Content fields
+    ###############
+    cover_image = models.ForeignKey(
+        'wagtailimages.Image',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name='+',
+        verbose_name=_('Cover image'),
+    )
+
+
+    ###############
+    # Index fields
+    ###############
+
+    # Subclasses can override this to enabled index features by default.
+    index_show_subpages_default = False
+
+    # Subclasses can override this to query on a specific
+    # page model, rather than the default wagtail Page.
+    index_query_pagemodel = 'wagtailcore.Page'
+
+    # Subclasses can override these fields to enable custom
+    # ordering based on specific subpage fields.
+    index_order_by_default = '-first_published_at'
+    index_order_by_choices = (
+        ('-first_published_at', _('Date first published, newest to oldest')),
+        ('first_published_at', _('Date first published, oldest to newest')),
+        ('-last_published_at', _('Date updated, newest to oldest')),
+        ('last_published_at', _('Date updated, oldest to newest')),
+        ('title', _('Title, alphabetical')),
+        ('-title', _('Title, reverse alphabetical')),
+    )
+
+    index_show_subpages = models.BooleanField(
+        default=index_show_subpages_default,
+        verbose_name=_('Show list of child pages')
+    )
+    index_order_by = models.CharField(
+        max_length=255,
+        choices=index_order_by_choices,
+        default=index_order_by_default,
+        verbose_name=_('Order child pages by'),
+    )
+    index_num_per_page = models.PositiveIntegerField(
+        default=10,
+        verbose_name=_('Number per page'),
+    )
+
+
+    ###############
+    # Layout fields
+    ###############
+
+    custom_template = models.CharField(
+        blank=True,
+        max_length=255,
+        choices=None,
+        verbose_name=_('Template')
+    )
+
+
+    ###############
+    # SEO fields
+    ###############
+
+    og_image = models.ForeignKey(
+        'wagtailimages.Image',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name='+',
+        verbose_name=_('Open Graph preview image'),
+        help_text=_('The image shown when linking to this page on social media. If blank, defaults to article cover image, or logo in Settings > Layout > Logo')
+    )
+    struct_org_type = models.CharField(
+        default='',
+        blank=True,
+        max_length=255,
+        choices=schema.SCHEMA_ORG_CHOICES,
+        verbose_name=_('Organization type'),
+        help_text=_('If blank, no structured data will be used on this page.')
+    )
+    struct_org_name = models.CharField(
+        default='',
+        blank=True,
+        max_length=255,
+        verbose_name=_('Organization name'),
+        help_text=_('Leave blank to use the site name in Settings > Sites')
+    )
+    struct_org_logo = models.ForeignKey(
+        'wagtailimages.Image',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name='+',
+        verbose_name=_('Organization logo'),
+        help_text=_('Leave blank to use the logo in Settings > Layout > Logo')
+    )
+    struct_org_image = models.ForeignKey(
+        'wagtailimages.Image',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name='+',
+        verbose_name=_('Photo of Organization'),
+        help_text=_('A photo of the facility. This photo will be cropped to 1:1, 4:3, and 16:9 aspect ratios automatically.')
+    )
+    struct_org_phone = models.CharField(
+        blank=True,
+        max_length=255,
+        verbose_name=_('Telephone number'),
+        help_text=_('Include country code for best results. For example: +1-216-555-8000')
+    )
+    struct_org_address_street = models.CharField(
+        blank=True,
+        max_length=255,
+        verbose_name=_('Street address'),
+        help_text=_('House number and street. For example, 55 Public Square Suite 1710')
+    )
+    struct_org_address_locality = models.CharField(
+        blank=True,
+        max_length=255,
+        verbose_name=_('City'),
+        help_text=_('City or locality. For example, Cleveland')
+    )
+    struct_org_address_region = models.CharField(
+        blank=True,
+        max_length=255,
+        verbose_name=_('State'),
+        help_text=_('State, province, county, or region. For example, OH')
+    )
+    struct_org_address_postal = models.CharField(
+        blank=True,
+        max_length=255,
+        verbose_name=_('Postal code'),
+        help_text=_('Zip or postal code. For example, 44113')
+    )
+    struct_org_address_country = models.CharField(
+        blank=True,
+        max_length=255,
+        verbose_name=_('Country'),
+        help_text=_('For example, USA. Two-letter ISO 3166-1 alpha-2 country code is also acceptible https://en.wikipedia.org/wiki/ISO_3166-1')
+    )
+    struct_org_geo_lat = models.DecimalField(
+        blank=True,
+        null=True,
+        max_digits=10,
+        decimal_places=8,
+        verbose_name=_('Geographic latitude')
+    )
+    struct_org_geo_lng = models.DecimalField(
+        blank=True,
+        null=True,
+        max_digits=10,
+        decimal_places=8,
+        verbose_name=_('Geographic longitude')
+    )
+    struct_org_hours = StreamField(
+        [
+            ('hours', OpenHoursBlock()),
+        ],
+        blank=True,
+        verbose_name=_('Hours of operation')
+    )
+    struct_org_actions = StreamField(
+        [
+            ('actions', StructuredDataActionBlock())
+        ],
+        blank=True,
+        verbose_name=_('Actions')
+    )
+    struct_org_extra_json = models.TextField(
+        blank=True,
+        verbose_name=_('Additional Organization markup'),
+        help_text=_('Additional JSON-LD inserted into the Organization dictionary. Must be properties of https://schema.org/Organization or the selected organization type.')
+    )
+
+
+    ###############
+    # Search
+    ###############
+
+    search_fields = [
+        index.SearchField('title', partial_match=True, boost=3),
+        index.SearchField('seo_title', partial_match=True, boost=3),
+        index.SearchField('search_description', boost=2),
+        index.FilterField('title'),
+        index.FilterField('id'),
+        index.FilterField('live'),
+        index.FilterField('owner'),
+        index.FilterField('content_type'),
+        index.FilterField('path'),
+        index.FilterField('depth'),
+        index.FilterField('locked'),
+        index.FilterField('first_published_at'),
+        index.FilterField('last_published_at'),
+        index.FilterField('latest_revision_created_at'),
+        index.FilterField('index_show_subpages'),
+        index.FilterField('index_order_by'),
+        index.FilterField('custom_template'),
+    ]
+
+    ###############
+    # Panels
+    ###############
+
+    content_panels = (
+        Page.content_panels +
+        [
+            ImageChooserPanel('cover_image'),
+        ]
+    )
+
+    layout_panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel('custom_template')
+            ],
+            heading=_('Visual Design')
+        ),
+        MultiFieldPanel(
+            [
+                FieldPanel('index_show_subpages'),
+                FieldPanel('index_num_per_page'),
+                FieldPanel('index_order_by'),
+            ],
+            heading=_('Show Child Pages')
+        )
+    ]
+
+    promote_panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel('slug'),
+                FieldPanel('seo_title'),
+                FieldPanel('search_description'),
+                ImageChooserPanel('og_image'),
+            ],
+            _('Page Meta Data')
+        ),
+        MultiFieldPanel(
+            [
+                HelpPanel(
+                    heading=_('About Organization Structured Data'),
+                    content=_("""The fields below help define brand, contact, and storefront
+                    information to search engines. This information should be filled out on
+                    the site’s root page (Home Page). If your organization has multiple locations,
+                    then also fill this info out on each location page using that particular
+                    location’s info."""),
+                ),
+                FieldPanel('struct_org_type'),
+                FieldPanel('struct_org_name'),
+                ImageChooserPanel('struct_org_logo'),
+                ImageChooserPanel('struct_org_image'),
+                FieldPanel('struct_org_phone'),
+                FieldPanel('struct_org_address_street'),
+                FieldPanel('struct_org_address_locality'),
+                FieldPanel('struct_org_address_region'),
+                FieldPanel('struct_org_address_postal'),
+                FieldPanel('struct_org_address_country'),
+                FieldPanel('struct_org_geo_lat'),
+                FieldPanel('struct_org_geo_lng'),
+                StreamFieldPanel('struct_org_hours'),
+                StreamFieldPanel('struct_org_actions'),
+                FieldPanel('struct_org_extra_json'),
+            ],
+            _('Structured Data - Organization')
+        ),
+    ]
+
+    settings_panels = Page.settings_panels
+
+    def __init__(self, *args, **kwargs):
+        """
+        Inject custom choices and defalts into the form fields
+        to enable customization by subclasses.
+        """
+        super().__init__(*args, **kwargs)
+
+        klassname = self.__class__.__name__.lower()
+        template_choices = cr_settings['FRONTEND_TEMPLATES_PAGES'].get('*', ()) + \
+                           cr_settings['FRONTEND_TEMPLATES_PAGES'].get(klassname, ())
+
+
+        self._meta.get_field('index_order_by').choices = self.index_order_by_choices
+        self._meta.get_field('custom_template').choices = template_choices
+        if not self.id:
+            self.index_order_by = self.index_order_by_default
+            self.index_show_subpages = self.index_show_subpages_default
+
+    @classmethod
+    def get_edit_handler(cls):
+        """
+        Override to "lazy load" the panels overriden by subclasses.
+        """
+        return TabbedInterface([
+            ObjectList(cls.content_panels, heading='Content'),
+            ObjectList(cls.layout_panels, heading='Layout'),
+            ObjectList(cls.promote_panels, heading='SEO', classname="seo"),
+            ObjectList(cls.settings_panels, heading='Settings', classname="settings"),
+        ]).bind_to_model(cls)
+
+    def get_struct_org_name(self):
+        """
+        Gets org name for sturctured data using a fallback.
+        """
+        if self.struct_org_name:
+            return self.struct_org_name
+        return self.get_site().site_name
+
+    def get_struct_org_logo(self):
+        """
+        Gets logo for structured data using a fallback.
+        """
+        if self.struct_org_logo:
+            return self.struct_org_logo
+        else:
+            layout_settings = LayoutSettings.for_site(self.get_site())
+            if layout_settings.logo:
+                return layout_settings.logo
+        return None
+
+    def get_template(self, request, *args, **kwargs):
+        """
+        Override parent to serve different templates based on querystring.
+        """
+        if 'amp' in request.GET and hasattr(self, 'amp_template'):
+            seo_settings = SeoSettings.for_site(request.site)
+            if seo_settings.amp_pages:
+                if request.is_ajax():
+                    return self.ajax_template or self.amp_template
+                return self.amp_template
+
+        if self.custom_template:
+            return self.custom_template
+
+        return super(CoderedPage, self).get_template(request, args, kwargs)
+
+    def get_index_children(self):
+        """
+        Override to return query of subpages as defined by `index_` variables.
+        """
+        if self.index_query_pagemodel:
+            querymodel = resolve_model_string(self.index_query_pagemodel, self._meta.app_label)
+            return querymodel.objects.child_of(self).order_by(self.index_order_by)
+
+        return super().get_children().live()
+
+    def get_context(self, request, *args, **kwargs):
+        """
+        Add child pages and paginated child pages to context.
+        """
+        context = super().get_context(request)
+
+        if self.index_show_subpages:
+            all_children = self.get_index_children()
+            paginator = Paginator(all_children, self.index_num_per_page)
+            page = request.GET.get('p', 1)
+            try:
+                paged_children = paginator.page(page)
+            except:
+                paged_children = paginator.page(1)
+
+            context['index_paginated'] = paged_children
+            context['index_children'] = all_children
+
+        return context
+
+
+###############################################################################
+# Abstract pages providing pre-built common website functionality, suitable for subclassing.
+# These are abstract so subclasses can override fields if desired.
+###############################################################################
+
+
+class CoderedWebPage(CoderedPage):
+    """
+    Provides a body and body-related functionality.
+    This is abstract so that subclasses can override the body StreamField.
+    """
+    class Meta:
+        verbose_name = _('CodeRed Web Page')
+        abstract = True
+
+    template = 'coderedcms/pages/web_page.html'
+
+    # Child pages should override based on what blocks they want in the body.
+    # Default is LAYOUT_STREAMBLOCKS which is the fullest editor experience.
+    body = StreamField(LAYOUT_STREAMBLOCKS, null=True, blank=True)
+
+    # Search fields
+    search_fields = (
+        CoderedPage.search_fields +
+        [index.SearchField('body')]
+    )
+
+    # Panels
+    content_panels = (
+        CoderedPage.content_panels +
+        [StreamFieldPanel('body'),]
+    )
+
+    @property
+    def body_preview(self):
+        """
+        A shortened, non-HTML version of the body.
+        """
+        # add spaces between tags for legibility
+        body = str(self.body).replace('>', '> ')
+        # strip tags
+        body = strip_tags(body)
+        # truncate and add ellipses
+        return body[:200] + "..."
+
+
+class CoderedArticlePage(CoderedWebPage):
+    """
+    Article, suitable for news or blog content.
+    """
+    class Meta:
+        verbose_name = _('CodeRed Article')
+        abstract = True
+
+    template = 'coderedcms/pages/article_page.html'
+    amp_template = 'coderedcms/pages/article_page.amp.html'
+
+    # Override body to provide simpler content
+    body = StreamField(CONTENT_STREAMBLOCKS, null=True, blank=True)
+
+    caption = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Caption'),
+    )
+    author = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        null=True,
+        blank=True,
+        editable=True,
+        on_delete=models.SET_NULL,
+        related_name='articles',
+        verbose_name=_('Author'),
+    )
+    author_display = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Display author as'),
+        help_text=_('Override how the author’s name displays on this article.'),
+    )
+    date_display = models.DateField(
+        null=True,
+        blank=True,
+        verbose_name=_('Display publish date'),
+    )
+
+    def get_author_name(self):
+        """
+        Gets author name using a fallback.
+        """
+        if self.author_display:
+            return self.author_display
+        if self.author:
+            return self.author.get_full_name()
+        return ''
+
+    def get_pub_date(self):
+        """
+        Gets published date.
+        """
+        if self.date_display:
+            return self.date_display
+        return ''
+
+    def get_description(self):
+        """
+        Gets the description using a fallback.
+        """
+        if self.search_description:
+            return self.search_description
+        if self.caption:
+            return self.caption
+        if self.body_preview:
+            return self.body_preview
+        return ''
+
+    search_fields = (
+        CoderedWebPage.search_fields +
+        [
+            index.SearchField('caption', boost=2),
+            index.FilterField('author'),
+            index.FilterField('author_display'),
+            index.FilterField('date_display'),
+        ]
+    )
+
+    content_panels = (
+        CoderedWebPage.content_panels +
+        [
+            MultiFieldPanel(
+                [
+                    FieldPanel('caption'),
+                ],
+                _('Additional Content')
+            ),
+            MultiFieldPanel(
+                [
+                    FieldPanel('author'),
+                    FieldPanel('author_display'),
+                    FieldPanel('date_display'),
+                ],
+                _('Publication Info')
+            )
+        ]
+    )
+
+
+class CoderedArticleIndexPage(CoderedWebPage):
+    """
+    Shows a list of article sub-pages.
+    """
+    class Meta:
+        verbose_name = _('CodeRed Article Index Page')
+        abstract = True
+
+    template = 'coderedcms/pages/article_index_page.html'
+
+    index_show_subpages_default = True
+
+    show_images = models.BooleanField(
+        default=True,
+        verbose_name=_('Show images'),
+    )
+    show_captions = models.BooleanField(
+        default=True,
+    )
+    show_meta = models.BooleanField(
+        default=True,
+        verbose_name=_('Show author and date info'),
+    )
+    show_preview_text = models.BooleanField(
+        default=True,
+        verbose_name=_('Show preview text'),
+    )
+
+    layout_panels = (
+        CoderedWebPage.layout_panels +
+        [
+            MultiFieldPanel(
+                [
+                    FieldPanel('show_images'),
+                    FieldPanel('show_captions'),
+                    FieldPanel('show_meta'),
+                    FieldPanel('show_preview_text'),
+                ],
+                heading=_('Child page display')
+            ),
+        ]
+    )
+
+
+class CoderedFormPage(CoderedWebPage):
+    """
+    This is basically a clone of wagtail.contrib.forms.models.AbstractForm
+    with changes in functionality and extending CoderedWebPage vs wagtailcore.Page.
+    """
+    class Meta:
+        verbose_name = _('CodeRed Form Page')
+        abstract = True
+
+    template = 'coderedcms/pages/form_page.html'
+    landing_page_template = 'coderedcms/pages/form_page_landing.html'
+
+    base_form_class = WagtailAdminFormPageForm
+
+    form_builder = CoderedFormBuilder
+
+    submissions_list_view_class = CoderedSubmissionsListView
+
+    ### Custom codered fields
+
+    to_address = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Email form submissions to'),
+        help_text=_('Optional - email form submissions to this address. Separate multiple addresses by comma.')
+    )
+    subject = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Subject'),
+    )
+    save_to_database = models.BooleanField(
+        default=True,
+        verbose_name=_('Save form submissions'),
+        help_text=_('Submissions are saved to database and can be exported at any time.')
+    )
+    thank_you_page = models.ForeignKey(
+        'wagtailcore.Page',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name='+',
+        verbose_name=_('Thank you page'),
+        help_text=_('The page users are redirected to after submitting the form.'),
+    )
+    button_text = models.CharField(
+        max_length=255,
+        default=_('Submit'),
+        verbose_name=_('Button text'),
+    )
+    button_style = models.CharField(
+        blank=True,
+        choices=cr_settings['FRONTEND_BTN_STYLE_CHOICES'],
+        default=cr_settings["FRONTEND_BTN_STYLE_DEFAULT"],
+        max_length=255,
+        verbose_name=_('Button style'),
+    )
+    button_size = models.CharField(
+        blank=True,
+        choices=cr_settings['FRONTEND_BTN_SIZE_CHOICES'],
+        default=cr_settings["FRONTEND_BTN_SIZE_DEFAULT"],
+        max_length=255,
+        verbose_name=_('Button Size'),
+    )
+    button_css_class = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Button CSS class'),
+        help_text=_('Custom CSS class applied to the submit button.'),
+    )
+    form_css_class = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Form CSS Class'),
+        help_text=_('Custom CSS class applied to <form> element.'),
+    )
+    form_id = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Form ID'),
+        help_text=_('Custom ID applied to <form> element.'),
+    )
+    form_golive_at = models.DateTimeField(
+        blank=True,
+        null=True,
+        verbose_name=_('Form go live date/time'),
+        help_text=_('Date and time when the FORM goes live on the page.'),
+    )
+    form_expire_at = models.DateTimeField(
+        blank=True,
+        null=True,
+        verbose_name=_('Form expiry date/time'),
+        help_text=_('Date and time when the FORM will no longer be available on the page.'),
+    )
+
+    content_panels = (
+        CoderedWebPage.content_panels +
+        [
+            FormSubmissionsPanel(),
+            InlinePanel('form_fields', label="Form fields"),
+            MultiFieldPanel(
+                [
+                    PageChooserPanel('thank_you_page'),
+                    FieldPanel('button_text'),
+                    FieldPanel('button_style'),
+                    FieldPanel('button_size'),
+                    FieldPanel('button_css_class'),
+                    FieldPanel('form_css_class'),
+                    FieldPanel('form_id'),
+                ],
+                _('Form Settings')
+            ),
+            MultiFieldPanel(
+                [
+                    FieldPanel('save_to_database'),
+                    FieldPanel('to_address'),
+                    FieldPanel('subject'),
+                ],
+                _('Form Submissions')
+            ),
+            InlinePanel('confirmation_emails', label=_('Confirmation Emails'))
+        ]
+    )
+
+    settings_panels = (
+        CoderedPage.settings_panels +
+        [
+            MultiFieldPanel(
+                [
+                    FieldRowPanel(
+                        [
+                            FieldPanel('form_golive_at'),
+                            FieldPanel('form_expire_at'),
+                        ],
+                        classname='label-above',
+                    ),
+                ],
+                _('Form Scheduled Publishing'),
+            )
+        ]
+    )
+
+    @property
+    def form_live(self):
+        """
+        A boolean on whether or not the <form> element should be shown on the page.
+        """
+        return (self.form_golive_at is None or self.form_golive_at <= timezone.now()) and \
+               (self.form_expire_at is None or self.form_expire_at >= timezone.now())
+
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        if not hasattr(self, 'landing_page_template'):
+            name, ext = os.path.splitext(self.template)
+            self.landing_page_template = name + '_landing' + ext
+
+    def get_form_fields(self):
+        """
+        Form page expects `form_fields` to be declared.
+        If you want to change backwards relation name,
+        you need to override this method.
+        """
+
+        return self.form_fields.all()
+
+    def get_data_fields(self):
+        """
+        Returns a list of tuples with (field_name, field_label).
+        """
+
+        data_fields = [
+            ('submit_time', _('Submission date')),
+        ]
+        data_fields += [
+            (field.clean_name, field.label)
+            for field in self.get_form_fields()
+        ]
+
+        return data_fields
+
+    def get_form_class(self):
+        fb = self.form_builder(self.get_form_fields())
+        return fb.get_form_class()
+
+    def get_form_parameters(self):
+        return {}
+
+    def get_form(self, *args, **kwargs):
+        form_class = self.get_form_class()
+        form_params = self.get_form_parameters()
+        form_params.update(kwargs)
+
+        return form_class(*args, **form_params)
+
+    def get_landing_page_template(self, request, *args, **kwargs):
+        return self.landing_page_template
+
+    def get_submission_class(self):
+        """
+        Returns submission class.
+
+        You can override this method to provide custom submission class.
+        Your class must be inherited from AbstractFormSubmission.
+        """
+
+        return FormSubmission
+
+    def process_form_submission(self, request, form):
+        """
+        Accepts form instance with submitted data, user and page.
+        Creates submission instance.
+
+        You can override this method if you want to have custom creation logic.
+        For example, if you want to save reference to a user.
+        """
+        processed_data = {}
+
+        # Handle file uploads
+        for key, val in form.cleaned_data.items():
+            if type(val) == InMemoryUploadedFile or type(val) == TemporaryUploadedFile:
+                # Save the file and get its URL
+                file_system = FileSystemStorage(
+                    location=cr_settings['PROTECTED_MEDIA_ROOT'],
+                    base_url=cr_settings['PROTECTED_MEDIA_URL']
+                )
+                filename = file_system.save(file_system.get_valid_name(val.name), val)
+                processed_data[key] = file_system.url(filename)
+            else:
+                processed_data[key] = val
+
+        # Get submission
+        form_submission = self.get_submission_class()(
+            form_data=json.dumps(processed_data, cls=DjangoJSONEncoder),
+            page=self,
+        )
+
+        # Save to database
+        if self.save_to_database:
+            form_submission.save()
+
+        # Send the mails
+        if self.to_address:
+            self.send_summary_mail(request, form, processed_data)
+
+        if self.confirmation_emails:
+            for email in self.confirmation_emails.all():
+                from_address = email.from_address
+
+                if from_address == '':
+                    from_address = GeneralSettings.for_site(request.site).from_email_address
+
+                template_body = Template(email.body)
+                template_to = Template(email.to_address)
+                template_from_email = Template(from_address)
+                template_cc = Template(email.cc_address)
+                template_bcc = Template(email.bcc_address)
+                template_subject = Template(email.subject)
+                context = Context(self.data_to_dict(processed_data))
+
+                message = EmailMessage(
+                    body=template_body.render(context),
+                    to=template_to.render(context).split(','),
+                    from_email=template_from_email.render(context),
+                    cc=template_cc.render(context).split(','),
+                    bcc=template_bcc.render(context).split(','),
+                    subject=template_subject.render(context),
+                )
+
+                message.content_subtype = 'html'
+                message.send()
+
+        return processed_data
+
+    def send_summary_mail(self, request, form, processed_data):
+        """
+        Sends a form submission summary email.
+        """
+        addresses = [x.strip() for x in self.to_address.split(',')]
+        content = []
+        for field in form:
+            value = processed_data[field.name]
+            if isinstance(value, list):
+                value = ', '.join(value)
+            content.append('{0}: {1}'.format(field.label, utils.attempt_protected_media_value_conversion(request, value)))
+        content = '\n'.join(content)
+        send_mail(
+            self.subject,
+            content,
+            GeneralSettings.for_site(Site.objects.get(is_default_site=True)).from_email_address,
+            addresses
+        )
+
+    def data_to_dict(self, processed_data):
+        """
+        Converts processed form data into a dictionary suitable
+        for rendering in a context.
+        """
+        dictionary = {}
+
+        for key, value in processed_data.items():
+            dictionary[key.replace('-', '_')] = value
+            if isinstance(value, list):
+                dictionary[key] = ', '.join(value)
+
+        return dictionary
+
+    def render_landing_page(self, request, *args, form_submission=None, **kwargs):
+        """
+        Renders the landing page.
+
+        You can override this method to return a different HttpResponse as
+        landing page. E.g. you could return a redirect to a separate page.
+        """
+        if self.thank_you_page:
+            return redirect(self.thank_you_page.url)
+
+        context = self.get_context(request)
+        context['form_submission'] = form_submission
+        return render(
+            request,
+            self.get_landing_page_template(request),
+            context
+        )
+
+    def serve_submissions_list_view(self, request, *args, **kwargs):
+        """
+        Returns list submissions view for admin.
+
+        `list_submissions_view_class` can bse set to provide custom view class.
+        Your class must be inherited from SubmissionsListView.
+        """
+        view = self.submissions_list_view_class.as_view()
+        return view(request, form_page=self, *args, **kwargs)
+
+    def serve(self, request, *args, **kwargs):
+        if request.method == 'POST':
+            form = self.get_form(request.POST, request.FILES, page=self, user=request.user)
+
+            if form.is_valid():
+                form_submission = self.process_form_submission(request, form)
+                return self.render_landing_page(request, form_submission, *args, **kwargs)
+        else:
+            form = self.get_form(page=self, user=request.user)
+
+        context = self.get_context(request)
+        context['form'] = form
+        return render(
+            request,
+            self.get_template(request),
+            context
+        )
+
+    preview_modes = [
+        ('form', _('Form')),
+        ('landing', _('Thank you page')),
+    ]
+
+    def serve_preview(self, request, mode):
+        if mode == 'landing':
+            request.is_preview = True
+            return self.render_landing_page(request)
+
+        return super().serve_preview(request, mode)

+ 247 - 0
coderedcms/models/snippet_models.py

@@ -0,0 +1,247 @@
+"""
+Snippets are for content that is re-usable in nature.
+"""
+
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from modelcluster.fields import ParentalKey
+from modelcluster.models import ClusterableModel
+from wagtail.admin.edit_handlers import (
+    FieldPanel,
+    InlinePanel,
+    MultiFieldPanel,
+    StreamFieldPanel)
+from wagtail.core.fields import StreamField
+from wagtail.core.models import Orderable
+from wagtail.images.edit_handlers import ImageChooserPanel
+from wagtail.snippets.models import register_snippet
+
+from coderedcms.blocks import HTML_STREAMBLOCKS, LAYOUT_STREAMBLOCKS, NAVIGATION_STREAMBLOCKS
+from coderedcms.settings import cr_settings
+
+
+@register_snippet
+class Carousel(ClusterableModel):
+    """
+    Model that represents a Carousel. Can be modified through the snippets UI.
+    Selected through Page StreamField bodies by the CarouselSnippetChooser in
+    snippet_choosers.py
+    """
+    class Meta:
+        verbose_name = _('Carousel')
+
+    name = models.CharField(
+        max_length=255,
+        verbose_name=_('Name'),
+    )
+    show_controls = models.BooleanField(
+        default=True,
+        verbose_name=_('Show controls'),
+        help_text=_('Shows arrows on the left and right of the carousel to advance next or previous slides.'),
+    )
+    show_indicators = models.BooleanField(
+        default=True,
+        verbose_name=_('Show indicators'),
+        help_text=_('Shows small indicators at the bottom of the carousel based on the number of slides.'),
+    )
+    animation = models.CharField(
+        blank=True,
+        max_length=20,
+        choices=cr_settings['FRONTEND_CAROUSEL_FX_CHOICES'],
+        default=cr_settings['FRONTEND_CAROUSEL_FX_DEFAULT'],
+        verbose_name=_('Animation'),
+        help_text=_('The animation when transitioning between slides.'),
+    )
+
+    panels = (
+        [
+            MultiFieldPanel(
+                heading=_('Slider'),
+                children=[
+                    FieldPanel('name'),
+                    FieldPanel('show_controls'),
+                    FieldPanel('show_indicators'),
+                    FieldPanel('animation'),
+                ]
+            ),
+            InlinePanel('carousel_slides', label=_('Slides'))
+        ]
+    )
+
+    def __str__(self):
+        return self.name
+
+
+class CarouselSlide(Orderable, models.Model):
+    """
+    Represents a slide for the Carousel model. Can be modified through the
+    snippets UI.
+    """
+    class Meta:
+        verbose_name = _('Carousel Slide')
+
+    carousel = ParentalKey(
+        Carousel,
+        related_name='carousel_slides',
+        verbose_name=_('Carousel'),
+    )
+    image = models.ForeignKey(
+        'wagtailimages.Image',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name='+',
+        verbose_name=_('Image'),
+    )
+    background_color = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Background color'),
+        help_text=_('Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)'),
+    )
+    custom_css_class = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Custom CSS class'),
+    )
+    custom_id = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Custom ID'),
+    )
+
+    content = StreamField(HTML_STREAMBLOCKS, blank=True)
+
+    panels = (
+        [
+            ImageChooserPanel('image'),
+            FieldPanel('background_color'),
+            FieldPanel('custom_css_class'),
+            FieldPanel('custom_id'),
+            StreamFieldPanel('content'),
+        ]
+    )
+
+    def __str__(self):
+        return self.name
+
+
+@register_snippet
+class Navbar(models.Model):
+    """
+    Snippet for site navigation bars (header, main menu, etc.)
+    """
+    class Meta:
+        verbose_name = _('Navigation Bar')
+
+    name = models.CharField(
+        max_length=255,
+        verbose_name=_('Name'),
+    )
+    custom_css_class = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Custom CSS Class'),
+    )
+    custom_id = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Custom ID'),
+    )
+    menu_items = StreamField(
+        NAVIGATION_STREAMBLOCKS,
+        verbose_name=_('Navigation links'),
+    )
+
+    panels = [
+        FieldPanel('name'),
+        MultiFieldPanel(
+            [
+                FieldPanel('custom_css_class'),
+                FieldPanel('custom_id'),
+            ],
+            heading=_('Attributes')
+        ),
+        StreamFieldPanel('menu_items')
+    ]
+
+    def __str__(self):
+        return self.name
+
+
+@register_snippet
+class Footer(models.Model):
+    """
+    Snippet for website footer content.
+    """
+    class Meta:
+        verbose_name = _('Footer')
+
+    name = models.CharField(
+        max_length=255,
+        verbose_name=_('Name'),
+    )
+    custom_css_class = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Custom CSS Class'),
+    )
+    custom_id = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Custom ID'),
+    )
+    content = StreamField(
+        LAYOUT_STREAMBLOCKS,
+        verbose_name=_('Content'),
+    )
+
+    panels = [
+        FieldPanel('name'),
+        MultiFieldPanel(
+            [
+                FieldPanel('custom_css_class'),
+                FieldPanel('custom_id'),
+            ],
+            heading=_('Attributes')
+        ),
+        StreamFieldPanel('content')
+    ]
+
+    def __str__(self):
+        return self.name
+
+
+class CoderedEmail(ClusterableModel):
+    """
+    General purpose abstract clusterable model used for holding email information.
+    Most likely this should be subclassed with addition of a ParentalKey.
+    """
+    class Meta:
+        abstract = True
+        verbose_name = _('CodeRed Email')
+
+    to_address = models.CharField(max_length=255, blank=True, verbose_name=_('To Addresses'), help_text=_('Comma separated list'))
+    from_address = models.CharField(max_length=255, blank=True, verbose_name=_('From Address'))
+    cc_address = models.CharField(max_length=255, blank=True, verbose_name=_('CC'), help_text=_('Comma separated list'))
+    bcc_address = models.CharField(max_length=255, blank=True, verbose_name=_('BCC'), help_text=_('Comma separated list'))
+    subject = models.CharField(max_length=255, blank=True, verbose_name=_('Subject'))
+    body = models.TextField(blank=True, verbose_name=_('Body'))
+
+    panels = (
+        [
+            MultiFieldPanel(
+                [
+                    FieldPanel('to_address'),
+                    FieldPanel('from_address'),
+                    FieldPanel('cc_address'),
+                    FieldPanel('bcc_address'),
+                    FieldPanel('subject'),
+                    FieldPanel('body'),
+                ],
+                _('Email Message')
+            ),
+        ])
+
+    def __str__(self):
+        return self.subject

+ 370 - 0
coderedcms/models/wagtailsettings_models.py

@@ -0,0 +1,370 @@
+"""
+Custom wagtail settings used by CodeRed CMS.
+Settings are user-configurable on a per-site basis (multisite).
+Global project or developer settings should be defined in coderedcms.settings.py .
+"""
+
+import json
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from wagtail.admin.edit_handlers import HelpPanel, FieldPanel, MultiFieldPanel
+from wagtail.images.edit_handlers import ImageChooserPanel
+from wagtail.contrib.settings.models import BaseSetting, register_setting
+
+from coderedcms.settings import cr_settings
+
+
+@register_setting(icon='fa-facebook-official')
+class SocialMediaSettings(BaseSetting):
+    """
+    Social media accounts.
+    """
+    class Meta:
+        verbose_name = _('Social Media')
+
+    facebook = models.URLField(
+        blank=True,
+        verbose_name=_('Facebook'),
+        help_text=_('Your Facebook page URL'),
+    )
+    twitter = models.URLField(
+        blank=True,
+        verbose_name=_('Twitter'),
+        help_text=_('Your Twitter page URL'),
+    )
+    instagram = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Instagram'),
+        help_text=_('Your Instagram username, without the @'),
+    )
+    youtube = models.URLField(
+        blank=True,
+        verbose_name=_('YouTube'),
+        help_text=_('Your YouTube channel or user account URL'),
+    )
+    linkedin = models.URLField(
+        blank=True,
+        verbose_name=_('LinkedIn'),
+        help_text=_('Your LinkedIn page URL'),
+    )
+    googleplus = models.URLField(
+        blank=True,
+        verbose_name=_('Google'),
+        help_text=_('Your Google+ page or Google business listing URL'),
+    )
+
+    @property
+    def twitter_handle(self):
+        """
+        Gets the handle of the twitter account from a URL.
+        """
+        return self.twitter.strip().strip('/').split('/')[-1]
+
+    @property
+    def social_json(self):
+        """
+        Returns non-blank social accounts as a JSON list.
+        """
+        socialist = [
+            self.facebook,
+            self.twitter,
+            self.instagram,
+            self.youtube,
+            self.linkedin,
+            self.googleplus,
+        ]
+        socialist = list(filter(None, socialist))
+        return json.dumps(socialist)
+
+    panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel('facebook'),
+                FieldPanel('twitter'),
+                FieldPanel('instagram'),
+                FieldPanel('youtube'),
+                FieldPanel('linkedin'),
+                FieldPanel('googleplus'),
+            ],
+            _('Social Media Accounts'),
+        )
+    ]
+
+
+@register_setting(icon='fa-desktop')
+class LayoutSettings(BaseSetting):
+    """
+    Branding, navbar, and theme settings.
+    """
+    class Meta:
+        verbose_name = _('Layout')
+
+    logo = models.ForeignKey(
+        'wagtailimages.Image',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name='+',
+        verbose_name=_('Logo'),
+        help_text=_('Brand logo used in the navbar and throughout the site')
+    )
+    favicon = models.ForeignKey(
+        'wagtailimages.Image',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name='favicon',
+        verbose_name=_('Favicon'),
+    )
+    navbar_color_scheme = models.CharField(
+        blank=True,
+        max_length=50,
+        choices=cr_settings['FRONTEND_NAVBAR_COLOR_SCHEME_CHOICES'],
+        default=cr_settings['FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT'],
+        verbose_name=_('Navbar color scheme'),
+        help_text=_('Optimizes text and other navbar elements for use with light or dark backgrounds.'),
+    )
+    navbar_class = models.CharField(
+        blank=True,
+        max_length=255,
+        default=cr_settings['FRONTEND_NAVBAR_CLASS_DEFAULT'],
+        verbose_name=_('Navbar CSS class'),
+        help_text=_('Custom classes applied to navbar e.g. "bg-light", "bg-dark", "bg-primary".'),
+    )
+    navbar_fixed = models.BooleanField(
+        default=False,
+        verbose_name=_('Fixed navbar'),
+        help_text=_('Fixed navbar will remain at the top of the page when scrolling.'),
+    )
+    navbar_wrapper_fluid = models.BooleanField(
+        default=True,
+        verbose_name=_('Full width navbar'),
+        help_text=_('The navbar will fill edge to edge.'),
+    )
+    navbar_content_fluid = models.BooleanField(
+        default=False,
+        verbose_name=_('Full width navbar contents'),
+        help_text=_('Content within the navbar will fill edge to edge.'),
+    )
+    navbar_collapse_mode = models.CharField(
+        blank=True,
+        max_length=50,
+        choices=cr_settings['FRONTEND_NAVBAR_COLLAPSE_MODE_CHOICES'],
+        default=cr_settings['FRONTEND_NAVBAR_COLLAPSE_MODE_DEFAULT'],
+        verbose_name=_('Collapse navbar menu'),
+        help_text=_('Control on what screen sizes to show and collapse the navbar menu links.'),
+    )
+    navbar_format = models.CharField(
+        blank=True,
+        max_length=50,
+        choices=cr_settings['FRONTEND_NAVBAR_FORMAT_CHOICES'],
+        default=cr_settings['FRONTEND_NAVBAR_FORMAT_DEFAULT'],
+        verbose_name=_('Navbar format'),
+    )
+    navbar_search = models.BooleanField(
+        default=True,
+        verbose_name=_('Search box'),
+        help_text=_('Show search box in navbar')
+    )
+    frontend_theme = models.CharField(
+        blank=True,
+        max_length=50,
+        choices=cr_settings['FRONTEND_THEME_CHOICES'],
+        default=cr_settings['FRONTEND_THEME_DEFAULT'],
+        verbose_name=_('Theme variant'),
+        help_text=cr_settings['FRONTEND_THEME_HELP'],
+    )
+
+    panels = [
+        MultiFieldPanel(
+            [
+                ImageChooserPanel('logo'),
+                ImageChooserPanel('favicon'),
+            ],
+            heading=_('Branding')
+        ),
+        MultiFieldPanel(
+            [
+                FieldPanel('navbar_color_scheme'),
+                FieldPanel('navbar_class'),
+                FieldPanel('navbar_fixed'),
+                FieldPanel('navbar_wrapper_fluid'),
+                FieldPanel('navbar_content_fluid'),
+                FieldPanel('navbar_collapse_mode'),
+                FieldPanel('navbar_format'),
+                FieldPanel('navbar_search'),
+            ],
+            heading=_('Site Navbar Layout')
+        ),
+        MultiFieldPanel(
+            [
+                FieldPanel('frontend_theme'),
+            ],
+            heading=_('Theming')
+        ),
+    ]
+
+
+@register_setting(icon='fa-google')
+class AnalyticsSettings(BaseSetting):
+    """
+    Tracking and Google Analytics.
+    """
+    class Meta:
+        verbose_name = _('Tracking')
+
+    ga_tracking_id = models.CharField(
+        blank=True,
+        max_length=255,
+        verbose_name=_('GA Tracking ID'),
+        help_text=_('Your Google Analytics tracking ID (begins with "UA-")'),
+    )
+    ga_track_button_clicks = models.BooleanField(
+        default=False,
+        verbose_name=_('Track button clicks'),
+        help_text=_('Track all button clicks using Google Analytics event tracking. Event tracking details can be specified in each button’s advanced settings options.'),
+    )
+
+    panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel('ga_tracking_id'),
+                FieldPanel('ga_track_button_clicks'),
+            ],
+            heading=_('Google Analytics')
+        )
+    ]
+
+
+@register_setting(icon='fa-universal-access')
+class ADASettings(BaseSetting):
+    """
+    Accessibility related options.
+    """
+    class Meta:
+        verbose_name = 'Accessibility'
+
+    skip_navigation = models.BooleanField(
+        default=False,
+        verbose_name=_('Show skip navigation link'),
+        help_text=_('Shows a "Skip Navigation" link above the navbar that takes you directly to the main content.'),
+    )
+
+    panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel('skip_navigation'),
+            ],
+            heading=_('Accessibility')
+        )
+    ]
+
+
+@register_setting(icon='cog')
+class GeneralSettings(BaseSetting):
+    """
+    Various site-wide settings. A good place to put
+    one-off settings that don't belong anywhere else.
+    """
+    default_robot = """User-agent: *
+Disallow: /admin/
+
+User-agent: *
+Disallow: /django-admin/
+
+User-agent: *
+Allow: /
+
+Sitemap: /sitemap.xml"""
+
+    from_email_address = models.EmailField(
+        blank=True,
+        max_length=255,
+        verbose_name=_('From email address'),
+        help_text=_('The default email address this site uses to send emails.'),
+    )
+    search_num_results = models.PositiveIntegerField(
+        default=10,
+        verbose_name=_('Number of results per page'),
+    )
+    robots = models.TextField(
+        blank=True,
+        default=default_robot,
+        verbose_name=_('robots.txt'),
+        help_text=_('Enter the contents of a robots.txt file.'),
+    )
+
+    panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel('from_email_address'),
+            ],
+            _('Email')
+        ),
+        MultiFieldPanel(
+            [
+                FieldPanel('search_num_results'),
+            ],
+            _('Search Settings')
+        ),
+        MultiFieldPanel(
+            [
+                FieldPanel('robots'),
+            ],
+            _('Robots.txt')
+        ),
+        MultiFieldPanel(
+            [
+                HelpPanel(template='coderedcms/includes/wagtailadmin_cache.html',),
+            ],
+            _('Performance')
+        )
+    ]
+
+    class Meta:
+        verbose_name = _('General')
+
+
+@register_setting(icon='fa-line-chart')
+class SeoSettings(BaseSetting):
+    """
+    Additional search engine optimization and meta tags
+    that can be turned on or off.
+    """
+    class Meta:
+        verbose_name = _('SEO')
+
+    og_meta = models.BooleanField(
+        default=True,
+        verbose_name=_('Use OpenGraph Markup'),
+        help_text=_('Show an optimized preview when linking to this site on Facebook, Linkedin, Twitter, and others. See http://ogp.me/.'),
+    )
+    twitter_meta = models.BooleanField(
+        default=True,
+        verbose_name=_('Use Twitter Markup'),
+        help_text=_('Shows content as a "card" when linking to this site on Twitter. See https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards.'),
+    )
+    struct_meta = models.BooleanField(
+        default=True,
+        verbose_name=_('Use Structured Data'),
+        help_text=_('Optimizes information about your organization for search engines. See https://schema.org/.'),
+    )
+    amp_pages = models.BooleanField(
+        default=True,
+        verbose_name=_('Use AMP Pages'),
+        help_text=_('Generates an alternate AMP version of Article pages that are preferred by search engines. See https://www.ampproject.org/'),
+    )
+
+    panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel('og_meta'),
+                FieldPanel('twitter_meta'),
+                FieldPanel('struct_meta'),
+                FieldPanel('amp_pages'),
+                HelpPanel(content=_('If these settings are enabled, the corresponding values in each page’s SEO tab are used.')),
+            ],
+            heading=_('Search Engine Optimization')
+        )
+    ]

+ 18 - 0
coderedcms/project_template/.gitattributes

@@ -0,0 +1,18 @@
+# Set the default line ending behavior.
+* text eol=lf
+
+# Explicitly declare text files you want to always be normalized and converted
+# to native line endings on checkout.
+*.py text
+*.html text
+*.js text
+*.css text
+*.json text
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.pdf binary
+*.sqlite3 binary

+ 21 - 0
coderedcms/project_template/Dockerfile

@@ -0,0 +1,21 @@
+FROM python:3.6
+LABEL maintainer="info@coderedcorp.com"
+
+ENV PYTHONUNBUFFERED 1
+ENV DJANGO_ENV dev
+
+COPY ./requirements.txt /code/requirements.txt
+RUN pip install -r /code/requirements.txt
+RUN pip install gunicorn
+
+COPY . /code/
+WORKDIR /code/
+
+RUN python manage.py migrate
+
+RUN useradd coderedcms
+RUN chown -R coderedcms /code
+USER coderedcms
+
+EXPOSE 8000
+CMD exec gunicorn {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 --workers 3

+ 10 - 0
coderedcms/project_template/manage.py

@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev")
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)

+ 0 - 0
coderedcms/project_template/project_name/__init__.py


+ 0 - 0
coderedcms/project_template/project_name/settings/__init__.py


+ 188 - 0
coderedcms/project_template/project_name/settings/base.py

@@ -0,0 +1,188 @@
+"""
+Django settings for {{ project_name }} project.
+
+Generated by 'django-admin startproject' using Django {{ django_version }}.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
+"""
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+
+PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+BASE_DIR = os.path.dirname(PROJECT_DIR)
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    # This project
+    'website',
+
+    # CodeRed CMS
+    'coderedcms',
+    'bootstrap4',
+    'modelcluster',
+    'taggit',
+    'wagtailfontawesome',
+
+    # Wagtail
+    'wagtail.contrib.forms',
+    'wagtail.contrib.redirects',
+    'wagtail.embeds',
+    'wagtail.sites',
+    'wagtail.users',
+    'wagtail.snippets',
+    'wagtail.documents',
+    'wagtail.images',
+    'wagtail.search',
+    'wagtail.core',
+    'wagtail.contrib.settings',
+    'wagtail.contrib.modeladmin',
+    'wagtail.contrib.table_block',
+    'wagtail.admin',
+
+    # Django
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    "django.contrib.sitemaps",
+]
+
+MIDDLEWARE = [
+    # Common functionality
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.common.CommonMiddleware',
+
+    # Security
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'django.middleware.security.SecurityMiddleware',
+
+    # Error reporting
+    'django.middleware.common.BrokenLinkEmailsMiddleware',
+
+    # CMS functionality
+    'wagtail.core.middleware.SiteMiddleware',
+    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
+]
+
+ROOT_URLCONF = '{{ project_name }}.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+                'wagtail.contrib.settings.context_processors.settings',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = '{{ project_name }}.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'America/New_York'
+
+USE_I18N = False
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
+
+STATICFILES_FINDERS = [
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+]
+
+STATIC_ROOT = os.path.join(BASE_DIR, 'static')
+STATIC_URL = '/static/'
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+MEDIA_URL = '/media/'
+
+
+# Login
+
+LOGIN_URL = 'wagtailadmin_login'
+LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+
+
+# Wagtail settings
+
+WAGTAIL_SITE_NAME = "{{ project_name }}"
+
+WAGTAIL_ENABLE_UPDATE_CHECK = False
+
+# Base URL to use when referring to full URLs within the Wagtail admin backend -
+# e.g. in notification emails. Don't include '/admin' or a trailing slash
+BASE_URL = 'http://example.com'
+
+
+# Bootstrap
+
+BOOTSTRAP4 = {
+    # set to blank since coderedcms already loads jquery and bootstrap
+    'jquery_url': '',
+    'base_url': '',
+    # remove green highlight on inputs
+    'success_css_class': ''
+}

+ 18 - 0
coderedcms/project_template/project_name/settings/dev.py

@@ -0,0 +1,18 @@
+from .base import *
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '{{ secret_key }}'
+
+ALLOWED_HOSTS = ['*']
+
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+
+CODERED_CACHE_PAGES = False
+
+try:
+    from .local import *
+except ImportError:
+    pass

+ 75 - 0
coderedcms/project_template/project_name/settings/prod.py

@@ -0,0 +1,75 @@
+from .base import *
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = False
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '{{ secret_key }}'
+
+# Add your site's domain name(s) here.
+ALLOWED_HOSTS = []
+
+# To send email from the server, we recommend django_sendmail_backend
+# Or specify your own email backend such as an SMTP server.
+EMAIL_BACKEND = 'django_sendmail_backend.backends.EmailBackend'
+
+# A list of people who get error notifications.
+ADMINS = [('Admin Name', 'admin@localhost')]
+
+# A list in the same format as ADMINS that specifies who should get broken link
+# notifications when BrokenLinkEmailsMiddleware is enabled.
+MANAGERS = ADMINS
+
+# Email address used to send error messages to ADMINS.
+SERVER_EMAIL = '{{ project_name }}@localhost'
+
+# Default email address used to send messages from the website.
+DEFAULT_FROM_EMAIL = '{{ project_name}}@localhost'
+
+#DATABASES = {
+#    'default': {
+#        'ENGINE': 'django.db.backends.mysql',
+#        'HOST': 'localhost',
+#        'NAME': '{{ project_name }}',
+#        'USER': '{{ project_name }}',
+#        'PASSWORD': '',
+#    }
+#}
+
+# Use template caching to speed up wagtail admin and front-end.
+# Requires reloading web server to pick up template changes.
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+                'wagtail.contrib.settings.context_processors.settings',
+            ],
+            'loaders': [
+                ('django.template.loaders.cached.Loader', [
+                    'django.template.loaders.filesystem.Loader',
+                    'django.template.loaders.app_directories.Loader',
+                ]),
+            ],
+        },
+    },
+]
+
+CACHES = {
+    'default': {
+        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
+        'LOCATION': os.path.join(BASE_DIR, 'cache'),
+        'KEY_PREFIX': 'coderedcms',
+        'TIMEOUT': 3600, # in seconds
+    }
+}
+
+try:
+    from .local import *
+except ImportError:
+    pass

+ 37 - 0
coderedcms/project_template/project_name/urls.py

@@ -0,0 +1,37 @@
+from django.conf import settings
+from django.conf.urls import include, url
+from django.contrib import admin
+from wagtail.documents import urls as wagtaildocs_urls
+from coderedcms import admin_urls as coderedadmin_urls
+from coderedcms import search_urls as coderedsearch_urls
+from coderedcms import urls as codered_urls
+
+urlpatterns = [
+    # Admin
+    url(r'^django-admin/', admin.site.urls),
+    url(r'^admin/', include(coderedadmin_urls)),
+
+    # Documents
+    url(r'^docs/', include(wagtaildocs_urls)),
+
+    # Search
+    url(r'^search/', include(coderedsearch_urls)),
+
+    # For anything not caught by a more specific rule above, hand over to
+    # the page serving mechanism. This should be the last pattern in
+    # the list:
+    url(r'', include(codered_urls)),
+
+    # Alternatively, if you want CMS pages to be served from a subpath
+    # of your site, rather than the site root:
+    #    url(r'^pages/', include(codered_urls)),
+]
+
+
+if settings.DEBUG:
+    from django.conf.urls.static import static
+    from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+
+    # Serve static and media files from development server
+    urlpatterns += staticfiles_urlpatterns()
+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 16 - 0
coderedcms/project_template/project_name/wsgi.prod.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for {{ project_name }} project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.prod")
+
+application = get_wsgi_application()

+ 16 - 0
coderedcms/project_template/project_name/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for {{ project_name }} project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev")
+
+application = get_wsgi_application()

+ 8 - 0
coderedcms/project_template/requirements.txt

@@ -0,0 +1,8 @@
+coderedcms
+
+# django_sendmail_backend enables sending email from your web host server.
+# Remove this if using a different email backend.
+django_sendmail_backend
+
+# To use with a MySQL database (offered by most web hosts), uncomment the following.
+#mysqlclient

File diff suppressed because it is too large
+ 30 - 0
coderedcms/project_template/website/migrations/0001_initial.py


+ 56 - 0
coderedcms/project_template/website/migrations/0002_initial_data.py

@@ -0,0 +1,56 @@
+
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django import VERSION as DJANGO_VERSION
+from django.db import migrations
+
+
+def initial_data(apps, schema_editor):
+    ContentType = apps.get_model('contenttypes.ContentType')
+    Page = apps.get_model('wagtailcore.Page')
+    Site = apps.get_model('wagtailcore.Site')
+    WebPage = apps.get_model('website.WebPage')
+
+    # Create page content type
+    webpage_content_type, created = ContentType.objects.get_or_create(
+        model='webpage',
+        app_label='website',
+        defaults={'name': 'webpage'} if DJANGO_VERSION < (1, 8) else {}
+    )
+
+    # Delete the default home page generated by wagtail,
+    # and replace it with a more useful page type.
+    curr_homepage = Page.objects.filter(slug='home').delete()
+
+    homepage = WebPage.objects.create(
+        title = "Home Page",
+        slug='home',
+        custom_template='coderedcms/pages/web_page_notitle.html',
+        content_type=webpage_content_type,
+        path='00010001',
+        depth=2,
+        numchild=0,
+        url_path='/home/',
+    )
+
+    # Create a new default site
+    Site.objects.create(
+        hostname='localhost',
+        site_name='My New Website',
+        root_page_id=homepage.id,
+        is_default_site=True
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('coderedcms', '0001_initial'),
+        ('wagtailcore', '0002_initial_data'),
+        ('website', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RunPython(initial_data),
+    ]

+ 0 - 0
coderedcms/project_template/website/migrations/__init__.py


+ 82 - 0
coderedcms/project_template/website/models.py

@@ -0,0 +1,82 @@
+"""
+Createable pages used in CodeRed CMS.
+"""
+
+from modelcluster.fields import ParentalKey
+
+from coderedcms.forms import CoderedFormField
+from coderedcms.models import (
+    CoderedArticlePage,
+    CoderedArticleIndexPage,
+    CoderedEmail,
+    CoderedFormPage,
+    CoderedWebPage
+)
+
+
+class ArticlePage(CoderedArticlePage):
+    """
+    Article, suitable for news or blog content.
+    """
+    class Meta:
+        verbose_name = 'Article'
+
+    # Only allow this page to be created beneath an ArticleIndexPage.
+    parent_page_types = ['website.ArticleIndexPage']
+
+    template = 'coderedcms/pages/article_page.html'
+    amp_template = 'coderedcms/pages/article_page.amp.html'
+    search_template = 'coderedcms/pages/article_page.search.html'
+
+
+class ArticleIndexPage(CoderedArticleIndexPage):
+    """
+    Shows a list of article sub-pages.
+    """
+    class Meta:
+        verbose_name = 'Article Landing Page'
+
+    # Override to specify custom index ordering choice/default.
+    index_query_pagemodel = 'website.ArticlePage'
+    index_order_by_default = '-date_display'
+    index_order_by_choices = (('-date_display', 'Display publish date, newest first'),) + \
+        CoderedArticleIndexPage.index_order_by_choices
+
+    # Only allow ArticlePages beneath this page.
+    subpage_types = ['website.ArticlePage']
+
+    template = 'coderedcms/pages/article_index_page.html'
+
+
+class FormPage(CoderedFormPage):
+    """
+    A page with an html <form>.
+    """
+    class Meta:
+        verbose_name = 'Form'
+
+    template = 'coderedcms/pages/form_page.html'
+
+
+class FormPageField(CoderedFormField):
+    """
+    A field that links to a FormPage.
+    """
+    page = ParentalKey('FormPage', related_name='form_fields')
+
+class FormConfirmEmail(CoderedEmail):
+    """
+    Sends a confirmation email after submitting a FormPage.
+    """
+    page = ParentalKey('FormPage', related_name='confirmation_emails')
+
+
+class WebPage(CoderedWebPage):
+    """
+    General use page with featureful streamfield and SEO attributes.
+    Template renders all Navbar and Footer snippets in existance.
+    """
+    class Meta:
+        verbose_name = 'Web Page'
+
+    template = 'coderedcms/pages/web_page.html'

+ 0 - 0
coderedcms/project_template/website/static/css/custom.css


+ 0 - 0
coderedcms/project_template/website/static/js/custom.js


+ 165 - 0
coderedcms/schema.py

@@ -0,0 +1,165 @@
+SCHEMA_ORG_CHOICES = (
+    ('Organization','Organization'),
+    ('Airline','Organization > Airline'),
+    ('Corporation','Organization > Corporation'),
+    ('EducationalOrganization','Organization > EducationalOrganization'),
+    ('CollegeOrUniversity','Organization > EducationalOrganization > CollegeOrUniversity'),
+    ('ElementarySchool','Organization > EducationalOrganization > ElementarySchool'),
+    ('HighSchool','Organization > EducationalOrganization > HighSchool'),
+    ('MiddleSchool','Organization > EducationalOrganization > MiddleSchool'),
+    ('Preschool','Organization > EducationalOrganization > Preschool'),
+    ('School','Organization > EducationalOrganization > School'),
+    ('GovernmentOrganization','Organization > GovernmentOrganization'),
+    ('LocalBusiness','Organization > LocalBusiness'),
+    ('AnimalShelter','Organization > LocalBusiness > AnimalShelter'),
+    ('AutomotiveBusiness','Organization > LocalBusiness > AutomotiveBusiness'),
+    ('AutoBodyShop','Organization > LocalBusiness > AutomotiveBusiness > AutoBodyShop'),
+    ('AutoDealer','Organization > LocalBusiness > AutomotiveBusiness > AutoDealer'),
+    ('AutoPartsStore','Organization > LocalBusiness > AutomotiveBusiness > AutoPartsStore'),
+    ('AutoRental','Organization > LocalBusiness > AutomotiveBusiness > AutoRental'),
+    ('AutoRepair','Organization > LocalBusiness > AutomotiveBusiness > AutoRepair'),
+    ('AutoWash','Organization > LocalBusiness > AutomotiveBusiness > AutoWash'),
+    ('GasStation','Organization > LocalBusiness > AutomotiveBusiness > GasStation'),
+    ('MotorcycleDealer','Organization > LocalBusiness > AutomotiveBusiness > MotorcycleDealer'),
+    ('MotorcycleRepair','Organization > LocalBusiness > AutomotiveBusiness > MotorcycleRepair'),
+    ('ChildCare','Organization > LocalBusiness > ChildCare'),
+    ('Dentist','Organization > LocalBusiness > Dentist'),
+    ('DryCleaningOrLaundry','Organization > LocalBusiness > DryCleaningOrLaundry'),
+    ('EmergencyService','Organization > LocalBusiness > EmergencyService'),
+    ('FireStation','Organization > LocalBusiness > EmergencyService > FireStation'),
+    ('Hospital','Organization > LocalBusiness > EmergencyService > Hospital'),
+    ('PoliceStation','Organization > LocalBusiness > EmergencyService > PoliceStation'),
+    ('EmploymentAgency','Organization > LocalBusiness > EmploymentAgency'),
+    ('EntertainmentBusiness','Organization > LocalBusiness > EntertainmentBusiness'),
+    ('AdultEntertainment','Organization > LocalBusiness > EntertainmentBusiness > AdultEntertainment'),
+    ('AmusementPark','Organization > LocalBusiness > EntertainmentBusiness > AmusementPark'),
+    ('ArtGallery','Organization > LocalBusiness > EntertainmentBusiness > ArtGallery'),
+    ('Casino','Organization > LocalBusiness > EntertainmentBusiness > Casino'),
+    ('ComedyClub','Organization > LocalBusiness > EntertainmentBusiness > ComedyClub'),
+    ('MovieTheater','Organization > LocalBusiness > EntertainmentBusiness > MovieTheater'),
+    ('NightClub','Organization > LocalBusiness > EntertainmentBusiness > NightClub'),
+    ('FinancialService','Organization > LocalBusiness > FinancialService'),
+    ('AccountingService','Organization > LocalBusiness > FinancialService > AccountingService'),
+    ('AutomatedTeller','Organization > LocalBusiness > FinancialService > AutomatedTeller'),
+    ('BankOrCreditUnion','Organization > LocalBusiness > FinancialService > BankOrCreditUnion'),
+    ('InsuranceAgency','Organization > LocalBusiness > FinancialService > InsuranceAgency'),
+    ('FoodEstablishment','Organization > LocalBusiness > FoodEstablishment'),
+    ('Bakery','Organization > LocalBusiness > FoodEstablishment > Bakery'),
+    ('BarOrPub','Organization > LocalBusiness > FoodEstablishment > BarOrPub'),
+    ('Brewery','Organization > LocalBusiness > FoodEstablishment > Brewery'),
+    ('CafeOrCoffeeShop','Organization > LocalBusiness > FoodEstablishment > CafeOrCoffeeShop'),
+    ('FastFoodRestaurant','Organization > LocalBusiness > FoodEstablishment > FastFoodRestaurant'),
+    ('IceCreamShop','Organization > LocalBusiness > FoodEstablishment > IceCreamShop'),
+    ('Restaurant','Organization > LocalBusiness > FoodEstablishment > Restaurant'),
+    ('Winery','Organization > LocalBusiness > FoodEstablishment > Winery'),
+    ('GovernmentOffice','Organization > LocalBusiness > GovernmentOffice'),
+    ('PostOffice','Organization > LocalBusiness > GovernmentOffice > PostOffice'),
+    ('HealthAndBeautyBusiness','Organization > LocalBusiness > HealthAndBeautyBusiness'),
+    ('BeautySalon','Organization > LocalBusiness > HealthAndBeautyBusiness > BeautySalon'),
+    ('DaySpa','Organization > LocalBusiness > HealthAndBeautyBusiness > DaySpa'),
+    ('HairSalon','Organization > LocalBusiness > HealthAndBeautyBusiness > HairSalon'),
+    ('HealthClub','Organization > LocalBusiness > HealthAndBeautyBusiness > HealthClub'),
+    ('NailSalon','Organization > LocalBusiness > HealthAndBeautyBusiness > NailSalon'),
+    ('TattooParlor','Organization > LocalBusiness > HealthAndBeautyBusiness > TattooParlor'),
+    ('HomeAndConstructionBusiness','Organization > LocalBusiness > HomeAndConstructionBusiness'),
+    ('Electrician','Organization > LocalBusiness > HomeAndConstructionBusiness > Electrician'),
+    ('GeneralContractor','Organization > LocalBusiness > HomeAndConstructionBusiness > GeneralContractor'),
+    ('HVACBusiness','Organization > LocalBusiness > HomeAndConstructionBusiness > HVACBusiness'),
+    ('HousePainter','Organization > LocalBusiness > HomeAndConstructionBusiness > HousePainter'),
+    ('Locksmith','Organization > LocalBusiness > HomeAndConstructionBusiness > Locksmith'),
+    ('MovingCompany','Organization > LocalBusiness > HomeAndConstructionBusiness > MovingCompany'),
+    ('Plumber','Organization > LocalBusiness > HomeAndConstructionBusiness > Plumber'),
+    ('RoofingContractor','Organization > LocalBusiness > HomeAndConstructionBusiness > RoofingContractor'),
+    ('InternetCafe','Organization > LocalBusiness > InternetCafe'),
+    ('LegalService','Organization > LocalBusiness > LegalService'),
+    ('Attorney','Organization > LocalBusiness > LegalService > Attorney'),
+    ('Notary','Organization > LocalBusiness > LegalService > Notary'),
+    ('Library','Organization > LocalBusiness > Library'),
+    ('LodgingBusiness','Organization > LocalBusiness > LodgingBusiness'),
+    ('BedAndBreakfast','Organization > LocalBusiness > LodgingBusiness > BedAndBreakfast'),
+    ('Campground','Organization > LocalBusiness > LodgingBusiness > Campground'),
+    ('Hostel','Organization > LocalBusiness > LodgingBusiness > Hostel'),
+    ('Hotel','Organization > LocalBusiness > LodgingBusiness > Hotel'),
+    ('Motel','Organization > LocalBusiness > LodgingBusiness > Motel'),
+    ('Resort','Organization > LocalBusiness > LodgingBusiness > Resort'),
+    ('ProfessionalService','Organization > LocalBusiness > ProfessionalService'),
+    ('RadioStation','Organization > LocalBusiness > RadioStation'),
+    ('RealEstateAgent','Organization > LocalBusiness > RealEstateAgent'),
+    ('RecyclingCenter','Organization > LocalBusiness > RecyclingCenter'),
+    ('SelfStorage','Organization > LocalBusiness > SelfStorage'),
+    ('ShoppingCenter','Organization > LocalBusiness > ShoppingCenter'),
+    ('SportsActivityLocation','Organization > LocalBusiness > SportsActivityLocation'),
+    ('BowlingAlley','Organization > LocalBusiness > SportsActivityLocation > BowlingAlley'),
+    ('ExerciseGym','Organization > LocalBusiness > SportsActivityLocation > ExerciseGym'),
+    ('GolfCourse','Organization > LocalBusiness > SportsActivityLocation > GolfCourse'),
+    ('HealthClub','Organization > LocalBusiness > SportsActivityLocation > HealthClub'),
+    ('PublicSwimmingPool','Organization > LocalBusiness > SportsActivityLocation > PublicSwimmingPool'),
+    ('SkiResort','Organization > LocalBusiness > SportsActivityLocation > SkiResort'),
+    ('SportsClub','Organization > LocalBusiness > SportsActivityLocation > SportsClub'),
+    ('StadiumOrArena','Organization > LocalBusiness > SportsActivityLocation > StadiumOrArena'),
+    ('TennisComplex','Organization > LocalBusiness > SportsActivityLocation > TennisComplex'),
+    ('Store','Organization > LocalBusiness > Store'),
+    ('AutoPartsStore','Organization > LocalBusiness > Store > AutoPartsStore'),
+    ('BikeStore','Organization > LocalBusiness > Store > BikeStore'),
+    ('BookStore','Organization > LocalBusiness > Store > BookStore'),
+    ('ClothingStore','Organization > LocalBusiness > Store > ClothingStore'),
+    ('ComputerStore','Organization > LocalBusiness > Store > ComputerStore'),
+    ('ConvenienceStore','Organization > LocalBusiness > Store > ConvenienceStore'),
+    ('DepartmentStore','Organization > LocalBusiness > Store > DepartmentStore'),
+    ('ElectronicsStore','Organization > LocalBusiness > Store > ElectronicsStore'),
+    ('Florist','Organization > LocalBusiness > Store > Florist'),
+    ('FurnitureStore','Organization > LocalBusiness > Store > FurnitureStore'),
+    ('GardenStore','Organization > LocalBusiness > Store > GardenStore'),
+    ('GroceryStore','Organization > LocalBusiness > Store > GroceryStore'),
+    ('HardwareStore','Organization > LocalBusiness > Store > HardwareStore'),
+    ('HobbyShop','Organization > LocalBusiness > Store > HobbyShop'),
+    ('HomeGoodsStore','Organization > LocalBusiness > Store > HomeGoodsStore'),
+    ('JewelryStore','Organization > LocalBusiness > Store > JewelryStore'),
+    ('LiquorStore','Organization > LocalBusiness > Store > LiquorStore'),
+    ('MensClothingStore','Organization > LocalBusiness > Store > MensClothingStore'),
+    ('MobilePhoneStore','Organization > LocalBusiness > Store > MobilePhoneStore'),
+    ('MovieRentalStore','Organization > LocalBusiness > Store > MovieRentalStore'),
+    ('MusicStore','Organization > LocalBusiness > Store > MusicStore'),
+    ('OfficeEquipmentStore','Organization > LocalBusiness > Store > OfficeEquipmentStore'),
+    ('OutletStore','Organization > LocalBusiness > Store > OutletStore'),
+    ('PawnShop','Organization > LocalBusiness > Store > PawnShop'),
+    ('PetStore','Organization > LocalBusiness > Store > PetStore'),
+    ('ShoeStore','Organization > LocalBusiness > Store > ShoeStore'),
+    ('SportingGoodsStore','Organization > LocalBusiness > Store > SportingGoodsStore'),
+    ('TireShop','Organization > LocalBusiness > Store > TireShop'),
+    ('ToyStore','Organization > LocalBusiness > Store > ToyStore'),
+    ('WholesaleStore','Organization > LocalBusiness > Store > WholesaleStore'),
+    ('TelevisionStation','Organization > LocalBusiness > TelevisionStation'),
+    ('TouristInformationCenter','Organization > LocalBusiness > TouristInformationCenter'),
+    ('TravelAgency','Organization > LocalBusiness > TravelAgency'),
+    ('MedicalOrganization','Organization > MedicalOrganization'),
+    ('Dentist','Organization > MedicalOrganization > Dentist'),
+    ('Hospital','Organization > MedicalOrganization > Hospital'),
+    ('Pharmacy','Organization > MedicalOrganization > Pharmacy'),
+    ('Physician','Organization > MedicalOrganization > Physician'),
+    ('NGO','Organization > NGO'),
+    ('PerformingGroup','Organization > PerformingGroup'),
+    ('DanceGroup','Organization > PerformingGroup > DanceGroup'),
+    ('MusicGroup','Organization > PerformingGroup > MusicGroup'),
+    ('TheaterGroup','Organization > PerformingGroup > TheaterGroup'),
+    ('SportsOrganization','Organization > SportsOrganization'),
+    ('SportsTeam','Organization > SportsOrganization > SportsTeam'),
+)
+
+SCHEMA_ACTION_CHOICES = (
+    ('OrderAction', 'OrderAction'),
+    ('ReserveAction', 'ReserveAction'),
+)
+
+SCHEMA_RESULT_CHOICES = (
+    ('Reservation', 'Reservation'),
+    ('BusReservation', 'BusReservation'),
+    ('EventReservation', 'EventReservation'),
+    ('FlightReservation', 'FlightReservation'),
+    ('FoodEstablishmentReservation', 'FoodEstablishmentReservation'),
+    ('LodgingReservation', 'LodgingReservation'),
+    ('RentalCarReservation', 'RentalCarReservation'),
+    ('ReservationPackage', 'ReservationPackage'),
+    ('TaxiReservation', 'TaxiReservation'),
+    ('TrainReservation','TrainReservation'),
+)

+ 8 - 0
coderedcms/search_urls.py

@@ -0,0 +1,8 @@
+from django.conf.urls import url
+
+from coderedcms.views import search
+from coderedcms.utils import cache_page
+
+urlpatterns = [
+    url(r'', cache_page(search), name='codered_search'),
+]

+ 184 - 0
coderedcms/settings.py

@@ -0,0 +1,184 @@
+import os
+from django.conf import settings
+from django.utils.lru_cache import lru_cache
+
+
+PROJECT_DIR = settings.PROJECT_DIR if getattr(settings, 'PROJECT_DIR') else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+BASE_DIR = settings.BASE_DIR if getattr(settings, 'BASE_DIR') else os.path.dirname(PROJECT_DIR)
+
+DEFAULTS = {
+    'CACHE_PAGES': True,
+    'CACHE_BACKEND': 'default',
+
+    'PROTECTED_MEDIA_URL' : '/protected/',
+    'PROTECTED_MEDIA_ROOT' :  os.path.join(BASE_DIR, 'protected'),
+    'PROTECTED_MEDIA_UPLOAD_WHITELIST': [],
+    'PROTECTED_MEDIA_UPLOAD_BLACKLIST': ['.sh', '.exe', '.bat', '.ps1', '.app', '.jar', '.py', '.php', '.pl', '.rb'],
+
+    'FRONTEND_BTN_SIZE_DEFAULT': '',
+    'FRONTEND_BTN_SIZE_CHOICES': (
+        ('btn-sm', 'Small'),
+        ('', 'Default'),
+        ('btn-lg', 'Large'),
+    ),
+
+    'FRONTEND_BTN_STYLE_DEFAULT': 'btn-primary',
+    'FRONTEND_BTN_STYLE_CHOICES': (
+        ('btn-primary', 'Primary'),
+        ('btn-secondary', 'Secondary'),
+        ('btn-success', 'Success'),
+        ('btn-danger', 'Danger'),
+        ('btn-warning', 'Warning'),
+        ('btn-info', 'Info'),
+        ('btn-link', 'Link'),
+        ('btn-light', 'Light'),
+        ('btn-dark', 'Dark'),
+        ('btn-outline-primary', 'Outline Primary'),
+        ('btn-outline-secondary', 'Outline Secondary'),
+        ('btn-success', 'Outline Success'),
+        ('btn-outline-danger', 'Outline Danger'),
+        ('btn-outline-warning', 'Outline Warning'),
+        ('btn-outline-info', 'Outline Info'),
+        ('btn-outline-light', 'Outline Light'),
+        ('btn-outline-dark', 'Outline Dark'),
+    ),
+
+    'FRONTEND_CAROUSEL_FX_DEFAULT': '',
+    'FRONTEND_CAROUSEL_FX_CHOICES': (
+        ('', 'Slide'),
+        ('carousel-fade', 'Fade'),
+    ),
+
+    'FRONTEND_COL_SIZE_DEFAULT': '',
+    'FRONTEND_COL_SIZE_CHOICES': (
+        ('', 'Automatically size'),
+        ('12', 'Full row'),
+        ('6',  'Half - 1/2 column'),
+        ('4',  'Thirds - 1/3 column'),
+        ('8',  'Thirds - 2/3 column'),
+        ('3',  'Quarters - 1/4 column'),
+        ('9',  'Quarters - 3/4 column'),
+        ('2',  'Sixths - 1/6 column'),
+        ('10', 'Sixths - 5/6 column'),
+        ('1',  'Twelfths - 1/12 column'),
+        ('5',  'Twelfths - 5/12 column'),
+        ('7',  'Twelfths - 7/12 column'),
+        ('11', 'Twelfths - 11/12 column'),
+    ),
+
+    'FRONTEND_COL_BREAK_DEFAULT': 'md',
+    'FRONTEND_COL_BREAK_CHOICES': (
+        ('', 'Always expanded'),
+        ('sm', 'sm - Expand on small screens (phone, 576px) and larger'),
+        ('md', 'md - Expand on medium screens (tablet, 768px) and larger'),
+        ('lg', 'lg - Expand on large screens (laptop, 992px) and larger'),
+        ('xl', 'xl - Expand on extra large screens (wide monitor, 1200px)'),
+    ),
+
+    'FRONTEND_NAVBAR_FORMAT_DEFAULT': '',
+    'FRONTEND_NAVBAR_FORMAT_CHOICES': (
+        ('', 'Default Bootstrap Navbar'),
+        ('codered-navbar-center', 'Centered logo at top'),
+    ),
+
+    'FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT': 'navbar-light',
+    'FRONTEND_NAVBAR_COLOR_SCHEME_CHOICES': (
+        ('navbar-light', 'Light - for use with a light-colored navbar'),
+        ('navbar-dark', 'Dark - for use with a dark-colored navbar'),
+    ),
+
+    'FRONTEND_NAVBAR_CLASS_DEFAULT': 'bg-light',
+
+    'FRONTEND_NAVBAR_COLLAPSE_MODE_DEFAULT': 'navbar-expand-lg',
+    'FRONTEND_NAVBAR_COLLAPSE_MODE_CHOICES': (
+        ('', 'Never show menu - Always collapse menu behind a button'),
+        ('navbar-expand-sm', 'sm - Show on small screens (phone size) and larger'),
+        ('navbar-expand-md', 'md - Show on medium screens (tablet size) and larger'),
+        ('navbar-expand-lg', 'lg - Show on large screens (laptop size) and larger'),
+        ('navbar-expand-xl', 'xl - Show on extra large screens (desktop, wide monitor)'),
+    ),
+
+    'FRONTEND_THEME_HELP': 'Change the color palette of your site with a Bootstrap theme. Powered by Bootswatch https://bootswatch.com/.',
+    'FRONTEND_THEME_DEFAULT': '',
+    'FRONTEND_THEME_CHOICES': (
+        ('', 'Default - Classic Bootstrap'),
+        ('cerulean', 'Cerulean - A calm blue sky'),
+        ('cosmo', 'Cosmo - An ode to Metro'),
+        ('cyborg', 'Cyborg - Jet black and electric blue'),
+        ('darkly', 'Darkly - Flatly in night mode'),
+        ('flatly', 'Flatly - Flat and modern'),
+        ('journal', 'Journal - Crisp like a new sheet of paper'),
+        ('litera', 'Litera - The medium is the message'),
+        ('lumen', 'Lumen - Light and shadow'),
+        ('lux', 'Lux - A touch of class'),
+        ('materia', 'Materia - Material is the metaphor'),
+        ('minty', 'Minty - A fresh feel'),
+        ('pulse', 'Pulse - A trace of purple'),
+        ('sandstone', 'Sandstone - A touch of warmth'),
+        ('simplex', 'Simplex - Mini and minimalist'),
+        ('sketchy', 'Sketchy - A hand-drawn look for mockups and mirth'),
+        ('slate', 'Slate - Shades of gunmetal gray'),
+        ('solar', 'Solar - A dark spin on Solarized'),
+        ('spacelab', 'Spacelab - Silvery and sleek'),
+        ('superhero', 'Superhero - The brave and the blue'),
+        ('united', 'United - Ubuntu orange and unique font'),
+        ('yeti', 'Yeti - A friendly foundation'),
+    ),
+
+    'FRONTEND_TEMPLATES_BLOCKS': {
+        'cardblock': (
+            ('coderedcms/blocks/card_block.html', 'Card'),
+            ('coderedcms/blocks/card_head.html', 'Card with header'),
+            ('coderedcms/blocks/card_foot.html', 'Card with footer'),
+            ('coderedcms/blocks/card_head_foot.html', 'Card with header and footer'),
+            ('coderedcms/blocks/card_blurb.html', 'Blurb - rounded image and no border'),
+            ('coderedcms/blocks/card_img.html', 'Cover image - use image as background'),
+        ),
+        'cardgridblock': (
+            ('coderedcms/blocks/cardgrid_group.html', 'Card group - attached cards of equal size'),
+            ('coderedcms/blocks/cardgrid_deck.html', 'Card deck - separate cards of equal size'),
+            ('coderedcms/blocks/cardgrid_columns.html', 'Card masonry - fluid brick pattern'),
+        ),
+        'pagelistblock': (
+            ('coderedcms/blocks/pagelist_block.html', 'General, simple list'),
+            ('coderedcms/blocks/pagelist_list_group.html', 'General, list group navigation panel'),
+            ('coderedcms/blocks/pagelist_article_media.html', 'Article, media format'),
+            ('coderedcms/blocks/pagelist_article_card_group.html', 'Article, card group - attached cards of equal size'),
+            ('coderedcms/blocks/pagelist_article_card_deck.html', 'Article, card deck - separate cards of equal size'),
+            ('coderedcms/blocks/pagelist_article_card_columns.html', 'Article, card masonry - fluid brick pattern'),
+        ),
+        # templates that are available for all block types
+        '*': (
+            ('', 'Default'),
+        ),
+    },
+
+    'FRONTEND_TEMPLATES_PAGES': {
+        # templates that are available for all page types
+        '*': (
+            ('', 'Default'),
+            ('coderedcms/pages/web_page.html', 'Web page showing title and cover image'),
+            ('coderedcms/pages/web_page_notitle.html', 'Web page without title and cover image'),
+            ('coderedcms/pages/base.html', 'Blank page - no navbar or footer'),
+        ),
+
+    },
+}
+
+@lru_cache()
+def get_config():
+    config = DEFAULTS.copy()
+    for var in config:
+        cr_var = 'CODERED_%s' % var
+        if hasattr(settings, cr_var):
+            config[var] = getattr(settings, cr_var)
+    return config
+
+cr_settings = get_config()
+
+try:
+    import bootstrap4.bootstrap as bootstrap
+except:
+    import bootstrap3.bootstrap as bootstrap
+
+get_bootstrap_setting = bootstrap.get_bootstrap_setting

+ 354 - 0
coderedcms/static/css/codered-admin.css

@@ -0,0 +1,354 @@
+/* Font sizes and inputs */
+
+html, body {
+    color: #000;
+    background-color:#fff;
+    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol",sans-serif !important;
+    font-size:1rem;
+}
+
+.content-wrapper {
+    background-color:unset;
+    border:unset;
+}
+
+header {
+    font-size:0.9rem;
+}
+
+.summary ul.stats span {
+    font-family:inherit !important;
+    font-weight:600 !important;
+}
+
+@media screen and (min-width:50em) {
+    .summary ul.stats li::before {
+        font-size:5em !important;
+    }
+    .summary ul.stats span {
+        font-size:3.5em !important;
+    }
+}
+
+code, pre,
+.monospace textarea {
+    font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
+    font-size: 87%;
+}
+
+h1, h2, h3, h4, h5, h6,
+.button, .halloeditor, .tagit, input, select, textarea {
+    font-family:inherit;
+}
+
+.button, .halloeditor, .tagit, input, select, textarea {
+    font-size:1em;
+}
+
+.halloeditor, .tagit, input, select, textarea {
+    background-color:#eee;
+    border:1px solid #eee;
+    padding-top:0.5em;
+    padding-bottom:0.5em;
+    font-weight:normal;
+}
+input[type='checkbox'], input[type='radio'] {
+    border:1.5px solid #ccc;
+}
+.halloeditor:hover, .tagit:hover, input:hover, select:hover, textarea:hover {
+    background-color:#eee;
+    border:1px solid #eee;
+}
+input[type='checkbox']:hover, input[type='radio']:hover {
+    border:1.5px solid #ccc;
+}
+input[type="checkbox"]::before, input[type="radio"]::before {
+    position: relative;
+    top:unset;
+}
+.date_field .input::after, .date_field .input::before, .date_time_field .input::after, .date_time_field .input::before, .iconfield .input::after, .iconfield .input::before, .time_field .input::after, .time_field .input::before, .url_field .input::after, .url_field .input::before {
+    font-size:1.5em;
+    top:0.35em;
+}
+.full input {
+    background-color:white;
+}
+.full input:focus, .halloeditor:focus, .tagit:focus, input:focus, select:focus, textarea:focus {
+    border-color:#54d0d0;
+    background-color:#f4fcfc;
+}
+.button,
+.search-bar .button-filter {
+    height:2.25em;
+}
+a.button {
+    line-height:2.2em;
+}
+.dropdown .button, .dropdown button, .dropdown input[type="button"], .dropdown input[type="reset"], .dropdown input[type="submit"] {
+    height:2.5em;
+    line-height:2.5em;
+}
+.dropdown .dropdown-toggle {
+    line-height:2.5em;
+}
+.dropdown ul a {
+    padding:0.5em 1em;
+}
+.c-dropdown__button, .c-dropdown__toggle {
+    vertical-align: middle;
+}
+.choice_field .input select,
+.model_choice_field .input select,
+.typed_choice_field .input select {
+    margin-right:-3em;
+}
+.choice_field .input select ~ span:after,
+.model_choice_field .input select ~ span:after,
+.typed_choice_field .input select ~ span:after {
+    font-size:2em !important;
+    right:unset;
+}
+
+.field-content {
+    width:100%;
+}
+
+@media screen and (min-width:50em) {
+    li.sequence-member .fields > li,
+    ul.fields > li > .field {
+        display: flex;
+        flex-direction: row;
+        flex-wrap: nowrap;
+        align-items: flex-start;
+        justify-content: flex-start;
+    }
+    label {
+        display:block;
+        float:none;
+        margin: 0 auto;
+        width:15em;
+        min-width:15em;
+    }
+    .sequence-member label,
+    .sequence-member .label {
+        margin: 0;
+        width:10em;
+        min-width:10em;
+    }
+    .object.full .field-content {
+        flex-grow: 1;
+        margin: 0 auto;
+    }
+    li.sequence-member .struct-block .sequence-container {
+        display:block;
+        flex-grow: 1;
+        margin: 0 auto;
+    }
+    li.codered-collapsible label+.field,
+    ul.fields > li label+.field-content,
+    ul.fields > li label+.field {
+        display:block;
+        flex-grow: 1;
+        float:none;
+        margin: 0 auto;
+    }
+    .input {
+        width:100%;
+    }
+    .row-flush {
+        margin: unset;
+    }
+    .breadcrumb {
+        margin-left: -20px;
+        margin-right: -20px;
+    }
+}
+
+
+/* General layout */
+
+.content {
+    padding-bottom:100px;
+    background-color:unset;
+}
+
+.responsive-img {
+    width: 100%;
+    height: auto;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+
+table.listing colgroup col {
+    width:auto;
+}
+
+label {
+    padding-top:0;
+    color:inherit;
+    font-weight:normal;
+}
+
+.post-page-meta {
+    text-align: center;
+}
+
+.text-center {
+    text-align: center;
+}
+
+.no-padding
+{
+    padding: 0px;
+}
+
+.tab-nav a {
+    font-size:0.9em;
+    padding:0.5em 1.5em;
+    max-height:1.5em;
+}
+.tab-nav li.settings a {
+    padding:0.5em 1.5em;
+}
+.tab-nav li.seo a::before {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    font-family: "FontAwesome";
+    vertical-align: middle;
+    text-transform: none;
+    content: "\f201";
+    margin-right: .5em;
+    font-size: 1.2em;
+    font-weight: 400;
+}
+
+.logo {
+    margin: 0 auto;
+    padding: 15px;
+}
+.logo img {
+    width:auto;
+    height:auto;
+    max-height:80px;
+    max-width:100%;
+}
+
+.nav-search input {
+    padding: 0.5em;
+}
+.nav-search button::before {
+    line-height:2.5em
+}
+
+.nav-main a {
+    padding: 0.6em 0.8em;
+}
+.nav-submenu .menu-item a {
+    font-size:0.9em;
+    padding: 0.6em 0 0.6em 2em;
+}
+.nav-submenu .menu-item a::before {
+    margin-left:0;
+}
+
+.nav-main-solo {
+  top: 43px;
+  bottom: 0;
+  overflow: auto;
+  width: 100%;
+  list-style: none;
+  background-color: #222;
+}
+
+.nav-main-solo a {
+  text-decoration: none;
+  display: block;
+  color: #aaa;
+  padding: 0.39em;
+  font-size: 0.85em;
+  font-weight: 300;
+}
+
+.nav-main-solo a:hover, .nav-main-solo a:focus,
+.nav-main .account:hover, .nav-main .account:focus {
+  outline: none;
+  background-color: #0a0a0a;
+  color: #fff;
+}
+
+.nav-main .footer {
+    font-size:0.9em;
+}
+
+.submenu-active * {
+    box-sizing: border-box;
+}
+.nav-submenu .footer {
+    color:#666;
+    position:relative;
+    width:100%;
+}
+
+@media screen and (min-width:50em) {
+    li.submenu-active .nav-submenu a {
+        padding-left:1em;
+        width:100%;
+    }
+    .nav-submenu {
+        display:none;
+    }
+    .nav-submenu h2, .nav-submenu ul {
+        width:100%;
+    }
+    .nav-main .account em {
+        margin-top:1em;
+        font-size:0.9em;
+    }
+    .submenu-active .nav-submenu {
+        display:block;
+    }
+}
+
+.c-explorer__item__link {
+    padding: 1em;
+}
+
+.nice-padding {
+    padding-left:20px;
+    padding-right:20px;
+}
+
+
+/* Custom */
+
+.button-advanced-settings {
+    display:block;
+    background-color:#aaa;
+    font-size:0.8em;
+    font-weight:600;
+}
+.button-advanced-settings:hover {
+    background-color:#999;
+}
+
+.power-by {
+    background-color:#1a1a1a;
+    border-top:1px solid #333;
+    font-size:12px;
+    text-align: center;
+}
+
+.power-by a {
+    color:#fff;
+    padding: 5px;
+}
+.power-by a:hover {
+    background-color:#f00;
+}
+
+.power-by img {
+    width:85px;
+    height:auto;
+    padding-left:2px;
+}

+ 240 - 0
coderedcms/static/css/codered-editor.css

@@ -0,0 +1,240 @@
+/* Reset fonts to system */
+.title .halloeditor, .title input, .title textarea,
+.Draftail-Editor .DraftEditor-root {
+    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol" !important;
+}
+
+
+/* Inputs and form fields */
+.title .halloeditor, .title input, .title textarea {
+    font-size:2.5em;
+}
+.full .halloeditor, .full input, .full textarea {
+    padding-top:1em;
+    padding-bottom:1em;
+    background-color:transparent;
+}
+
+label,
+.sequence-member label,
+.sequence-member .label {
+    font-size:0.9em;
+    padding:0.5em 0.5em 0.5em 0;
+}
+
+.sequence-member input[type='text'],
+.sequence-member input[type='email'],
+.sequence-member input[type='url'],
+.sequence-member input[type='number'],
+.sequence-member input[type='date'],
+.sequence-member input[type='time'],
+.sequence-member input[type='datetime'],
+.sequence-member input[type='datetime-local'],
+.sequence-member textarea,
+.sequence-member select,
+.sequence-member .richtext,
+.sequence-member .tagit {
+    background-color:white;
+    border:1px solid #ddd;
+}
+
+.sequence-member input:focus,
+.sequence-member textarea:focus,
+.sequence-member select:focus,
+.sequence-member .richtext:focus,
+.sequence-member .tagit:focus {
+    border-color:#54d0d0;
+    background-color:#f4fcfc;
+}
+
+.input input[type='text'],
+.input input[type='email'],
+.input input[type='url'],
+.input input[type='number'],
+.input input[type='date'],
+.input input[type='time'],
+.input input[type='datetime'],
+.input input[type='datetime-local'],
+.input select,
+.input textarea {
+    min-width:300px;
+    width:auto;
+}
+.input textarea,
+.full .input input {
+    width:100%;
+}
+
+
+/* Override and enhance the streamfield editor */
+
+.chooser .unchosen::before, .chooser .chosen::before {
+    color:rgba(0,0,0,0.2)
+}
+
+li.sequence-member {
+    border:none;
+}
+
+li.sequence-member li.sequence-member {
+    background-color:rgba(0,0,0,0.05);
+    border:none;
+    margin:0 0 50px 0;
+
+}
+li.sequence-member:hover {
+    background-color:rgba(0,0,0,0.05);
+    border:none;
+}
+
+li.sequence-member .sequence-member-inner,
+li.sequence-member .struct-block .sequence-container .sequence-member-inner {
+    padding:1em;
+}
+
+@media screen and (max-width:49.99em) {
+    li.sequence-member .struct-block .sequence-container {
+        width:100%;
+    }
+}
+
+.field.char_field.widget-draftail_rich_text_area .field-content,
+.field.char_field.widget-textarea .field-content {
+    width:100%;
+    max-width:100%;
+}
+
+.sequence-controls {
+    border: none;
+    background-color: inherit;
+}
+.sequence-controls h3 label {
+    font-weight: bold;
+    display: inline;
+    vertical-align: middle;
+}
+
+.objects * {
+    box-sizing: border-box;
+}
+
+.object.stream-field {
+    margin-top:50px;
+}
+
+.object > h2 {
+    font-weight: 600;
+    height: 3em;
+    line-height: 1em;
+}
+.object > h2 label::before, .object > h2::before {
+    line-height:1.5em;
+}
+.object.empty .add .button::before {
+    line-height:2em;
+    height:2em;
+    width:2.3em;
+
+}
+
+.object .multiple {
+    width:100%;
+    max-width:100%;
+}
+
+.object fieldset {
+    width:100%;
+    max-width:100%;
+}
+.object > fieldset {
+    padding-top:6em;
+}
+.object > fieldset fieldset {
+    padding-top:0;
+}
+
+li.sequence-member .struct-block .fields {
+    width:100%;
+    max-width:100%;
+}
+
+.stream-menu.stream-menu-closed .toggle {
+    color:#888;
+}
+
+.tab-content section {
+    padding-top:0;
+}
+
+.fields > li, .field-col {
+    padding-bottom:0.5em;
+}
+
+.tagit, input, select, textarea {
+    color:#000;
+}
+.tagit, input, select, textarea {
+    font-weight:normal;
+}
+
+
+/* Draftail */
+
+
+.Draftail-Toolbar {
+    background-color:transparent !important;
+    border-radius:0 !important;
+    color:rgba(0,0,0,0.2) !important;
+    padding: 0 !important;
+    transition: all ease .25s;
+}
+.Draftail-Toolbar:hover {
+    color:rgba(0,0,0,0.7) !important;
+    transition: all ease .25s;
+}
+.Draftail-Toolbar .Draftail-ToolbarButton--active {
+    background-color:rgba(0,0,0,0.05) !important;
+    border:none;
+    transition: all ease .25s;
+}
+.Draftail-Toolbar:hover .Draftail-ToolbarButton--active {
+    background-color:rgba(0,0,0,0.2) !important;
+    transition: all ease .25s;
+}
+.Draftail-Toolbar .Draftail-ToolbarGroup + .Draftail-ToolbarGroup::before {
+    content: none;
+}
+.Draftail-Toolbar .Draftail-ToolbarGroup + .Draftail-ToolbarGroup {
+    margin-left: 1em;
+}
+li.sequence-member li > .field .Draftail-Editor,
+li.sequence-member li.sequence-member .Draftail-Editor {
+    background-color: #fff !important;
+    border-radius:5px;
+    border:1px solid #ddd !important;
+    padding: 0.5em 1em;
+}
+li.sequence-member li.sequence-member .Draftail-Editor {
+    margin:-1em;
+}
+li.sequence-member li > .field .Draftail-Editor {
+    margin: 0;
+}
+
+.Draftail-Editor .DraftEditor-root {
+    color:#000 !important;
+}
+.Draftail-DividerBlock {
+    background-color:#ccc !important;
+}
+
+
+/* custom */
+
+.codered-collapsible {
+    padding:0;
+}
+
+.codered-collapsible fieldset {
+    padding: 10px 0 0;
+}

+ 330 - 0
coderedcms/static/css/codered-front.css

@@ -0,0 +1,330 @@
+/********************
+CodeRed CMS custom styles
+********************/
+
+
+/* Hero Unit */
+
+.hero-bg {
+    background-size: cover;
+    background-repeat:no-repeat;
+    background-position: center center;
+    color:white;
+}
+
+.hero-bg.parallax {
+    background-attachment: fixed;
+}
+
+.hero-bg.tile {
+    background-size: initial;
+    background-repeat:repeat;
+}
+
+.hero-fg {
+    padding:80px 0;
+}
+
+
+/* Articles */
+
+#content,
+.codered-article,
+.codered-article .container {
+    background-color:inherit;
+}
+.codered-article .container {
+    margin-bottom:50px;
+}
+.codered-article .article-body {
+    margin-top:3em;
+    padding-bottom:1em;
+}
+.codered-article .article-author-img {
+    max-height:3em;
+}
+@media(min-width:768px) {
+    .codered-article .article-body {
+        font-size:1.2em;
+    }
+    .codered-article > img {
+        margin-bottom:-15vw;
+    }
+    .codered-article.has-img .container {
+        position:relative;
+        padding:5vw;
+        box-shadow:0 0 20px rgba(0,0,0,0.5);
+    }
+}
+
+
+/* Other */
+
+.social-media-list{
+    list-style:none;
+    font-size: 30px;
+
+}
+
+.social-media-list li{
+    display:inline;
+    padding: 10px;
+}
+.social-media-list a:hover{
+    text-decoration: none;
+}
+
+.leaders{
+    max-width: 40em;
+    padding: 0;
+    overflow-x: hidden;
+}
+
+.leaders:before{
+    float:left;
+    width: 0;
+    white-space: nowrap;
+    content:
+        ". . . . . . . . . . . . . . . . . . . . "
+        ". . . . . . . . . . . . . . . . . . . . "
+        ". . . . . . . . . . . . . . . . . . . . "
+        ". . . . . . . . . . . . . . . . . . . . "
+        ". . . . . . . . . . . . . . . . . . . . "
+        ". . . . . . . . . . . . . . . . . . . . "
+        ". . . . . . . . . . . . . . . . . . . . "
+        ". . . . . . . . . . . . . . . . . . . . ";
+}
+
+.leaders span:first-child{
+    padding-right: 0.33em;
+    background: white;
+}
+
+.leaders span + span{
+    float:right;
+    padding-left: 0.33em;
+    padding-right: 15px;
+    background:white;
+    position:relative;
+    z-index: 1;
+    margin-right: -15px;
+}
+
+
+/* Lightbox */
+
+.modal-lightbox {
+    max-width:100vw;
+    text-align:center;
+}
+
+.modal-lightbox img {
+    max-height:90vh;
+    max-width:90vw;
+}
+
+
+
+/********************
+Bootstrap 4 overrides and enhancements
+********************/
+
+
+/* Containers */
+
+[class^='container'] [class^='container'] {
+    width:100%;
+    padding:0;
+    margin:0;
+}
+
+
+/* Navbar */
+
+.navbar-brand img {
+    height: 50px;
+    width: auto;
+}
+
+.-fixed-img-offset {
+    margin-top:76px;
+}
+.-fixed-offset {
+    margin-top:56px;
+}
+.codered-navbar-center-fixed-img-offset {
+    margin-top:175px;
+}
+.codered-navbar-center-fixed-offset {
+    margin-top:56px;
+}
+
+.codered-navbar-center {
+    text-align:center;
+}
+.codered-navbar-center .navbar-collapse {
+    justify-content: center;
+}
+.codered-navbar-center .navbar-brand {
+    margin:0;
+}
+.codered-navbar-center .navbar-brand img {
+    height:150px;
+    width:auto;
+}
+
+/* sm */
+@media(min-width:576px) {
+    .codered-navbar-center.navbar-expand-sm > .container,
+    .codered-navbar-center.navbar-expand-sm > .container-fluid {
+        display:block;
+    }
+
+    .codered-navbar-center-fixed-img-offset.navbar-expand-sm {
+        margin-top:217px;
+    }
+    .codered-navbar-center-fixed-offset.navbar-expand-sm {
+        margin-top:96px;
+    }
+}
+/* md */
+@media(min-width:768px) {
+    .codered-navbar-center.navbar-expand-md > .container,
+    .codered-navbar-center.navbar-expand-md > .container-fluid {
+        display:block;
+    }
+    .codered-navbar-center-fixed-img-offset.navbar-expand-md {
+        margin-top:217px;
+    }
+    .codered-navbar-center-fixed-offset.navbar-expand-md {
+        margin-top:96px;
+    }
+}
+/* lg */
+@media(min-width:992px) {
+    .codered-navbar-center.navbar-expand-lg > .container,
+    .codered-navbar-center.navbar-expand-lg > .container-fluid {
+        display:block;
+    }
+    .codered-navbar-center-fixed-img-offset.navbar-expand-sm {
+        margin-top:217px;
+    }
+    .codered-navbar-center-fixed-offset.navbar-expand-sm {
+        margin-top:96px;
+    }
+}
+/* xl */
+@media(min-width:1200px) {
+    .codered-navbar-center.navbar-expand-xl > .container,
+    .codered-navbar-center.navbar-expand-xl > .container-fluid {
+        display:block;
+    }
+    .codered-navbar-center-fixed-img-offset.navbar-expand-xl {
+        margin-top:217px;
+    }
+    .codered-navbar-center-fixed-offset.navbar-expand-xl {
+        margin-top:96px;
+    }
+}
+
+/* Cards */
+
+.card {
+    background-size:cover;
+    background-position: center center;
+}
+
+/* Carousel */
+
+.container-fluid .carousel {
+    margin: 0 -15px;
+}
+
+.carousel .no-image {
+    height:500px;
+}
+
+
+/********************
+Pygments source code formatting.
+********************/
+
+.code-title {
+    border-radius:5px 5px 0 0;
+    font-size:0.8em;
+    padding:0.5em 1em;
+}
+.pygments {
+    padding:1em;
+    border-radius:5px;
+}
+.code-title+.pygments {
+    border-radius: 0 0 5px 5px;
+}
+.pygments .hll { background-color: #ffffcc }
+.pygments .c { color: #888888 } /* Comment */
+.pygments .err { color: #a61717; background-color: #e3d2d2 } /* Error */
+.pygments .k { color: #008800; font-weight: bold } /* Keyword */
+.pygments .ch { color: #888888 } /* Comment.Hashbang */
+.pygments .cm { color: #888888 } /* Comment.Multiline */
+.pygments .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
+.pygments .cpf { color: #888888 } /* Comment.PreprocFile */
+.pygments .c1 { color: #888888 } /* Comment.Single */
+.pygments .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
+.pygments .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
+.pygments .ge { font-style: italic } /* Generic.Emph */
+.pygments .gr { color: #aa0000 } /* Generic.Error */
+.pygments .gh { color: #333333 } /* Generic.Heading */
+.pygments .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
+.pygments .go { color: #888888 } /* Generic.Output */
+.pygments .gp { color: #555555 } /* Generic.Prompt */
+.pygments .gs { font-weight: bold } /* Generic.Strong */
+.pygments .gu { color: #666666 } /* Generic.Subheading */
+.pygments .gt { color: #aa0000 } /* Generic.Traceback */
+.pygments .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
+.pygments .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
+.pygments .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
+.pygments .kp { color: #008800 } /* Keyword.Pseudo */
+.pygments .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
+.pygments .kt { color: #888888; font-weight: bold } /* Keyword.Type */
+.pygments .m { color: #0000DD; font-weight: bold } /* Literal.Number */
+.pygments .s { color: #dd2200; } /* Literal.String */
+.pygments .na { color: #336699 } /* Name.Attribute */
+.pygments .nb { color: #003388 } /* Name.Builtin */
+.pygments .nc { color: #bb0066; font-weight: bold } /* Name.Class */
+.pygments .no { color: #003366; font-weight: bold } /* Name.Constant */
+.pygments .nd { color: #555555 } /* Name.Decorator */
+.pygments .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
+.pygments .nf { color: #0066bb; font-weight: bold } /* Name.Function */
+.pygments .nl { color: #336699; font-style: italic } /* Name.Label */
+.pygments .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
+.pygments .py { color: #336699; font-weight: bold } /* Name.Property */
+.pygments .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
+.pygments .nv { color: #336699 } /* Name.Variable */
+.pygments .ow { color: #008800 } /* Operator.Word */
+.pygments .w { color: #bbbbbb } /* Text.Whitespace */
+.pygments .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
+.pygments .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
+.pygments .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
+.pygments .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
+.pygments .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
+.pygments .sa { color: #dd2200; } /* Literal.String.Affix */
+.pygments .sb { color: #dd2200; } /* Literal.String.Backtick */
+.pygments .sc { color: #dd2200; } /* Literal.String.Char */
+.pygments .dl { color: #dd2200; } /* Literal.String.Delimiter */
+.pygments .sd { color: #dd2200; } /* Literal.String.Doc */
+.pygments .s2 { color: #dd2200; } /* Literal.String.Double */
+.pygments .se { color: #0044dd; } /* Literal.String.Escape */
+.pygments .sh { color: #dd2200; } /* Literal.String.Heredoc */
+.pygments .si { color: #3333bb; } /* Literal.String.Interpol */
+.pygments .sx { color: #22bb22; } /* Literal.String.Other */
+.pygments .sr { color: #008800; } /* Literal.String.Regex */
+.pygments .s1 { color: #dd2200; } /* Literal.String.Single */
+.pygments .ss { color: #aa6600; } /* Literal.String.Symbol */
+.pygments .bp { color: #003388 } /* Name.Builtin.Pseudo */
+.pygments .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
+.pygments .vc { color: #336699 } /* Name.Variable.Class */
+.pygments .vg { color: #dd7700 } /* Name.Variable.Global */
+.pygments .vi { color: #3333bb } /* Name.Variable.Instance */
+.pygments .vm { color: #336699 } /* Name.Variable.Magic */
+.pygments .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */

BIN
coderedcms/static/img/codered.png


+ 31 - 0
coderedcms/static/js/codered-editor.js

@@ -0,0 +1,31 @@
+$(document).ready(function(){
+    $(document).on('click', '.codered-collapsible button', function(){
+        var $fieldset = $(this).parent().find('fieldset');
+
+        if (!$(this).parent().hasClass('collapsed')) {
+            $(this).parent().addClass('collapsed');
+            $fieldset.hide('fast');
+        } else {
+            $(this).parent().removeClass('collapsed');
+            $fieldset.show('fast');
+        }
+    });
+
+    $(document).on('click', 'a.codered-clearcache', function(event) {
+        event.preventDefault();
+        $el = $(this);
+        // show spinner
+        $el.addClass('icon icon-spinner');
+        oldtext = $el.html();
+        $el.html($el.data("clicked-text"));
+        // make ajax call
+        $.ajax({
+            url: $el.attr('href')
+        })
+        .always(function(msg) {
+            $el.after("<div>" + msg + "</div>")
+            $el.removeClass('icon icon-spinner');
+            $el.html(oldtext);
+        });
+    })
+});

+ 103 - 0
coderedcms/static/js/codered-front.js

@@ -0,0 +1,103 @@
+libs = {
+    modernizr: {
+        url: "https://cdnjs.cloudflare.com/ajax/libs/modernizr/2.8.3/modernizr.min.js",
+        integrity: "sha256-0rguYS0qgS6L4qVzANq4kjxPLtvnp5nn2nB5G1lWRv4=",
+    },
+    moment: {
+        url: "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js",
+        integrity: "sha256-CutOzxCRucUsn6C6TcEYsauvvYilEniTXldPa6/wu0k="
+    },
+    pickerbase: {
+        url: "https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.5.6/compressed/picker.js",
+        integrity: "sha256-A1y8n02GW5dvJFkEOX7UCbzJoko8kqgWUquWf9TWFS8=",
+        head: '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.5.6/compressed/themes/default.css" integrity="sha256-HJnF0By+MMhHfGTHjMMD7LlFL0KAQEMyWB86VbeFn4k=" crossorigin="anonymous" />'
+    },
+    pickadate: {
+        url: "https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.5.6/compressed/picker.date.js",
+        integrity: "sha256-rTh8vmcE+ZrUK3k9M6QCNZIBmAd1vumeuJkagq0EU3g=",
+        head: '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.5.6/compressed/themes/default.date.css" integrity="sha256-Ex8MCGbDP5+fEwTgLt8IbGaIDJu2uj88ZDJgZJrxA4Y=" crossorigin="anonymous" />'
+    },
+    pickatime: {
+        url: "https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.5.6/compressed/picker.time.js",
+        integrity: "sha256-vFMKre5X5oQN63N+oJU9cJzn22opMuJ+G9FWChlH5n8=",
+        head: '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.5.6/compressed/themes/default.time.css" integrity="sha256-0GwWH1zJVNiu4u+bL27FHEpI0wjV0hZ4nSSRM2HmpK8=" crossorigin="anonymous" />'
+    }
+}
+
+function load_script(lib, success) {
+    // lib is an entry in `libs` above.
+    if(lib.head) {
+        $('head').append(lib.head);
+    }
+    $.ajax({
+        url: lib.url,
+        dataType: "script",
+        integrity: lib.integrity,
+        crossorigin: "anonymous",
+        success: success
+    });
+}
+
+
+$(document).ready(function()
+{
+    /*** Forms ***/
+    if ( $('form').length > 0) {
+        load_script(libs.modernizr, function() {
+            if ( (!Modernizr.inputtypes.date || !Modernizr.inputtypes.time) && $("input[type='date'], input[type='time']").length > 0) {
+                load_script(libs.pickerbase, function() {
+                    $(document).trigger("base-picker-loaded");
+                });
+            }
+            if(!Modernizr.inputtypes.date && $("input[type='date']").length > 0) {
+                $(document).on("base-picker-loaded", function() {
+                    load_script(libs.pickadate, function() {
+                        // Show date picker
+                        $("input[type='date']").pickadate({
+                            format: 'mm/dd/yyyy',
+                            selectMonths: true,
+                            selectYears: true
+                        });
+                    });
+                });
+            }
+            if(!Modernizr.inputtypes.time && $("[type='time']").length > 0) {
+                $(document).on("base-picker-loaded", function() {
+                    load_script(libs.pickatime, function() {
+                        // Show time picker
+                        $("input[type='time']").pickatime({
+                            format: 'h:i A',
+                            interval: 15
+                        });
+                    });
+                });
+            }
+            if (!Modernizr.inputtypes['datetime-local'] && $("input[type='datetime-local']").length > 0) {
+                load_script(libs.moment, function() {
+                    // Show formatting help text
+                    $('.datetime-help').show();
+                    // Format input on blur
+                    $("[type='datetime-local']").blur(function() {
+                        var clean = $.trim($(this).val());
+                        if (clean != '') {
+                            clean = moment(clean).format("L LT");
+                            $(this).val(clean);
+                        }
+                    });
+                });
+            }
+        });
+    }
+
+
+    /*** Lightbox ***/
+    $('.lightbox-preview').on('click', function(event) {
+        var orig_src = $(this).find('img').data('original-src');
+        var orig_alt = $(this).find('img').attr('alt');
+        var orig_ttl = $(this).find('img').attr('title');
+        var $lightbox = $($(this).data('target'));
+        $lightbox.find('img').attr('src', orig_src);
+        $lightbox.find('img').attr('alt', orig_alt);
+        $lightbox.find('img').attr('title', orig_ttl);
+    });
+});

+ 12 - 0
coderedcms/templates/404.html

@@ -0,0 +1,12 @@
+{% extends "coderedcms/pages/web_page.html" %}
+
+{% block title %}Page not found{% endblock %}
+
+{% block body_class %}404{% endblock %}
+
+{% block content %}
+<div class="container">
+    <h1>Page not found</h1>
+    <h2>Sorry, this page could not be found.</h2>
+</div>
+{% endblock %}

+ 27 - 0
coderedcms/templates/500.html

@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+        <title>Internal server error</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <style>
+        body {
+            font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
+            line-height:1.5;
+            padding:2em;
+        }
+        hr { border: 0.5px solid #ddd; }
+        h1, h2 { font-weight:normal; }
+        </style>
+    </head>
+    <body>
+        <h1>Internal server error</h1>
+        <hr>
+        <h2>Sorry, our site has experienced an error.</h2>
+        <p>
+            The error has been logged and reported to an engineer.
+            Please try again soon. If the issue persists, contact us to resolve.
+        </p>
+    </body>
+</html>

+ 17 - 0
coderedcms/templates/coderedcms/blocks/article_block_card.html

@@ -0,0 +1,17 @@
+{% load wagtailimages_tags %}
+
+<div class="card mb-3">
+    {% if article.cover_image %}
+        {% image article.cover_image fill-600x300 as cover_image %}
+        <a href="{{article.url}}" title="{{article.title}}"><img class="card-img-top" src="{{cover_image.url}}" alt="{{cover_image.title}}"></a>
+    {% endif %}
+    <div class="card-body">
+        <h5 class="card-title">{{article.title}}</h5>
+        <div class="card-subtitle mb-2 text-muted">{{article.get_pub_date}} {% if article.get_pub_date and article.get_author_name %} &bull; {% endif %} {{article.get_author_name}}</div>
+        <div class="card-subtitle mb-2 text-muted">{{article.caption}}</div>
+        {% if self.show_preview %}
+        <p class="card-text">{{article.body_preview}}</p>
+        {% endif %}
+        <a href="{{article.url}}" title="{{article.title}}">Read more &raquo;</a>
+    </div>
+</div>

+ 6 - 0
coderedcms/templates/coderedcms/blocks/base_block.html

@@ -0,0 +1,6 @@
+<div class="{{self.settings.custom_css_class}}"
+     {% if self.settings.custom_id %}
+     id="{{self.settings.custom_id}}"
+     {% endif %} >
+{% block block_render %}{% endblock %}
+</div>

+ 45 - 0
coderedcms/templates/coderedcms/blocks/base_link_block.html

@@ -0,0 +1,45 @@
+{% load wagtailcore_tags wagtailimages_tags coderedcms_tags %}
+
+{% block menu_item %}
+    {% is_menu_item_dropdown value as has_dropdown %}
+
+    <a href="{% block url %}#{% endblock %}"
+    {% if value.settings.custom_id %}id="{{value.settings.custom_id}}"{% endif %}
+    class="{{aclass}} {% if has_dropdown %}dropdown-toggle{% endif %} {% if value.settings.custom_css_class %}{{value.settings.custom_css_class}}{% endif %}"
+    {% if has_dropdown %}data-toggle="dropdown"
+    role="button"
+    aria-haspopup="true"
+    aria-expanded="false"
+    {% endif %}>
+
+        {% if value.image %}
+            {% image value.image max-200x200 as img %}
+            <img src="{{img.url}}" class="w-100" alt="{{img.image.title}}"/>
+        {% elif value.display_text %}
+            {{value.display_text|safe}}
+        {% elif value.page %}
+            {{value.page.title}}
+        {% elif value.document %}
+            {{value.document.title}}
+        {% endif %}
+
+        {% if has_dropdown and settings.coderedcms.LayoutSettings.bootstrap_major_version == 3 %}
+        <span class="caret"></span>
+        {% endif %}
+    </a>
+
+
+    {% if has_dropdown %}
+    <ul class="dropdown-menu">
+        {% for sub_link in value.sub_links %}
+            <li>{% include_block sub_link with aclass="dropdown-item" %}</li>
+        {% endfor %}
+        {% if value.show_child_links %}
+            {% for child in value.page.get_children %}
+                <li><a class="dropdown-item" href="{% pageurl child %}">{{child.title}}</a></li>
+            {% endfor %}
+        {% endif %}
+    </ul>
+    {% endif %}
+
+{% endblock %}

+ 13 - 0
coderedcms/templates/coderedcms/blocks/button_block.html

@@ -0,0 +1,13 @@
+<a href="{{self.url}}"
+	{% if settings.coderedcms.AnalyticsSettings.ga_track_button_clicks and not format == 'amp' %}
+		onclick="ga('send', 'event', '{{self.settings.ga_tracking_event_category|default:'Button'}}', 'click', '{{self.settings.ga_tracking_event_label|default:self.button_title}}');"
+	{% endif %}
+  title="{{ self.button_title|safe }}"
+  class="btn {{ self.button_style }} {{ self.button_size }} {{ self.settings.custom_css_class }}"
+  {% if self.settings.custom_id %}id="{{ self.settings.custom_id }}"{% endif %}>
+	{% if self.button_title %}
+		{{ self.button_title|safe }}
+	{% else %}
+		{{ self.url }}
+	{% endif %}
+</a>

+ 17 - 0
coderedcms/templates/coderedcms/blocks/card_block.html

@@ -0,0 +1,17 @@
+{% load wagtailcore_tags wagtailimages_tags %}
+
+<div class="card mb-3 {{self.settings.custom_css_class}}"
+{% if self.settings.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+    {% if self.image %}
+    {% image self.image fill-900x600 as card_img %}
+    <img class="card-img-top w-100" src="{{card_img.url}}" alt="{{card_img.title}}">
+    {% endif %}
+    <div class="card-body">
+        {% if self.title %}<h5 class="card-title">{{self.title}}</h5>{% endif %}
+        {% if self.subtitle %}<h6 class="card-subtitle mb-2 text-muted">{{self.subtitle}}</h6>{% endif %}
+        <div class="card-text">{{self.description}}</div>
+        {% for button in self.links %}
+        {% include_block button %}
+        {% endfor %}
+    </div>
+</div>

+ 19 - 0
coderedcms/templates/coderedcms/blocks/card_blurb.html

@@ -0,0 +1,19 @@
+{% load wagtailcore_tags wagtailimages_tags %}
+
+<div class="card mb-3 border-0 bg-transparent text-center {{self.settings.custom_css_class}}"
+{% if self.settings.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+    {% if self.image %}
+    {% image self.image fill-150x150 as card_img %}
+    <img class="rounded-circle w-25 mx-auto" src="{{card_img.url}}" alt="{{card_img.title}}">
+    {% endif %}
+    <div class="card-body">
+        {% if self.title %}<h5 class="card-title">{{self.title}}</h5>{% endif %}
+        {% if self.subtitle %}<h6 class="card-subtitle mb-2 text-muted">{{self.subtitle}}</h6>{% endif %}
+        <div class="card-text">{{self.description}}</div>
+    </div>
+    <div class="card-footer border-0 bg-transparent">
+        {% for button in self.links %}
+        {% include_block button %}
+        {% endfor %}
+    </div>
+</div>

+ 19 - 0
coderedcms/templates/coderedcms/blocks/card_foot.html

@@ -0,0 +1,19 @@
+{% load wagtailcore_tags wagtailimages_tags %}
+
+<div class="card mb-3 {{self.settings.custom_css_class}}"
+{% if self.settings.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+    {% if self.image %}
+    {% image self.image fill-900x600 as card_img %}
+    <img class="card-img-top w-100" src="{{card_img.url}}" alt="{{card_img.title}}">
+    {% endif %}
+    <div class="card-body">
+        {% if self.title %}<h5 class="card-title">{{self.title}}</h5>{% endif %}
+        {% if self.subtitle %}<h6 class="card-subtitle mb-2 text-muted">{{self.subtitle}}</h6>{% endif %}
+        <div class="card-text">{{self.description}}</div>
+    </div>
+    <div class="card-footer">
+        {% for button in self.links %}
+        {% include_block button %}
+        {% endfor %}
+    </div>
+</div>

+ 17 - 0
coderedcms/templates/coderedcms/blocks/card_head.html

@@ -0,0 +1,17 @@
+{% load wagtailcore_tags wagtailimages_tags %}
+
+<div class="card mb-3 {{self.settings.custom_css_class}}"
+{% if self.settings.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+    {% if self.title %}<h5 class="card-header">{{self.title}}</h5>{% endif %}
+    {% if self.image %}
+    {% image self.image fill-900x600 as card_img %}
+    <img class="w-100" src="{{card_img.url}}" alt="{{card_img.title}}">
+    {% endif %}
+    <div class="card-body">
+        {% if self.subtitle %}<h6 class="card-subtitle mb-2 text-muted">{{self.subtitle}}</h6>{% endif %}
+        <div class="card-text">{{self.description}}</div>
+        {% for button in self.links %}
+        {% include_block button %}
+        {% endfor %}
+    </div>
+</div>

+ 19 - 0
coderedcms/templates/coderedcms/blocks/card_head_foot.html

@@ -0,0 +1,19 @@
+{% load wagtailcore_tags wagtailimages_tags %}
+
+<div class="card mb-3 {{self.settings.custom_css_class}}"
+{% if self.settings.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+    {% if self.title %}<h5 class="card-header">{{self.title}}</h5>{% endif %}
+    {% if self.image %}
+    {% image self.image fill-900x600 as card_img %}
+    <img class="w-100" src="{{card_img.url}}" alt="{{card_img.title}}">
+    {% endif %}
+    <div class="card-body">
+        {% if self.subtitle %}<h6 class="card-subtitle mb-2 text-muted">{{self.subtitle}}</h6>{% endif %}
+        <div class="card-text">{{self.description}}</div>
+    </div>
+    <div class="card-footer">
+        {% for button in self.links %}
+        {% include_block button %}
+        {% endfor %}
+    </div>
+</div>

+ 19 - 0
coderedcms/templates/coderedcms/blocks/card_img.html

@@ -0,0 +1,19 @@
+{% load wagtailcore_tags wagtailimages_tags %}
+
+<div class="card mb-3 bg-dark text-white {{self.settings.custom_css_class}}"
+{% if self.settings.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}
+{% if self.image %}
+{% image self.image fill-1000x1000 as card_img %}
+style="background-image:url('{{card_img.url}}');"
+{% endif %} >
+    <div class="card-body">
+        {% if self.title %}<h5 class="card-title">{{self.title}}</h5>{% endif %}
+        {% if self.subtitle %}<h6 class="card-subtitle mb-2 text-muted">{{self.subtitle}}</h6>{% endif %}
+        <div class="card-text">{{self.description}}</div>
+    </div>
+    <div class="card-footer border-0 bg-transparent">
+        {% for button in self.links %}
+        {% include_block button %}
+        {% endfor %}
+    </div>
+</div>

+ 10 - 0
coderedcms/templates/coderedcms/blocks/cardgrid_columns.html

@@ -0,0 +1,10 @@
+{% extends "coderedcms/blocks/grid_block.html" %}
+{% load wagtailcore_tags %}
+
+{% block grid_content %}
+<div class="card-columns {{self.settings.custom_css_class}}">
+      {% for block in self.content %}
+            {% include_block block %}
+      {% endfor %}
+</div>
+{% endblock %}

+ 10 - 0
coderedcms/templates/coderedcms/blocks/cardgrid_deck.html

@@ -0,0 +1,10 @@
+{% extends "coderedcms/blocks/grid_block.html" %}
+{% load wagtailcore_tags %}
+
+{% block grid_content %}
+<div class="card-deck {{self.settings.custom_css_class}}">
+      {% for block in self.content %}
+            {% include_block block %}
+      {% endfor %}
+</div>
+{% endblock %}

+ 10 - 0
coderedcms/templates/coderedcms/blocks/cardgrid_group.html

@@ -0,0 +1,10 @@
+{% extends "coderedcms/blocks/grid_block.html" %}
+{% load wagtailcore_tags %}
+
+{% block grid_content %}
+<div class="card-group {{self.settings.custom_css_class}}">
+      {% for block in self.content %}
+            {% include_block block %}
+      {% endfor %}
+</div>
+{% endblock %}

+ 47 - 0
coderedcms/templates/coderedcms/blocks/carousel_block.html

@@ -0,0 +1,47 @@
+{% extends "coderedcms/blocks/base_block.html" %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block block_render %}
+
+<div id="carousel-{{self.carousel.id}}" class="carousel slide {{self.carousel.animation}}" data-ride="carousel">
+
+    {% if self.carousel.show_indicators %}
+    <ol class="carousel-indicators">
+    {% for item in self.carousel.carousel_slides.all %}
+        <li data-target="#carousel-{{self.carousel.id}}" data-slide-to="{{forloop.counter0}}" {% if forloop.counter0 == 0 %}class="active"{% endif %}></li>
+    {% endfor %}
+    </ol>
+    {% endif %}
+
+    <div class="carousel-inner">
+    {% for item in self.carousel.carousel_slides.all %}
+        {% block carousel_slide %}
+        <div class="carousel-item {% if not item.image %}no-img{% endif %} {% if forloop.counter0 == 0 %}active{% endif %} {{item.custom_css_class}}" %{% if item.custom_id %}id="#{{item.custom_id}}"{% endif %} style="{% if item.background_color %}background-color: {{item.background_color}};{% endif %}">
+            {% block carousel_slide_image %}
+                {% if item.image %}
+                {% image item.image fill-2000x1000 as carouselimage %}
+                <img class="d-block w-100" src="{{carouselimage.url}}" alt="{{carouselimage.image.title}}" />
+                {% endif %}
+            {% endblock %}
+            <div class="carousel-caption">
+                {% include_block item.content %}
+            </div>
+        </div>
+        {% endblock %}
+    {% endfor %}
+    </div>
+
+    {% if self.carousel.show_controls %}
+    <a class="carousel-control-prev" href="#carousel-{{self.carousel.id}}" role="button" data-slide="prev">
+        <span class="carousel-control-prev-icon" aria-hidden="true"></span>
+        <span class="sr-only">Previous</span>
+    </a>
+    <a class="carousel-control-next" href="#carousel-{{self.carousel.id}}" role="button" data-slide="next">
+        <span class="carousel-control-next-icon" aria-hidden="true"></span>
+        <span class="sr-only">Next</span>
+    </a>
+    {% endif %}
+
+</div>
+
+{% endblock %}

+ 12 - 0
coderedcms/templates/coderedcms/blocks/code_block.html

@@ -0,0 +1,12 @@
+{% extends "coderedcms/blocks/base_block.html" %}
+
+{% block block_render %}
+
+{% if self.title %}
+<div class="code-title bg-primary text-white">{{self.title}}</div>
+{% endif %}
+<div class="pygments bg-light">
+    {{code_html}}
+</div>
+
+{% endblock %}

+ 9 - 0
coderedcms/templates/coderedcms/blocks/column_block.html

@@ -0,0 +1,9 @@
+{% load wagtailcore_tags %}
+
+<div class="col{% if self.settings.column_breakpoint %}-{{ self.settings.column_breakpoint }}{% endif %}{% if self.column_size %}-{{ self.column_size }}{% endif %}
+{{self.settings.custom_css_class}}"
+{% if self.settings.custom_id %}id="{{self.settings.custom_id}}"{% endif %}>
+      {% for block in self.content %}
+      {% include_block block %}
+      {% endfor %}
+</div>

+ 7 - 0
coderedcms/templates/coderedcms/blocks/document_link_block.html

@@ -0,0 +1,7 @@
+{% extends 'coderedcms/blocks/base_link_block.html' %}
+
+{% block url %}
+	{% if value.document %}
+    	{{value.document.url}}
+    {% endif %}
+{% endblock %}

+ 26 - 0
coderedcms/templates/coderedcms/blocks/download_block.html

@@ -0,0 +1,26 @@
+{% load wagtailcore_tags %}
+
+<a href="{{self.downloadable_file.url}}" download="{{self.downloadable_file.url}}"
+	{% if settings.coderedcms.AnalyticsSettings.ga_track_button_clicks or self.onclick %}
+	{% if not format == 'amp' %}
+		onclick="{% if settings.coderedcms.AnalyticsSettings.ga_track_button_clicks %}ga('send', 'event', '{{self.settings.ga_tracking_event_category}}', 'click', '{{self.settings.ga_tracking_event_label}}');{% endif %}
+		{% if self.onclick %}{{ self.onclick }}{% endif %}"
+	{% endif %}
+	{% endif %}
+  title="{{ self.alt_text|safe }}"
+  class='btn {{ self.button_style }} {{ self.button_size }} {{ self.settings.custom_css_class }}'
+  {% if self.settings.custom_id %}id="{{ self.settings.custom_id }}"{% endif %}>
+{% if self.button_title %}
+  {{ self.button_title|safe }}
+{% else %}
+  Download {{ self.downloadable_file.title }}
+{% endif %}
+</a>
+
+{% if self.automatic_download and not format == 'amp' %}
+<script>
+	$(document).ready(function(){
+		window.open("{{self.downloadable_file.url}}", '_blank');
+	});
+</script>
+{% endif %}

+ 12 - 0
coderedcms/templates/coderedcms/blocks/embed_video_block.html

@@ -0,0 +1,12 @@
+{% load coderedcms_tags %}
+
+<div class="embed-responsive embed-responsive-16by9 {{self.settings.custom_css_class}}"
+{% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+	{% if self.url|get_embed_video_provider == 'youtube' %}
+		<iframe class="embed-responsive-item" width="560" height="315" src="https://www.youtube.com/embed/{{ self.url|get_embed_video_code }}" frameborder="0" allowfullscreen></iframe>
+	{% endif %}
+
+	{% if self.url|get_embed_video_provider == 'vimeo' %}
+    	<iframe class="embed-responsive-item" width="640" height="360" src="https://player.vimeo.com/video/{{ self.url|get_embed_video_code }}" frameborder="0" allowfullscreen></iframe>
+	{% endif %}
+</div>

+ 5 - 0
coderedcms/templates/coderedcms/blocks/external_link_block.html

@@ -0,0 +1,5 @@
+{% extends 'coderedcms/blocks/base_link_block.html' %}
+
+{% block url %}
+    {{value.link}}
+{% endblock %}

+ 8 - 0
coderedcms/templates/coderedcms/blocks/google_map.html

@@ -0,0 +1,8 @@
+<div class="embed-responsive embed-responsive-16by9 {{self.settings.custom_css_clas}}"
+{% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+	{% if self.place_id %}
+		<iframe class="embed-responsive-item" width="100%" style="border:0" src="https://www.google.com/maps/embed/v1/place?q=place_id:{{ self.place_id }}&zoom={{ self.map_zoom_level }}&key={{ self.api_key }}" allowfullscreen></iframe>
+	{% else %}
+		<iframe class="embed-responsive-item" width="100%" style="border:0" src="https://maps.google.com/maps?q={{ self.search }}&output=embed" allowfullscreen></iframe>
+	{% endif %}
+</div>

+ 14 - 0
coderedcms/templates/coderedcms/blocks/grid_block.html

@@ -0,0 +1,14 @@
+{% load wagtailcore_tags %}
+
+<div class="container{% if self.fluid %}-fluid{% endif %}"
+  {% if self.settings.custom_id %} id="{{self.settings.custom_id}}"{% endif %} >
+
+  {% block grid_content %}
+  <div class="row {{self.settings.custom_css_class}}">
+      {% for column in self.content %}
+            {% include_block column %}
+      {% endfor %}
+  </div>
+  {% endblock %}
+
+</div>

+ 4 - 0
coderedcms/templates/coderedcms/blocks/h1_block.html

@@ -0,0 +1,4 @@
+<h1 {% if self.custom_css_class %}class="{{self.settings.custom_css_class}}"{% endif %}
+{% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+	{{ self.text }}
+</h1>

+ 4 - 0
coderedcms/templates/coderedcms/blocks/h2_block.html

@@ -0,0 +1,4 @@
+<h2 {% if self.custom_css_class %}class="{{self.settings.custom_css_class}}"{% endif %}
+{% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+	{{ self.text }}
+</h2>

+ 4 - 0
coderedcms/templates/coderedcms/blocks/h3_block.html

@@ -0,0 +1,4 @@
+<h3 {% if self.custom_css_class %}class="{{self.settings.custom_css_class}}"{% endif %}
+{% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+	{{ self.text }}
+</h3>

+ 20 - 0
coderedcms/templates/coderedcms/blocks/hero_block.html

@@ -0,0 +1,20 @@
+{% load wagtailimages_tags %}
+
+{% if not self.fluid %}
+<div class="container">
+{% endif %}
+
+{% image self.background_image max-2000x2000 as background_image %}
+<div class="hero-bg {% if self.is_parallax %}parallax{% endif %} {% if self.tile_image %}tile{% endif %} {% if self.settings.custom_css_class %}{{self.settings.custom_css_class}}{% endif %}"
+	style="{% if self.background_color %}background-color:{{self.background_color}};{% endif %}
+		   {% if background_image %}background-image: url({{background_image.url}});{% endif %}"
+	{% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+	<div class="hero-fg"
+		style="{% if self.foreground_color %}color:{{self.foreground_color}};{% endif %}">
+		{{self.content}}
+	</div>
+</div>
+
+{% if not self.fluid %}
+</div>
+{% endif %}

+ 20 - 0
coderedcms/templates/coderedcms/blocks/image_block.html

@@ -0,0 +1,20 @@
+{% load wagtailimages_tags %}
+
+{% image self.image max-1000x1000 as self_image %}
+
+{% if format == 'amp' %}
+
+<amp-img src="{{self_image.url}}"
+layout="responsive" width="{{self_image.width}}" height="{{self_image.height}}"
+class="{{self.custom_css_class}}"
+{% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}
+alt="{{self_image.image.title}}"></amp-img>
+
+{% else %}
+
+<img src="{{self_image.url}}"
+class="w-100 {{self.settings.custom_css_class}}"
+{% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}
+alt="{{self_image.image.title}}" />
+
+{% endif %}

+ 34 - 0
coderedcms/templates/coderedcms/blocks/image_gallery_block.html

@@ -0,0 +1,34 @@
+{% load wagtailcore_tags wagtailimages_tags coderedcms_tags %}
+
+{% get_pictures self.collection.id as pictures %}
+{% generate_random_id as modal_id %}
+
+<section class="{{self.settings.custom_css_class}}"
+    {% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+
+<div class="row">
+{% for picture in pictures %}
+    {% image picture fill-900x600 as picture_image %}
+    {% image picture original as original_image %}
+
+    {# insert a break every 4 columns #}
+    {% cycle '' '' '' '' '<div class="w-100"></div>' %}
+
+    <div class="col-sm my-3">
+        <a href="#" class="lightbox-preview" data-toggle="modal" data-target="#modal-{{modal_id}}">
+        <img class="thumbnail w-100" src="{{picture_image.url}}" data-original-src="{{original_image.url}}" alt="{{picture_image.image.title}}" title="{{picture_image.image.title}}" />
+        </a>
+    </div>
+
+{% endfor %}
+</div>
+
+<div id="modal-{{modal_id}}" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-lightbox">
+        <div class="modal-body">
+            <img src="" alt="" />
+        </div>
+    </div>
+</div>
+
+</section>

+ 11 - 0
coderedcms/templates/coderedcms/blocks/image_link_block.html

@@ -0,0 +1,11 @@
+{% load wagtailimages_tags %}
+
+<a href="{{self.url}}"
+	{% if settings.coderedcms.AnalyticsSettings.ga_track_button_clicks and not format == 'amp' %}
+		onclick="ga('send', 'event', '{{self.settings.ga_tracking_event_category|default:'ImageLink'}}', 'click', '{{self.settings.ga_tracking_event_label|default:self.alt_text}}');"
+	{% endif %}
+  title="{{ self.alt_text|safe }}"
+  {% if self.settings.custom_id %}id="{{ self.settings.custom_id }}"{% endif %}>
+	{% image self.image max-1000x1000 as self_image %}
+	<img src="{{self_image.url}}" class="img-fluid {{self.settings.custom_css_class}}" alt="{{self.alt_text|safe}}" />
+</a>

+ 34 - 0
coderedcms/templates/coderedcms/blocks/modal_block.html

@@ -0,0 +1,34 @@
+{% extends 'coderedcms/blocks/base_block.html' %}
+
+{% load wagtailcore_tags wagtailimages_tags coderedcms_tags %}
+
+{% block block_render %}
+
+{% generate_random_id as modal_id %}
+<a href="#" data-toggle="modal" data-target="#{{modal_id}}" type="button" class="btn {{self.button_size}} {{self.button_style}}">{{self.button_title}}</a>
+
+<div id="{{modal_id}}" class="modal fade" role="dialog">
+  <div class="modal-dialog">
+
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5 class="modal-title">{% include_block self.header %}</h5>
+        <button type="button" class="close modal-close" data-dismiss="modal">
+          <span aria-hidden="true">&times;</span>
+        </button>
+      </div>
+
+      <div class="modal-body">
+        <div>
+          {% include_block self.content %}
+        </div>
+      </div>
+
+      <div class="modal-footer">
+          {% include_block self.footer %}
+      </div>
+    </div>
+
+  </div>
+</div>
+{% endblock %}

+ 8 - 0
coderedcms/templates/coderedcms/blocks/page_link_block.html

@@ -0,0 +1,8 @@
+{% extends 'coderedcms/blocks/base_link_block.html' %}
+{% load wagtailcore_tags %}
+
+{% block url %}
+	{% if value.page %}
+    	{% pageurl value.page %}
+    {% endif %}
+{% endblock %}

+ 14 - 0
coderedcms/templates/coderedcms/blocks/pagelist_article_card_columns.html

@@ -0,0 +1,14 @@
+{% extends 'coderedcms/blocks/base_block.html' %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block block_render %}
+
+    <div class="card-columns">
+        {% for page in pages %}
+        {% with page.specific as article %}
+            {% include "coderedcms/blocks/article_block_card.html" %}
+        {% endwith %}
+        {% endfor %}
+    </div>
+
+{% endblock %}

+ 14 - 0
coderedcms/templates/coderedcms/blocks/pagelist_article_card_deck.html

@@ -0,0 +1,14 @@
+{% extends 'coderedcms/blocks/base_block.html' %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block block_render %}
+
+    <div class="card-deck">
+        {% for page in pages %}
+        {% with page.specific as article %}
+            {% include "coderedcms/blocks/article_block_card.html" %}
+        {% endwith %}
+        {% endfor %}
+    </div>
+
+{% endblock %}

+ 14 - 0
coderedcms/templates/coderedcms/blocks/pagelist_article_card_group.html

@@ -0,0 +1,14 @@
+{% extends 'coderedcms/blocks/base_block.html' %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block block_render %}
+
+    <div class="card-group">
+        {% for page in pages %}
+        {% with page.specific as article %}
+            {% include "coderedcms/blocks/article_block_card.html" %}
+        {% endwith %}
+        {% endfor %}
+    </div>
+
+{% endblock %}

+ 25 - 0
coderedcms/templates/coderedcms/blocks/pagelist_article_media.html

@@ -0,0 +1,25 @@
+{% extends 'coderedcms/blocks/base_block.html' %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block block_render %}
+
+    {% for page in pages %}
+    {% with page.specific as article %}
+    <div class="media">
+        {% if article.cover_image %}
+            {% image article.cover_image fill-150x100 as cover_image %}
+            <a href="{{article.url}}"><img class="mr-3" src="{{cover_image.url}}" alt="{{cover_image.title}}"></a>
+        {% endif %}
+        <div class="media-body">
+            <h5 class="mt-0"><a href="{{article.url}}">{{article.title}}</a></h5>
+            <div class="mb-2 text-muted">{{article.get_pub_date}} {% if article.get_pub_date and article.get_author_name %} &bull; {% endif %} {{article.get_author_name}}</div>
+            <div class="mb-2 text-muted">{{article.caption}}</div>
+            {% if self.show_preview %}
+            <p>{{article.body_preview}}</p>
+            {% endif %}
+        </div>
+    </div>
+    {% endwith %}
+    {% endfor %}
+
+{% endblock %}

+ 16 - 0
coderedcms/templates/coderedcms/blocks/pagelist_block.html

@@ -0,0 +1,16 @@
+{% extends 'coderedcms/blocks/base_block.html' %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block block_render %}
+
+<ul>
+    {% for page in pages %}
+    {% with page=page.specific %}
+    <li><a href="{{page.url}}">
+        {{page.title}} {% if self.show_preview %}<small class="text-muted">– {{page.body_preview}}</small>{% endif %}
+    </a></li>
+    {% endwith %}
+    {% endfor %}
+</div>
+
+{% endblock %}

+ 21 - 0
coderedcms/templates/coderedcms/blocks/pagelist_list_group.html

@@ -0,0 +1,21 @@
+{% extends 'coderedcms/blocks/base_block.html' %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block block_render %}
+
+<div class="list-group">
+    {% for page in pages %}
+    {% with page=page.specific %}
+    <a class="list-group-item list-group-item-action flex-column align-items-start {% if request.path == page.url %}active{% endif %}" href="{{page.url}}">
+    {% if self.show_preview %}
+        <h5 class="mb-1">{{page.title}}</h5>
+        <p class="mb-1">{{page.body_preview}}</p>
+    {% else %}
+        {{page.title}}
+    {% endif %}
+    </a>
+    {% endwith %}
+    {% endfor %}
+</div>
+
+{% endblock %}

+ 9 - 0
coderedcms/templates/coderedcms/blocks/pricelist_block.html

@@ -0,0 +1,9 @@
+{% extends "coderedcms/blocks/base_block.html" %}
+{% load wagtailcore_tags %}
+
+{% block block_render %}
+    <h3>{% include_block self.heading %}</h3>
+    {% for item in self.items %}
+        {% include_block item %}
+    {% endfor %}
+{% endblock %}

+ 16 - 0
coderedcms/templates/coderedcms/blocks/pricelistitem_block.html

@@ -0,0 +1,16 @@
+{% load wagtailimages_tags %}
+
+<div class="row">
+{% if self.image %}
+    <div class="col-md-4">
+    {% image self.image original as imagedata %}
+    <img src="{{imagedata.url}}" alt="Photo of {{self.name}}" class="w-100" />
+    </div>
+    <div class="col-md-8" style="overflow-x: hidden;">
+{% else %}
+    <div class="col-md-12" style="overflow-x: hidden;">
+{% endif %}
+    <strong class="leaders"><span>{{self.name}}</span><span>{{self.price}}</span></strong>
+    <p>{{self.description}}</p>
+    </div>
+</div>

+ 7 - 0
coderedcms/templates/coderedcms/blocks/quote_block.html

@@ -0,0 +1,7 @@
+{% block block_render %}
+    <blockquote class="blockquote {{self.settings.custom_css_class}}"
+    {% if self.settings.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+        <p class="mb-0">{{self.text}}</p>
+        {% if self.author %}<footer class="blockquote-footer">{{self.author}}</footer>{% endif %}
+    </blockquote>
+{% endblock %}

+ 22 - 0
coderedcms/templates/coderedcms/blocks/struct_data_action.json

@@ -0,0 +1,22 @@
+{
+    "target": {
+      "@type": "EntryPoint",
+      "urlTemplate": "{{self.target}}",
+      "inLanguage": "{{self.language}}",
+      "actionPlatform": [
+        "http://schema.org/DesktopWebPlatform",
+        "http://schema.org/IOSPlatform",
+        "http://schema.org/AndroidPlatform"
+      ]
+    },
+    {% if self.result_type %}
+    "result": {
+      "@type": "{{self.result_type}}",
+      "name": "{{self.result_name}}"
+    },
+    {% endif %}
+    {% if self.extra_json %}
+    {{self.extra_json}},
+    {% endif %}
+    "@type": "{{self.action_type}}"
+}

+ 6 - 0
coderedcms/templates/coderedcms/blocks/struct_data_hours.json

@@ -0,0 +1,6 @@
+{
+    "@type": "OpeningHoursSpecification",
+    "dayOfWeek": {{self.days_json|safe}},
+    "opens": "{{self.start_time|date:'H:i'}}",
+    "closes": "{{self.end_time|date:'H:i'}}"
+}

+ 29 - 0
coderedcms/templates/coderedcms/blocks/table_block.html

@@ -0,0 +1,29 @@
+<table class="table {{ self.settings.custom_css_class }}" {% if self.custom_css_id %}id="{{self.settings.custom_css_id}}"{% endif %}>
+    {% if self.table.first_row_is_table_header %}
+        <thead>
+            <tr>
+                {% for cell in self.table.data.0 %}
+                    <th scope="col">{{ cell|default_if_none:'' }}</th>
+                {% endfor %}
+            </tr>
+        </thead>
+    {% endif %}
+
+    <tbody>
+        {% for cell in self.table.data.0 %}
+            {% if not self.table.first_row_is_table_header %}<td>{{ cell|default_if_none:'' }}</td>{% endif %}
+        {% endfor %}        
+
+        {% for row in self.table.data|slice:"1:" %}
+            <tr>
+                {% for cell in row %}
+                    {% if self.table.first_col_is_header and cell == row.0 %}
+                        <th scope="row">{{ cell|default_if_none:'' }}</th>
+                    {% else %}
+                        <td>{{ cell|default_if_none:'' }}</td>
+                    {% endif %}
+                {% endfor %}
+            </tr>
+        {% endfor %}
+    </tbody>
+</table>

+ 4 - 0
coderedcms/templates/coderedcms/formfields/date.html

@@ -0,0 +1,4 @@
+<input type="date" name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %} {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}" {% endif %}>
+<noscript>
+    <small class="form-text text-muted">Format mm/dd/yyyy</small>
+</noscript>

+ 7 - 0
coderedcms/templates/coderedcms/formfields/datetime.html

@@ -0,0 +1,7 @@
+<input type="datetime-local" name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %} {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}" {% endif %}>
+<noscript>
+    <small class="form-text text-muted">Format mm/dd/yyyy 4:00 PM</small>
+</noscript>
+<div class="datetime-help" style="display:none;">
+    <small class="form-text text-muted">Format mm/dd/yyyy 4:00 PM</small>
+</div>

+ 4 - 0
coderedcms/templates/coderedcms/formfields/time.html

@@ -0,0 +1,4 @@
+<input type="time" name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %} {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}" {% endif %}>
+<noscript>
+    <small class="form-text text-muted">Format 4:00 PM</small>
+</noscript>

+ 22 - 0
coderedcms/templates/coderedcms/includes/pagination.html

@@ -0,0 +1,22 @@
+{% load i18n coderedcms_tags %}
+
+{% if items.paginator.num_pages > 1 %}
+<hr>
+<nav aria-label="Page navigation">
+    <ul class="pagination justify-content-center">
+        <li class="page-item {% if not items.has_previous %}disabled{% endif %}">
+            <a class="page-link" href="{% if items.has_previous %}?{% query_update request.GET 'p' items.previous_page_number as q %}{{q.urlencode}}{% else %}#{% endif %}" aria-label="Previous">
+                &laquo; {% trans 'Previous' %}
+            </a>
+        </li>
+        <li class="page-item disabled">
+            <span class="page-link">{% trans 'Page' %} {{ items.number }} {% trans 'of' %} {{ items.paginator.num_pages }}</span>
+        </li>
+        <li class="page-item {% if not items.has_next %}disabled{% endif %}">
+            <a class="page-link" href="{% if items.has_next %}?{% query_update request.GET 'p' items.next_page_number as q %}{{q.urlencode}}{% else %}#{% endif %}" aria-label="Next">
+                {% trans 'Next' %} &raquo;
+            </a>
+        </li>
+    </ul>
+</nav>
+{% endif %}

+ 50 - 0
coderedcms/templates/coderedcms/includes/struct_data_article.json

@@ -0,0 +1,50 @@
+{% load wagtailimages_tags %}
+
+{
+  "@context": "http://schema.org",
+  "mainEntityOfPage": {
+    "@type": "WebPage",
+    "@id": "{{page.get_full_url}}"
+  },
+  "headline": "{{page.title}}",
+  "description": "{{page.get_description}}",
+
+  {# Get different aspect ratios. Use huge numbers because wagtail will not upscale, #}
+  {# but will max out at the image's original resultion using the specified aspect ratio. #}
+  {# Google wants them high resolution. #}
+  {% if page.og_image %}
+    {% image page.struct_org_image fill-10000x10000 as img_11 %}
+    {% image page.struct_org_image fill-40000x30000 as img_21 %}
+    {% image page.struct_org_image fill-16000x9000 as img_169 %}
+    "image": [
+        "{{self.get_site.root_url}}{{img_11.url}}",
+        "{{self.get_site.root_url}}{{img_21.url}}",
+        "{{self.get_site.root_url}}{{img_169.url}}"
+    ],
+  {% elif page.cover_image %}
+    {% image page.cover_image fill-10000x10000 as img_11 %}
+    {% image page.cover_image fill-40000x30000 as img_21 %}
+    {% image page.cover_image fill-16000x9000 as img_169 %}
+    "image": [
+        "{{self.get_site.root_url}}{{img_11.url}}",
+        "{{self.get_site.root_url}}{{img_21.url}}",
+        "{{self.get_site.root_url}}{{img_169.url}}"
+    ],
+  {% endif %}
+
+  "datePublished": "{{page.get_pub_date|date:'c'}}",
+  "dateModified": "{{page.last_published_at|date:'c'}}",
+
+  "author": {
+    "@type": "Person",
+    "name": "{{page.get_author_name}}"
+  },
+
+  {% if page.struct_org_type %}
+  "publisher": {% include "coderedcms/includes/struct_data_org.json" with page=page org_mode=True %},
+  {% elif page.get_site.root_page.specific.struct_org_type %}
+  "publisher": {% include "coderedcms/includes/struct_data_org.json" with page=page.get_site.root_page.specific org_mode=True %},
+  {% endif %}
+
+  "@type": "Article"
+}

Some files were not shown because too many files changed in this diff