瀏覽代碼

Merge master into 33-breadcrumb-changes after resolving conflicts

Edd Baldry 8 年之前
父節點
當前提交
cab38ff3bc

+ 4 - 0
.dockerignore

@@ -0,0 +1,4 @@
+Dockerfile
+docker-compose.yml
+Procfile
+Vagrantfile

+ 0 - 1
.gitignore

@@ -12,7 +12,6 @@ bakerydemo/media/*
 bakerydemo/settings/local.py
 bakerydemodb
 __pycache__
-.*
 .vagrant/
 /.vagrant/
 /Vagrantfile.local

+ 23 - 0
.travis.yml

@@ -0,0 +1,23 @@
+sudo: required
+services:
+  - docker
+
+env:
+  global:
+    - GREP_TIMEOUT=360
+
+before_install:
+  - sudo apt-get update
+  - sudo apt-get install -qy -o Dpkg::Options::="--force-confold" docker-engine coreutils
+
+script:
+  # Bring up the postgres, redis, and app containers
+  - docker-compose up --build -d
+
+  - timeout $GREP_TIMEOUT grep -m 1 'Running migrations' <(docker-compose logs --follow app 2>&1)
+  - timeout $GREP_TIMEOUT grep -m 1 'spawned uWSGI http 1' <(docker-compose logs --follow app 2>&1)
+  - docker-compose run app /venv/bin/python /code/manage.py check
+
+after_script:
+  - docker-compose logs
+  - docker images

+ 44 - 0
Dockerfile

@@ -0,0 +1,44 @@
+FROM python:3.5-alpine
+
+ADD requirements/ /requirements/
+RUN set -ex \
+	&& apk add --no-cache --virtual .build-deps \
+		gcc \
+		g++ \
+		make \
+		libc-dev \
+		musl-dev \
+		linux-headers \
+		pcre-dev \
+		postgresql-dev \
+		libjpeg-turbo-dev \
+	&& pyvenv /venv \
+	&& /venv/bin/pip install -U pip \
+	&& LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "/venv/bin/pip install -r /requirements/production.txt" \
+	&& runDeps="$( \
+		scanelf --needed --nobanner --recursive /venv \
+			| awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
+			| sort -u \
+			| xargs -r apk info --installed \
+			| sort -u \
+	)" \
+	&& apk add --virtual .python-rundeps $runDeps \
+	&& apk del .build-deps
+RUN apk add --no-cache postgresql-client
+RUN mkdir /code/
+WORKDIR /code/
+ADD . /code/
+EXPOSE 8000
+
+# Add custom environment variables needed by Django or your settings file here:
+ENV DJANGO_SETTINGS_MODULE=bakerydemo.settings.production DJANGO_DEBUG=off
+
+# uWSGI configuration (customize as needed):
+ENV UWSGI_VIRTUALENV=/venv UWSGI_WSGI_FILE=bakerydemo/wsgi_production.py UWSGI_HTTP=:8000 UWSGI_MASTER=1 UWSGI_WORKERS=2 UWSGI_THREADS=8 UWSGI_UID=1000 UWSGI_GID=2000
+
+# Call collectstatic with dummy environment variables:
+RUN DATABASE_URL=postgres://none REDIS_URL=none /venv/bin/python manage.py collectstatic --noinput
+
+# start uWSGI, using a wrapper script to allow us to easily add more commands to container startup:
+ENTRYPOINT ["/code/docker-entrypoint.sh"]
+CMD ["/venv/bin/uwsgi", "--http-auto-chunked", "--http-keepalive"]

+ 1 - 1
Procfile

@@ -1,2 +1,2 @@
 release: yes "yes" | python manage.py migrate
-web: gunicorn bakerydemo.heroku_wsgi --log-file -
+web: uwsgi --http-socket=:$PORT --master --workers=2 --threads=8 --die-on-term --wsgi-file=bakerydemo/wsgi_production.py

+ 3 - 1
app.json

@@ -4,7 +4,9 @@
   "repository": "https://github.com/wagtail/bakerydemo",
   "keywords": ["wagtail", "django"],
   "env": {
-    "DJANGO_SETTINGS_MODULE": "bakerydemo.settings.heroku"
+    "DJANGO_DEBUG": "off",
+    "DJANGO_SETTINGS_MODULE": "bakerydemo.settings.production",
+    "DJANGO_SECURE_SSL_REDIRECT": "on"
   },
   "scripts": {
     "postdeploy": "django-admin.py migrate && django-admin.py load_initial_data && echo 'from wagtail.wagtailimages.models import Rendition; Rendition.objects.all().delete()' | django-admin.py shell"

+ 26 - 0
bakerydemo/locations/migrations/0003_auto_20170214_2220.py

@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-14 22:20
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('locations', '0002_auto_20170211_2229'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='locationoperatinghours',
+            name='day',
+            field=models.CharField(choices=[('Mon', 'Mon'), ('Tue', 'Tue'), ('Wed', 'Weds'), ('Thu', 'Thu'), ('Fri', 'Fri'), ('Sat', 'Sat'), ('Sun', 'Sun')], default='Mon', max_length=4),
+        ),
+        migrations.AlterField(
+            model_name='locationpage',
+            name='lat_long',
+            field=models.CharField(help_text="Comma separated lat/long. (Ex. 64.144367, -21.939182)                    Right click Google Maps and select 'What's Here'", max_length=36, validators=[django.core.validators.RegexValidator(code='invalid_lat_long', message='Lat Long must be a comma-separated numeric lat and long', regex='^(\\-?\\d+(\\.\\d+)?),\\s*(\\-?\\d+(\\.\\d+)?)$')]),
+        ),
+    ]

+ 29 - 0
bakerydemo/locations/migrations/0004_auto_20170215_1334.py

@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-15 13:34
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import wagtail.wagtailcore.blocks
+import wagtail.wagtailcore.fields
+import wagtail.wagtailembeds.blocks
+import wagtail.wagtailimages.blocks
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('locations', '0003_auto_20170214_2220'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='locationpage',
+            name='body',
+            field=wagtail.wagtailcore.fields.StreamField((('heading_block', wagtail.wagtailcore.blocks.StructBlock((('heading_text', wagtail.wagtailcore.blocks.CharBlock(classname='title', required=True)), ('size', wagtail.wagtailcore.blocks.ChoiceBlock(blank=True, choices=[('', 'Select a header size'), ('h2', 'H2'), ('h3', 'H3'), ('h4', 'H4')], required=False))))), ('paragraph_block', wagtail.wagtailcore.blocks.RichTextBlock(icon='fa-paragraph', template='blocks/paragraph_block.html')), ('image_block', wagtail.wagtailcore.blocks.StructBlock((('image', wagtail.wagtailimages.blocks.ImageChooserBlock(required=True)), ('caption', wagtail.wagtailcore.blocks.CharBlock(required=False)), ('attribution', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('block_quote', wagtail.wagtailcore.blocks.StructBlock((('text', wagtail.wagtailcore.blocks.TextBlock()), ('attribute_name', wagtail.wagtailcore.blocks.CharBlock(blank=True, label='e.g. Guy Picciotto', required=False))))), ('embed_block', wagtail.wagtailembeds.blocks.EmbedBlock(help_text='Insert an embed URL e.g https://www.youtube.com/embed/SGJFWirQ3ks', icon='fa-s15', template='blocks/embed_block.html'))), blank=True, verbose_name='About page detail'),
+        ),
+        migrations.AddField(
+            model_name='locationpage',
+            name='introduction',
+            field=models.TextField(blank=True, help_text='Text to describe the index page'),
+        ),
+    ]

+ 21 - 0
bakerydemo/locations/migrations/0005_locationoperatinghours_closed.py

@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-15 13:38
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('locations', '0004_auto_20170215_1334'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='locationoperatinghours',
+            name='closed',
+            field=models.BooleanField(default='False', help_text='Tick if location is closed', verbose_name='Closed?'),
+            preserve_default=False,
+        ),
+    ]

+ 30 - 0
bakerydemo/locations/migrations/0006_auto_20170215_1408.py

@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-15 14:08
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('locations', '0005_locationoperatinghours_closed'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='locationoperatinghours',
+            name='closed',
+            field=models.BooleanField(help_text='Tick if location is closed on this day', verbose_name='Closed?'),
+        ),
+        migrations.AlterField(
+            model_name='locationoperatinghours',
+            name='closing_time',
+            field=models.TimeField(blank=True),
+        ),
+        migrations.AlterField(
+            model_name='locationoperatinghours',
+            name='opening_time',
+            field=models.TimeField(blank=True),
+        ),
+    ]

+ 25 - 0
bakerydemo/locations/migrations/0007_auto_20170215_1411.py

@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-15 14:11
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('locations', '0006_auto_20170215_1408'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='locationoperatinghours',
+            name='closing_time',
+            field=models.TimeField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='locationoperatinghours',
+            name='opening_time',
+            field=models.TimeField(blank=True, null=True),
+        ),
+    ]

+ 16 - 0
bakerydemo/locations/migrations/0008_merge_20170218_0921.py

@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-18 09:21
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('locations', '0007_auto_20170215_1411'),
+        ('locations', '0005_locationsindexpage_introduction'),
+    ]
+
+    operations = [
+    ]

+ 24 - 0
bakerydemo/locations/migrations/0009_auto_20170219_0942.py

@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-19 09:42
+from __future__ import unicode_literals
+
+from django.db import migrations
+import wagtail.wagtailcore.blocks
+import wagtail.wagtailcore.fields
+import wagtail.wagtailembeds.blocks
+import wagtail.wagtailimages.blocks
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('locations', '0008_merge_20170218_0921'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='locationpage',
+            name='body',
+            field=wagtail.wagtailcore.fields.StreamField((('heading_block', wagtail.wagtailcore.blocks.StructBlock((('heading_text', wagtail.wagtailcore.blocks.CharBlock(classname='title', required=True)), ('size', wagtail.wagtailcore.blocks.ChoiceBlock(blank=True, choices=[('', 'Select a header size'), ('h2', 'H2'), ('h3', 'H3'), ('h4', 'H4')], required=False))))), ('paragraph_block', wagtail.wagtailcore.blocks.RichTextBlock(icon='fa-paragraph', template='blocks/paragraph_block.html')), ('image_block', wagtail.wagtailcore.blocks.StructBlock((('image', wagtail.wagtailimages.blocks.ImageChooserBlock(required=True)), ('caption', wagtail.wagtailcore.blocks.CharBlock(required=False)), ('attribution', wagtail.wagtailcore.blocks.CharBlock(required=False))))), ('block_quote', wagtail.wagtailcore.blocks.StructBlock((('text', wagtail.wagtailcore.blocks.TextBlock()), ('attribute_name', wagtail.wagtailcore.blocks.CharBlock(blank=True, label='e.g. Guy Picciotto', required=False))))), ('embed_block', wagtail.wagtailembeds.blocks.EmbedBlock(help_text='Insert an embed URL e.g https://www.youtube.com/embed/SGJFWirQ3ks', icon='fa-s15', template='blocks/embed_block.html'))), blank=True, verbose_name='About this location'),
+        ),
+    ]

+ 76 - 23
bakerydemo/locations/models.py

@@ -1,55 +1,81 @@
+from datetime import datetime
+
+from django.conf import settings
 from django.core.validators import RegexValidator
 from django.db import models
 
 from modelcluster.fields import ParentalKey
 
-from wagtail.wagtailadmin.edit_handlers import FieldPanel, InlinePanel
+from wagtail.wagtailadmin.edit_handlers import (
+    FieldPanel,
+    InlinePanel,
+    StreamFieldPanel)
 from wagtail.wagtailcore.models import Orderable, Page
-from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
+from wagtail.wagtailimages.edit_handlers import (
+    ImageChooserPanel,
+    )
+from wagtail.wagtailcore.fields import StreamField
 from wagtail.wagtailsearch import index
 
+from bakerydemo.base.blocks import BaseStreamBlock
+
 
 class OperatingHours(models.Model):
     """
     Django model to capture operating hours for a Location
     """
-    MONDAY = 'MON'
-    TUESDAY = 'TUE'
-    WEDNESDAY = 'WED'
-    THURSDAY = 'THU'
-    FRIDAY = 'FRI'
-    SATURDAY = 'SAT'
-    SUNDAY = 'SUN'
+    MONDAY = 'Mon'
+    TUESDAY = 'Tue'
+    WEDNESDAY = 'Wed'
+    THURSDAY = 'Thu'
+    FRIDAY = 'Fri'
+    SATURDAY = 'Sat'
+    SUNDAY = 'Sun'
 
     DAY_CHOICES = (
-        (MONDAY, 'MON'),
-        (TUESDAY, 'TUE'),
-        (WEDNESDAY, 'WED'),
-        (THURSDAY, 'THU'),
-        (FRIDAY, 'FRI'),
-        (SATURDAY, 'SAT'),
-        (SUNDAY, 'SUN'),
+        (MONDAY, 'Mon'),
+        (TUESDAY, 'Tue'),
+        (WEDNESDAY, 'Weds'),
+        (THURSDAY, 'Thu'),
+        (FRIDAY, 'Fri'),
+        (SATURDAY, 'Sat'),
+        (SUNDAY, 'Sun'),
     )
 
     day = models.CharField(
-        max_length=3,
+        max_length=4,
         choices=DAY_CHOICES,
         default=MONDAY,
     )
-    opening_time = models.TimeField()
-    closing_time = models.TimeField()
+    opening_time = models.TimeField(
+        blank=True,
+        null=True)
+    closing_time = models.TimeField(
+        blank=True,
+        null=True)
+    closed = models.BooleanField(
+        "Closed?",
+        blank=True,
+        help_text='Tick if location is closed on this day'
+        )
 
     panels = [
         FieldPanel('day'),
         FieldPanel('opening_time'),
         FieldPanel('closing_time'),
+        FieldPanel('closed'),
     ]
 
     class Meta:
         abstract = True
 
     def __str__(self):
-        return '{}: {} - {}'.format(self.day, self.opening_time, self.closing_time)
+        return '{}: {} - {} {}'.format(
+            self.day,
+            self.opening_time.strftime('%H:%M'),
+            self.closing_time.strftime('%H:%M'),
+            settings.TIME_ZONE
+        )
 
 
 class LocationOperatingHours(Orderable, OperatingHours):
@@ -97,7 +123,9 @@ class LocationPage(Page):
     """
     Detail for a specific bakery location.
     """
-
+    introduction = models.TextField(
+        help_text='Text to describe the index page',
+        blank=True)
     address = models.TextField()
     image = models.ForeignKey(
         'wagtailimages.Image',
@@ -118,6 +146,13 @@ class LocationPage(Page):
             ),
         ]
     )
+    body = StreamField(
+        BaseStreamBlock(), verbose_name="About this location", blank=True
+    )
+    # We've defined the StreamBlock() within blocks.py that we've imported on
+    # line 12. Defining it in a different file gives us consistency across the
+    # site, though StreamFields _can_ be created on a per model basis if you
+    # have a use case for it
 
     # Search index configuration
     search_fields = Page.search_fields + [
@@ -126,19 +161,37 @@ class LocationPage(Page):
 
     # Editor panels configuration
     content_panels = Page.content_panels + [
+        FieldPanel('introduction', classname="full"),
+        StreamFieldPanel('body'),
         FieldPanel('address', classname="full"),
         FieldPanel('lat_long'),
         ImageChooserPanel('image'),
-        InlinePanel('hours_of_operation', label="Hours of Operation")
+        InlinePanel('hours_of_operation', label="Hours of Operation"),
     ]
 
     def __str__(self):
         return self.title
 
-    def opening_hours(self):
+    @property
+    def operating_hours(self):
         hours = self.hours_of_operation.all()
         return hours
 
+    def is_open(self):
+        # Determines if the location is currently open
+        now = datetime.now()
+        current_time = now.time()
+        current_day = now.strftime('%a').upper()
+        try:
+            self.operating_hours.get(
+                day=current_day,
+                opening_time__lte=current_time,
+                closing_time__gte=current_time
+            )
+            return True
+        except LocationOperatingHours.DoesNotExist:
+            return False
+
     def get_context(self, request):
         context = super(LocationPage, self).get_context(request)
         context['lat'] = self.lat_long.split(",")[0]

+ 0 - 22
bakerydemo/settings/heroku.py

@@ -1,22 +0,0 @@
-import dj_database_url
-
-from .base import *
-
-
-# Accept all hostnames, since we don't know in advance which hostname will be used for any given Heroku instance.
-# IMPORTANT: Set this to a real hostname when using this in production!
-# See https://docs.djangoproject.com/en/1.10/ref/settings/#allowed-hosts
-ALLOWED_HOSTS = ['*', ]
-
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
-
-# BASE_URL required for notification emails
-BASE_URL = 'http://localhost:8000'
-
-db_from_env = dj_database_url.config(conn_max_age=500)
-DATABASES['default'].update(db_from_env)
-
-# Simplified static file serving.
-# https://warehouse.python.org/project/whitenoise/
-
-STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage'

+ 68 - 0
bakerydemo/settings/production.py

@@ -0,0 +1,68 @@
+import os
+import dj_database_url
+import random
+import string
+
+from .base import *
+
+DEBUG = os.getenv('DJANGO_DEBUG', 'off') == 'on'
+
+# DJANGO_SECRET_KEY *should* be specified in the environment. If it's not, generate an ephemeral key.
+if 'DJANGO_SECRET_KEY' in os.environ:
+    SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
+else:
+    # Use if/else rather than a default value to avoid calculating this if we don't need it
+    print("WARNING: DJANGO_SECRET_KEY not found in os.environ. Generating ephemeral SECRET_KEY.")
+    SECRET_KEY = ''.join([random.SystemRandom().choice(string.printable) for i in range(50)])
+
+# Make sure Django can detect a secure connection properly on Heroku:
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
+# Redirect all requests to HTTPS
+SECURE_SSL_REDIRECT = os.getenv('DJANGO_SECURE_SSL_REDIRECT', 'off') == 'on'
+
+# Accept all hostnames, since we don't know in advance which hostname will be used for any given Heroku instance.
+# IMPORTANT: Set this to a real hostname when using this in production!
+# See https://docs.djangoproject.com/en/1.10/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(';')
+
+EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+
+# BASE_URL required for notification emails
+BASE_URL = 'http://localhost:8000'
+
+db_from_env = dj_database_url.config(conn_max_age=500)
+DATABASES['default'].update(db_from_env)
+
+# Simplified static file serving.
+# https://warehouse.python.org/project/whitenoise/
+
+MIDDLEWARE.append('whitenoise.middleware.WhiteNoiseMiddleware')
+STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+
+if 'AWS_STORAGE_BUCKET_NAME' in os.environ:
+    AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
+    AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID', '')
+    AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY', '')
+    AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME
+    AWS_AUTO_CREATE_BUCKET = True
+
+    INSTALLED_APPS.append('storages')
+    MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
+    DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
+
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'handlers': {
+        'console': {
+            'class': 'logging.StreamHandler',
+        },
+    },
+    'loggers': {
+        'django': {
+            'handlers': ['console'],
+            'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
+        },
+    },
+}

+ 0 - 13
bakerydemo/templates/about_page.html

@@ -1,13 +0,0 @@
-{% extends "base.html" %}
-{% load wagtailimages_tags %}
-
-{% block content %}
-    {{ page.title }}
-
-    <div class="image">
-        {% image page.image width-500 as photo %}
-              <img src="{{ photo.url }}" width="{{ photo.width }}" height="{{ photo.height }}" alt="{{ photo.alt }}" />
-    </div>
-
-    {{ page.body }}
-{% endblock content %}

+ 1 - 1
bakerydemo/templates/base.html

@@ -4,7 +4,7 @@
     {% include "includes/head.html" %}
 {% endblock head %}
 
-<body>
+<body class="{% block body_class %}template-{{ self.get_verbose_name|slugify }}{% endblock %}">
 {% wagtailuserbar %}
 
 {% block header %}

+ 1 - 6
bakerydemo/templates/base/about_page.html

@@ -2,12 +2,7 @@
 {% load wagtailimages_tags %}
 
 {% block content %}
-    {{ page.title }}
-
-    <div class="image">
-        {% image page.image width-500 as photo %}
-              <img src="{{ photo.url }}" width="{{ photo.width }}" height="{{ photo.height }}" alt="{{ photo.alt }}" />
-    </div>
+    {% include "base/include/header.html" %}
 
     {{ page.body }}
 {% endblock content %}

+ 79 - 24
bakerydemo/templates/locations/location_page.html

@@ -1,35 +1,89 @@
 {% extends "base.html" %}
-{% load wagtailimages_tags %}
+{% load wagtailimages_tags navigation_tags %}
 
 {% block head-extra %}
-  <style>
-      /* Needed for Google map embed */
-      #map {
-        height: 100%;
-      }
-      .maps.embed-container {
-         pointer-events: none;
-      }
-      html, body {
-        height: 100%;
-        margin: 0;
-        padding: 0;
-      }
-  </style>
+    <style>
+        /* Following two selectors needed for Google map embed */
+        #map {
+          height: 100%;
+        }
+    </style>
 {% endblock head-extra %}
 
 {% block content %}
