Switch to new.css and cleanup markup & models

This commit is contained in:
Gabriel Augendre 2020-12-24 17:49:47 +01:00
parent 8bf3438746
commit e23b988c55
No known key found for this signature in database
GPG key ID: 1E693F4CE4AEE7B4
30 changed files with 347 additions and 628 deletions

View file

@ -5,7 +5,7 @@ from django.contrib.admin import register
from django.contrib.auth.admin import UserAdmin
from django.shortcuts import redirect
from .models import Article, Page, User
from .models import Article, User
admin.site.register(User, UserAdmin)
@ -35,6 +35,7 @@ class ArticleAdmin(admin.ModelAdmin):
("status", "published_at"),
("created_at", "updated_at"),
"views_count",
"has_code",
]
},
),
@ -92,13 +93,3 @@ class ArticleAdmin(admin.ModelAdmin):
messages.success(request, "Item has been unpublished")
return redirect(".")
return super().response_change(request, obj)
@register(Page)
class PageAdmin(ArticleAdmin):
list_display = ["position"] + ArticleAdmin.list_display
def get_fieldsets(self, request, obj=None):
article_fieldsets = copy.deepcopy(ArticleAdmin.fieldsets)
article_fieldsets[0][1]["fields"][0] = ("title", "slug", "position")
return article_fieldsets

View file

