--- /dev/null
+from django.contrib import admin
+
+from . import models
+
+class PostAdmin(admin.ModelAdmin):
+ list_display = (
+ 'title',
+ 'publication_utc',
+ )
+
+admin.site.register(models.Post, PostAdmin)
--- /dev/null
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+ name = 'core'
--- /dev/null
+# 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)),
+ ],
+ ),
+ ]
--- /dev/null
+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))
--- /dev/null
+<!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>© {% 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>
--- /dev/null
+<nav class="pagination">
+ <span class="step-links">
+ {% if page_obj.has_previous %}
+ <a href="?page=1">« 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 »</a>
+ {% endif %}
+ </span>
+</nav>
--- /dev/null
+{% 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 %}
--- /dev/null
+{% 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 %}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+from django.test import TestCase
+
+# Create your tests here.
--- /dev/null
+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'),
+]
--- /dev/null
+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')
Django==3.1
+commonmark==0.9.1
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'core',
]
MIDDLEWARE = [
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)),
]
+++ /dev/null
-from django.http import HttpResponse
-
-def index(request):
- return HttpResponse('Hello, world')