{{ post.title }}
+{{ post.intro }}
+By {{ post.owner }}
+ + +diff --git a/services/cms/.gitignore b/services/cms/.gitignore
new file mode 100644
index 0000000..c59768b
--- /dev/null
+++ b/services/cms/.gitignore
@@ -0,0 +1 @@
+media/
\ No newline at end of file
diff --git a/services/cms/blog/__init__.py b/services/cms/blog/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/services/cms/blog/admin.py b/services/cms/blog/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/services/cms/blog/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/services/cms/blog/apps.py b/services/cms/blog/apps.py
new file mode 100644
index 0000000..1003897
--- /dev/null
+++ b/services/cms/blog/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ name = "blog"
diff --git a/services/cms/blog/migrations/0001_initial.py b/services/cms/blog/migrations/0001_initial.py
new file mode 100644
index 0000000..cec22d9
--- /dev/null
+++ b/services/cms/blog/migrations/0001_initial.py
@@ -0,0 +1,34 @@
+# Generated by Django 2.1.2 on 2018-11-11 02:20
+
+from django.db import migrations, models
+import django.db.models.deletion
+import wagtail.core.fields
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [("wagtailcore", "0040_page_draft_title")]
+
+ operations = [
+ migrations.CreateModel(
+ name="BlogIndexPage",
+ fields=[
+ (
+ "page_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="wagtailcore.Page",
+ ),
+ ),
+ ("intro", wagtail.core.fields.RichTextField(blank=True)),
+ ],
+ options={"abstract": False},
+ bases=("wagtailcore.page",),
+ )
+ ]
diff --git a/services/cms/blog/migrations/0002_blogpage.py b/services/cms/blog/migrations/0002_blogpage.py
new file mode 100644
index 0000000..0e04e49
--- /dev/null
+++ b/services/cms/blog/migrations/0002_blogpage.py
@@ -0,0 +1,34 @@
+# Generated by Django 2.1.2 on 2018-11-11 02:58
+
+from django.db import migrations, models
+import django.db.models.deletion
+import wagtail.core.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [("wagtailcore", "0040_page_draft_title"), ("blog", "0001_initial")]
+
+ operations = [
+ migrations.CreateModel(
+ name="BlogPage",
+ fields=[
+ (
+ "page_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="wagtailcore.Page",
+ ),
+ ),
+ ("date", models.DateField(verbose_name="Post date")),
+ ("intro", models.CharField(max_length=250)),
+ ("body", wagtail.core.fields.RichTextField(blank=True)),
+ ],
+ options={"abstract": False},
+ bases=("wagtailcore.page",),
+ )
+ ]
diff --git a/services/cms/blog/migrations/0003_blogpagegalleryimage.py b/services/cms/blog/migrations/0003_blogpagegalleryimage.py
new file mode 100644
index 0000000..a34d198
--- /dev/null
+++ b/services/cms/blog/migrations/0003_blogpagegalleryimage.py
@@ -0,0 +1,52 @@
+# Generated by Django 2.1.2 on 2018-11-16 18:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+import modelcluster.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("wagtailimages", "0021_image_file_hash"),
+ ("blog", "0002_blogpage"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="BlogPageGalleryImage",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "sort_order",
+ models.IntegerField(blank=True, editable=False, null=True),
+ ),
+ ("caption", models.CharField(blank=True, max_length=250)),
+ (
+ "image",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="+",
+ to="wagtailimages.Image",
+ ),
+ ),
+ (
+ "page",
+ modelcluster.fields.ParentalKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="gallery_images",
+ to="blog.BlogPage",
+ ),
+ ),
+ ],
+ options={"ordering": ["sort_order"], "abstract": False},
+ )
+ ]
diff --git a/services/cms/blog/migrations/0004_auto_20181119_1950.py b/services/cms/blog/migrations/0004_auto_20181119_1950.py
new file mode 100644
index 0000000..8263194
--- /dev/null
+++ b/services/cms/blog/migrations/0004_auto_20181119_1950.py
@@ -0,0 +1,33 @@
+# Generated by Django 2.1.2 on 2018-11-19 19:50
+
+from django.db import migrations, models
+import django.db.models.deletion
+import modelcluster.contrib.taggit
+import modelcluster.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('taggit', '0002_auto_20150616_2121'),
+ ('blog', '0003_blogpagegalleryimage'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BlogPageTag',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('content_object', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_items', to='blog.BlogPage')),
+ ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blog_blogpagetag_items', to='taggit.Tag')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='blogpage',
+ name='tags',
+ field=modelcluster.contrib.taggit.ClusterTaggableManager(blank=True, help_text='A comma-separated list of tags.', through='blog.BlogPageTag', to='taggit.Tag', verbose_name='Tags'),
+ ),
+ ]
diff --git a/services/cms/blog/migrations/0005_blogtagindexpage.py b/services/cms/blog/migrations/0005_blogtagindexpage.py
new file mode 100644
index 0000000..8fea19f
--- /dev/null
+++ b/services/cms/blog/migrations/0005_blogtagindexpage.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.1.2 on 2018-11-19 20:04
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('wagtailcore', '0040_page_draft_title'),
+ ('blog', '0004_auto_20181119_1950'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BlogTagIndexPage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ ]
diff --git a/services/cms/blog/migrations/__init__.py b/services/cms/blog/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/services/cms/blog/models.py b/services/cms/blog/models.py
new file mode 100644
index 0000000..139cea0
--- /dev/null
+++ b/services/cms/blog/models.py
@@ -0,0 +1,117 @@
+from django.db import models
+
+from modelcluster.fields import ParentalKey
+from modelcluster.contrib.taggit import ClusterTaggableManager
+from taggit.models import TaggedItemBase
+
+from wagtail.core.models import Page, Orderable
+from wagtail.core.fields import RichTextField
+from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel
+from wagtail.images.edit_handlers import ImageChooserPanel
+from wagtail.search import index
+
+
+class BlogIndexPage(Page):
+ """Entry point of the blog. Displays its children `BlogPages`"""
+
+ intro = RichTextField(blank=True)
+
+ content_panels = Page.content_panels + [FieldPanel("intro", classname="full")]
+
+ def get_context(self, request, *args, **kwargs):
+ """
+ Overrides the default context to include only published pages, ordered by reverse chronological order.
+ These child pages can be accessed in the template using `blogpages` instead of page.get_children.
+ """
+ context = super().get_context(request)
+ blogpages = self.get_children().live().order_by("-first_published_at")
+ context["blogpages"] = blogpages
+ return context
+
+
+class BlogPageTag(TaggedItemBase):
+ """Provides a simple tagging feature to Blog Pages"""
+
+ content_object = ParentalKey(
+ "BlogPage", related_name="tagged_items", on_delete=models.CASCADE
+ )
+
+
+class BlogPage(Page):
+ """Models a blog page entry."""
+
+ date = models.DateField("Post date")
+ intro = models.CharField(max_length=250)
+ body = RichTextField(blank=True)
+ tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
+
+ def main_image(self):
+ """Returns the first image associated with a `BlogPage` from `BlogPageGalleryImage`"""
+ gallery_item = self.gallery_images.first()
+ if gallery_item:
+ return gallery_item.image
+ else:
+ return None
+
+ search_fields = Page.search_fields + [
+ index.SearchField("intro"),
+ index.SearchField("body"),
+ ]
+
+ content_panels = Page.content_panels + [
+ # Grouping Date and tags together for readability in the Admin panel
+ MultiFieldPanel(
+ [FieldPanel("date"), FieldPanel("tags")], heading="Blog information"
+ ),
+ FieldPanel("intro"),
+ FieldPanel("body", classname="full"),
+ InlinePanel("gallery_images", label="Gallery images"),
+ ]
+
+
+class BlogPageGalleryImage(Orderable):
+ """
+ An image gallery descendant to a `BlogPage`
+
+ Inherits a `sort_order` field from the `Orderable` object which keeps track of image ordering.
+ The `ParentalKey` works similarly to a `ForeignKey` but also defines this class as a child of `BlogPage` model.
+ This means that the image gallery associated with a particular `BlogPage` instance is treated as a part of the page
+ in operations like submitting for moderation, and tracking version history.
+
+ The `BlogPageGalleryImage.image` is a `ForeignKey to the Wagtail built-in `Image` model where images themselves are
+ stored. The `ImageChooserPanel` provides a popup interface for choosing an existing image or uploading a new one.
+ This allows for the same image to exist in multiple galleries - effectively creating a many-to-many relationship
+ between pages and images.
+ """
+
+ page = ParentalKey(
+ BlogPage, on_delete=models.CASCADE, related_name="gallery_images"
+ )
+ image = models.ForeignKey(
+ # on_delete here means that is an image is deleted from the system, the gallery entry is deleted as well.
+ "wagtailimages.Image",
+ on_delete=models.CASCADE,
+ related_name="+",
+ )
+ caption = models.CharField(blank=True, max_length=250)
+
+ panels = [ImageChooserPanel("image"), FieldPanel("caption")]
+
+
+class BlogTagIndexPage(Page):
+ """
+ Models the Index page of blogs by tag.
+
+ Even though this model does not define any fields of its own, it is a subclass of Page and is added to the Wagtail
+ ecosystem. This means that it can be given a title and a URL in the admin
+
+ """
+
+ def get_context(self, request, *args, **kwargs):
+ """Filters by tage passed in request query string"""
+ tag = request.GET.get('tag')
+ blogpages = BlogPage.objects.filter(tags__name=tag)
+
+ context = super().get_context(request)
+ context['blogpages'] = blogpages
+ return context
\ No newline at end of file
diff --git a/services/cms/blog/templates/blog/blog_index_page.html b/services/cms/blog/templates/blog/blog_index_page.html
new file mode 100644
index 0000000..74b579f
--- /dev/null
+++ b/services/cms/blog/templates/blog/blog_index_page.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block body_class %}helvetica{% endblock %}
+
+{% block content %}
+
+ {% include 'header.html' %}
+
+ {{ post.intro }} By {{ post.owner }} {{ item.caption }} {{ post.intro }} By {{ post.owner }}{{ page.title }}
+
+ {% for post in blogpages %}
+ {% with post=post.specific %}
+ {{ post.title }}
+ {{ page.title }}
+
+ {% if page.tags.all.count %}
+ Showing pages tagged "{{ request.GET.tag }}"
+ {% endif %}
+
+ {% for post in blogpages %}
+ {% with post=post.specific %}
+ {{ post.title }}
+
+ As a college educator I learned what does and does not work for students. I strive to bring each of my students + along thier learning journey. +
++ Design & Development, eCommerce, Responsive Design. I can deliver you a stunning website. +
++ Quite affectionate and outgoing. + She loves to get chin scratches and will + roll around on the floor waiting for you give her more of them. +
+