-    <h1>{{ page.title }}</h1>
-    <figure>
-      {% image self.image fill-600x600 %}
-    </figure>
+{# @TODO This is identical to the header within blog_page.html. We should create an include #}
+{% image self.image fill-1920x600 as hero_img %}
+<div class="container-fluid hero" style="background-image:url('{{ hero_img.url }}')">
+<div class="hero-gradient-mask"></div>
+    <div class="container">
+        <div class="row">
+            <div class="col-md-7">
+                <h1>{{ page.title }}</h1>
+                <p class="stand-first">
+                {% if page.is_open %}
+                  This location is currently open
+                {% else %}
+                  Sorry, this location is currently closed
+                {% endif %}
+                </p>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="container">
+    <div class="row">
+        <div class="col-md-7">
+          <div class="row">
+            {% if page.introduction %}
+                <div class="intro col-md-7"><p>{{ page.introduction }}</p></div>
+            {% endif %}
+
+            {% if page.operating_hours %}
+                <div class="col-md-4 col-md-offset-1 location-opening">
+                <h3>Opening hours</h3>
+                {% for hours in page.operating_hours %}
+                  <time itemprop="openingHours" datetime="{{ hours }}" class="location-time">
+                    <span class="day">{{ hours.day }}</span>:
+                    <span class="hours">
+                      {% if hours.closed == True %}
+                        Closed
+                        {% else %}
+                        {% if hours.opening_time %}
+                          {{ hours.opening_time }}
+                        {% endif %} -
+                        {% if hours.closing_time %}
+                          {{ hours.closing_time }}
+                        {% endif %}
+                      {% endif %}
+                      </span></time>
+                {% endfor %}
+                </div>
+            {% endif %}
+          </div>
+        </div>
+    </div>
+</div>
+<div class="container-flex">
+  <div class="row">
+    <div class="col-md-2 col-md-offset-5 location-address">
+      <h3>Address</h3>
+        <address>{{ page.address|linebreaks }}</address>
+    </div>
+  </div>
 
-    <p>{{ page.address|linebreaks }}</p>
+  <div class="map-container">
     <div id="map" class="maps embed-container"></div>
+  </div>
+</div>
 
-    {% for hours in page.opening_hours %}
-        <li>{{ hours }}</li>
-    {% endfor %}
+<div class="container">
+  <div class="row">
+    <div class="col-md-7 location-body">
+      {{ page.body }}
+    </div>
+  </div>
+</div>
 
     <script>
       var map;
@@ -39,7 +93,8 @@
               lat: {{lat}},
               lng: {{long}}
           },