@ -1,6 +1,6 @@
from django.conf import settings
from articles.models import Article, Page
from articles.models import Article
from attachments.models import Attachment
IGNORED_PATHS = [
@ -8,12 +8,6 @@ IGNORED_PATHS = [
]
def pages(request):
if request.path in IGNORED_PATHS:
return {}
return {"pages": Page.objects.filter(status=Article.PUBLISHED).exclude(position=0)}
def drafts_count(request):
if request.path in IGNORED_PATHS:
return {}
@ -52,3 +46,11 @@ def open_graph_image_url(request):
if open_graph_image:
url = open_graph_image.processed_file.get_full_absolute_url(request)
return {"open_graph_image_url": url}
def blog_metadata(request):
context = {}
context["blog_title"] = settings.BLOG["title"]
context["blog_description"] = settings.BLOG["description"]
context["blog_author"] = settings.BLOG["author"]
return context

View file

@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2020-12-24 16:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("articles", "0022_article_keywords"),
]
operations = [
migrations.AddField(
model_name="article",
name="has_code",
field=models.BooleanField(blank=True, default=False),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 3.1.3 on 2020-12-24 16:46
from django.db import migrations, models
def forwards_func(apps, schema_editor):
Article = apps.get_model("articles", "Article")
db_alias = schema_editor.connection.alias
Article.objects.using(db_alias).filter(slug="about-me").update(is_home=True)
def reverse_func(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("articles", "0023_article_has_code"),
]
operations = [
migrations.AddField(
model_name="article",
name="is_home",
field=models.BooleanField(blank=True, default=False),
),
migrations.DeleteModel(
name="Page",
),
migrations.RunPython(forwards_func, reverse_func),
]

View file

@ -18,11 +18,6 @@ class User(AbstractUser):
pass
class ArticleManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(page__isnull=True)
class AdminUrlMixin:
def get_admin_url(self):
content_type = ContentType.objects.get_for_model(self.__class__)
@ -49,18 +44,14 @@ class Article(AdminUrlMixin, models.Model):
views_count = models.IntegerField(default=0)
slug = models.SlugField(unique=True, max_length=255)
keywords = models.CharField(max_length=255, blank=True)
objects = models.Manager()
without_pages = ArticleManager()
has_code = models.BooleanField(default=False, blank=True)
is_home = models.BooleanField(default=False, blank=True)
class Meta:
ordering = ["-published_at"]
def __str__(self):
type_ = "Article"
if hasattr(self, "page"):
type_ = "Page"
return f"{self.title} ({type_})"
return f"{self.title}"
def get_absolute_url(self):
return reverse("article-detail", kwargs={"slug": self.slug})
@ -116,11 +107,3 @@ class Article(AdminUrlMixin, models.Model):
if not self.slug:
self.slug = slugify(self.title)
return super().save(*args, **kwargs)
class Page(Article):
objects = models.Manager()
position = models.IntegerField(default=0)
class Meta:
ordering = ["position", "-published_at"]

View file

@ -1,48 +0,0 @@
.codehilite .hll { background-color: #49483e }
.codehilite { background: #232629; color: #cccccc }
.codehilite .c { color: #777777; font-style: italic } /* Comment */
.codehilite .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.codehilite .k { color: #7686bb; font-weight: bold } /* Keyword */
.codehilite .ch { color: #777777; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #777777; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #777777; font-style: italic } /* Comment.Preproc */
.codehilite .cpf { color: #777777; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #777777; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #777777; font-style: italic } /* Comment.Special */
.codehilite .gp { color: #ffffff } /* Generic.Prompt */
.codehilite .kc { color: #7686bb; font-weight: bold } /* Keyword.Constant */
.codehilite .kd { color: #7686bb; font-weight: bold } /* Keyword.Declaration */
.codehilite .kn { color: #7686bb; font-weight: bold } /* Keyword.Namespace */
.codehilite .kp { color: #7686bb; font-weight: bold } /* Keyword.Pseudo */
.codehilite .kr { color: #7686bb; font-weight: bold } /* Keyword.Reserved */
.codehilite .kt { color: #7686bb; font-weight: bold } /* Keyword.Type */
.codehilite .m { color: #4FB8CC } /* Literal.Number */
.codehilite .s { color: #51cc99 } /* Literal.String */
.codehilite .nf { color: #6a6aff } /* Name.Function */
.codehilite .nx { color: #e2828e } /* Name.Other */
.codehilite .nv { color: #7AB4DB; font-weight: bold } /* Name.Variable */
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
.codehilite .mb { color: #4FB8CC } /* Literal.Number.Bin */
.codehilite .mf { color: #4FB8CC } /* Literal.Number.Float */
.codehilite .mh { color: #4FB8CC } /* Literal.Number.Hex */
.codehilite .mi { color: #4FB8CC } /* Literal.Number.Integer */
.codehilite .mo { color: #4FB8CC } /* Literal.Number.Oct */
.codehilite .sa { color: #51cc99 } /* Literal.String.Affix */
.codehilite .sb { color: #51cc99 } /* Literal.String.Backtick */
.codehilite .sc { color: #51cc99 } /* Literal.String.Char */
.codehilite .dl { color: #51cc99 } /* Literal.String.Delimiter */
.codehilite .sd { color: #51cc99 } /* Literal.String.Doc */
.codehilite .s2 { color: #51cc99 } /* Literal.String.Double */
.codehilite .se { color: #51cc99 } /* Literal.String.Escape */
.codehilite .sh { color: #51cc99 } /* Literal.String.Heredoc */
.codehilite .si { color: #51cc99 } /* Literal.String.Interpol */
.codehilite .sx { color: #51cc99 } /* Literal.String.Other */
.codehilite .sr { color: #51cc99 } /* Literal.String.Regex */
.codehilite .s1 { color: #51cc99 } /* Literal.String.Single */
.codehilite .ss { color: #51cc99 } /* Literal.String.Symbol */
.codehilite .fm { color: #6a6aff } /* Name.Function.Magic */
.codehilite .vc { color: #7AB4DB; font-weight: bold } /* Name.Variable.Class */
.codehilite .vg { color: #BE646C; font-weight: bold } /* Name.Variable.Global */
.codehilite .vi { color: #7AB4DB; font-weight: bold } /* Name.Variable.Instance */
.codehilite .vm { color: #7AB4DB; font-weight: bold } /* Name.Variable.Magic */
.codehilite .il { color: #4FB8CC } /* Literal.Number.Integer.Long */

View file

@ -1,66 +0,0 @@
.codehilite .hll { background-color: #ffffcc }
.codehilite { background: #ffffff; }
.codehilite .c { color: #aaaaaa; font-style: italic } /* Comment */
.codehilite .err { color: #FF0000; background-color: #FFAAAA } /* Error */
.codehilite .k { color: #0000aa } /* Keyword */
.codehilite .ch { color: #aaaaaa; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #aaaaaa; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #4c8317 } /* Comment.Preproc */
.codehilite .cpf { color: #aaaaaa; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #aaaaaa; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #0000aa; font-style: italic } /* Comment.Special */
.codehilite .gd { color: #aa0000 } /* Generic.Deleted */
.codehilite .ge { font-style: italic } /* Generic.Emph */
.codehilite .gr { color: #aa0000 } /* Generic.Error */
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.codehilite .gi { color: #00aa00 } /* Generic.Inserted */
.codehilite .go { color: #888888 } /* Generic.Output */
.codehilite .gp { color: #555555 } /* Generic.Prompt */
.codehilite .gs { font-weight: bold } /* Generic.Strong */
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.codehilite .gt { color: #aa0000 } /* Generic.Traceback */
.codehilite .kc { color: #0000aa } /* Keyword.Constant */
.codehilite .kd { color: #0000aa } /* Keyword.Declaration */
.codehilite .kn { color: #0000aa } /* Keyword.Namespace */
.codehilite .kp { color: #0000aa } /* Keyword.Pseudo */
.codehilite .kr { color: #0000aa } /* Keyword.Reserved */
.codehilite .kt { color: #00aaaa } /* Keyword.Type */
.codehilite .m { color: #009999 } /* Literal.Number */
.codehilite .s { color: #aa5500 } /* Literal.String */
.codehilite .na { color: #1e90ff } /* Name.Attribute */
.codehilite .nb { color: #00aaaa } /* Name.Builtin */
.codehilite .nc { color: #00aa00; text-decoration: underline } /* Name.Class */
.codehilite .no { color: #aa0000 } /* Name.Constant */
.codehilite .nd { color: #888888 } /* Name.Decorator */
.codehilite .ni { color: #880000; font-weight: bold } /* Name.Entity */
.codehilite .nf { color: #00aa00 } /* Name.Function */
.codehilite .nn { color: #00aaaa; text-decoration: underline } /* Name.Namespace */
.codehilite .nt { color: #1e90ff; font-weight: bold } /* Name.Tag */
.codehilite .nv { color: #aa0000 } /* Name.Variable */
.codehilite .ow { color: #0000aa } /* Operator.Word */
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
.codehilite .mb { color: #009999 } /* Literal.Number.Bin */
.codehilite .mf { color: #009999 } /* Literal.Number.Float */
.codehilite .mh { color: #009999 } /* Literal.Number.Hex */
.codehilite .mi { color: #009999 } /* Literal.Number.Integer */
.codehilite .mo { color: #009999 } /* Literal.Number.Oct */
.codehilite .sa { color: #aa5500 } /* Literal.String.Affix */
.codehilite .sb { color: #aa5500 } /* Literal.String.Backtick */
.codehilite .sc { color: #aa5500 } /* Literal.String.Char */
.codehilite .dl { color: #aa5500 } /* Literal.String.Delimiter */
.codehilite .sd { color: #aa5500 } /* Literal.String.Doc */
.codehilite .s2 { color: #aa5500 } /* Literal.String.Double */
.codehilite .se { color: #aa5500 } /* Literal.String.Escape */
.codehilite .sh { color: #aa5500 } /* Literal.String.Heredoc */
.codehilite .si { color: #aa5500 } /* Literal.String.Interpol */
.codehilite .sx { color: #aa5500 } /* Literal.String.Other */
.codehilite .sr { color: #009999 } /* Literal.String.Regex */
.codehilite .s1 { color: #aa5500 } /* Literal.String.Single */
.codehilite .ss { color: #0000aa } /* Literal.String.Symbol */
.codehilite .bp { color: #00aaaa } /* Name.Builtin.Pseudo */
.codehilite .fm { color: #00aa00 } /* Name.Function.Magic */
.codehilite .vc { color: #aa0000 } /* Name.Variable.Class */
.codehilite .vg { color: #aa0000 } /* Name.Variable.Global */
.codehilite .vi { color: #aa0000 } /* Name.Variable.Instance */
.codehilite .vm { color: #aa0000 } /* Name.Variable.Magic */
.codehilite .il { color: #009999 } /* Literal.Number.Integer.Long */

View file

@ -0,0 +1,12 @@
window.onload = function () {
const adminLinkElement = document.querySelector(".article-detail .metadata a.admin-link");
if (adminLinkElement === undefined || adminLinkElement === null) {
return;
}
const adminLocation = adminLinkElement.href;
document.addEventListener("keydown", event => {
if (event.code === "KeyE") {
window.location = adminLocation;
}
})
}

View file

@ -1,32 +0,0 @@
function activateDarkMode() {
document.getElementById("code-dark").removeAttribute("disabled");
}
function activateLightMode() {
document.getElementById("code-dark").setAttribute("disabled", "true");
}
function darkModeListener(e) {
if (e.matches) {
activateDarkMode();
} else {
activateLightMode();
}
}
let mql = window.matchMedia("(prefers-color-scheme: dark)");
darkModeListener(mql);
mql.addListener(darkModeListener);
window.onload = function () {
const adminLinkElement = document.querySelector(".article-detail .metadata a.admin-link");
if (adminLinkElement === undefined || adminLinkElement === null) {
return;
}
const adminLocation = adminLinkElement.href;
document.addEventListener("keydown", event => {
if (event.code === "KeyE") {
window.location = adminLocation;
}
})
}

View file

@ -1,310 +0,0 @@
:root {
--accent: #226997;
--accent-light: #7c96a9;
--main: #111111;
--main2: #575757;
--main3: #7d7d7d;
--background: #ffffff;
--background2: #f7f7f7;
--background3: #d0d0d0;
--success-background: #d4edda;
--success-text: #155724;
--error-background: #f8d7da;
--error-text: #721c24;
--warning-background: #fff3cd;
--warning-text: #856404;
--description-margin: .2rem;
}
html {
font-size: 110%;
max-width: calc(640px + 2em);
padding: 8px 0 3em;
margin: 0 auto;
}
body {
margin: 0 1em;
font-family: Arial, sans-serif;
color: var(--main);
background-color: var(--background);
}
p {
line-height: 1.7;
}
li {
line-height: 1.4;
}
body img {
max-width: 100%;
}
a {
color: var(--main);
text-decoration: none;
border-bottom: .3ex solid var(--accent);
}
.index-page h2, .article-detail h2 {
margin-top: 2em;
}
.article-list h2 a {
border-color: transparent;
}
.pic-container {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
}
.pic-container .profile-pic {
max-width: 200px;
min-width: 100px;
max-height: 200px;
min-height: 100px;
border-radius: 10%;
padding: 1rem;
flex-shrink: 1;
flex-grow: 0;
}
.article-list + .article-list {
margin-top: 4em;
}
.article-list h2, h1 {
margin-bottom: var(--description-margin);
}
.article-detail h1 {
font-size: 2em;
}
.metadata {
margin-top: var(--description-margin);
color: var(--main2);
}
.pagination {
display: flex;
justify-content: space-between;
}
code {
white-space: pre-wrap;
}
.codehilite {
border-radius: .5ex;
background-color: var(--background2);
border: 1px solid var(--background3);
}
.codehilite pre {
padding: 0;
margin: 1em;
}
section.comments {
border-top: 2px dashed var(--background3);
margin-top: 2em;
}
footer {
border-top: 2px solid var(--background2);
margin-top: 2em;
font-size: 80%;
color: var(--main3);
}
footer a {
color: var(--main3);
border-bottom-color: var(--accent-light);
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
padding: 1ex;
text-align: left;
}
tr {
border-bottom: 1px solid var(--background3);
}
tr:hover {
background-color: var(--background2);
}
.pill {
font-size: 60%;
background-color: var(--main2);
color: var(--background2);
padding: .5ex 1ex;
border-radius: 1ex;
vertical-align: 15%;
}
blockquote {
background-color: var(--background2);
margin: 0;
padding: 1px .5em 1px 1.5em;
border-left: 6px solid var(--background3);
}
details {
padding: .6rem 1rem;
background: var(--background2);
border: 1px solid var(--background3);
border-radius: 4px;
}
summary {
cursor: pointer;
}
details .codehilite {
border: none;
}
/* FORMS */
.helptext {
color: var(--main3);
font-size: 80%;
}
.helptext a {
color: var(--main3);
}
p.helptext {
margin-bottom: 0;
}
form table th {
vertical-align: top;
}
form table tr th {
font-weight: normal;
}
form tr.required th {
font-weight: bold;
}
form table tr {
border-bottom: none;
}
form {
background-color: var(--background2);
border-radius: .5ex;
padding: 1em;
}
form button[type=submit] {
font-size: 1em;
padding: .9ex 1.2ex;
border-radius: .8ex;
background-color: var(--accent);
border: none;
color: var(--background);
cursor: pointer;
margin-top: 1em;
}
form .error, form .error .helptext, .errorlist {
color: var(--error-text);
background-color: var(--error-background);
}
.errorlist {
margin-top: 0;
list-style-type: none;
padding-left: 0;
}
.errorlist.nonfield {
margin-bottom: 0;
}
tr.spacer {
padding: 1em;
}
textarea, input {
width: 100%;
}
/* MESSAGES */
.messages p {
background-color: var(--background2);
padding: .5ex 1ex;
}
.messages .error {
background-color: var(--error-background);
color: var(--error-text);
}
.messages .success {
background-color: var(--success-background);
color: var(--success-text);
}
.messages .warning {
background-color: var(--warning-background);
color: var(--warning-text);
}
/* LINKS */
a:hover, a:focus {
text-decoration: none;
background-color: var(--accent);
color: var(--background);
}
footer a:hover, footer a:focus {
background-color: var(--accent-light);
}
@media (prefers-color-scheme: dark) {
:root {
--accent: #226997;
--main: #eeeeee;
--main2: #cecece;
--main3: #b1b1b1;
--background: #111111;
--background2: #282828;
--success-background: #155724;
--success-text: #d4edda;
--error-background: #721c24;
--error-text: #f8d7da;
--warning-background: #856404;
--warning-text: #fff3cd;
}
img {
opacity: .75;
transition: opacity .2s ease-in-out;
}
img:hover {
opacity: 1;
}
#manage ul ul li img {
opacity: 1;
filter: invert(1);
}
}

View file

@ -1,3 +0,0 @@
{% if user.is_authenticated %}
| <a class="admin-link" href="{{ article.get_admin_url }}">Edit</a>
{% endif %}

View file

@ -1,21 +1,14 @@
{% extends 'articles/base.html' %}
{% block title %}{{ article.title }} | {% endblock %}
{% block content %}
<article class="article-detail">
<article>
<h1>{{ article.title }}{% if article.status != article.PUBLISHED %}
({{ article.status }}){% endif %}</h1>
{% include "articles/metadata_snippet.html" %}
{% include "articles/snippets/metadata.html" %}
<div>
{{ article.get_formatted_content|safe }}
</div>
</article>
<section class="comments">
<h2>Comments</h2>
<p>
Comments are hard to manage. I tried enabling them but I only got spam.
If you want to react to this content or interact with me, please head to the
<a href="/about-me/">about me</a> page.&nbsp;😉
</p>
</section>
{% endblock %}

View file

@ -1,38 +1,44 @@
{% extends 'articles/base.html' %}
{% block override_header %}
{% include "articles/snippets/style_home.html" %}
{% endblock %}
{% block title %}{% endblock %}
{% block content %}
<h1>{{ blog_title }}{% if title %} &middot; {{ title }}{% endif %}</h1>
<p class="metadata">{{ blog_description }} {% include "articles/admin_link_snippet.html" %}</p>
<section class="blog-posts">
<section>
<h2>Blog posts</h2>
<ul>
{% for article in articles %}
<li>
{% include "articles/datetime_snippet.html" %}
: <a href="{% url 'article-detail' slug=article.slug %}">{{ article.title }}</a>
</li>
{% empty %}
<li>No article here. Come back later 🙂</li>
{% endfor %}
</ul>
<table>
<tr><th scope="col">Date</th><th scope="col">Title</th></tr>
{% for article in articles %}
<tr>
<td>{% include "articles/snippets/datetime.html" %}</td>
<td><a href="{% url 'article-detail' slug=article.slug %}">{{ article.title }}</a></td>
</tr>
{% empty %}
<tr><td colspan="2">No article here. Come back later 🙂</td></tr>
{% endfor %}
</table>
</section>
<nav class="pagination">
<div class="older">
<div>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">⇠ Older</a>
{% endif %}
</div>
<div class="newer">
<div>
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Newer ⇢</a>
{% endif %}
</div>
</nav>
<section class="index-page">
<h2>{{ article.title }}</h2>
<div>
{{ article.get_formatted_content|safe }}
</div>
</section>
{% if article %}
<section>
<h2>{{ article.title }}{% include "articles/snippets/admin_link.html" %}</h2>
<div>
{{ article.get_formatted_content|safe }}
</div>
</section>
{% endif %}
{% endblock %}

View file

@ -6,57 +6,45 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="Gabriel Augendre">
<meta name="color-scheme" content="light dark">
{% if article %}
<meta name="keywords" content="{{ article.keywords }}">
<meta name="description" content="{{ article.get_description }}">
<meta property="og:title" content="{{ article.title }}" />
<meta property="og:type" content="article">
<meta property="og:description" content="{{ article.get_description }}">
<meta property="og:site_name" content="{{ blog_title }}">
{% endif %}
{% if open_graph_image_url %}
<meta property="og:image" content="{{ open_graph_image_url }}">
{% endif %}
<title>{% block title %}Home | {% endblock %}Gab's Notes by Gabriel Augendre</title>
<link rel="stylesheet" id="code-light" href="{% static 'code-light.css' %}" type="text/css">
<link rel="stylesheet" id="code-dark" href="{% static 'code-dark.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'style.css' %}" type="text/css">
<script src="{% static 'scripts.js' %}" async></script>
{% include "articles/snippets/page_metadata.html" %}
<title>{% block title %}Home | {% endblock %}{{ blog_title }} by {{ blog_author }}</title>
<link rel="alternate" type="application/rss+xml" title="Gab's Notes » Feed" href="{% url 'complete-feed' %}">
{% include "articles/favicon.html" %}
{% include "articles/analytics.html" %}
{% include "articles/snippets/style.html" %}
{% if article and article.has_code %}
{% include "articles/snippets/code_style.html" %}
{% endif %}
{% if user.is_authenticated %}
<script src="{% static 'edit-keymap.js' %}" async></script>
{% endif %}
{% include "articles/snippets/favicon.html" %}
{% include "articles/snippets/analytics.html" %}
{% block override_header %}{% endblock %}
</head>
<body>
<nav>
<a href="{% url 'articles-list' %}">Articles list</a>
&centerdot;
{% if user.is_authenticated %}
<a href="{% url 'admin:articles_article_add' %}">Write</a>
<a href="{% url 'drafts-list' %}">View drafts <span class="pill">{{ drafts_count }}</span></a>
<header>
<h1>{{ blog_title }}</h1>
<p>{{ blog_description }}</p>
<nav>
<a href="{% url 'articles-list' %}">Home</a>
&centerdot;
<a href="{% url 'admin:index' %}">Admin</a>
<a href="{% url 'admin:logout' %}?next=/">Log out</a>
{% else %}
<a href="{% url 'admin:login' %}?next=/">Log in</a>
{% endif %}
{% if pages %}
<a href="{% url 'complete-feed' %}">RSS</a>
&centerdot;
{% for page in pages %}
<a href="{% url 'article-detail' slug=page.slug %}">{{ page.title }}</a>
{% endfor %}
{% endif %}
</nav>
{% if messages %}
<div class="messages">
{% for message in messages %}
<p class="{{ message.level_tag }}">{{ message|safe }}</p>
{% endfor %}
</div>
{% endif %}
<div class="content">
{% block content %}
{% endblock %}
</div>
{% if user.is_authenticated %}
<a href="{% url 'admin:articles_article_add' %}">Write</a>
&centerdot;
<a href="{% url 'drafts-list' %}">View drafts <span class="pill">{{ drafts_count }}</span></a>
&centerdot;
<a href="{% url 'admin:index' %}">Admin</a>
&centerdot;
<a href="{% url 'admin:logout' %}?next=/">Log out</a>
{% else %}
<a href="{% url 'admin:login' %}?next=/">Log in</a>
{% endif %}
</nav>
</header>
{% block content %}
{% endblock %}
<footer>
<p>

View file

@ -1,5 +0,0 @@
{% load i18n %}
<p class="metadata">
{% include "articles/datetime_snippet.html" %}
{% include "articles/admin_link_snippet.html" %}
</p>

View file

@ -0,0 +1,3 @@
{% if user.is_authenticated %}
| <a id="admin-link" href="{{ article.get_admin_url }}">Edit</a>
{% endif %}

View file

@ -0,0 +1,121 @@
<style>
@media(prefers-color-scheme: light) {
.codehilite .hll { background-color: #ffffcc }
.codehilite { background: #ffffff; }
.codehilite .c { color: #aaaaaa; font-style: italic } /* Comment */
.codehilite .err { color: #FF0000; background-color: #FFAAAA } /* Error */
.codehilite .k { color: #0000aa } /* Keyword */
.codehilite .ch { color: #aaaaaa; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #aaaaaa; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #4c8317 } /* Comment.Preproc */
.codehilite .cpf { color: #aaaaaa; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #aaaaaa; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #0000aa; font-style: italic } /* Comment.Special */
.codehilite .gd { color: #aa0000 } /* Generic.Deleted */
.codehilite .ge { font-style: italic } /* Generic.Emph */
.codehilite .gr { color: #aa0000 } /* Generic.Error */
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.codehilite .gi { color: #00aa00 } /* Generic.Inserted */
.codehilite .go { color: #888888 } /* Generic.Output */
.codehilite .gp { color: #555555 } /* Generic.Prompt */
.codehilite .gs { font-weight: bold } /* Generic.Strong */
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.codehilite .gt { color: #aa0000 } /* Generic.Traceback */
.codehilite .kc { color: #0000aa } /* Keyword.Constant */
.codehilite .kd { color: #0000aa } /* Keyword.Declaration */
.codehilite .kn { color: #0000aa } /* Keyword.Namespace */
.codehilite .kp { color: #0000aa } /* Keyword.Pseudo */
.codehilite .kr { color: #0000aa } /* Keyword.Reserved */
.codehilite .kt { color: #00aaaa } /* Keyword.Type */
.codehilite .m { color: #009999 } /* Literal.Number */
.codehilite .s { color: #aa5500 } /* Literal.String */
.codehilite .na { color: #1e90ff } /* Name.Attribute */
.codehilite .nb { color: #00aaaa } /* Name.Builtin */
.codehilite .nc { color: #00aa00; text-decoration: underline } /* Name.Class */
.codehilite .no { color: #aa0000 } /* Name.Constant */
.codehilite .nd { color: #888888 } /* Name.Decorator */
.codehilite .ni { color: #880000; font-weight: bold } /* Name.Entity */
.codehilite .nf { color: #00aa00 } /* Name.Function */
.codehilite .nn { color: #00aaaa; text-decoration: underline } /* Name.Namespace */
.codehilite .nt { color: #1e90ff; font-weight: bold } /* Name.Tag */
.codehilite .nv { color: #aa0000 } /* Name.Variable */
.codehilite .ow { color: #0000aa } /* Operator.Word */
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
.codehilite .mb { color: #009999 } /* Literal.Number.Bin */
.codehilite .mf { color: #009999 } /* Literal.Number.Float */
.codehilite .mh { color: #009999 } /* Literal.Number.Hex */
.codehilite .mi { color: #009999 } /* Literal.Number.Integer */
.codehilite .mo { color: #009999 } /* Literal.Number.Oct */
.codehilite .sa { color: #aa5500 } /* Literal.String.Affix */
.codehilite .sb { color: #aa5500 } /* Literal.String.Backtick */
.codehilite .sc { color: #aa5500 } /* Literal.String.Char */
.codehilite .dl { color: #aa5500 } /* Literal.String.Delimiter */
.codehilite .sd { color: #aa5500 } /* Literal.String.Doc */
.codehilite .s2 { color: #aa5500 } /* Literal.String.Double */
.codehilite .se { color: #aa5500 } /* Literal.String.Escape */
.codehilite .sh { color: #aa5500 } /* Literal.String.Heredoc */
.codehilite .si { color: #aa5500 } /* Literal.String.Interpol */
.codehilite .sx { color: #aa5500 } /* Literal.String.Other */
.codehilite .sr { color: #009999 } /* Literal.String.Regex */
.codehilite .s1 { color: #aa5500 } /* Literal.String.Single */
.codehilite .ss { color: #0000aa } /* Literal.String.Symbol */
.codehilite .bp { color: #00aaaa } /* Name.Builtin.Pseudo */
.codehilite .fm { color: #00aa00 } /* Name.Function.Magic */
.codehilite .vc { color: #aa0000 } /* Name.Variable.Class */
.codehilite .vg { color: #aa0000 } /* Name.Variable.Global */
.codehilite .vi { color: #aa0000 } /* Name.Variable.Instance */
.codehilite .vm { color: #aa0000 } /* Name.Variable.Magic */
.codehilite .il { color: #009999 } /* Literal.Number.Integer.Long */
}
@media(prefers-color-scheme: dark) {
.codehilite .hll { background-color: #49483e }
.codehilite { background: #232629; color: #cccccc }
.codehilite .c { color: #777777; font-style: italic } /* Comment */
.codehilite .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.codehilite .k { color: #7686bb; font-weight: bold } /* Keyword */
.codehilite .ch { color: #777777; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #777777; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #777777; font-style: italic } /* Comment.Preproc */
.codehilite .cpf { color: #777777; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #777777; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #777777; font-style: italic } /* Comment.Special */
.codehilite .gp { color: #ffffff } /* Generic.Prompt */
.codehilite .kc { color: #7686bb; font-weight: bold } /* Keyword.Constant */
.codehilite .kd { color: #7686bb; font-weight: bold } /* Keyword.Declaration */
.codehilite .kn { color: #7686bb; font-weight: bold } /* Keyword.Namespace */
.codehilite .kp { color: #7686bb; font-weight: bold } /* Keyword.Pseudo */
.codehilite .kr { color: #7686bb; font-weight: bold } /* Keyword.Reserved */
.codehilite .kt { color: #7686bb; font-weight: bold } /* Keyword.Type */
.codehilite .m { color: #4FB8CC } /* Literal.Number */
.codehilite .s { color: #51cc99 } /* Literal.String */
.codehilite .nf { color: #6a6aff } /* Name.Function */
.codehilite .nx { color: #e2828e } /* Name.Other */
.codehilite .nv { color: #7AB4DB; font-weight: bold } /* Name.Variable */
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
.codehilite .mb { color: #4FB8CC } /* Literal.Number.Bin */
.codehilite .mf { color: #4FB8CC } /* Literal.Number.Float */
.codehilite .mh { color: #4FB8CC } /* Literal.Number.Hex */
.codehilite .mi { color: #4FB8CC } /* Literal.Number.Integer */
.codehilite .mo { color: #4FB8CC } /* Literal.Number.Oct */
.codehilite .sa { color: #51cc99 } /* Literal.String.Affix */
.codehilite .sb { color: #51cc99 } /* Literal.String.Backtick */
.codehilite .sc { color: #51cc99 } /* Literal.String.Char */
.codehilite .dl { color: #51cc99 } /* Literal.String.Delimiter */
.codehilite .sd { color: #51cc99 } /* Literal.String.Doc */
.codehilite .s2 { color: #51cc99 } /* Literal.String.Double */
.codehilite .se { color: #51cc99 } /* Literal.String.Escape */
.codehilite .sh { color: #51cc99 } /* Literal.String.Heredoc */
.codehilite .si { color: #51cc99 } /* Literal.String.Interpol */
.codehilite .sx { color: #51cc99 } /* Literal.String.Other */
.codehilite .sr { color: #51cc99 } /* Literal.String.Regex */
.codehilite .s1 { color: #51cc99 } /* Literal.String.Single */
.codehilite .ss { color: #51cc99 } /* Literal.String.Symbol */
.codehilite .fm { color: #6a6aff } /* Name.Function.Magic */
.codehilite .vc { color: #7AB4DB; font-weight: bold } /* Name.Variable.Class */
.codehilite .vg { color: #BE646C; font-weight: bold } /* Name.Variable.Global */
.codehilite .vi { color: #7AB4DB; font-weight: bold } /* Name.Variable.Instance */
.codehilite .vm { color: #7AB4DB; font-weight: bold } /* Name.Variable.Magic */
.codehilite .il { color: #4FB8CC } /* Literal.Number.Integer.Long */
}
</style>

View file

@ -1,3 +1,4 @@
{% load i18n %}
{% if article.published_at %}
<time datetime="{{ article.published_at|date:CUSTOM_ISO }}">{{ article.published_at|date:ISO_DATE }}</time>
{% else %}

View file

@ -0,0 +1,4 @@
<p>
Published on {% include "articles/snippets/datetime.html" %}
{% include "articles/snippets/admin_link.html" %}
</p>

View file

@ -0,0 +1,11 @@
{% if article %}
<meta name="keywords" content="{{ article.keywords }}">
<meta name="description" content="{{ article.get_description }}">
<meta property="og:title" content="{{ article.title }}" />
<meta property="og:type" content="article">
<meta property="og:description" content="{{ article.get_description }}">
<meta property="og:site_name" content="{{ blog_title }}">
{% endif %}
{% if open_graph_image_url %}
<meta property="og:image" content="{{ open_graph_image_url }}">
{% endif %}

View file

@ -0,0 +1,29 @@
<style>
/* new.css 1.1.2 */
:root{--nc-font-sans:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--nc-font-mono:Consolas,monaco,'Ubuntu Mono','Liberation Mono','Courier New',Courier,monospace;--nc-tx-1:#000000;--nc-tx-2:#1A1A1A;--nc-bg-1:#FFFFFF;--nc-bg-2:#F6F8FA;--nc-bg-3:#E5E7EB;--nc-lk-1:#0070F3;--nc-lk-2:#0366D6;--nc-lk-tx:#FFFFFF;--nc-ac-1:#79FFE1;--nc-ac-tx:#0C4047}@media (prefers-color-scheme:dark){:root{--nc-tx-1:#ffffff;--nc-tx-2:#eeeeee;--nc-bg-1:#000000;--nc-bg-2:#111111;--nc-bg-3:#222222;--nc-lk-1:#3291FF;--nc-lk-2:#0070F3;--nc-lk-tx:#FFFFFF;--nc-ac-1:#7928CA;--nc-ac-tx:#FFFFFF}}*{margin:0;padding:0}address,area,article,aside,audio,blockquote,datalist,details,dl,fieldset,figure,form,iframe,img,input,meter,nav,ol,optgroup,option,output,p,pre,progress,ruby,section,table,textarea,ul,video{margin-bottom:1rem}button,html,input,select{font-family:var(--nc-font-sans)}body{margin:0 auto;max-width:750px;padding:2rem;border-radius:6px;overflow-x:hidden;word-break:break-word;overflow-wrap:break-word;background:var(--nc-bg-1);color:var(--nc-tx-2);font-size:1.03rem;line-height:1.5}::selection{background:var(--nc-ac-1);color:var(--nc-ac-tx)}h1,h2,h3,h4,h5,h6{line-height:1;color:var(--nc-tx-1);padding-top:.875rem}h1,h2,h3{color:var(--nc-tx-1);padding-bottom:2px;margin-bottom:8px;border-bottom:1px solid var(--nc-bg-2)}h4,h5,h6{margin-bottom:.3rem}h1{font-size:2.25rem}h2{font-size:1.85rem}h3{font-size:1.55rem}h4{font-size:1.25rem}h5{font-size:1rem}h6{font-size:.875rem}a{color:var(--nc-lk-1)}a:hover{color:var(--nc-lk-2)}abbr:hover{cursor:help}blockquote{padding:1.5rem;background:var(--nc-bg-2);border-left:5px solid var(--nc-bg-3)}abbr{cursor:help}blockquote :last-child{padding-bottom:0;margin-bottom:0}header{background:var(--nc-bg-2);border-bottom:1px solid var(--nc-bg-3);padding:2rem 1.5rem;margin:-2rem calc(0px - (50vw - 50%)) 2rem;padding-left:calc(50vw - 50%);padding-right:calc(50vw - 50%)}header h1,header h2,header h3{padding-bottom:0;border-bottom:0}header>:first-child{margin-top:0;padding-top:0}header>:last-child{margin-bottom:0}a button,button,input[type=button],input[type=reset],input[type=submit]{font-size:1rem;display:inline-block;padding:6px 12px;text-align:center;text-decoration:none;white-space:nowrap;background:var(--nc-lk-1);color:var(--nc-lk-tx);border:0;border-radius:4px;box-sizing:border-box;cursor:pointer;color:var(--nc-lk-tx)}a button[disabled],button[disabled],input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default;opacity:.5;cursor:not-allowed}.button:focus,.button:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:var(--nc-lk-2)}code,kbd,pre,samp{font-family:var(--nc-font-mono)}code,kbd,pre,samp{background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px;padding:3px 6px;font-size:.9rem}kbd{border-bottom:3px solid var(--nc-bg-3)}pre{padding:1rem 1.4rem;max-width:100%;overflow:auto}pre code{background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}code pre{display:inline;background:inherit;font-size:inherit;color:inherit;border:0;padding:0;margin:0}details{padding:.6rem 1rem;background:var(--nc-bg-2);border:1px solid var(--nc-bg-3);border-radius:4px}summary{cursor:pointer;font-weight:700}details[open]{padding-bottom:.75rem}details[open] summary{margin-bottom:6px}details[open]>:last-child{margin-bottom:0}dt{font-weight:700}dd::before{content:'→ '}hr{border:0;border-bottom:1px solid var(--nc-bg-3);margin:1rem auto}fieldset{margin-top:1rem;padding:2rem;border:1px solid var(--nc-bg-3);border-radius:4px}legend{padding:auto .5rem}table{border-collapse:collapse;width:100%}td,th{border:1px solid var(--nc-bg-3);text-align:left;padding:.5rem}th{background:var(--nc-bg-2)}tr:nth-child(even){background:var(--nc-bg-2)}table caption{font-weight:700;margin-bottom:.5rem}textarea{max-width:100%}ol,ul{padding-left:2rem}li{margin-top:.4rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}mark{padding:3px 6px;background:var(--nc-ac-1);color:var(--nc-ac-tx)}input,select,textarea{padding:6px 12px;margin-bottom:.5rem;background:var(--nc-bg-2);color:var(--nc-tx-2);border:1px solid var(--nc-bg-3);border-radius:4px;box-shadow:none;box-sizing:border-box}img{max-width:100%}
</style>
<style>
body {
max-width: 640px;
}
footer {
border-top: 2px solid var(--nc-bg-2);
margin-top: 2em;
font-size: 80%;
color: var(--nc-tx-2);
}
footer > :first-child {
margin-top: 1em;
}
.pill {
font-size: 60%;
background-color: var(--nc-tx-2);
color: var(--nc-bg-1);
padding: .5ex 1ex;
border-radius: 1ex;
vertical-align: 15%;
}
</style>

View file

@ -0,0 +1,23 @@
<style>
.pic-container {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
}
.pic-container .profile-pic {
max-width: 200px;
min-width: 100px;
max-height: 200px;
min-height: 100px;
border-radius: 10%;
padding: 1rem;
flex-shrink: 1;
flex-grow: 0;
}
.pagination {
display: flex;
justify-content: space-between;
}
</style>

View file

@ -1,7 +1,7 @@
import pytest
from django.utils import timezone
from articles.models import Article, Page, User
from articles.models import Article, User
@pytest.fixture()
@ -40,17 +40,3 @@ def unpublished_article(author: User) -> Article:
slug="some-draft-article-slug",
content="## some draft article markdown\n\n[a draft article link](https://article.com)",
)
@pytest.fixture()
@pytest.mark.django_db
def published_page(author: User) -> Page:
return Page.objects.create(
title="Some interesting page title",
status=Article.PUBLISHED,
author=author,
published_at=timezone.now(),
slug="some-page-slug",
content="## some page markdown\n\n[a page link](https://page.com)",
position=2,
)

View file

@ -3,7 +3,7 @@ from django.test import Client
from django.urls import reverse
from model_bakery import baker
from articles.models import Article, Page, User
from articles.models import Article, User
from articles.views.feeds import CompleteFeed
@ -22,11 +22,3 @@ def test_feed_limits_number_of_articles(client: Client, author: User):
res = client.get(reverse("complete-feed"))
content = res.content.decode("utf-8")
assert content.count("<item>") == CompleteFeed.FEED_LIMIT
@pytest.mark.django_db
def test_page_not_rendered_in_feed(client: Client, published_page: Page):
res = client.get(reverse("complete-feed"))
assert res.status_code == 200
content = res.content.decode("utf-8")
assert published_page.title not in content

View file

@ -3,20 +3,16 @@ from django.test import Client
from django.urls import reverse
from model_bakery import baker
from articles.models import Article, Page, User
from articles.models import Article, User
@pytest.mark.django_db
def test_can_access_list(
client: Client, published_article: Article, published_page: Page
):
def test_can_access_list(client: Client, published_article: Article):
res = client.get(reverse("articles-list"))
assert res.status_code == 200
content = res.content.decode("utf-8")
for art in [published_article, published_page]:
assert art.title in content
assert published_article.title in content
assert published_article.get_abstract() not in content
assert published_page.get_formatted_content() not in content
@pytest.mark.django_db
@ -43,11 +39,6 @@ def test_access_article_by_slug(client: Client, published_article: Article):
_test_access_article_by_slug(client, published_article)
@pytest.mark.django_db
def test_access_page_by_slug(client: Client, published_page: Page):
_test_access_article_by_slug(client, published_page)
def _test_access_article_by_slug(client: Client, item: Article):
res = client.get(reverse("article-detail", kwargs={"slug": item.slug}))
assert res.status_code == 200

View file

@ -11,7 +11,7 @@ class CompleteFeed(Feed):
description = settings.BLOG["description"]
def items(self):
return Article.without_pages.filter(status=Article.PUBLISHED).order_by(
return Article.objects.filter(status=Article.PUBLISHED).order_by(
"-published_at"
)[: self.FEED_LIMIT]

View file

@ -1,11 +1,9 @@
from typing import Union
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import F
from django.views import generic
from articles.models import Article, Page
from articles.models import Article
class BaseArticleListView(generic.ListView):
@ -21,14 +19,14 @@ class BaseArticleListView(generic.ListView):
class ArticlesListView(BaseArticleListView):
model = Article
context_object_name = "articles"
queryset = Article.without_pages.filter(status=Article.PUBLISHED)
queryset = Article.objects.filter(status=Article.PUBLISHED)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
index_page = Page.objects.filter(
status=Article.PUBLISHED, position=0
).first() # type: Page
context["article"] = index_page
home_article = Article.objects.filter(
status=Article.PUBLISHED, is_home=True
).first() # type: Article
context["article"] = home_article
return context
@ -55,10 +53,8 @@ class ArticleDetailView(generic.DetailView):
return queryset
return queryset.filter(status=Article.PUBLISHED)
def get_object(self, queryset=None) -> Union[Article, Page]:
def get_object(self, queryset=None) -> Article:
obj = super().get_object(queryset) # type: Article
if hasattr(obj, "page"):
obj = obj.page # type: Page
if not self.request.user.is_authenticated:
obj.views_count = F("views_count") + 1
obj.save(update_fields=["views_count"])

View file

@ -96,12 +96,12 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"articles.context_processors.pages",
"articles.context_processors.drafts_count",
"articles.context_processors.date_format",
"articles.context_processors.git_version",
"articles.context_processors.plausible",
"articles.context_processors.open_graph_image_url",
"articles.context_processors.blog_metadata",
],
},
},
@ -176,6 +176,7 @@ AUTH_USER_MODEL = "articles.User"
BLOG = {
"title": "Gab's Notes",
"author": "Gabriel Augendre",
"description": "My take on tech-related subjects (but not only).",
"base_url": os.getenv("BLOG_BASE_URL", "https://gabnotes.org/"),
"repo": {