Compare commits

..

No commits in common. "blog-browse" and "master" have entirely different histories.

159 changed files with 1468 additions and 3382 deletions

3
.gitignore vendored
View File

@ -18,6 +18,3 @@ db/data/ibtmp1
db/data/multi-master.info db/data/multi-master.info
db/data/mysql_upgrade_info db/data/mysql_upgrade_info
gawa/media/CACHE gawa/media/CACHE
gawa/static/CACHE
site/static/CACHE
gawa/media/img/links/favicons

6
.gitmodules vendored
View File

@ -1,6 +0,0 @@
[submodule "blog"]
path = site/blog
url = https://github.com/xenocrat/chyrp-lite
[submodule "gawa/static/tagify"]
path = gawa/static/tagify
url = https://github.com/yairEO/tagify

View File

@ -1,35 +1,8 @@
# Gawa # gawa
Gawa is my personal website. I've personally written it using the django framework.
Gawa is my personal website. I've personally written it using the Django framework.
## Credentials
These are the Credentials for logging into the admin panel:
| Username | Password |
|----------|----------|
| `root` | `root` |
### Blog
| Username | Password |
|--------------------------------------------------|----------------|
| [`contact@cscherr.de`](mailto:contact@cscherr.de) | `hrCcDa0jBspG` |
## License ## License
Bootstrap: MIT Licensed Bootstrap: MIT Licensed
Django: BSD 3-Clause "New" or "Revised" License Django: BSD 3-Clause "New" or "Revised" License
__Gawa: MIT Licensed, see LICENSE__ ###### Gawa: MIT Licensed, see LICENSE
## Dependencies
| Description | Package (fedora) |
|----------------|------------------|
| Database stuff | `libpq-devel` |
| Database stuff | `mariadb` |
## Security
- [ ] Do something about the files in the blog dir
(.git dir for example)

BIN
db/data/gawa/auth_group.frm Normal file

Binary file not shown.

BIN
db/data/gawa/auth_group.ibd Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
db/data/gawa/auth_user.frm Normal file

Binary file not shown.

BIN
db/data/gawa/auth_user.ibd Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

2
db/data/gawa/db.opt Normal file
View File

@ -0,0 +1,2 @@
default-character-set=utf8mb4
default-collation=utf8mb4_general_ci

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,62 +1,66 @@
services: services:
db: db:
image: mariadb image: mariadb
container_name: gawa-db
networks:
- internal
ports: ports:
- 3306:3306 - 3306:3306
environment: environment:
# MYSQL_DATABASE: gawa MYSQL_DATABASE: gawa
# MYSQL_USER: gawa MYSQL_USER: gawa
# MYSQL_PASSWORD: changethisforprod MYSQL_PASSWORD: changethisforprod
MYSQL_ROOT_PASSWORD: root MYSQL_ROOT_PASSWORD: root
volumes: volumes:
# - db_data:/var/lib/mysql - ./db/data:/var/lib/mysql
- ./docker/db/scripts:/docker-entrypoint-initdb.d/
main: gawa:
build: ./docker/main build: ./web
command: bash -c "echo 'setting django up' container_name: gawa
&& python manage.py collectstatic --noinput command: python manage.py runserver 0.0.0.0:80
&& sleep 3
&& python manage.py migrate
&& DJANGO_SUPERUSER_PASSWORD='root' python manage.py createsuperuser\
--username root --noinput --email software@cscherr.de || true
&& python manage.py runserver 0.0.0.0:80
"
volumes: volumes:
- ./gawa:/app - ./gawa:/app
ports:
- 8080:80
environment: environment:
- MARIADB_NAME=gawa - MARIADB_NAME=gawa
- MARIADB_USER=gawa - MARIADB_USER=gawa
- MARIADB_PASSWORD=changethisforprod - MARIADB_PASSWORD=changethisforprod
networks:
- internal
depends_on: depends_on:
- db - db
caddy: nginx:
image: caddy # only for developement. Use dedicated static container in prod
restart: unless-stopped image: nginx
ports: container_name: gawa-web
- "80:80"
- "8081:8081"
# - "443:443"
# - "443:443/udp"
volumes: volumes:
- $PWD/docker/caddy/Caddyfile:/etc/caddy/Caddyfile - ./web/templates:/etc/nginx/templates
- caddy_data:/data - ./gawa/static:/var/www/static
- caddy_config:/config - ./gawa/media:/var/www/media
- ./gawa/static:/srv/static
- ./gawa/media:/srv/media
db-admin:
image: phpmyadmin
ports: ports:
- 8080:80 - 80:80
environment:
- NGINX_HOST=0.0.0.0
- NGINX_PORT=80
networks:
- internal
depends_on:
- gawa
phpmyadmin:
image: phpmyadmin
container_name: gawa-db-admin
networks:
- internal
ports:
- "127.0.0.1:8082:80"
environment: environment:
- PMA_HOST=db - PMA_HOST=db
- PMA_ABSOLUTE_URI=http://localhost:8080
depends_on:
- db
volumes:
caddy_data: networks:
caddy_config: internal:
# db_data: driver: bridge

View File

@ -1,3 +0,0 @@
FROM3
Run \
wget 'https://github.com/writefreely/writefreely/releases/download/v0.14.0/writefreely_0.14.0_linux_amd64.tar.gz'

View File