-          zoom: 8
+          zoom: 15,
+          scrollwheel: false
         });
         var marker = new google.maps.Marker({
           position: {

+ 30 - 9
bakerydemo/templates/locations/locations_index_page.html

@@ -1,13 +1,34 @@
-{% extends "base.html" %}{% load wagtailimages_tags wagtailcore_tags %}
+{% extends "base.html" %}
+{% load wagtailcore_tags navigation_tags wagtailimages_tags %}
 
 {% block content %}
-    {{ page.title }}
-    {{ page.introduction }}
-
-    {% for location in locations %}
-        <div>
-            <a href="{% pageurl location %}">{{ location.title }}</a>
-            {% image location.image width-150 %}
+<div class="container">
+    <div class="row">
+        <div class="col-md-12">
+            <h1>{{ page.title }}</h1>
+            <p>{{ page.introduction }}</p>
         </div>
-    {% endfor %}
+    </div>
+</div>
+
+<div class="container">
+    <div class="row no-gutters">
+        {% for location in locations %}
+            <div class="col-md-6 location-list-item">
+                <a href="{% pageurl location %}">
+                <h1 class="location-list-title">
+                
+        
+                    {% image location.image fill-660x270-c75 as image %}
+                    <img src="{{ image.url }}" width="{{ image.width }}" height="{{ image.height }}" alt="{{ image.alt }}" class="" />
+                    
+                    <span class="title">{{ location.title }}</span>
+                
+                </h1></a>
+                    <address>{{ location.address }}</address>
+                    <a href="https://google.com/maps/?q={{ location.lat_long }}" class="btn">Map</a>
+            </div>
+        {% endfor %}
+    </div>
+</div>
 {% endblock content %}

+ 0 - 0
bakerydemo/heroku_wsgi.py → bakerydemo/wsgi_production.py


+ 33 - 0
docker-compose.yml

@@ -0,0 +1,33 @@
+version: '2'
+
+services:
+  db:
+    environment:
+      POSTGRES_DB: app_db
+      POSTGRES_USER: app_user
+      POSTGRES_PASSWORD: changeme
+    restart: always
+    image: postgres:9.6
+    expose:
+      - "5432"
+  redis:
+    restart: always
+    image: redis:3.0
+    expose:
+      - "6379"
+  app:
+    environment:
+      DJANGO_SECRET_KEY: changeme
+      DATABASE_URL: postgres://app_user:changeme@db/app_db
+      REDIS_URL: redis://redis
+    build:
+      context: .
+      dockerfile: ./Dockerfile
+    links:
+      - db:db
+      - redis:redis
+    ports:
+      - "8000:8000"
+    depends_on:
+      - db
+      - redis

+ 19 - 0
docker-entrypoint.sh

@@ -0,0 +1,19 @@
+#!/bin/sh
+set -e
+
+until psql $DATABASE_URL -c '\l'; do
+  >&2 echo "Postgres is unavailable - sleeping"
+  sleep 1
+done
+
+>&2 echo "Postgres is up - continuing"
+
+if [ "$1" = '/venv/bin/uwsgi' ]; then
+    /venv/bin/python manage.py migrate --noinput
+fi
+
+if [ "x$DJANGO_LOAD_INITIAL_DATA" = 'xon' ]; then
+	/venv/bin/python manage.py load_initial_data
+fi
+
+exec "$@"

+ 50 - 33
readme.md

@@ -26,7 +26,7 @@ Run the following commands:
 
 ```bash
 git clone git@github.com:wagtail/bakerydemo.git
-cd wagtaildemo
+cd bakerydemo
 vagrant up
 vagrant ssh
 # then, within the SSH session:
@@ -38,9 +38,39 @@ interface at [http://localhost:8000/admin/](http://localhost:8000/admin/).
 
 Log into the admin with the credentials ``admin / changeme``.
 
-Setup without Vagrant
------
-Don't want to set up a whole VM to try out Wagtail? No problem.
+Setup with Docker
+-----------------
+
+### Dependencies
+* [Docker](https://docs.docker.com/engine/installation/)
+
+### Installation
+Run the following commands:
+
+```bash
+git clone git@github.com:wagtail/bakerydemo.git
+cd bakerydemo
+docker-compose up --build -d
+docker-compose run app /venv/bin/python manage.py load_initial_data
+```
+
+The demo site will now be accessible at [http://localhost:8000/](http://localhost:8000/) and the Wagtail admin
+interface at [http://localhost:8000/admin/](http://localhost:8000/admin/).
+
+Log into the admin with the credentials ``admin / changeme``.
+
+**Important:** This `docker-compose.yml` is configured for local testing only, and is not intended for production use.
+
+### Debugging
+To tail the logs from the Docker containers in realtime, run:
+
+```bash
+docker-compose logs -f
+```
+
+Local Setup
+-----------
+Don't want to set up a whole VM nor use Docker to try out Wagtail? No problem.
 
 ### Dependencies
 * [PIP](https://github.com/pypa/pip)
@@ -89,53 +119,40 @@ update in the browser. Once finished, click `View` to see the public site.
 
 Log into the admin with the credentials ``admin / changeme``.
 
+To prevent the demo site from regenerating a new Django `SECRET_KEY` each time Heroku restarts your site, you should set
+a `DJANGO_SECRET_KEY` environment variable in Heroku using the web interace or the [CLI](https://devcenter.heroku.com/articles/heroku-cli). If using the CLI, you can set a `SECRET_KEY` like so:
+
+    heroku config:set DJANGO_SECRET_KEY=changeme
+
 To learn more about Heroku, read [Deploying Python and Django Apps on Heroku](https://devcenter.heroku.com/articles/deploying-python).
 
 ### Storing Wagtail Media Files on AWS S3
 
-If you have deployed the demo site to Heroku, you may want to perform some additional setup.  Heroku uses an
-[ephemeral filesystem](https://devcenter.heroku.com/articles/dynos#ephemeral-filesystem).  In laymen's terms, this means
-that uploaded images will disappear at a minimum of once per day, and on each application deployment.  To mitigate this,
-you can host your media on S3.
+If you have deployed the demo site to Heroku or via Docker, you may want to perform some additional setup.  Heroku uses an
+[ephemeral filesystem](https://devcenter.heroku.com/articles/dynos#ephemeral-filesystem), and Docker-based hosting
+environments typically work in the same manner.  In laymen's terms, this means that uploaded images will disappear at a
+minimum of once per day, and on each application deployment. To mitigate this, you can host your media on S3.
 
 This documentation assumes that you have an AWS account, an IAM user, and a properly configured S3 bucket. These topics
 are outside of the scope of this documentation; the following [blog post](https://wagtail.io/blog/amazon-s3-for-media-files/)
 will walk you through those steps.
 
-Next, you will need to add `django-storages` and `boto3` to `requirements/heroku.txt`.
-
-Then you will need to edit `settings/heroku.py`:
-
-    INSTALLED_APPS.append('storages')
-
-You will also need to add the S3 bucket and access credentials for the IAM user that you created.
-
-    AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME', 'changeme')
-    AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID', 'changeme')
-    AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY', 'changeme')
-    AWS_S3_CUSTOM_DOMAIN = '{}.s3.amazonaws.com'.format(AWS_STORAGE_BUCKET_NAME)
-
-Next, you will need to set these values in the Heroku environment.  The next steps assume that you have
-the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) installed and configured. You will
-execute the following commands to set the aforementioned environment variables:
+This demo site comes preconfigured with a production settings file that will enable S3 for uploaded media storage if
+``AWS_STORAGE_BUCKET_NAME`` is defined in the shell environment. All you need to do is set the following environment
+variables. If using Heroku, you will first need to install and configure the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli). Then, execute the following commands to set the aforementioned environment variables:
 
     heroku config:set AWS_STORAGE_BUCKET_NAME=changeme
     heroku config:set AWS_ACCESS_KEY_ID=changeme
     heroku config:set AWS_SECRET_ACCESS_KEY=changeme
 
-Do not forget to replace the `changeme` with the actual values for your AWS account.
-
-Finally, we need to configure the `MEDIA_URL` as well as inform Django that we want to use `boto3` for the storage
-backend:
-
-    MEDIA_URL = 'https://{}/'.format(AWS_S3_CUSTOM_DOMAIN)'
-    DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+Do not forget to replace the `changeme` with the actual values for your AWS account. If you're using a different hosting
+environment, set the same environment variables there using the method appropriate for your environment.
 
-Commit these changes and push to Heroku and you should now have persistent media storage!
+Once Heroku restarts your application or your Docker container is refreshed, you should have persistent media storage!
 
 ### Sending email from the contact form
 
-The following setting in `base.py` and `heroku.py` ensures that live email is not sent by the demo contact form.
+The following setting in `base.py` and `production.py` ensures that live email is not sent by the demo contact form.
 
 `EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'`
 

+ 1 - 1
requirements.txt

@@ -1 +1 @@
--r requirements/heroku.txt
+-r requirements/production.txt

+ 3 - 1
requirements/heroku.txt → requirements/production.txt

@@ -1,6 +1,8 @@
 -r base.txt
 # Additional dependencies for Heroku deployment
 dj-database-url==0.4.1
-gunicorn==19.6.0
+uwsgi==2.0.14
 psycopg2==2.6.2
 whitenoise==3.2.2
+boto==2.45.0
+django-storages==1.5.2