Get a basic blog up and running
authorDavid Kerkeslager <kerkeslager@gmail.com>
Thu, 13 Aug 2020 05:16:08 +0000 (01:16 -0400)
committerDavid Kerkeslager <kerkeslager@gmail.com>
Thu, 13 Aug 2020 05:16:08 +0000 (01:16 -0400)
18 files changed:
core/__init__.py [new file with mode: 0644]
core/admin.py [new file with mode: 0644]
core/apps.py [new file with mode: 0644]
core/migrations/0001_initial.py [new file with mode: 0644]
core/migrations/__init__.py [new file with mode: 0644]
core/models.py [new file with mode: 0644]
core/templates/core/base.html [new file with mode: 0644]
core/templates/core/page_nav.html [new file with mode: 0644]
core/templates/core/post_detail.html [new file with mode: 0644]
core/templates/core/post_list.html [new file with mode: 0644]
core/templates/core/style.css [new file with mode: 0644]
core/tests.py [new file with mode: 0644]
core/urls.py [new file with mode: 0644]
core/views.py [new file with mode: 0644]
requirements.txt
styx/settings.py
styx/urls.py
styx/views.py [deleted file]

diff --git a/core/__init__.py b/core/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/core/admin.py b/core/admin.py
new file mode 100644 (file)
index 0000000..a225f61
--- /dev/null
@@ -0,0 +1,11 @@
+from django.contrib import admin
+
+from . import models
+
+class PostAdmin(admin.ModelAdmin):
+    list_display = (
+        'title',
+        'publication_utc',
+    )
+
+admin.site.register(models.Post, PostAdmin)
diff --git a/core/apps.py b/core/apps.py
new file mode 100644 (file)
index 0000000..26f78a8
--- /dev/null
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+    name = 'core'
diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..5fa67c6
--- /dev/null
@@ -0,0 +1,28 @@
+# Generated by Django 3.1 on 2020-08-13 04:09
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Post',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=256, null=True)),
+                ('slug', models.SlugField()),
+                ('publication_utc', models.DateTimeField(blank=True, null=True)),
+                ('body_markdown', models.TextField(null=True)),
+                ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+    ]
diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/core/models.py b/core/models.py
new file mode 100644 (file)
index 0000000..16fe65d
--- /dev/null
@@ -0,0 +1,37 @@
+import re
+
+from django.contrib.auth.models import User
+from django.db import models
+from django.utils.safestring import mark_safe
+
+import commonmark
+
+class Post(models.Model):
+    title = models.CharField(max_length=256, null=True)
+    slug = models.SlugField()
+    author = models.ForeignKey(User, on_delete=models.CASCADE)
+    publication_utc = models.DateTimeField(null=True, blank=True)
+    body_markdown = models.TextField(null=True)
+
+    def __str__(self):
+        return self.title
+
+    @property
+    def body_html(self):
+        return mark_safe(commonmark.commonmark(self.body_markdown))
+
+    @property
+    def teaser_html(self):
+        paragraphs = re.split(r'\n(\s*\n)+', self.body_markdown)
+
+        if len(paragraphs) == 0:
+            return ''
+
+        teaser_markdown = paragraphs[0]
+
+        for p in paragraphs[1:]:
+            if len(teaser_markdown) > 512:
+                break
+            teaser_markdown += '\n\n' + p
+
+        return mark_safe(commonmark.commonmark(teaser_markdown))
diff --git a/core/templates/core/base.html b/core/templates/core/base.html
new file mode 100644 (file)
index 0000000..ba5ab1f
--- /dev/null
@@ -0,0 +1,39 @@
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+    <title>{% block title %}{% endblock %}</title>
+
+    <style>
+      {% include 'core/style.css' %}
+    </style>
+  </head>
+
+  <body>
+    <header>
+      <h1>Styx</h1>
+    </header>
+
+    <nav>
+      <a href='{% url "post-list" %}'>Home</a>
+    </nav>
+
+    <main>
+      {% block content %}{% endblock %}
+    </main>
+
+    <footer>
+      <p>&copy; {% now 'Y' %} Styx</p>
+      <section class="licenses">
+        Licenses:
+        <div>
+          <span>Content: <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a></span>
+          <span>Code: <a rel="license" href="https://www.gnu.org/licenses/gpl.html">GPL 3.0</a></span>
+        </div>
+      </section>
+    </footer>
+  </body>
+</html>
diff --git a/core/templates/core/page_nav.html b/core/templates/core/page_nav.html
new file mode 100644 (file)
index 0000000..c823dc7
--- /dev/null
@@ -0,0 +1,17 @@
+<nav class="pagination">
+  <span class="step-links">
+    {% if page_obj.has_previous %}
+      <a href="?page=1">&laquo; first</a>
+      <a href="?page={{ page_obj.previous_page_number }}">previous</a>
+    {% endif %}
+
+    <span class="current">
+      Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
+    </span>
+
+    {% if page_obj.has_next %}
+      <a href="?page={{ page_obj.next_page_number }}">next</a>
+      <a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
+    {% endif %}
+  </span>
+</nav>
diff --git a/core/templates/core/post_detail.html b/core/templates/core/post_detail.html
new file mode 100644 (file)
index 0000000..a539a14
--- /dev/null
@@ -0,0 +1,15 @@
+{% extends 'core/base.html' %}
+
+{% block content %}
+
+<article>
+  <h1>{{ object.title }}</h1>
+
+  <time>{{ post.publication_utc|date:'Y-m-d H:i' }}</time>
+
+  <div class='body'>
+    {{ object.body_html }}
+  </div>
+</article>
+
+{% endblock %}
diff --git a/core/templates/core/post_list.html b/core/templates/core/post_list.html
new file mode 100644 (file)
index 0000000..8d6561c
--- /dev/null
@@ -0,0 +1,27 @@
+{% extends 'core/base.html' %}
+
+{% block content %}
+
+{% include 'core/page_nav.html' %}
+
+{% for post in page_obj %}
+<article>
+  <h1>
+    <a href='{% url "post-detail" post.slug %}'>
+      {{ post.title }}
+    </a>
+  </h1>
+
+  <div class='teaser'>
+    {{ post.teaser_html }}
+  </div>
+
+  <a class='read-more' href='{% url "post-detail" post.slug %}'>
+    Read more
+  </a>
+</article>
+{% endfor %}
+
+{% include 'core/page_nav.html' %}
+
+{% endblock %}
diff --git a/core/templates/core/style.css b/core/templates/core/style.css
new file mode 100644 (file)
index 0000000..d208566
--- /dev/null
@@ -0,0 +1,114 @@
+html { height: 100%; font-family: "Lucida Grande", Verdana, Arial, sans-serif;
+}
+
+h1, h2, h3, h4, h5, h6 {
+  margin: 0;
+  font-family: Palatino, serif;
+}
+
+time {
+  font-family: Verdana, sans-serif;
+}
+
+p {
+  line-height: 1.8;
+  margin-bottom: 1.2rem;
+}
+
+p:last-child {
+  margin-bottom: 0;
+}
+
+body {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  margin: 0;
+  min-height: 100%;
+}
+
+body > header {
+  margin: 5rem 0;
+}
+
+body > header > h1 {
+  font-size: 5rem;
+}
+
+body > nav {
+  margin-bottom: 4rem;
+}
+
+body > main {
+  width: calc(100% - 4rem);
+  max-width: 40rem;
+  min-height: 100%;
+}
+
+body > footer {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  margin: 5rem 0;
+}
+
+body > footer .licenses {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-top: 1rem;
+}
+
+body > footer .licenses > div {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+
+  margin-left: 1rem;
+}
+
+body > footer .licenses > div > span:not(:first-child) {
+  margin-top: 1rem;
+}
+
+article {
+  display: flex;
+  flex-direction: column;
+
+  margin: 2.5rem 0;
+}
+
+article h1 a,
+article h1 a:visited {
+  text-decoration: none;
+}
+
+article h1 a:hover,
+article h1 a:active{
+  text-decoration: underline;
+}
+
+article h1 {
+  font-size: 2rem;
+}
+
+article a.read-more {
+  margin-top: 1rem;
+}
+
+@media (prefers-color-scheme: dark) {
+  html {
+    color: #dfdede;
+    background: #121212;
+  }
+
+  a {
+    color: #dfdede;
+  }
+
+  article.teaser:not(:first-of-type) {
+    border-top: 1px solid #dfdddd;
+  }
+}
diff --git a/core/tests.py b/core/tests.py
new file mode 100644 (file)
index 0000000..7ce503c
--- /dev/null
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/core/urls.py b/core/urls.py
new file mode 100644 (file)
index 0000000..4addd71
--- /dev/null
@@ -0,0 +1,8 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+    path('', views.PostListView.as_view(), name='post-list'),
+    path('p/<slug:slug>/', views.PostDetailView.as_view(), name='post-detail'),
+]
diff --git a/core/views.py b/core/views.py
new file mode 100644 (file)
index 0000000..168fa3e
--- /dev/null
@@ -0,0 +1,21 @@
+import datetime
+
+from django.contrib.auth.models import User
+from django.views.generic.detail import DetailView
+from django.views.generic.list import ListView
+
+from . import models
+
+class PostDetailView(DetailView):
+    model = models.Post
+
+class PostListView(ListView):
+    model = models.Post
+    paginate_by = 10
+
+    def get_queryset(self):
+        now = datetime.datetime.utcnow()
+
+        return super().get_queryset().filter(
+            publication_utc__lte=now,
+        ).order_by('-publication_utc')
index 4aba371..3a001d5 100644 (file)
@@ -1 +1,2 @@
 Django==3.1
+commonmark==0.9.1
index 6b58f2a..f57fcf7 100644 (file)
@@ -52,6 +52,7 @@ INSTALLED_APPS = [
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
+    'core',
 ]
 
 MIDDLEWARE = [
index c1dbf92..94c89d9 100644 (file)
@@ -14,11 +14,11 @@ Including another URLconf
     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
 """
 from django.contrib import admin
-from django.urls import path
+from django.urls import include, path
 
-from . import views
+from core import urls as core_urls
 
 urlpatterns = [
     path('admin/', admin.site.urls),
-    path('', views.index),
+    path('', include(core_urls)),
 ]
diff --git a/styx/views.py b/styx/views.py
deleted file mode 100644 (file)
index 453e180..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-from django.http import HttpResponse
-
-def index(request):
-    return HttpResponse('Hello, world')