@ -1,26 +0,0 @@
http://localhost {
handle_path /static/* {
root * /srv/static
file_server {
hide .git
precompressed zstd br gzip
}
}
handle_path /media/* {
root * /srv/media
file_server {
hide .git
precompressed zstd br gzip
}
}
reverse_proxy http://main
}
# http://localhost:8080 {
# reverse_proxy http://db-admin
# }
http://localhost:8081 {
reverse_proxy http://blog:2368
}

View File

@ -1,5 +0,0 @@
-- CREATE USER root@'%' IDENTIFIED BY 'root';
CREATE USER gawa@'%' IDENTIFIED BY 'changethisforprod';
GRANT ALL PRIVILEGES ON `gawa`.* TO 'gawa'@'%';
CREATE USER blog@'%' IDENTIFIED BY 'blogpass';
GRANT ALL PRIVILEGES ON `blog`.* TO 'blog'@'%';

View File

@ -1,24 +0,0 @@
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- Host: db
-- Erstellungszeit: 27. Sep 2023 um 18:44
-- Server-Version: 10.11.3-MariaDB-1:10.11.3+maria~ubu2204
-- PHP-Version: 8.1.19
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Datenbank: `gawa`
--
CREATE DATABASE IF NOT EXISTS `gawa` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `gawa`;

View File

@ -1,12 +0,0 @@
Django>=4.0,<5.0
psycopg2>=2.8
mysqlclient>=1.4.3
django_compressor>=2.2
django-libsass>=0.7
pillow>=9.0.0
colorlog>=6.7.0
favicon>=0.7.0
markdown>=3.4.4
Pygments>=2.16.1
toml>=0.10
beautifulsoup4>=4.12.2

View File

@ -1,7 +1,5 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.http.response import HttpResponseRedirect
from .models import * from .models import *
@admin.register(Category) @admin.register(Category)
@ -17,7 +15,6 @@ def regenerate(modeladmin, request, queryset):
for obj in queryset: for obj in queryset:
obj.regenerate() obj.regenerate()
@admin.register(BlogPost) @admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin): class BlogPostAdmin(admin.ModelAdmin):
""" """
@ -27,16 +24,3 @@ class BlogPostAdmin(admin.ModelAdmin):
date_hierarchy = "date" date_hierarchy = "date"
ordering = ['title_de', 'title_en'] ordering = ['title_de', 'title_en']
actions = [regenerate] actions = [regenerate]
change_list_template = "admin/blogpost.html"
def get_urls(self):
urls = super().get_urls()
my_urls = [
path('sync/', self.sync_with_fs),
]
return my_urls + urls
def sync_with_fs(self, request):
BlogPost.sync_with_fs()
return HttpResponseRedirect("../")

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,23 +0,0 @@
date = "2023-10-02 09:59:00.127936"
update = "2023-10-02 10:59:00.127936"
keywords = ["bash"]
category = "Guide"
featured = true
public = true
thumbnail = "img/thumbnails/bash.png"
[lang.en]
title = "bash arrays"
subtitle = "how to work with bash arrays"
desc = """
bash scripting can be kind of weird.
This guide explains how they work without any fuzz.
"""
[lang.de]
title = "Bash Arrays"
subtitle = "Wie man mit Bash Arrays arbeitet"
desc = """
Bash Skripte sind manchmal etwas seltsam.
Hier wird erklärt, wie man mit Bash Arrays arbeitet.
"""

View File

@ -1,99 +0,0 @@
**NOTE**
This is a stolen article from [opensource.com](https://opensource.com/article/18/5/you-dont-know-bash-intro-bash-arrays)
about bash scripting. It's a good article and I've decided to use it to test my
markdown rendering.
# GERMAN VERY YES YES
# Bash scripting
[TOC]
| very | important | table |
|--------|-----------|-----------|
| v | i | t |
| yes | super | important |
| really | really | really |
cool math: $$1+2 \le \frac{1}{2}$$
cool math: $1+2 \le \frac{1}{2}$
## Wait, but why?
Writing about Bash is challenging because it's remarkably easy for an article
to devolve into a manual that focuses on syntax oddities. Rest assured,
however, the intent of this article is to avoid having you RTFM.
## A real (actually useful) example
To that end, let's consider a real-world scenario and how Bash can help:
You are leading a new effort at your company to evaluate and optimize the
runtime of your internal data pipeline. As a first step, you want to do a
parameter sweep to evaluate how well the pipeline makes use of threads. For
the sake of simplicity, we'll treat the pipeline as a compiled C++ black box
where the only parameter we can tweak is the number of threads reserved for
data processing: `./pipeline --threads 4.`
## The basics
The first thing we'll do is define an array containing the values of the
`--threads` parameter that we want to test:
```bash
allThreads=(1 2 4 8 16 32 64 128)
```
In this example, all the elements are numbers, but it need not be the
case—arrays in Bash can contain both numbers and strings, e.g., `myArray=(1
2 "three" 4 "five")` is a valid expression. And just as with any other Bash
variable, make sure to leave no spaces around the equal sign. Otherwise,
Bash will treat the variable name as a program to execute, and the `=` as its
first parameter!
Now that we've initialized the array, let's retrieve a few of its
elements. You'll notice that simply doing `echo $allThreads` will output only
the first element.
To understand why that is, let's take a step back and revisit how we usually
output variables in Bash. Consider the following scenario:
```bash
type="article" echo "Found 42 $type"
```
Say the variable $type is given to us as a singular noun and we want to add
an `s` at the end of our sentence. We can't simply add an s to `$type` since
that would turn it into a different variable, `$types`. And although we could
utilize code contortions such as `echo "Found 42 "$type"s"`, the best way
to solve this problem is to use curly braces: `echo "Found 42 ${type}s"`,
which allows us to tell Bash where the name of a variable starts and ends
(interestingly, this is the same syntax used in JavaScript/ES6 to inject
variables and expressions in template literals).
So as it turns out, although Bash variables don't generally require curly
brackets, they are required for arrays. In turn, this allows us to specify
the index to access, e.g., `echo ${allThreads[1]}` returns the second element
of the array. Not including brackets, e.g.,`echo $allThreads[1]`, leads Bash
to treat `[1]` as a string and output it as such.
Yes, Bash arrays have odd syntax, but at least they are zero-indexed, unlike
some other languages (I'm looking at you, R).[^1]
## Looping through arrays
Although in the examples above we used integer indices in our arrays, let's
consider two occasions when that won't be the case: First, if we wanted the
$i-th element of the array, where $i is a variable containing the index of
interest, we can retrieve that element using: echo ${allThreads[$i]}. Second,
to output all the elements of an array, we replace the numeric index with
the @ symbol (you can think of @ as standing for all):
```bash
type="article"
echo "Found 42 $type"
```
*[RTFM]: Read the Fucking Manual
*[HTML]: Hyper Text Markup Language
[^1]: Example Footnote

View File

@ -1,98 +0,0 @@
**NOTE**
This is a stolen article from [opensource.com](https://opensource.com/article/18/5/you-dont-know-bash-intro-bash-arrays)
about bash scripting. It's a good article and I've decided to use it to test my
markdown rendering.
# Bash scripting
[TOC]
| very | important | table |
|--------|-----------|-----------|
| v | i | t |
| yes | super | important |
| really | really | really |
cool math: $$1+2 \le \frac{1}{2}$$
cool math: $1+2 \le \frac{1}{2}$
## Wait, but why?
Writing about Bash is challenging because it's remarkably easy for an article
to devolve into a manual that focuses on syntax oddities. Rest assured,
however, the intent of this article is to avoid having you RTFM.
## A real (actually useful) example
To that end, let's consider a real-world scenario and how Bash can help:
You are leading a new effort at your company to evaluate and optimize the
runtime of your internal data pipeline. As a first step, you want to do a
parameter sweep to evaluate how well the pipeline makes use of threads. For
the sake of simplicity, we'll treat the pipeline as a compiled C++ black box
where the only parameter we can tweak is the number of threads reserved for
data processing: `./pipeline --threads 4.`
## The basics
The first thing we'll do is define an array containing the values of the
`--threads` parameter that we want to test:
```bash
allThreads=(1 2 4 8 16 32 64 128)
```
In this example, all the elements are numbers, but it need not be the
case—arrays in Bash can contain both numbers and strings, e.g., `myArray=(1
2 "three" 4 "five")` is a valid expression. And just as with any other Bash
variable, make sure to leave no spaces around the equal sign. Otherwise,
Bash will treat the variable name as a program to execute, and the `=` as its
first parameter!
Now that we've initialized the array, let's retrieve a few of its
elements. You'll notice that simply doing `echo $allThreads` will output only
the first element.
To understand why that is, let's take a step back and revisit how we usually
output variables in Bash. Consider the following scenario:
```bash
type="article" echo "Found 42 $type"
```
Say the variable $type is given to us as a singular noun and we want to add
an `s` at the end of our sentence. We can't simply add an s to `$type` since
that would turn it into a different variable, `$types`. And although we could
utilize code contortions such as `echo "Found 42 "$type"s"`, the best way
to solve this problem is to use curly braces: `echo "Found 42 ${type}s"`,
which allows us to tell Bash where the name of a variable starts and ends
(interestingly, this is the same syntax used in JavaScript/ES6 to inject
variables and expressions in template literals).
So as it turns out, although Bash variables don't generally require curly
brackets, they are required for arrays. In turn, this allows us to specify
the index to access, e.g., `echo ${allThreads[1]}` returns the second element
of the array. Not including brackets, e.g.,`echo $allThreads[1]`, leads Bash
to treat `[1]` as a string and output it as such.
Yes, Bash arrays have odd syntax, but at least they are zero-indexed, unlike
some other languages (I'm looking at you, R).[^1]
## Looping through arrays
Although in the examples above we used integer indices in our arrays, let's
consider two occasions when that won't be the case: First, if we wanted the
$i-th element of the array, where $i is a variable containing the index of
interest, we can retrieve that element using: echo ${allThreads[$i]}. Second,
to output all the elements of an array, we replace the numeric index with
the @ symbol (you can think of @ as standing for all):
```bash
type="article"
echo "Found 42 $type"
```
*[RTFM]: Read the Fucking Manual
*[HTML]: Hyper Text Markup Language
[^1]: Example Footnote

View File

@ -1,47 +1,12 @@
# Generated by Django 3.2.21 on 2023-10-02 08:14 # Generated by Django 3.2.19 on 2023-06-03 12:03
import blog.models from django.db import migrations
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
('start', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('slug', models.SlugField(unique=True)),
],
options={
'verbose_name': 'Category',
'verbose_name_plural': 'Categories',
},
),
migrations.CreateModel(
name='BlogPost',
fields=[
('searchable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='start.searchable')),
('body_en', models.TextField(blank=True, default='Dieser Artikel ist nicht auf deutsch verfügbar.')),
('body_de', models.TextField(blank=True, default='This aritcle is not available in english.')),
('thumbnail', models.ImageField(blank=True, default='img/thumbnails/default.jpg', upload_to='img/thumbnails')),
('featured', models.BooleanField(default=False)),
('langs', models.CharField(default="{'en': False, 'de': False}", max_length=64)),
('slug', models.SlugField()),
('category', models.ForeignKey(default=blog.models.Category.get_or_create_uncategorized, on_delete=django.db.models.deletion.SET_DEFAULT, to='blog.category')),
],
options={
'verbose_name': 'blog post',
'verbose_name_plural': 'blog posts',
},
bases=('start.searchable',),
),
] ]

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.19 on 2023-06-03 12:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('start', '0002_keyword_searchable_staticsite'),
('blog', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('slug', models.SlugField()),
],
),
migrations.CreateModel(
name='BlogPost',
fields=[
('searchable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='start.searchable')),
('body', models.TextField()),
('thumbnail', models.ImageField(blank=True, upload_to='')),
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='blog.category')),
],
bases=('start.searchable',),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2023-06-03 18:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_blogpost_category'),
]
operations = [
migrations.AddField(
model_name='blogpost',
name='public',
field=models.BooleanField(default=True),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.5 on 2023-10-02 19:03 # Generated by Django 3.2.19 on 2023-06-03 19:06
from django.db import migrations, models from django.db import migrations, models
@ -6,13 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('blog', '0001_initial'), ('blog', '0003_blogpost_public'),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AddField(
model_name='blogpost', model_name='blogpost',
name='slug', name='slug',
field=models.SlugField(unique=True), field=models.SlugField(default='test'),
preserve_default=False,
), ),
] ]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.19 on 2023-06-03 22:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0004_blogpost_slug'),
]
operations = [
migrations.AddField(
model_name='blogpost',
name='featured',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='blogpost',
name='thumbnail',
field=models.ImageField(blank=True, upload_to='img/thumbnails'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.19 on 2023-06-03 22:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0005_auto_20230604_0050'),
]
operations = [
migrations.AddField(
model_name='blogpost',
name='markdown',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.19 on 2023-06-03 23:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_blogpost_markdown'),
]
operations = [
migrations.RemoveField(
model_name='blogpost',
name='public',
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.19 on 2023-06-03 23:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0007_remove_blogpost_public'),
]
operations = [
migrations.AlterModelOptions(
name='blogpost',
options={'verbose_name': 'blog post', 'verbose_name_plural': 'blog posts'},
),
migrations.AlterModelOptions(
name='category',
options={'verbose_name': 'Category', 'verbose_name_plural': 'Categories'},
),
]

View File

@ -1,35 +1,10 @@
import toml
import ast
import re
import os
import pathlib
import markdown
from bs4 import BeautifulSoup
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from start.models import Keyword, Searchable from start.models import Searchable
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
EXTENSIONS = [
"extra",
"admonition",
"codehilite",
"meta",
"toc"
]
EXTENSION_CONFIGS = {
'codehilite': {
'linenums': True,
'pygments_style': 'monokai'
},
}
MD = markdown.Markdown(extensions=EXTENSIONS,
extension_configs=EXTENSION_CONFIGS)
class Category(models.Model): class Category(models.Model):
""" """
A category of blog posts A category of blog posts
@ -38,7 +13,7 @@ class Category(models.Model):
Maybe some day it would be cool if these were Searchable Maybe some day it would be cool if these were Searchable
""" """
name= models.CharField(max_length=50) name= models.CharField(max_length=50)
slug = models.SlugField(unique=True) slug = models.SlugField()
class Meta: class Meta:
verbose_name = _("Category") verbose_name = _("Category")
@ -47,54 +22,17 @@ class Category(models.Model):
def __str__(self): def __str__(self):
return f"{{<{self.__class__.__name__}>\"{self.name}\"}}" return f"{{<{self.__class__.__name__}>\"{self.name}\"}}"
@staticmethod
def get_or_create_uncategorized():
try:
return Category.objects.get(slug="uncategorized")
except Category.DoesNotExist:
return Category.objects.create(
slug="uncategorized", name="uncategorized")
class BlogPost(Searchable): class BlogPost(Searchable):
""" """
Should contain a blogpost Should contain a blogpost
""" """
DATA_DIR = "/app/blog/data/articles" body = models.TextField()
DEFAULT_LANGS = {'en': False, 'de': False} category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
META_TOP_KEYS = [ thumbnail = models.ImageField(blank=True, upload_to="img/thumbnails")
"date",
"update",
"keywords",
"category",
"featured",
"public",
"lang"]
META_LANG_KEYS = ["title", "subtitle", "desc"]
body_en = models.TextField(blank=True,
default="""Dieser Artikel ist nicht auf deutsch verfügbar.""")
body_de = models.TextField(blank=True,
default="""This aritcle is not available in english.""")
category = models.ForeignKey(
Category, on_delete=models.SET_DEFAULT, blank=False,
default=Category.get_or_create_uncategorized)
thumbnail = models.ImageField(
blank=True,
upload_to="img/thumbnails",
default="img/thumbnails/default.jpg")
featured = models.BooleanField(default=False) featured = models.BooleanField(default=False)
langs = models.CharField( markdown = models.BooleanField(default=False)
default=DEFAULT_LANGS.__repr__(), max_length=64) slug = models.SlugField()
slug = models.SlugField(unique=True, blank=False)
def save(self):
# check if the slug is empty if we remove whitespaces
if len(self.slug.strip()) == 0:
logger.error(
f"trying to save '{self.__class__}' with empty slug: {self}")
raise ValueError(f"trying to save '{self.__class__}' with empty slug: {self}")
super().save()
def regenerate(self): def regenerate(self):
""" """
@ -103,181 +41,9 @@ class BlogPost(Searchable):
Implements the abstract method of Searchable Implements the abstract method of Searchable
""" """
logger.info(f"regenerating {self.__class__.__name__} object: {self}") logger.info(f"regenerating {self.__class__.__name__} object: {self}")
# url stuff
self.suburl = f"/blog/{self.category.name}/{self.slug}" self.suburl = f"/blog/{self.category.name}/{self.slug}"
# load from markdown
# self.sync_file()
# redundand vvvv
self.save() self.save()
@staticmethod
def __patch_html(html: str) -> str:
soup = BeautifulSoup(html)
# add bootstrap classes to regular tables
tables = soup.select("table")
for table in tables:
if 'class' in table.attrs and "codehilitetable" in table.attrs['class']:
# table has at least one class already AND
# this table is a codehighlighting table,
# not a regular one
continue
# set the bootstrap classes for tables
table.attrs['class'] = 'table table-striped border'
return soup.prettify()
def sync_file(self):
"""
generate an article fromm it's original markdown file
"""
logger = logging.getLogger(__name__)
logger.info(f"syncing article to markdown for: {self}")
# read metadata
try:
fmeta = open(f"{self.DATA_DIR}/{self.slug}.toml", "r")
except Exception as e:
logger.error(f"could not find meta file for '{self}'")
return
data = toml.load(fmeta)
langs = self.get_langs()
for key in self.META_TOP_KEYS:
if key not in data:
logger.error(f"Key '{key}' missing in meta file for '{self}'")
raise ValueError(
f"Key '{key}' missing in meta file for '{self}'")
for lang in self.DEFAULT_LANGS.keys():
if lang not in data['lang']:
langs[lang] = False
else:
langs[lang] = True
self.set_langs(langs)
for lang in data['lang']:
for key in self.META_LANG_KEYS:
if key not in data['lang'][lang]:
logger.warning(
f"Key '{key}' ('{lang}') missing in meta file for '{self}'")
langs[lang] = False
else:
langs[lang] = True
if not langs[lang]:
# no translation for this language
continue
with open(f"{self.DATA_DIR}/{lang}-{self.slug}.md") as f_en:
MD.reset()
body: str = f_en.read()
match lang:
case "en":
self.title_en = data['lang'][lang]["title"]
self.subtitle_en = data['lang'][lang]["subtitle"]
self.desc_en = data['lang'][lang]["desc"]
self.body_en = BlogPost.__patch_html(MD.convert(body))
case "de":
self.title_de = data['lang'][lang]["title"]
self.subtitle_de = data['lang'][lang]["subtitle"]
self.desc_de = data['lang'][lang]["desc"]
self.body_de = BlogPost.__patch_html(MD.convert(body))
case _:
logger.error(
f"unknown language '{lang}' in meta file for '{self}'")
self.date = data["date"]
self.update = data["update"]
self.featured = data["featured"]
self.public = data["public"]
# NOTE: thumbnail is optional
if "thumbnail" in data:
self.thumbnail = data["thumbnail"]
# NOTE: category is optional
if "category" in data:
try:
category: Category = Category.objects.get(
slug=data['category'])
except Category.DoesNotExist:
category = Category.objects.create(
name=data['category'], slug=data['category'])
self.category = category
self.save()
for keyword in data["keywords"]:
try:
self.keywords.add(Keyword.objects.get(slug=keyword))
except Keyword.DoesNotExist:
self.keywords.create(
slug=keyword, text_en=keyword, text_de=keyword)
self.save()
def get_langs(self) -> dict[str, bool]:
"""
get available languages
"""
# SECURITY:
# If someone could inject the langs field, literal_eval will evaluate to
# any literal Python value. This should not be a concern.
# If we would use a regular eval here, an attacker who controls one of
# the `langs` of a BlogPost could run arbitrary Python code.
try:
# literal_eval is safe because it only parses literals, not any kind
# of python expression. If the given str is not a valid literal,
# ValueError will be raised
langs = ast.literal_eval(str(self.langs))
return langs
except ValueError as e:
logger.error(
f"could not safely evaluate 'langs' for '{self}': {e}")
raise e
def set_langs(self, langs: dict[str, bool]):
"""
set available languages
"""
self.langs = langs.__repr__()
@ classmethod
def sync_with_fs(cls):
"""
Sync all Blog Posts with the filesystem.
Caution: Will delete all Blog Posts
"""
# logger.name = logger.name + ".sync_with_fs"
# delete all existing objects
BlogPost.objects.all().delete()
# check if the DATA_DIR is OK
data_dir = pathlib.Path(cls.DATA_DIR)
if not data_dir.exists():
logger.error(f"'{cls.DATA_DIR} does not exist'")
if not data_dir.is_dir():
logger.error(f"'{cls.DATA_DIR} is not a directory'")
files = [f for f in os.listdir(data_dir) if (
data_dir.joinpath(f)).is_file()]
# find the meta file
regex = r"^(.*)\.toml"
# filepath, slug
files = [[f, ""] for f in files]
for file in files:
# parse file name
try:
matches = re.match(regex, file[0])
if matches is None:
# file is not a toml / meta file
files.remove(file)
continue
else:
current_lang = matches.group(1)
file[1] = matches.group(1)
obj = BlogPost(slug=file[1])
obj.sync_file()
obj.regenerate()
except Exception as e:
logger.error(f"Could not create BlogPost for '{file[1]}': {e}")
files.remove(file)
class Meta: class Meta:
verbose_name = _("blog post") verbose_name = _("blog post")
verbose_name_plural = _("blog posts") verbose_name_plural = _("blog posts")

View File

@ -1,6 +0,0 @@
{% extends 'admin/change_list.html' %} {% block object-tools %}
<form action="sync" method="POST">
{% csrf_token %}
<button type="submit" class="button" style="padding: 4px">Sync with FS</button>
</form>
{{ block.super }} {% endblock %}

View File

@ -1,96 +1,26 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load helper_tags %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
{% block languagecode %} {% block languagecode %}{{ LANGUAGE_CODE }}{% endblock languagecode %}
{{ LANGUAGE_CODE }} {% block title %}{% translate "cscherr.de" %} - {% translate "Blog" %}{% endblock title %}
{% endblock languagecode %}
{% block title %}
{% translate "cscherr.de" %} - {% translate "Blog" %}
{% endblock title %}
{% block nav %} {% block nav %}
{% include 'nav.html' %} {% include 'nav.html' %}
{% endblock nav %} {% endblock nav %}
{% block head %} {% block headscripts %}
<script type="text/javascript" <script type="text/javascript" id="MathJax-script" async
id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3.0.0/es5/tex-mml-chtml.js">
async </script>
src="https://cdn.jsdelivr.net/npm/mathjax@3.0.0/es5/tex-mml-chtml.js"></script> {% endblock headscripts %}
{% endblock head %}
{% block main %} {% block main %}
<div class="container-xl"> <div class="container-xl">
<article> <div class="jumbotron text-center">
<div class="jumbotron my-5"> <h1>{{ post.title }} <small class="">{{ post.subtitle }}</small></h1>
<div class="row"> <img src="{{ post.thumbnail.url }}" alt="thumbnail">
<div class="col">
<picture>
<img src="{{ post.thumbnail.url }}" alt="thumbnail" class="img-fluid">
</picture>
</div> </div>
<div class="col"> <div class="container">
<div class="row py-5"> $$x=\frac{-b+\sqrt{b^2-4ac}}{2a}$$
{% if LANGUAGE_CODE == "de" %} {{ post.body | safe }}
<h1 class="">
{{ post.title_de }}
<br>
<small class="fs-4">{{ post.subtitle_de }}</small>
</h1>
<br>
{% elif LANGUAGE_CODE == "en" %}
<h1 class="">
{{ post.title_en }}
<br>
<small class="fs-4">{{ post.subtitle_en }}</small>
</h1>
<br>
{% else %}
<h1 class="">
{{ post.title_en }}
<br>
<small class="fs-4">{{ post.subtitle_en }}</small>
</h1>
<br>
{% endif %}
</div>
<div class="row py-5">
{% if LANGUAGE_CODE == "de" %}
<p class="lead">{{ post.desc_de }}</p>
{% elif LANGUAGE_CODE == "en" %}
<p class="lead">{{ post.desc_en }}</p>
{% else %}
<p class="lead">{{ post.desc_en }}</p>
{% endif %}
</div>
<div class="row py-5">
<p>
<b>{{ post.category.name }}<b>
</p>
</div>
</div>
</div>
</div>
<hr>
<div class="my-5">
{% if LANGUAGE_CODE == "de" %}
{{ post.body_de | safe }}
{% elif LANGUAGE_CODE == "en" %}
{{ post.body_en | safe }}
{% else %}
{{ post.body_en | safe }}
{% endif %}
</div>
<hr>
<div class="row text-center">
<div class="col">
{% format_time post.date as date %}
<p>{% trans "published" %}: {{ date }}</p>
</div>
<div class="col">
{% format_time post.update as update %}
<p>{% trans "updated" %}: {{ update }}</p>
</div>
</div>
</article>
</div> </div>
{% include 'blog/featured.html' %} {% include 'blog/featured.html' %}
</div>
{% endblock main %} {% endblock main %}

View File

@ -1,208 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% load helper_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block languagecode %}
{{ LANGUAGE_CODE }}
{% endblock languagecode %}
{% block title %}
{% translate "cscherr.de" %} - {% translate "Blog" %}
{% endblock title %}
{% block blockname %}
{% endblock blockname %}
{% block nav %}
{% include 'nav.html' %}
{% endblock nav %}
{% block main %}
<div class="container-fluid">
<div class="row mb-5">
<div class="col col-xxl mb-5" id="headline">
<a class="text-reset link-offset-2 link-underline link-underline-opacity-0"
href="{% url "blog:browse" %}"
style="display: inline-block">
<h1 class="display-1 w-0">Browse</h1>
</a>
{# center headline on small screens #}
<script>
if (screen.width < 480) {
col = document.getElementById("headline")
col.classList.add("text-center")
};
</script>
</div>
<div class="col-sm-3">
<div class="container-fluid w-100 h-100 p-2">
<form class="w-100 h-100"
role="search"
action=""
method="GET"
novalidate
id="filter-form">
<div class="py-2">
<input type="search"
name="search"
class="form-control flex-fill py-2"
defaultValue=""
aria-label="Search"
placeholder="{% trans "Search" %}"
required=""
{% if filters.search %}value="{{ filters.search }}"{% endif %}
id="id_search">
</div>
<div class="py-2">
<select class="form-select py-2"
aria-label="Large select example"
defaultValue=""
name="category">
<option value="">{% trans "select category" %}</option>
{% for category in categories %}
<option {% if filters.category.slug == category.slug %}selected{% endif %}
value="{{ category.slug }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="py-2">
<input class="tagify"
name="keywords"
defaultValue=""
placeholder="{% trans "Keywords" %}"
{% if filters.keywords %} value="{% for keyword in filters.keywords %}{{ keyword }} {% endfor %}
"
{% endif %}>
</div>
<div class="row">
<div class="col">
<div class="py-2">
<!-- will reset to the filters encoded in
the url by GET attributes, not to the empty
form. -->
<input type="reset"
class="btn bg-primary fw-bold py-2 float-end w-100 reset-empty-button">
</div>
</div>
<div class="col">
<div class="py-2">
<button id="filter-button"
class="btn bg-primary fw-bold py-2 float-end w-100"
type="submit">{% trans "Filter" %}</button>
</div>
</div>
</div>
<script>
var input = document.querySelector('input[class=tagify]');
new Tagify(input, {
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(','),
enforceWhitelist: true,
whitelist : [{% for keyword in keywords %}'{{ keyword.slug | safe }}', {% endfor %}],
dropdown : {
classname : "tagify-dropdown", // TODO: style better
enabled : 1, // show the dropdown immediately on focus
maxItems : 5,
position : "text", // place the dropdown near the typed text
closeOnSelect : false, // keep the dropdown open after selecting a suggestion
highlightFirst: true
}
})
</script>
</form>
</div>
</div>
</div>
<div class="row mb-5">
<div class="container-fluid p-0 w-100 h-100">
{% if not posts %}
<div class="text-center">
<img src="/media/img/http/404.svg"
class="img-fluid pb-5 pt-2 darkmode-invert"
style="max-height: 650px"
alt="404" />
<h2 class="display-5">{% trans "No posts found for your filters." %}</h2>
</div>
{% else %}
<div class="row gap-3 cardrow">
{% for post in posts %}
<div class="card col mx-auto my-2 py-2">
<a class="text-reset link-offset-2 link-underline link-underline-opacity-0"
href=" {% url 'blog:post' post.category.slug post.slug %}">
<img src="{{ post.thumbnail.url }}"
class="card-img-top img-fluid mx-auto d-block"
style="max-height: 150px;
width: auto;
padding-top: 8px"
alt="thumbnail" />
<div class="card-body" style="height: 100px">
{% if LANGUAGE_CODE == "de" %}
<h5 class="card-title">
{{ post.title_de }}<small class="text-body-secondary">{{ post.subtitle }}</small>
</h5>
<p class="card-text">{{ post.desc_de }}</p>
{% elif LANGUAGE_CODE == "en" %}
<h5 class="card-title">
{{ post.title_en }}<small class="text-body-secondary">{{ post.subtitle }}</small>
</h5>
<p class="card-text">{{ post.desc_en }}</p>
{% else %}
<h5 class="card-title">
{{ post.title_en }}<small class="text-body-secondary">{{ post.subtitle }}</small>
</h5>
<p class="card-text">{{ post.desc_en }}</p>
{% endif %}
</div>
<div class="container pt-5">
<ul class="list-group list-group-flush">
<li class="list-group-item">
{% translate "category" %}: <b>{{ post.category.name }}</b>
</li>
{% for keyword in post.keywords.all %}
{% if LANGUAGE_CODE == "de" %}
<li class="list-group-item">{{ keyword.text_de }}</li>
{% elif LANGUAGE_CODE == "en" %}
<li class="list-group-item">{{ keyword.text_en }}</li>
{% else %}
<li class="list-group-item">{{ keyword.text_en }}</li>
{% endif %}
{% endfor %}
</ul>
</div>
<div class="container p-1 text-center" style="border-top: solid">
<li class="list-group-item">
{% format_time post.date "%F" as date %}
{% trans "published" %}: {{ date }}
</li>
</div>
</a>
</div>
{% empty %}
{% endfor %}
</div>
<nav aria-label="Page navigation example" class="mt-5">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?{% query_transform page=1 %}">First</a>
</li>
<li class="page-item">
<a class="page-link"
href="?{% query_transform page=page_obj.previous_page_number %}">{{ page_obj.previous_page_number }}</a>
</li>
{% endif %}
<li class="page-item">
<a class="page-link" href="">{{ page_obj.number }}</a>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link"
href="?{% query_transform page=page_obj.next_page_number %}">{{ page_obj.next_page_number }}</a>
</li>
<li class="page-item">
<a class="page-link"
href="?{% query_transform page=page_obj.paginator.num_pages %}">Last</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
{% endblock main %}

View File

@ -1,40 +1,27 @@
{% load i18n %} {% load i18n %}
{% load helper_tags %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
<div class="container-lg mt-5"> <div class="container-lg mt-5">
<h4 class="">{% trans "Featured" %}</h4> <h4 class="">{% trans "Featured" %}</h4>
<div class="row gap-1 cardrow"> <div class="row row-cols-1 row-cols-md-5 my-4">
{% for post in featured_posts %} {% for post in featured_posts %}
<div class="card col mx-auto my-2"> <div class="card col m-2 p-0">
<a class="text-reset link-offset-2 link-underline link-underline-opacity-0" <a class="text-reset link-offset-2 link-underline link-underline-opacity-0" href=" {% url 'blog:post' post.category.slug post.slug %}">
href=" {% url 'blog:post' post.category.slug post.slug %}"> <img src="{{ post.thumbnail.url }}" class="card-img-top img-fluid" style="max-height: 150px;" alt="thumbnail">
<img src="{{ post.thumbnail.url }}" <div class="card-body" style="height: 100px;">
class="card-img-top img-fluid mx-auto d-block"
style="max-height: 150px; width: auto; padding-top: 8px;"
alt="thumbnail" />
<div class="card-body" style="height: 100px">
{% if LANGUAGE_CODE == "de" %} {% if LANGUAGE_CODE == "de" %}
<h5 class="card-title"> <h5 class="card-title">{{ post.title_de }}<small class="text-body-secondary">{{ post.subtitle }}</small></h5>
{{ post.title_de }}<small class="text-body-secondary">{{ post.subtitle }}</small>
</h5>
<p class="card-text">{{ post.desc_de }}</p> <p class="card-text">{{ post.desc_de }}</p>
{% elif LANGUAGE_CODE == "en" %} {% elif LANGUAGE_CODE == "en" %}
<h5 class="card-title"> <h5 class="card-title">{{ post.title_en }}<small class="text-body-secondary">{{ post.subtitle }}</small></h5>
{{ post.title_en }}<small class="text-body-secondary">{{ post.subtitle }}</small>
</h5>
<p class="card-text">{{ post.desc_en }}</p> <p class="card-text">{{ post.desc_en }}</p>
{% else %} {% else %}
<h5 class="card-title"> <h5 class="card-title">{{ post.title_en }}<small class="text-body-secondary">{{ post.subtitle }}</small></h5>
{{ post.title_en }}<small class="text-body-secondary">{{ post.subtitle }}</small>
</h5>
<p class="card-text">{{ post.desc_en }}</p> <p class="card-text">{{ post.desc_en }}</p>
{% endif %} {% endif %}
</div> </div>
<div class="container pt-5"> <div class="container pt-5">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"> <li class="list-group-item">{% translate "category" %}: <b>{{ post.category.name }}</b></li>
{% translate "category" %}: <b>{{ post.category.name }}</b>
</li>
{% for keyword in post.keywords.all %} {% for keyword in post.keywords.all %}
{% if LANGUAGE_CODE == "de" %} {% if LANGUAGE_CODE == "de" %}
<li class="list-group-item">{{ keyword.text_de }}</li> <li class="list-group-item">{{ keyword.text_de }}</li>
@ -46,12 +33,6 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div class="container p-1 text-center" style="border-top: solid">
<li class="list-group-item">
{% format_time post.date "%F" as date %}
{% trans "published" %}: {{ date }}
</li>
</div>
</a> </a>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -1,15 +1,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
{% block languagecode %} {% block languagecode %}{{ LANGUAGE_CODE }}{% endblock languagecode %}
{{ LANGUAGE_CODE }} {% block title %}{% translate "cscherr.de" %} - {% translate "Blog" %}{% endblock title %}
{% endblock languagecode %}
{% block title %}
{% translate "cscherr.de" %} - {% translate "Blog" %}
{% endblock title %}
{% block main %} {% block main %}
<div class="container-xl"> <div class="container-xl">
<div class="container text-center jumbotron my-3"></div>
<div class="jumbotron text-center"> <div class="jumbotron text-center">
<h1>{% translate "Was gibt es hier?" %}</h1> <h1>{% translate "Was gibt es hier?" %}</h1>
<p>{% translate "Blog" %}</p> <p>{% translate "Blog" %}</p>
@ -45,12 +40,6 @@
<div class="container text-center jumbotron my-3"> <div class="container text-center jumbotron my-3">
<h1 class="my-4">{% translate "Looking for anything specific?" %}</h1> <h1 class="my-4">{% translate "Looking for anything specific?" %}</h1>
{% include 'main_search_form.html' %} {% include 'main_search_form.html' %}
<h4 class="my-5">
<a href="{% url 'blog:browse' %}"
class="link-offset-2 link-underline link-underline-opacity-0">
{% translate "Browse articles" %}
</a>
</h4>
</div> </div>
{% include 'blog/featured.html' %} {% include 'blog/featured.html' %}
</div> </div>

View File

@ -5,6 +5,7 @@ from . import views
app_name: str = "blog" app_name: str = "blog"
urlpatterns = [ urlpatterns = [
path("", views.Index.as_view(), name="index"), path("", views.Index.as_view(), name="index"),
path("browse", views.Browse.as_view(), name="browse"), path("categories", views.CategoryList.as_view(), name="category_list"),
path("<slug:slug>", views.ArticleList.as_view(), name="article_list"),
path("<slug:category>/<slug:slug>", views.Post.as_view(), name="post"), path("<slug:category>/<slug:slug>", views.Post.as_view(), name="post"),
] ]

View File

@ -1,20 +1,13 @@
import json from django.shortcuts import Http404, HttpResponse, get_object_or_404, render
import ast
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, render
from django.http.response import Http404, HttpResponse
from django.http.request import HttpRequest
from django.utils.translation import get_language from django.utils.translation import get_language
from django.views.generic import TemplateView, DetailView, ListView, View from django.views.generic import TemplateView, DetailView, ListView, View
from django.db.models import Q from .models import BlogPost, Category
from .models import BlogPost, Category, Keyword
from start.views import SearchableView from start.views import SearchableView
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Index(TemplateView, SearchableView): class Index(TemplateView, SearchableView):
""" """
The index page of the gawa/blog app. The index page of the gawa/blog app.
@ -27,11 +20,10 @@ class Index(TemplateView, SearchableView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter( context['featured_posts'] = BlogPost.objects.filter(featured=True, public=True)
featured=True, public=True) logger.debug(f"loaded featured posts: {context['featured_posts']}")
return context return context
class Post(DetailView): class Post(DetailView):
""" """
Main page of a blog post Main page of a blog post
@ -44,9 +36,49 @@ class Post(DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True) context['featured_posts'] = BlogPost.objects.filter(featured=True)
logger.debug(f"loaded featured posts: {context['featured_posts']}")
return context return context
class Browse(ListView): def get_object(self, queryset=None):
obj = get_object_or_404(
BlogPost,
category__slug=self.kwargs['category'], # first slug is category
slug=self.kwargs['slug'] # second slug is article itself
)
match get_language():
case 'de':
logger.debug("setting language unspecific attributes for language: de")
obj.title = obj.title_de
obj.subtitle = obj.subtitle_de
obj.desc = obj.desc_de
case 'en':
logger.debug("setting language unspecific attributes for language: en")
obj.title = obj.title_en
obj.subtitle = obj.subtitle_en
obj.desc = obj.desc_en
case _:
# this should not happen, but who knows what dumb stuff users will come up with
logger.warning("article for unsupported language was requested")
return obj
class CategoryList(ListView):
"""
Scroll through a list of blog Categories
"""
model=Category
template_name = "blog/categories.html"
context_object_name = "categories"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True)
logger.debug(f"loaded featured posts: {context['featured_posts']}")
return context
class ArticleList(ListView):
""" """
Scroll through a list of blog posts Scroll through a list of blog posts
@ -55,100 +87,11 @@ class Browse(ListView):
""" """
model=BlogPost model=BlogPost
template_name = "blog/browse.html" template_name = "blog/posts.html"
context_object_name = "posts" context_object_name = "posts"
paginate_by = 2
allow_empty = False # but we have a special get method
allow_empty = True
def get_queryset(self):
objects = BlogPost.objects.all()
if "category" in self.request.GET and len(
self.request.GET["category"].strip()) > 0:
category = self.request.GET["category"]
try:
category = Category.objects.get(slug=category)
objects = objects.filter(category=category)
except Category.DoesNotExist:
objects = objects.none()
if "search" in self.request.GET and len(
self.request.GET["search"].strip()) > 0:
search = self.request.GET["search"]
# __icontains matches those attributes that contain the
# search string without caring about lower/upper cases
objects = objects.filter(
Q(title_en__icontains=search) |
Q(title_de__icontains=search) |
Q(subtitle_en__icontains=search) |
Q(subtitle_de__icontains=search) |
Q(desc_en__icontains=search) |
Q(desc_de__icontains=search) |
# Q(body_en__icontains=search) |
# Q(body_de__icontains=search) |
Q(slug__icontains=search)
)
if "keywords" in self.request.GET and len(
self.request.GET["keywords"].strip()) > 0:
raw_keywords = self.request.GET["keywords"]
raw_keywords = raw_keywords.split('+')
keywords: list[Keyword] = []
for raw_keyword in raw_keywords:
try:
keywords.append(Keyword.objects.get(slug=raw_keyword))
except Keyword.DoesNotExist:
pass
objects = objects.filter(keywords__in=keywords)
return objects
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True) context['featured_posts'] = BlogPost.objects.filter(featured=True)
context['categories'] = Category.objects.all() logger.debug(f"loaded featured posts: {context['featured_posts']}")
context['keywords'] = Keyword.objects.all()
context["filters"] = {}
if "category" in self.request.GET and len(
self.request.GET["category"].strip()) > 0:
category = self.request.GET["category"]
try:
category = Category.objects.get(slug=category)
context["filters"]["category"] = category
except Category.DoesNotExist:
context["filters"]["category"] = None
if "search" in self.request.GET and len(
self.request.GET["search"].strip()) > 0:
search = self.request.GET["search"]
context["filters"]["search"] = search
if "keywords" in self.request.GET and len(
self.request.GET["keywords"].strip()) > 0:
keywords = self.request.GET["keywords"]
keywords = keywords.split('+')
context["filters"]["keywords"] = keywords
return context return context
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
self.object_list = self.get_queryset()
allow_empty = self.get_allow_empty()
if not allow_empty:
# When pagination is enabled and object_list is a queryset,
# it's better to do a cheap query than to load the unpaginated
# queryset in memory.
if self.get_paginate_by(self.object_list) is not None and hasattr(
self.object_list, 'exists'):
is_empty = not self.object_list.exists()
else:
is_empty = not self.object_list
else:
is_empty = False
context = self.get_context_data()
context["is_empty"] = is_empty
response = self.render_to_response(context)
if is_empty:
response.status_code = 404
return response

View File

@ -11,8 +11,6 @@ https://docs.djangoproject.com/en/3.2/ref/settings/
""" """
# for getting envvars # for getting envvars
import logging
from django.utils.translation import gettext_lazy as _
import os import os
# default django # default django
@ -63,6 +61,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'start.middleware.LangBasedOnUrlMiddleware',
] ]
ROOT_URLCONF = 'gawa.urls' ROOT_URLCONF = 'gawa.urls'
@ -101,6 +100,7 @@ DATABASES = {
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
@ -123,6 +123,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/ # https://docs.djangoproject.com/en/3.2/topics/i18n/
from django.utils.translation import gettext_lazy as _
LANGUAGES = [ LANGUAGES = [
("de", _("German")), ("de", _("German")),
@ -172,6 +173,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Logging configs # Logging configs
import logging
myServerFormatter = ServerFormatter myServerFormatter = ServerFormatter
myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S" myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S"
@ -295,4 +297,4 @@ LOGGING = {
#MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media" #MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media"
MEDIA_ROOT = "/app/media" MEDIA_ROOT = "/app/media"
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
# FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload" FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload"

View File

@ -14,14 +14,14 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.conf.urls import url
from django.contrib import admin from django.contrib import admin
from django.urls import include, re_path
from django.urls import include, path from django.urls import include, path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
re_path('i18n/', include('django.conf.urls.i18n')), url(r'^i18n/', include('django.conf.urls.i18n')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="557.15094"
height="210.8916"
viewBox="0 0 557.15094 210.8916"
version="1.1"
id="svg1"
inkscape:export-filename="404.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px" />
<defs
id="defs1">
<rect
x="292.2478"
y="154.0531"
width="462.1593"
height="240.14159"
id="rect1" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-239.50485,-280.19422)">
<text
xml:space="preserve"
id="text1"
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:24.5524px;font-family:'Source Code Pro';-inkscape-font-specification:'Source Code Pro, Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;white-space:pre;shape-inside:url(#rect1);fill:#3b3b3b;fill-opacity:1"
transform="matrix(13.034066,0,0,13.034066,-3571.9156,-1800.7033)"><tspan
x="292.24805"
y="175.53608"
id="tspan2">404</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -10,7 +10,6 @@ def regenerate(modeladmin, request, queryset):
for obj in queryset: for obj in queryset:
obj.regenerate() obj.regenerate()
@admin.register(Keyword) @admin.register(Keyword)
class KeywordAdmin(admin.ModelAdmin): class KeywordAdmin(admin.ModelAdmin):
""" """
@ -18,25 +17,21 @@ class KeywordAdmin(admin.ModelAdmin):
""" """
list_display = ["text_en", "text_de"] list_display = ["text_en", "text_de"]
@admin.register(StaticSite) @admin.register(StaticSite)
class StaticSiteAdmin(admin.ModelAdmin): class StaticSiteAdmin(admin.ModelAdmin):
""" """
Admin Interface for StaticSite Admin Interface for StaticSite
""" """
list_display = ["title_en", "subtitle_en", list_display = ["title_en", "subtitle_en", "title_de", "subtitle_de", "suburl"]
"title_de", "subtitle_de", "suburl"]
ordering = ['title_de', 'title_en'] ordering = ['title_de', 'title_en']
actions = [regenerate] actions = [regenerate]
@admin.register(Searchable) @admin.register(Searchable)
class SearchableAdmin(admin.ModelAdmin): class SearchableAdmin(admin.ModelAdmin):
""" """
Abstract Admin Interface for all Searchables Abstract Admin Interface for all Searchables
""" """
list_display = ["title_en", "subtitle_en", list_display = ["title_en", "subtitle_en", "title_de", "subtitle_de", "suburl"]
"title_de", "subtitle_de", "suburl"]
ordering = ['title_de', 'title_en'] ordering = ['title_de', 'title_en']
actions = [regenerate] actions = [regenerate]
@ -46,7 +41,6 @@ class LinkAdmin(admin.ModelAdmin):
""" """
Admin Interface for Links Admin Interface for Links
""" """
list_display = ["title_en", "title_de", "url", list_display = ["title_en", "title_de", "url", "suburl", "favicon", "status"]
"suburl", "favicon", "status", "personal"]
ordering = ['status', 'title_de', 'title_en'] ordering = ['status', 'title_de', 'title_en']
actions = [regenerate] actions = [regenerate]

Some files were not shown because too many files have changed in this diff Show More