Compare commits

...

86 Commits

Author SHA1 Message Date
Christoph J. Scherr 0ed70bbae9
load links from file interface 2024-03-03 19:35:22 +01:00
Christoph J. Scherr 45b0f4dca0
add links toml 2024-02-11 14:14:18 +01:00
Christoph J. Scherr 1e66315971
ignore db, idc 2024-02-09 22:57:21 +01:00
Christoph J. Scherr 22fc989f5e
fix all bold 2024-02-09 22:57:14 +01:00
Christoph J. Scherr 7391147cc6
data updat 2024-02-09 22:55:38 +01:00
Christoph J. Scherr 39757f64a7 updated links 2023-10-10 12:29:43 +02:00
Christoph J. Scherr 4e322f7211 remove searchable model 2023-10-10 12:00:31 +02:00
Christoph J. Scherr e868e0ccee translations 2023-10-10 11:35:11 +02:00
Christoph J. Scherr 61f7f67c6c host on local ip 2023-10-10 11:35:07 +02:00
Christoph J. Scherr 3754ba5359 better blog browse on mobile 2023-10-09 23:16:45 +02:00
Christoph J. Scherr 327200a4b0 better legal info, but not complete 2023-10-09 23:10:52 +02:00
Christoph J. Scherr ce34a0b89b pagination settings and translation 2023-10-09 21:55:30 +02:00
Christoph J. Scherr 79819f584f Merge pull request 'translation' (#43) from translation into devel
Reviewed-on: #43
2023-10-08 21:29:35 +02:00
Christoph J. Scherr a5e0c08c13 startpage styling 2023-10-08 21:24:57 +02:00
Christoph J. Scherr 57174c88a1 write translations 2023-10-08 21:22:47 +02:00
Christoph J. Scherr 57dbd03371 restructuring 2023-10-08 19:46:12 +02:00
Christoph J. Scherr 3fc146f1c5 rm cache from repo 2023-10-08 14:52:43 +02:00
Christoph J. Scherr 3d055592fc Merge pull request 'blog-browse' (#40) from blog-browse into devel
Reviewed-on: #40
2023-10-08 14:46:08 +02:00
Christoph J. Scherr f6dfb7b0b7 pagination behaves well with empty results 2023-10-08 14:29:56 +02:00
Christoph J. Scherr 247fe8e206 pagination works
not compatible with search?
2023-10-08 14:11:48 +02:00
Christoph J. Scherr 917e6b6167 card view is now usable on mobile 2023-10-08 13:49:19 +02:00
Christoph J. Scherr 1f2e66b1ef tagify validation 2023-10-07 19:43:32 +02:00
Christoph J. Scherr 260005dcf0 reset button empties 2023-10-07 19:34:13 +02:00
Christoph J. Scherr 21f3b19111 infoscreen for empty 2023-10-07 17:55:53 +02:00
Christoph J. Scherr f76c18efaf keep filter data
foo
2023-10-07 16:30:04 +02:00
Christoph J. Scherr 559eb3f9fa add search 2023-10-07 15:26:36 +02:00
Christoph J. Scherr 06e3172234 browse category filter 2023-10-07 15:03:12 +02:00
Christoph J. Scherr 8ef158c034 browse mobile friendly frontend 2023-10-07 14:02:01 +02:00
Christoph J. Scherr 62d7fbe57b add category to frontend 2023-10-07 11:12:54 +02:00
Christoph J. Scherr fb438c26b1 tagify styling 2023-10-03 15:48:22 +02:00
Christoph J. Scherr bf30427679 add early browse 2023-10-02 23:08:33 +02:00
Christoph J. Scherr a613786c7b add tagify 2023-10-02 23:08:20 +02:00
Christoph J. Scherr 72407c1a3e add browse to index 2023-10-02 22:32:35 +02:00
Christoph J. Scherr 0d8afc96f2 center featured cards 2023-10-02 22:24:02 +02:00
Christoph J. Scherr 448c54aeb3 upload static 2023-10-02 22:05:30 +02:00
Christoph J. Scherr ba27629036 Merge pull request 'path markdown html; table mod' (#35) from blog-style into devel
Reviewed-on: #35
2023-10-02 21:41:32 +02:00
Christoph J. Scherr 64f84d58bc path markdown html; table mod 2023-10-02 21:37:00 +02:00
Christoph J. Scherr 9b7c73a977 Merge pull request 'revamp-file-discover' (#33) from revamp-file-discover into devel
Reviewed-on: #33
2023-10-02 21:09:58 +02:00
Christoph J. Scherr 83d75823a4 don't save empty slugs for blogposts 2023-10-02 21:08:11 +02:00
Christoph J. Scherr c22807921d loading optimized, empty post bug 2023-10-02 20:54:43 +02:00
Christoph J. Scherr 3534f5399e Merge pull request 'update to django 4.2 and updated language switcher' (#32) from update-django into devel
Reviewed-on: #32
2023-10-02 20:43:52 +02:00
Christoph J. Scherr f7f0675d26 update to django 4.2;updated language switcher 2023-10-02 20:43:03 +02:00
Christoph J. Scherr 48a0d26745 browse url and template 2023-10-02 19:45:50 +02:00
Christoph J. Scherr c0750fdb30 visual improvement 2023-10-02 18:07:01 +02:00
Christoph J. Scherr 1ce0d52302 Merge pull request 'blog-markdown' (#31) from blog-markdown into devel
Reviewed-on: #31
2023-10-02 11:31:41 +02:00
Christoph J. Scherr 7e3f33a824 ignore already created superuser 2023-10-02 11:30:52 +02:00
Christoph J. Scherr c59d243cc0 add timezone to dates 2023-10-02 11:30:42 +02:00
Christoph J. Scherr d287b3291d image scaling and styling 2023-10-02 11:16:23 +02:00
Christoph J. Scherr e8c332fce4 add date to card view 2023-10-02 11:04:48 +02:00
Christoph J. Scherr 6f2267e18a add timestamp to blogpost 2023-10-02 10:59:15 +02:00
Christoph J. Scherr 50db49911f langs setting 2023-10-02 10:33:01 +02:00
Christoph J. Scherr 05497e10e1 add time to date and add update field 2023-10-02 10:23:03 +02:00
Christoph J. Scherr a9a90e9bd4 toml metafile works 2023-10-02 09:49:04 +02:00
Christoph J. Scherr 3bf0dd42c7 tonl parse pretty good 2023-10-02 03:07:43 +02:00
Christoph J. Scherr 3be2f095c0 keywords work SOMEHOW 2023-10-02 01:41:50 +02:00
Christoph J. Scherr b269b452fd keywords are created 2023-10-02 00:56:26 +02:00
Christoph J. Scherr adac986018 add unique slug to keywords 2023-10-02 00:52:54 +02:00
Christoph J. Scherr a2fc4eb8b8 featured filter keywords out 2023-10-01 19:33:43 +02:00
Christoph J. Scherr 6ad7f0cfbd thumbnail loading 2023-10-01 19:06:35 +02:00
Christoph J. Scherr c42653df80 format 2023-10-01 18:09:38 +02:00
Christoph J. Scherr 37c3104bc5 generate category 2023-10-01 18:09:34 +02:00
Christoph J. Scherr 6d9660f44c formatting 2023-10-01 17:05:44 +02:00
Christoph J. Scherr f8c3577a54 move js 2023-10-01 16:12:30 +02:00
Christoph J. Scherr 05ade38f2f move js to custom.js 2023-10-01 02:10:58 +02:00
Christoph J. Scherr ed75a20862 moar logs 2023-10-01 02:10:46 +02:00
Christoph J. Scherr a027fd7582 compat 2023-10-01 02:10:37 +02:00
Christoph J. Scherr 42ea8d14d4 generation works but frontend crash 2023-10-01 00:41:19 +02:00
Christoph J. Scherr d81b20e391 file title parsing 2023-09-30 19:47:18 +02:00
Christoph J. Scherr a687700c85 add admin button for sync 2023-09-30 19:03:46 +02:00
Christoph J. Scherr 2c8b562601 better loading 2023-09-29 20:05:12 +02:00
Christoph J. Scherr 56cd5943d2 syntax highlighting in markdown 2023-09-27 23:10:49 +02:00
Christoph J. Scherr ff231dfbc1 basic markdown loading 2023-09-27 22:35:14 +02:00
Christoph J. Scherr 7e7db5a480 better docker 2023-09-27 21:34:46 +02:00
Christoph J. Scherr 7d03b80f7a add a link 2023-09-27 20:45:30 +02:00
Christoph J. Scherr 50d17032af ghost blog integrated 2023-09-26 22:54:20 +02:00
Christoph J. Scherr eb1d4e85ed autogenerate static when building 2023-09-26 22:51:25 +02:00
Christoph J. Scherr d3c2c22e82 make database script based 2023-09-26 22:44:29 +02:00
Christoph J. Scherr 8a99e38c74 re add media 2023-09-26 22:23:02 +02:00
Christoph J. Scherr e561153284 db update 2023-09-26 22:21:56 +02:00
Christoph J. Scherr ab7ea1b17b fix compose file 2023-09-26 22:20:02 +02:00
Christoph J. Scherr 6de4f905d8 static srv somewhat working? 2023-09-26 22:10:12 +02:00
Christoph J. Scherr e87f6648cf working, but fix static stuff next 2023-09-25 23:34:26 +02:00
Christoph J. Scherr edf5f7e190 add legal info 2023-09-25 20:57:15 +02:00
Christoph J. Scherr 002656ea72
add personal_links 2023-07-15 14:50:14 +02:00
Christoph J. Scherr 6016a864d9
fix typo 2023-07-09 23:51:32 +02:00
Christoph J. Scherr f844fab25e
basic blog post view 2023-06-17 00:38:45 +02:00
149 changed files with 4506 additions and 1808 deletions

4
.gitignore vendored
View File

@ -17,4 +17,6 @@ db/data/ibdata1
db/data/ibtmp1
db/data/multi-master.info
db/data/mysql_upgrade_info
gawa/media/CACHE
gawa/static/CACHE
gawa/media/img/links/favicons
db/data

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[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,8 +1,40 @@
# gawa
Gawa is my personal website. I've personally written it using the django framework.
# Gawa
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
Bootstrap: MIT Licensed
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` |
## Deployment
* `git submodule init && git submodule update`
+ `docker compose up -d && docker compose logs -f`
## Security
- [ ] Do something about the files in the blog dir
(.git dir for example)

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,2 +0,0 @@
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.

View File

@ -1,66 +1,67 @@
services:
db:
image: mariadb
container_name: gawa-db
networks:
- internal
ports:
- 3306:3306
environment:
MYSQL_DATABASE: gawa
MYSQL_USER: gawa
MYSQL_PASSWORD: changethisforprod
# MYSQL_DATABASE: gawa
# MYSQL_USER: gawa
# MYSQL_PASSWORD: changethisforprod
MYSQL_ROOT_PASSWORD: root
volumes:
- ./db/data:/var/lib/mysql
# - db_data:/var/lib/mysql
- ./docker/db/scripts:/docker-entrypoint-initdb.d/
gawa:
build: ./web
container_name: gawa
command: python manage.py runserver 0.0.0.0:80
main:
build: ./docker/main
command: bash -c "echo 'setting django up'
&& python manage.py makemessages --all
&& python manage.py compilemessages
&& python manage.py collectstatic --noinput
&& python manage.py check
&& echo 'waiting a few seconds for database to start'
&& sleep 1
&& python manage.py migrate
&& python manage.py createcachetable
&& 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:
- ./gawa:/app
ports:
- 8080:80
environment:
- MARIADB_NAME=gawa
- MARIADB_USER=gawa
- MARIADB_PASSWORD=changethisforprod
networks:
- internal
depends_on:
- db
nginx:
# only for developement. Use dedicated static container in prod
image: nginx
container_name: gawa-web
caddy:
image: caddy
restart: unless-stopped
ports:
- "80:80"
- "8081:8081"
# - "443:443"
# - "443:443/udp"
volumes:
- ./web/templates:/etc/nginx/templates
- ./gawa/static:/var/www/static
- ./gawa/media:/var/www/media
ports:
- 80:80
environment:
- NGINX_HOST=0.0.0.0
- NGINX_PORT=80
networks:
- internal
depends_on:
- gawa
- $PWD/docker/caddy/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
- ./gawa/static:/srv/static
- ./gawa/media:/srv/media
phpmyadmin:
db-admin:
image: phpmyadmin
container_name: gawa-db-admin
networks:
- internal
ports:
- "127.0.0.1:8082:80"
- 8080:80
environment:
- PMA_HOST=db
- PMA_ABSOLUTE_URI=http://localhost:8080
depends_on:
- db
networks:
internal:
driver: bridge
volumes:
caddy_data:
caddy_config:
# db_data:

3
docker/blog/Dockerfile Normal file
View File

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

18
docker/caddy/Caddyfile Normal file
View File

@ -0,0 +1,18 @@
http://localhost, http://192.168.178.65 {
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
}

View File

@ -0,0 +1,5 @@
-- 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

@ -0,0 +1,24 @@
-- 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

@ -8,3 +8,5 @@ RUN apt update && apt install -y gettext && rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/
RUN pip install -r requirements.txt
COPY . /app/
RUN mkdir -p /app/static
RUN pygmentize -S nord -f html -a .codehilite > /app/static/codehighlight.css

View File

@ -0,0 +1,13 @@
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
django-rosetta>=0.9.9

View File

@ -1,5 +1,7 @@
from django.contrib import admin
from django.urls import path
from django.utils.translation import gettext as _
from django.http.response import HttpResponseRedirect
from .models import *
@admin.register(Category)
@ -10,11 +12,6 @@ class CategoryAdmin(admin.ModelAdmin):
list_display = ["name", "slug"]
@admin.action(description=_("Regenerate traits"))
def regenerate(modeladmin, request, queryset):
for obj in queryset:
obj.regenerate()
@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
"""
@ -23,4 +20,15 @@ class BlogPostAdmin(admin.ModelAdmin):
list_display = ["title_en", "subtitle_en", "title_de", "subtitle_de", "date", "category", "slug", "suburl", "public"]
date_hierarchy = "date"
ordering = ['title_de', 'title_en']
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,99 @@
**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

@ -0,0 +1,98 @@
**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,4 +1,4 @@
# Generated by Django 3.2.19 on 2023-06-03 12:03
# Generated by Django 4.2.6 on 2023-10-10 09:58
from django.db import migrations

View File

@ -1,35 +0,0 @@
# 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,58 @@
# Generated by Django 4.2.6 on 2023-10-10 09:58
import blog.models
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('start', '0002_initial'),
('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(unique=True)),
],
options={
'verbose_name': 'Category',
'verbose_name_plural': 'Categories',
},
),
migrations.CreateModel(
name='BlogPost',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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(unique=True)),
('title_de', models.CharField(default='Nicht übersetzt', max_length=50)),
('title_en', models.CharField(default='Not translated', max_length=50)),
('subtitle_de', models.CharField(blank=True, max_length=50)),
('subtitle_en', models.CharField(blank=True, max_length=50)),
('desc_de', models.TextField(blank=True, default='Keine Beschreibung', max_length=250)),
('desc_en', models.TextField(blank=True, default='no description', max_length=250)),
('date', models.DateTimeField(blank=True, null=True)),
('update', models.DateTimeField(blank=True, null=True)),
('suburl', models.CharField(blank=True, max_length=200, null=True)),
('public', models.BooleanField(default=False)),
('category', models.ForeignKey(default=blog.models.Category.get_or_create_uncategorized, on_delete=django.db.models.deletion.SET_DEFAULT, to='blog.category')),
('keywords', models.ManyToManyField(blank=True, to='start.keyword')),
],
options={
'verbose_name': 'blog post',
'verbose_name_plural': 'blog posts',
},
),
]

View File

@ -1,18 +0,0 @@
# 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,19 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-03 19:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0003_blogpost_public'),
]
operations = [
migrations.AddField(
model_name='blogpost',
name='slug',
field=models.SlugField(default='test'),
preserve_default=False,
),
]

View File

@ -1,23 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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

@ -1,21 +0,0 @@
# 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,10 +1,35 @@
import toml
import ast
import re
import os
import pathlib
import markdown
from bs4 import BeautifulSoup
from django.db import models
from django.utils.translation import gettext as _
from start.models import Searchable
from start.models import Keyword
import logging
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):
"""
A category of blog posts
@ -13,7 +38,7 @@ class Category(models.Model):
Maybe some day it would be cool if these were Searchable
"""
name = models.CharField(max_length=50)
slug = models.SlugField()
slug = models.SlugField(unique=True)
class Meta:
verbose_name = _("Category")
@ -22,17 +47,67 @@ class Category(models.Model):
def __str__(self):
return f"{{<{self.__class__.__name__}>\"{self.name}\"}}"
class BlogPost(Searchable):
@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(models.Model):
"""
Should contain a blogpost
"""
body = models.TextField()
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
thumbnail = models.ImageField(blank=True, upload_to="img/thumbnails")
featured = models.BooleanField(default=False)
markdown = models.BooleanField(default=False)
slug = models.SlugField()
DATA_DIR = "/app/blog/data/articles"
DEFAULT_LANGS = {'en': False, 'de': False}
META_TOP_KEYS = [
"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)
langs = models.CharField(
default=DEFAULT_LANGS.__repr__(), max_length=64)
slug = models.SlugField(unique=True, blank=False)
title_de = models.CharField(max_length=50, default="Nicht übersetzt")
title_en = models.CharField(max_length=50, default="Not translated")
subtitle_de = models.CharField(max_length=50, blank=True)
subtitle_en = models.CharField(max_length=50, blank=True)
desc_de = models.TextField(
blank=True, max_length=250, unique=False, default="Keine Beschreibung")
desc_en = models.TextField(
blank=True, max_length=250, unique=False, default="no description")
date = models.DateTimeField(blank=True, null=True)
update = models.DateTimeField(blank=True, null=True)
keywords = models.ManyToManyField(Keyword, blank=True)
suburl = models.CharField(max_length=200, blank=True, null=True)
public = models.BooleanField(default=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):
"""
@ -41,9 +116,181 @@ class BlogPost(Searchable):
Implements the abstract method of Searchable
"""
logger.info(f"regenerating {self.__class__.__name__} object: {self}")
# url stuff
self.suburl = f"/blog/{self.category.name}/{self.slug}"
# load from markdown
# self.sync_file()
# redundand vvvv
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
cls.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:
verbose_name = _("blog post")
verbose_name_plural = _("blog posts")

View File

@ -0,0 +1,6 @@
{% 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,26 +1,96 @@
{% 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 languagecode %}
{{ LANGUAGE_CODE }}
{% endblock languagecode %}
{% block title %}
{% translate "cscherr.de" %} - {% translate "Blog" %}
{% endblock title %}
{% block nav %}
{% include 'nav.html' %}
{% endblock nav %}
{% block headscripts %}
<script type="text/javascript" id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3.0.0/es5/tex-mml-chtml.js">
</script>
{% endblock headscripts %}
{% block head %}
<script type="text/javascript"
id="MathJax-script"
async
src="https://cdn.jsdelivr.net/npm/mathjax@3.0.0/es5/tex-mml-chtml.js"></script>
{% endblock head %}
{% block main %}
<div class="container-xl">
<div class="jumbotron text-center">
<h1>{{ post.title }} <small class="">{{ post.subtitle }}</small></h1>
<img src="{{ post.thumbnail.url }}" alt="thumbnail">
<article>
<div class="jumbotron my-5">
<div class="row">
<div class="col">
<picture>
<img src="{{ post.thumbnail.url }}" alt="thumbnail" class="img-fluid">
</picture>
</div>
<div class="container">
$$x=\frac{-b+\sqrt{b^2-4ac}}{2a}$$
{{ post.body | safe }}
<div class="col">
<div class="row py-5">
{% if LANGUAGE_CODE == "de" %}
<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>
{% include 'blog/featured.html' %}
</div>
{% endblock main %}

View File

@ -0,0 +1,208 @@
{% 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,27 +1,40 @@
{% load i18n %}
{% load helper_tags %}
{% get_current_language as LANGUAGE_CODE %}
<div class="container-lg mt-5">
<h4 class="">{% trans "Featured" %}</h4>
<div class="row row-cols-1 row-cols-md-5 my-4">
<div class="row gap-1 cardrow">
{% for post in featured_posts %}
<div class="card col m-2 p-0">
<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" style="max-height: 150px;" alt="thumbnail">
<div class="card-body" style="height: 100px;">
<div class="card col mx-auto my-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>
<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>
<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>
<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>
<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>
@ -33,6 +46,12 @@
{% 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>
{% endfor %}

View File

@ -1,12 +1,17 @@
{% extends 'base.html' %}
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% block languagecode %}{{ LANGUAGE_CODE }}{% endblock languagecode %}
{% block title %}{% translate "cscherr.de" %} - {% translate "Blog" %}{% endblock title %}
{% block languagecode %}
{{ LANGUAGE_CODE }}
{% endblock languagecode %}
{% block title %}
{% translate "cscherr.de" %} - {% translate "Blog" %}
{% endblock title %}
{% block main %}
<div class="container-xl">
<div class="container text-center jumbotron my-3"></div>
<div class="jumbotron text-center">
<h1>{% translate "Was gibt es hier?" %}</h1>
<h1>{% translate "What can be found here?" %}</h1>
<p>{% translate "Blog" %}</p>
</div>
<div class="row text-center">
@ -38,8 +43,12 @@
</div>
</div>
<div class="container text-center jumbotron my-3">
<h1 class="my-4">{% translate "Looking for anything specific?" %}</h1>
{% include 'main_search_form.html' %}
<h2 class="my-5">
<a href="{% url 'blog:browse' %}"
class="link-offset-2 link-underline link-underline-opacity-0">
{% translate "Browse posts" %}
</a>
</h2>
</div>
{% include 'blog/featured.html' %}
</div>

View File

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

View File

@ -1,14 +1,19 @@
from django.shortcuts import Http404, HttpResponse, get_object_or_404, render
import json
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.views.generic import TemplateView, DetailView, ListView, View
from .models import BlogPost, Category
from start.views import SearchableView
from django.db.models import Q
from .models import BlogPost, Category, Keyword
import logging
logger = logging.getLogger(__name__)
class Index(TemplateView, SearchableView):
class Index(TemplateView):
"""
The index page of the gawa/blog app.
@ -20,10 +25,11 @@ class Index(TemplateView, SearchableView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True, public=True)
logger.debug(f"loaded featured posts: {context['featured_posts']}")
context['featured_posts'] = BlogPost.objects.filter(
featured=True, public=True)
return context
class Post(DetailView):
"""
Main page of a blog post
@ -36,62 +42,110 @@ class Post(DetailView):
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
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):
class Browse(ListView):
"""
Scroll through a list of blog posts
There should also be some kind of filter, for category and date.
Of course, Articles will also show up in the MainSearchView
"""
model = BlogPost
template_name = "blog/posts.html"
template_name = "blog/browse.html"
context_object_name = "posts"
paginate_by = 20
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):
context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True)
logger.debug(f"loaded featured posts: {context['featured_posts']}")
context['categories'] = Category.objects.all()
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
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,6 +11,8 @@ https://docs.djangoproject.com/en/3.2/ref/settings/
"""
# for getting envvars
import logging
from django.utils.translation import gettext_lazy as _
import os
# default django
@ -21,15 +23,19 @@ from django.utils.log import ServerFormatter
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-z_t5-iawtas&1np9)01*4_z_&hy*7wgy1!o$3bnnniux3f1ds-'
# store CSRF Tokens in session cookies instead of in new cookies regardless of
# user.
CSRF_USE_SESSIONS = False
# SECURITY: browsers should only accept cookies over https
SESSION_COOKIE_SECURE = True
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
DEBUG = True
ALLOWED_HOSTS = ["*"]
@ -50,6 +56,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'compressor', # compiles bs5
'rosetta', # translation from admin interface
]
MIDDLEWARE = [
@ -61,7 +68,6 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
'start.middleware.LangBasedOnUrlMiddleware',
]
ROOT_URLCONF = 'gawa.urls'
@ -100,7 +106,6 @@ DATABASES = {
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
@ -123,16 +128,16 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
from django.utils.translation import gettext_lazy as _
LANGUAGES = [
("de", _("German")),
("en", _("English")),
]
LANGUAGE_CODE = 'de'
LANGUAGE_CODE = 'en'
# treat this ^^^ as the default
prefix_default_language = False
LOCALE_PATHS = ["/app/locale"]
TIME_ZONE = 'CET'
@ -173,7 +178,6 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Logging configs
import logging
myServerFormatter = ServerFormatter
myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S"
@ -290,11 +294,5 @@ LOGGING = {
},
}
# Media stuff
# this is where user uploaded files will go.
# TODO change this for prod
#MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media"
MEDIA_ROOT = "/app/media"
MEDIA_URL = "/media/"
FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload"

View File

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

Binary file not shown.

View File

@ -0,0 +1,574 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-03 19:27+0100\n"
"PO-Revision-Date: 2023-10-08 21:03+0200\n"
"Last-Translator: <software@cscherr.de>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Translated-Using: django-rosetta 0.9.9\n"
#: blog/models.py:44
msgid "Category"
msgstr "Kategorie"
#: blog/models.py:45
msgid "Categories"
msgstr "Kategorien"
#: blog/models.py:295
msgid "blog post"
msgstr "Blog Post"
#: blog/models.py:296
msgid "blog posts"
msgstr "Blog Posts"
#: blog/templates/blog/blogpost.html:9 blog/templates/blog/browse.html:9
#: blog/templates/blog/index.html:8 start/templates/400.html:5
#: start/templates/403.html:5 start/templates/404.html:5
#: start/templates/500.html:5 start/templates/nav.html:9
#: start/templates/start/index.html:8 start/templates/start/index.html:14
#: start/templates/start/legalinfo.html:8 start/templates/start/links.html:5
#: start/templates/start/search.html:5
msgid "cscherr.de"
msgstr "cscherr.de"
#: blog/templates/blog/blogpost.html:9 blog/templates/blog/browse.html:9
#: blog/templates/blog/index.html:8 blog/templates/blog/index.html:15
#: start/templates/nav.html:51 start/templates/start/index.html:115
msgid "Blog"
msgstr "Blog"
#: blog/templates/blog/blogpost.html:86 blog/templates/blog/browse.html:170
#: blog/templates/blog/featured.html:52
msgid "published"
msgstr "veröffentlicht"
#: blog/templates/blog/blogpost.html:90
msgid "updated"
msgstr "aktualisiert"
#: blog/templates/blog/browse.html:47 start/forms.py:15
msgid "Search"
msgstr "Suche"
#: blog/templates/blog/browse.html:57
msgid "select category"
msgstr "ausgewählte Kategorie"
#: blog/templates/blog/browse.html:68
msgid "Keywords"
msgstr "Schlüsselwörter"
#: blog/templates/blog/browse.html:87
msgid "Filter"
msgstr "Filter"
#: blog/templates/blog/browse.html:119
msgid "No posts found for your filters."
msgstr "Keine Posts für diese Filter gefunden."
#: blog/templates/blog/browse.html:154 blog/templates/blog/featured.html:36
msgid "category"
msgstr "Kategorie"
#: blog/templates/blog/featured.html:5
msgid "Featured"
msgstr "Vorgestellt"
#: blog/templates/blog/index.html:14
msgid "What can be found here?"
msgstr "Was gibt es hier?"
#: blog/templates/blog/index.html:19
msgid "Writeups"
msgstr "Writeups"
#: blog/templates/blog/index.html:21
msgid ""
"\n"
" Whenever I discover some interesting security thing, I will post "
"a writeup here.\n"
" "
msgstr ""
"\n"
"Falls ich etwas Interessantes zum Thema Sicherheit entdecke, wird es hier "
"einen Writeup dazu geben."
#: blog/templates/blog/index.html:27
msgid "Open Source"
msgstr "OpenSource"
#: blog/templates/blog/index.html:28
msgid ""
"\n"
" If something comes up, I may post Linux guides or my thoughts on "
"current processes here.\n"
" "
msgstr ""
"\n"
"Falls etwas aufkommt, poste ich hier womöglich Linux Guides oder meine "
"Gedanken über allesmögliche."
#: blog/templates/blog/index.html:33 start/templates/start/index.html:44
msgid "Selfhosting"
msgstr "Selfhosting"
#: blog/templates/blog/index.html:34
msgid ""
"\n"
" Selfhosting is something that I'm really fond of. There will be "
"guides and thoughts about that too\n"
" "
msgstr ""
"\n"
"Selfhosting liegt mir sehr am Herzen, auch dazu wird es Anleitungen und "
"Gedanken geben."
#: blog/templates/blog/index.html:39
msgid "Anything Really"
msgstr "Irgendwas"
#: blog/templates/blog/index.html:40
msgid ""
"\n"
" This is my personal Blog after all, I will put here whatever I "
"want and you can't stop me.\n"
" "
msgstr ""
"\n"
"Das ist mein persönlicher Blog. Ich lade hier hoch was auch immer ich möchte."
#: blog/templates/blog/index.html:49
msgid "Browse posts"
msgstr "Posts durchsuchen"
#: gawa/settings.py:133
msgid "German"
msgstr "Deutsch"
#: gawa/settings.py:134
msgid "English"
msgstr "Englisch"
#: start/admin.py:10
msgid "Regenerate traits"
msgstr "Eigenschaften regenerieren"
#: start/models.py:27
msgid "Keyword"
msgstr "Schlüsselwort"
#: start/models.py:28
msgid "keywords"
msgstr "Schlüsselwörter"
#: start/models.py:104
msgid "Link"
msgstr "Link"
#: start/models.py:105 start/templates/nav.html:36
#: start/templates/start/legalinfo.html:8 start/templates/start/links.html:5
msgid "Links"
msgstr "Links"
#: start/templates/400.html:5 start/templates/403.html:5
#: start/templates/404.html:5 start/templates/500.html:5
#: start/templates/start/index.html:8 start/templates/start/search.html:5
msgid "startpage"
msgstr "Startseite"
#: start/templates/400.html:8
msgid "Bad request"
msgstr "Fehlerhafte Anfrage"
#: start/templates/400.html:9
msgid "You sent bad data to the server."
msgstr "Du hast eine fehlerhafte Anfrage an den Server gesendet."
#: start/templates/403.html:8
msgid "Permission denied"
msgstr "Zugriff verweigert"
#: start/templates/403.html:9
msgid "You are not allowed to access this page."
msgstr "Du hast nicht die Berechtigung auf diese Seite zuzugreifen."
#: start/templates/404.html:8
msgid "Not found"
msgstr "Nicht gefunden"
#: start/templates/404.html:9
msgid "The resource you are looking for does not exist."
msgstr "Die Ressource die du suchst existiert nicht."
#: start/templates/500.html:8
msgid "Internal Server Error"
msgstr "Interner Server Fehler"
#: start/templates/500.html:9
msgid "Something went wrong."
msgstr "Etwas ist schief gelaufen."
#: start/templates/base.html:73
msgid ""
"\n"
" This website is developed by myself and is\n"
" <a href=\"https://git.cscherr.de/PlexSheep/"
"gawa\">OpenSource</a>.\n"
" <br><br>\n"
" I encourage you to search for and report "
"usuability and security issues\n"
" on this site, aswell as server. For more "
"information, see <a href=\"\">Reporting</a>.\n"
" "
msgstr ""
"\n"
"Diese Website wird von mir selbst entwickelt und ist\n"
"<a href=\"https://git.cscherr.de/PlexSheep/gawa\">OpenSource</a>.\n"
"<br><br>\n"
"Ich ermutige meine Besucher nach Sicherheits- und Nutzbarkeitsproblemen auf "
"dieser Seite und diesem Server zu suchen und melden. Für weitere "
"Informationen, siehe <a href=\"\">Reporting</a>. "
#: start/templates/base.html:87 start/templates/nav.html:42
#: start/templates/start/legalinfo.html:12
msgid "Legal Info"
msgstr "rechtliche Informationen"
#: start/templates/base.html:96
msgid "Quellcode dieser Website"
msgstr "Quellcode dieser Website"
#: start/templates/base.html:100
msgid "Contact"
msgstr "Kontakt"
#: start/templates/base.html:108
msgid "Deutschland"
msgstr "Deutschland"
#: start/templates/nav.html:27 start/templates/nav.html:30
#: start/templates/nav.html:54
msgid "Start"
msgstr "Start"
#: start/templates/nav.html:33 start/templates/start/index.html:25
msgid "Professional"
msgstr "Professionell"
#: start/templates/nav.html:57
msgid "Browse"
msgstr "Durchsuchen"
#: start/templates/nav.html:99
msgid "Anonym"
msgstr "Anonymous"
#: start/templates/nav.html:113
msgid "Logout"
msgstr "Abmelden"
#: start/templates/nav.html:117
msgid "Login"
msgstr "Anmelden"
#: start/templates/start/index.html:15
msgid "Personal Website"
msgstr "Persönliche Website"
#: start/templates/start/index.html:24 start/templates/start/index.html:78
msgid "Who am I?"
msgstr "Wer bin Ich?"
#: start/templates/start/index.html:29
msgid ""
"\n"
" I am <em>Christoph J. Scherr</em>, studying "
"Cybersecurity at <a href=\"https://mannheim.dhbw.de\">\n"
" <abbr title=\"Duale Hochschule Baden-"
"Württemberg\">DHBW</abbr>Mannheim, Germany</a> and passionate about "
"computers.\n"
" "
msgstr ""
"\n"
"Ich bin <em>Christoph J. Scherr</em>, studiere Cybersecurity an der <a "
"href=\"https://mannheim.dhbw.de\">\n"
"<abbr title=\"Duale Hochschule Baden-Württemberg\">DHBW</abbr>Mannheim, "
"Deutschland</a> und habe eine Leidenschaft für Computer."
#: start/templates/start/index.html:35
msgid ""
"\n"
" <h4>Work</h4>\n"
" My form of study means that I often change "
"between studying and working with my partner\n"
" company <a href=\"https://newtec."
"de\">NewTec</a>, which is a medium sized company with headquaters\n"
" in Pfaffenhofen, Bavaria, Germany. I work "
"mainly on programming on a <abbr title=\"System On a Chip\">\n"
" SOC</abbr> in <a href=\"https://rust-lang."
"org\">Rust</a> for industrial contexts.\n"
" "
msgstr ""
"\n"
"<h4>Arbeit</h4>\n"
"Als dualer Student wechsle ich alle paar Monate zwischen theoretischen "
"Vorlesungen und der Arbeit in meinem Partnerbetrieb, der <a href=\"https://"
"newtec.de\">NewTec</a>, einem mittelständischem Unternehmen\n"
"aus Pfaffenhofen, Bayern, Deutschland. Ich arbeite viel an der "
"Programmierung eines <abbr title=\"System On a Chip\">\n"
"SOC</abbr>s mit <a href=\"https://rust-lang.org\">Rust</a> für die "
"industrielle Anwendung."
#: start/templates/start/index.html:45
msgid ""
"\n"
" I have a fascination for Computers. As a "
"hobby, I run my own computer networks (like the one this website\n"
" is served from) running various services, "
"most of which would probably be considered overkill by most.\n"
" "
msgstr ""
"\n"
"Ich habe eine Faszination für Computer. Als Hobby betreibe ich mein eigenes "
"Computernetzwerk (von dem unter anderem diese Website zur Verfügung gestellt "
"wird). Die meisten meiner Dienste würden wohl von den Meisten als "
"übertrieben angesehen werden."
#: start/templates/start/index.html:51
msgid "Hacking"
msgstr "Hacking"
#: start/templates/start/index.html:52
msgid ""
"\n"
" It's not a coincidence that I study "
"Cybersecurity. While I like most fields of computer science,\n"
" computer security is the field that is most "
"interesting to me. It seems to have the highest impact\n"
" on the real world (everyone probably knows "
"of some company that got hacked and leaked a ton of user data\n"
" because their security was bad) and is a "
"giant, complex puzzle.\n"
" <br><br>\n"
" <abbr title=\"Capture the Flag\">CTF</abbr>s "
"make the very serious field of Security into a playing field,\n"
" but one that can actually help discover real "
"vulnerabilities and educate the players. I haven't done that\n"
" much, but it's a fun way to learn and "
"discover new things.\n"
" "
msgstr ""
"\n"
"Es ist kein Zufall, dass ich Cybersecurity studiere. Zwar mag ich auch die "
"meisten anderen Felder der Informatik, aber die Sicherheit bleibt mein "
"Favorit. Mein Eindruck ist, dass sie die stärksten Effekte auf die echte "
"Welt hat (Vermutlich kann jeder einen Vorfall nennen, bei dem eine große "
"Firma gehackt wurde und viele Nutzerdaten veröffentlicht wurden.). "
"Sicherheit ist ein großen, komplexes Rätsel.\n"
"<br><br>\n"
"<abbr title=\"Capture the Flag\">CTF</abbr>s können Sicherheit zu einem "
"Spiel machen, bei dem dennoch echte Probleme entdeckt und verstanden werden "
"können."
#: start/templates/start/index.html:66
msgid "further information"
msgstr "zusätzliche Informationen"
#: start/templates/start/index.html:79
msgid "Private"
msgstr "Privat"
#: start/templates/start/index.html:83
msgid ""
"\n"
" There is nothing to say that does not "
"require a headline.\n"
" "
msgstr ""
"\n"
"Es gibt nicht zu sagen, das keine eigene Überschrift benötigen würde."
#: start/templates/start/index.html:88
msgid "Music"
msgstr "Musik"
#: start/templates/start/index.html:89
#, fuzzy
#| msgid ""
#| "\n"
#| " Music is amazing. I don't find a lot of "
#| "time lately, but sometimes I feel like making music. Usually,\n"
#| " I prefer calm music. Some types of music "
#| "I like are:\n"
#| " <ul class=\"list-group pt-3 list-group-"
#| "flush w-75 mx-auto\">\n"
#| " <li class=\"list-group-item\">LoFi</"
#| "li>\n"
#| " <li class=\"list-group-item\">JPop, "
#| "specifically <a href=\"https://www.youtube.com/@Ado1024\">Ado</a></li>\n"
#| " <li class=\"list-group-"
#| "item\">Eurobeat, specifically <a href=\"https://www.youtube.com/"
#| "@TurboA\">Turbo</a></li>\n"
#| " <li class=\"list-group-"
#| "item\">Classical Music, for example <a href=\"https://www.youtube.com/"
#| "watch?v=ZPdk5GaIDjo\">\n"
#| " Vivaldis Vier Jahreszeiten</"
#| "a></li>\n"
#| " </ul>\n"
#| " "
msgid ""
"\n"
" Music is amazing. I don't find a lot of time "
"lately, but sometimes I feel like making music. Usually,\n"
" I prefer calm music. Some types of music I "
"like are:\n"
" <ul class=\"list-group pt-3 list-group-flush "
"w-90 mx-auto\">\n"
" <li class=\"list-group-item\">LoFi</li>\n"
" <li class=\"list-group-item\">JPop, "
"specifically <a href=\"https://www.youtube.com/@Ado1024\">Ado</a></li>\n"
" <li class=\"list-group-item\">Eurobeat, "
"specifically <a href=\"https://www.youtube.com/@TurboA\">Turbo</a></li>\n"
" <li class=\"list-group-item\">Classical "
"Music, for example <a href=\"https://www.youtube.com/watch?v=ZPdk5GaIDjo\">\n"
" Vivaldis Vier Jahreszeiten</a></"
"li>\n"
" </ul>\n"
" "
msgstr ""
"\n"
"Musik ist großartig. Ich habe in letzter Zeit nicht viel Zeit dazu gefunden, "
"aber gelegentlich produziere ich auch meine eigene Musik.\n"
"Normalerweise mag ich ruhige Musik, hier eine kurze Liste von Musik die ich "
"mag:\n"
"<ul class=\"list-group pt-3 list-group-flush w-75 mx-auto\">\n"
"<li class=\"list-group-item\">LoFi</li>\n"
"<li class=\"list-group-item\">JPop, specifically <a href=\"https://www."
"youtube.com/@Ado1024\">Ado</a></li>\n"
"<li class=\"list-group-item\">Eurobeat, specifically <a href=\"https://www."
"youtube.com/@TurboA\">Turbo</a></li>\n"
"<li class=\"list-group-item\">Classical Music, for example <a href=\"https://"
"www.youtube.com/watch?v=ZPdk5GaIDjo\">\n"
"Vivaldis Vier Jahreszeiten</a></li>\n"
"</ul>"
#: start/templates/start/index.html:102
msgid "Lanugages"
msgstr "Sprachen"
#: start/templates/start/index.html:103
msgid ""
"\n"
" Somehow, I find myself fascinated by the "
"japanese lanugage.\n"
" <br><nobr>日本語が好き。</nobr>However,\n"
" I'm still at the beginning of the journey.\n"
" <br><br>\n"
" I also speak and understand english, but "
"it's more of a \"utility language\" for me. Not to\n"
" offend anyone, but I don't think english is "
"a \"beautiful\" language, as german and japanese are\n"
" (in my personal opinion). The english "
"translation of this website was also made by myself.\n"
" "
msgstr ""
"\n"
"Die japanische Sprache fasziniert mich.\n"
"<br><nobr>日本語が好き。</nobr>Leider bin ich noch am Anfang dieser Reise.\n"
"<br><br>\n"
"Abgesehen davon spreche und verstehe ich Englisch, betrachte das aber eher "
"als „Nutzsprache“. Englisch ist (meiner Meinung nach) keine schöne Sprache, "
"im Gegensatz zu Deutsch und Japanisch. Die englische Übersetzung dieser "
"Website habe ich selbst erstellt. "
#: start/templates/start/legalinfo.html:18
msgid "Information according to §5 TMG"
msgstr "Angaben gemäß §5 TMG"
#: start/templates/start/legalinfo.html:22
msgid "Germany"
msgstr "Deutschland"
#: start/templates/start/links.html:8
msgid "Personal"
msgstr "Persönlich"
#: start/templates/start/links.html:42 start/templates/start/links.html:86
msgid "visit"
msgstr "öffnen"
#: start/templates/start/links.html:52
msgid "Others"
msgstr "Andere"
#: start/templates/start/search.html:9
msgid "Search for"
msgstr "Suche nach"
#~ msgid "Looking for anything specific?"
#~ msgstr "Suchst du nach etwas speziellem?"
#~ msgid "Searchable"
#~ msgstr "suchbares Objekt"
#~ msgid "Searchables"
#~ msgstr "suchbare Objekte"
#~ msgid "static site"
#~ msgstr "statische Seite"
#~ msgid "static sites"
#~ msgstr "statische Seiten"
#~ msgid "Go"
#~ msgstr "Los"
#~ msgid "Email "
#~ msgstr "E-Mail"
#~ msgid ""
#~ "\n"
#~ " Somehow, I find myself fascinated by the japanese "
#~ "lanugage. <nobr>日本語が好きだ。</nobr>\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ "Aus irgendeinem Grund bin ich von der japanischen Sprache fasziniert. "
#~ "<nobr>日本語が好きだ。</nobr>"
#, fuzzy
#~ msgid ""
#~ "\n"
#~ " Ich habe diese Website selbst programmiert "
#~ "und gehostet.\n"
#~ " Falls Sie einen Fehler belibiger Art finden, "
#~ "würde ich mich freuen,\n"
#~ " wenn Sie mich darüber benachrichtigen.\n"
#~ " <br><br>\n"
#~ " Die Suche nach Schwachstellen und Fehlern auf "
#~ "dieser Website ist für\n"
#~ " diesen Zweck ausdrücklich erlaubt.\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ "Ich habe diese Website selbst programmiert und gehostet.\n"
#~ "Falls Sie einen Fehler belibiger Art finden\n"
#~ "würde ich mich freuen, wenn Sie mich darüber benachrichtigen.\n"
#~ "<br><br>\n"
#~ "Die Suche nach Schwachstellen und Fehlern auf\n"
#~ "dieser Website ist für diesen Zweck ausdrücklich erlaubt.\n"
#~ " "

Binary file not shown.

View File

@ -0,0 +1,458 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-03 19:27+0100\n"
"PO-Revision-Date: 2023-10-08 15:18+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.3.2\n"
#: blog/models.py:44
msgid "Category"
msgstr ""
#: blog/models.py:45
msgid "Categories"
msgstr ""
#: blog/models.py:295
msgid "blog post"
msgstr ""
#: blog/models.py:296
msgid "blog posts"
msgstr ""
#: blog/templates/blog/blogpost.html:9 blog/templates/blog/browse.html:9
#: blog/templates/blog/index.html:8 start/templates/400.html:5
#: start/templates/403.html:5 start/templates/404.html:5
#: start/templates/500.html:5 start/templates/nav.html:9
#: start/templates/start/index.html:8 start/templates/start/index.html:14
#: start/templates/start/legalinfo.html:8 start/templates/start/links.html:5
#: start/templates/start/search.html:5
msgid "cscherr.de"
msgstr "cscherr.de"
#: blog/templates/blog/blogpost.html:9 blog/templates/blog/browse.html:9
#: blog/templates/blog/index.html:8 blog/templates/blog/index.html:15
#: start/templates/nav.html:51 start/templates/start/index.html:115
msgid "Blog"
msgstr "Blog"
#: blog/templates/blog/blogpost.html:86 blog/templates/blog/browse.html:170
#: blog/templates/blog/featured.html:52
msgid "published"
msgstr ""
#: blog/templates/blog/blogpost.html:90
msgid "updated"
msgstr ""
#: blog/templates/blog/browse.html:47 start/forms.py:15
msgid "Search"
msgstr ""
#: blog/templates/blog/browse.html:57
msgid "select category"
msgstr ""
#: blog/templates/blog/browse.html:68
msgid "Keywords"
msgstr ""
#: blog/templates/blog/browse.html:87
msgid "Filter"
msgstr ""
#: blog/templates/blog/browse.html:119
msgid "No posts found for your filters."
msgstr ""
#: blog/templates/blog/browse.html:154 blog/templates/blog/featured.html:36
msgid "category"
msgstr ""
#: blog/templates/blog/featured.html:5
msgid "Featured"
msgstr ""
#: blog/templates/blog/index.html:14
msgid "What can be found here?"
msgstr ""
#: blog/templates/blog/index.html:19
msgid "Writeups"
msgstr ""
#: blog/templates/blog/index.html:21
msgid ""
"\n"
" Whenever I discover some interesting security thing, I will post "
"a writeup here.\n"
" "
msgstr ""
#: blog/templates/blog/index.html:27
msgid "Open Source"
msgstr ""
#: blog/templates/blog/index.html:28
msgid ""
"\n"
" If something comes up, I may post Linux guides or my thoughts on "
"current processes here.\n"
" "
msgstr ""
#: blog/templates/blog/index.html:33 start/templates/start/index.html:44
msgid "Selfhosting"
msgstr ""
#: blog/templates/blog/index.html:34
msgid ""
"\n"
" Selfhosting is something that I'm really fond of. There will be "
"guides and thoughts about that too\n"
" "
msgstr ""
#: blog/templates/blog/index.html:39
msgid "Anything Really"
msgstr ""
#: blog/templates/blog/index.html:40
msgid ""
"\n"
" This is my personal Blog after all, I will put here whatever I "
"want and you can't stop me.\n"
" "
msgstr ""
#: blog/templates/blog/index.html:49
msgid "Browse posts"
msgstr ""
#: gawa/settings.py:133
msgid "German"
msgstr ""
#: gawa/settings.py:134
msgid "English"
msgstr ""
#: start/admin.py:10
msgid "Regenerate traits"
msgstr ""
#: start/models.py:27
msgid "Keyword"
msgstr ""
#: start/models.py:28
msgid "keywords"
msgstr ""
#: start/models.py:104
msgid "Link"
msgstr ""
#: start/models.py:105 start/templates/nav.html:36
#: start/templates/start/legalinfo.html:8 start/templates/start/links.html:5
msgid "Links"
msgstr ""
#: start/templates/400.html:5 start/templates/403.html:5
#: start/templates/404.html:5 start/templates/500.html:5
#: start/templates/start/index.html:8 start/templates/start/search.html:5
msgid "startpage"
msgstr "startpage"
#: start/templates/400.html:8
msgid "Bad request"
msgstr ""
#: start/templates/400.html:9
msgid "You sent bad data to the server."
msgstr ""
#: start/templates/403.html:8
msgid "Permission denied"
msgstr ""
#: start/templates/403.html:9
msgid "You are not allowed to access this page."
msgstr ""
#: start/templates/404.html:8
msgid "Not found"
msgstr ""
#: start/templates/404.html:9
msgid "The resource you are looking for does not exist."
msgstr ""
#: start/templates/500.html:8
msgid "Internal Server Error"
msgstr ""
#: start/templates/500.html:9
msgid "Something went wrong."
msgstr ""
#: start/templates/base.html:73
msgid ""
"\n"
" This website is developed by myself and is\n"
" <a href=\"https://git.cscherr.de/PlexSheep/"
"gawa\">OpenSource</a>.\n"
" <br><br>\n"
" I encourage you to search for and report "
"usuability and security issues\n"
" on this site, aswell as server. For more "
"information, see <a href=\"\">Reporting</a>.\n"
" "
msgstr ""
#: start/templates/base.html:87 start/templates/nav.html:42
#: start/templates/start/legalinfo.html:12
msgid "Legal Info"
msgstr ""
#: start/templates/base.html:96
msgid "Quellcode dieser Website"
msgstr "Source code of this website"
#: start/templates/base.html:100
msgid "Contact"
msgstr "Contact"
#: start/templates/base.html:108
msgid "Deutschland"
msgstr "Germany"
#: start/templates/nav.html:27 start/templates/nav.html:30
#: start/templates/nav.html:54
msgid "Start"
msgstr "Start"
#: start/templates/nav.html:33 start/templates/start/index.html:25
msgid "Professional"
msgstr ""
#: start/templates/nav.html:57
msgid "Browse"
msgstr ""
#: start/templates/nav.html:99
msgid "Anonym"
msgstr ""
#: start/templates/nav.html:113
msgid "Logout"
msgstr ""
#: start/templates/nav.html:117
msgid "Login"
msgstr ""
#: start/templates/start/index.html:15
msgid "Personal Website"
msgstr ""
#: start/templates/start/index.html:24 start/templates/start/index.html:78
msgid "Who am I?"
msgstr ""
#: start/templates/start/index.html:29
msgid ""
"\n"
" I am <em>Christoph J. Scherr</em>, studying "
"Cybersecurity at <a href=\"https://mannheim.dhbw.de\">\n"
" <abbr title=\"Duale Hochschule Baden-"
"Württemberg\">DHBW</abbr>Mannheim, Germany</a> and passionate about "
"computers.\n"
" "
msgstr ""
#: start/templates/start/index.html:35
msgid ""
"\n"
" <h4>Work</h4>\n"
" My form of study means that I often change "
"between studying and working with my partner\n"
" company <a href=\"https://newtec."
"de\">NewTec</a>, which is a medium sized company with headquaters\n"
" in Pfaffenhofen, Bavaria, Germany. I work "
"mainly on programming on a <abbr title=\"System On a Chip\">\n"
" SOC</abbr> in <a href=\"https://rust-lang."
"org\">Rust</a> for industrial contexts.\n"
" "
msgstr ""
#: start/templates/start/index.html:45
msgid ""
"\n"
" I have a fascination for Computers. As a "
"hobby, I run my own computer networks (like the one this website\n"
" is served from) running various services, "
"most of which would probably be considered overkill by most.\n"
" "
msgstr ""
#: start/templates/start/index.html:51
msgid "Hacking"
msgstr ""
#: start/templates/start/index.html:52
msgid ""
"\n"
" It's not a coincidence that I study "
"Cybersecurity. While I like most fields of computer science,\n"
" computer security is the field that is most "
"interesting to me. It seems to have the highest impact\n"
" on the real world (everyone probably knows "
"of some company that got hacked and leaked a ton of user data\n"
" because their security was bad) and is a "
"giant, complex puzzle.\n"
" <br><br>\n"
" <abbr title=\"Capture the Flag\">CTF</abbr>s "
"make the very serious field of Security into a playing field,\n"
" but one that can actually help discover real "
"vulnerabilities and educate the players. I haven't done that\n"
" much, but it's a fun way to learn and "
"discover new things.\n"
" "
msgstr ""
#: start/templates/start/index.html:66
msgid "further information"
msgstr ""
#: start/templates/start/index.html:79
msgid "Private"
msgstr ""
#: start/templates/start/index.html:83
msgid ""
"\n"
" There is nothing to say that does not "
"require a headline.\n"
" "
msgstr ""
#: start/templates/start/index.html:88
msgid "Music"
msgstr ""
#: start/templates/start/index.html:89
msgid ""
"\n"
" Music is amazing. I don't find a lot of time "
"lately, but sometimes I feel like making music. Usually,\n"
" I prefer calm music. Some types of music I "
"like are:\n"
" <ul class=\"list-group pt-3 list-group-flush "
"w-90 mx-auto\">\n"
" <li class=\"list-group-item\">LoFi</li>\n"
" <li class=\"list-group-item\">JPop, "
"specifically <a href=\"https://www.youtube.com/@Ado1024\">Ado</a></li>\n"
" <li class=\"list-group-item\">Eurobeat, "
"specifically <a href=\"https://www.youtube.com/@TurboA\">Turbo</a></li>\n"
" <li class=\"list-group-item\">Classical "
"Music, for example <a href=\"https://www.youtube.com/watch?v=ZPdk5GaIDjo\">\n"
" Vivaldis Vier Jahreszeiten</a></"
"li>\n"
" </ul>\n"
" "
msgstr ""
#: start/templates/start/index.html:102
msgid "Lanugages"
msgstr ""
#: start/templates/start/index.html:103
msgid ""
"\n"
" Somehow, I find myself fascinated by the "
"japanese lanugage.\n"
" <br><nobr>日本語が好き。</nobr>However,\n"
" I'm still at the beginning of the journey.\n"
" <br><br>\n"
" I also speak and understand english, but "
"it's more of a \"utility language\" for me. Not to\n"
" offend anyone, but I don't think english is "
"a \"beautiful\" language, as german and japanese are\n"
" (in my personal opinion). The english "
"translation of this website was also made by myself.\n"
" "
msgstr ""
#: start/templates/start/legalinfo.html:18
msgid "Information according to §5 TMG"
msgstr ""
#: start/templates/start/legalinfo.html:22
msgid "Germany"
msgstr ""
#: start/templates/start/links.html:8
msgid "Personal"
msgstr ""
#: start/templates/start/links.html:42 start/templates/start/links.html:86
msgid "visit"
msgstr ""
#: start/templates/start/links.html:52
msgid "Others"
msgstr ""
#: start/templates/start/search.html:9
msgid "Search for"
msgstr ""
#, fuzzy
#~| msgid ""
#~| "\n"
#~| " Ich habe diese Website selbst programmiert "
#~| "und gehostet. \n"
#~| " Falls Sie einen Fehler belibiger Art finden, "
#~| "würde ich mich freuen, \n"
#~| " wenn Sie mich darüber benachrichtigen.\n"
#~| " <br><br>\n"
#~| " Die Suche nach Schwachstellen und Fehlern "
#~| "auf dieser Website ist für \n"
#~| " diesen Zweck ausdrücklich erlaubt.\n"
#~| " "
#~ msgid ""
#~ "\n"
#~ " Ich habe diese Website selbst programmiert "
#~ "und gehostet.\n"
#~ " Falls Sie einen Fehler belibiger Art finden, "
#~ "würde ich mich freuen,\n"
#~ " wenn Sie mich darüber benachrichtigen.\n"
#~ " <br><br>\n"
#~ " Die Suche nach Schwachstellen und Fehlern auf "
#~ "dieser Website ist für\n"
#~ " diesen Zweck ausdrücklich erlaubt.\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ "I have programmed and hosted this Website by myself. If you find any "
#~ "issues or bugs,\n"
#~ "please contact me about it.\n"
#~ "\n"
#~ "\n"
#~ "\n"
#~ "I specifically allow the search for any issues this website might have. "

View File

@ -0,0 +1,50 @@
<?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>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,4 +1,6 @@
from django.contrib import admin
from django.http.response import HttpResponseRedirect
from django.urls import path
from django.db.models import CASCADE, AutoField, OneToOneField
from django.views.generic import View
from django.utils.translation import gettext as _
@ -10,6 +12,7 @@ def regenerate(modeladmin, request, queryset):
for obj in queryset:
obj.regenerate()
@admin.register(Keyword)
class KeywordAdmin(admin.ModelAdmin):
"""
@ -17,30 +20,24 @@ class KeywordAdmin(admin.ModelAdmin):
"""
list_display = ["text_en", "text_de"]
@admin.register(StaticSite)
class StaticSiteAdmin(admin.ModelAdmin):
"""
Admin Interface for StaticSite
"""
list_display = ["title_en", "subtitle_en", "title_de", "subtitle_de", "suburl"]
ordering = ['title_de', 'title_en']
actions = [regenerate]
@admin.register(Searchable)
class SearchableAdmin(admin.ModelAdmin):
"""
Abstract Admin Interface for all Searchables
"""
list_display = ["title_en", "subtitle_en", "title_de", "subtitle_de", "suburl"]
ordering = ['title_de', 'title_en']
actions = [regenerate]
@admin.register(Link)
class LinkAdmin(admin.ModelAdmin):
"""
Admin Interface for Links
"""
list_display = ["title_en", "title_de", "url", "suburl", "favicon", "status"]
ordering = ['status', 'title_de', 'title_en']
list_display = ["title_en", "title_de", "url", "favicon", "personal"]
ordering = ['title_de', 'title_en']
actions = [regenerate]
change_list_template = "admin/links.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):
Link.sync_with_fs()
return HttpResponseRedirect("../")

View File

@ -1,5 +1,6 @@
from django.apps import AppConfig
class StartConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'start'

View File

@ -0,0 +1,18 @@
# this file contains all data used to build the link entries in the database
[[category]]
name_de = "Notizen"
name_en = "Notes"
[[link]]
url = "https://www.noteapps.ca"
[[link]]
url = "https://silverbullet.md"
[[category]]
name_de = "Coding"
name_en = "Coding"
[[link]]
url = "https://neovim.io"

View File

@ -1,6 +1,7 @@
from django import forms
from django.utils.translation import gettext as _
class MainSearchForm(forms.Form):
search = forms.CharField(
max_length=100,

View File

@ -1,70 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-29 23:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: start/templates/start/base.html:16 start/templates/start/index.html:5
msgid "cscherr.de"
msgstr "cscherr.de"
#: start/templates/start/base.html:24
msgid "Start"
msgstr "Start"
#: start/templates/start/base.html:27
msgid "Blog"
msgstr "Blog"
#: start/templates/start/base.html:80
msgid ""
"\n"
" Ich habe diese Website selbst programmiert und "
"gehostet. \n"
" Falls Sie einen Fehler belibiger Art finden, "
"würde ich mich freuen, \n"
" wenn Sie mich darüber benachrichtigen.\n"
" <br><br>\n"
" Die Suche nach Schwachstellen und Fehlern auf "
"dieser Website ist für \n"
" diesen Zweck ausdrücklich erlaubt.\n"
" "
msgstr ""
"\n"
"Ich habe diese Website selbst programmiert und gehostet.\n"
"Falls Sie einen Fehler belibiger Art finden\n"
"würde ich mich freuen, wenn Sie mich darüber benachrichtigen.\n"
"<br><br>\n"
"Die Suche nach Schwachstellen und Fehlern auf\n"
"dieser Website ist für diesen Zweck ausdrücklich erlaubt.\n"
" "
#: start/templates/start/base.html:101
msgid "Quellcode dieser Website"
msgstr "Quellcode dieser Website"
#: start/templates/start/base.html:110
msgid "Contact"
msgstr "Kontakt"
#: start/templates/start/base.html:115
msgid "Deutschland"
msgstr "Deutschland"
#: start/templates/start/index.html:5
msgid "startpage"
msgstr "Startseite"

View File

@ -1,66 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-29 23:20+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: start/templates/start/base.html:16 start/templates/start/index.html:5
msgid "cscherr.de"
msgstr "cscherr.de"
#: start/templates/start/base.html:24
msgid "Start"
msgstr "Start"
#: start/templates/start/base.html:27
msgid "Blog"
msgstr "Blog"
#: start/templates/start/base.html:80
msgid ""
"\n"
" Ich habe diese Website selbst programmiert und "
"gehostet. \n"
" Falls Sie einen Fehler belibiger Art finden, "
"würde ich mich freuen, \n"
" wenn Sie mich darüber benachrichtigen.\n"
" <br><br>\n"
" Die Suche nach Schwachstellen und Fehlern auf "
"dieser Website ist für \n"
" diesen Zweck ausdrücklich erlaubt.\n"
" "
msgstr ""
"I have programmed and hosted this Website by myself. If you find any issues or bugs,\n"
"please contact me about it.\n"
"<br><br>\n"
"I specifically allow the search for any issues this website might have.\n"
#: start/templates/start/base.html:101
msgid "Quellcode dieser Website"
msgstr "Source code of this website"
#: start/templates/start/base.html:110
msgid "Contact"
msgstr "Contakt"
#: start/templates/start/base.html:115
msgid "Deutschland"
msgstr "Germany"
#: start/templates/start/index.html:5
msgid "startpage"
msgstr "startpage"

View File

@ -1,25 +0,0 @@
from django.shortcuts import HttpResponse, HttpResponseRedirect
from django.utils import translation
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from django.utils.regex_helper import re
from .forms import MainSearchForm
from .views import MainSearch
class LangBasedOnUrlMiddleware(MiddlewareMixin):
"""
used for switching the language
"""
@staticmethod
def process_request(request):
if hasattr(request, 'session'):
active_session_lang = request.session.get(translation.LANGUAGE_SESSION_KEY)
if active_session_lang == request.LANGUAGE_CODE:
return
if any(request.LANGUAGE_CODE in language for language in settings.LANGUAGES):
translation.activate(request.LANGUAGE_CODE)
request.session[translation.LANGUAGE_SESSION_KEY] = request.LANGUAGE_CODE

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.19 on 2023-06-03 12:03
# Generated by Django 4.2.6 on 2023-10-10 09:58
from django.db import migrations

View File

@ -0,0 +1,52 @@
# Generated by Django 4.2.6 on 2023-10-10 09:58
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('start', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Keyword',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(unique=True)),
('text_de', models.CharField(max_length=40)),
('text_en', models.CharField(max_length=40)),
],
options={
'verbose_name': 'Keyword',
'verbose_name_plural': 'keywords',
},
),
migrations.CreateModel(
name='Link',
fields=[
('url', models.URLField(primary_key=True, serialize=False, unique=True)),
('favicon', models.ImageField(blank=True, null=True, upload_to='img/links/favicons')),
('status', models.BooleanField(default=False)),
('personal', models.BooleanField(default=False)),
('title_de', models.CharField(default='Nicht übersetzt', max_length=50)),
('title_en', models.CharField(default='Not translated', max_length=50)),
('subtitle_de', models.CharField(blank=True, max_length=50)),
('subtitle_en', models.CharField(blank=True, max_length=50)),
('desc_de', models.TextField(blank=True, default='Keine Beschreibung', max_length=250)),
('desc_en', models.TextField(blank=True, default='no description', max_length=250)),
('date', models.DateTimeField(blank=True, null=True)),
('update', models.DateTimeField(blank=True, null=True)),
('suburl', models.CharField(blank=True, max_length=200, null=True)),
('public', models.BooleanField(default=False)),
('keywords', models.ManyToManyField(blank=True, to='start.keyword')),
],
options={
'verbose_name': 'Link',
'verbose_name_plural': 'Links',
},
),
]

View File

@ -1,46 +0,0 @@
# 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', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Keyword',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text_de', models.CharField(max_length=40)),
('text_en', models.CharField(max_length=40)),
],
),
migrations.CreateModel(
name='Searchable',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title_de', models.CharField(default='Titel DE', max_length=50)),
('title_en', models.CharField(default='title en', max_length=50)),
('subtitle_de', models.CharField(blank=True, max_length=50)),
('subtitle_en', models.CharField(blank=True, max_length=50)),
('desc_de', models.CharField(default='Beschreibung DE', max_length=250, unique=True)),
('desc_en', models.CharField(default='Description EN', max_length=250, unique=True)),
('date', models.DateField(blank=True, null=True)),
('suburl', models.CharField(blank=True, max_length=200, null=True)),
('keywords', models.ManyToManyField(to='start.Keyword')),
],
),
migrations.CreateModel(
name='StaticSite',
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')),
],
bases=('start.searchable',),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.19 on 2023-06-03 23:13
# Generated by Django 4.2.6 on 2023-10-10 10:09
from django.db import migrations
@ -6,12 +6,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_blogpost_markdown'),
('start', '0002_initial'),
]
operations = [
migrations.RemoveField(
model_name='blogpost',
name='public',
model_name='link',
name='status',
),
]

View File

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

View File

@ -1,35 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-04 21:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('start', '0003_searchable_public'),
]
operations = [
migrations.AlterModelOptions(
name='keyword',
options={'verbose_name': 'Keyword', 'verbose_name_plural': 'keywords'},
),
migrations.AlterModelOptions(
name='searchable',
options={'verbose_name': 'Searchable', 'verbose_name_plural': 'Searchables'},
),
migrations.AlterModelOptions(
name='staticsite',
options={'verbose_name': 'static site', 'verbose_name_plural': 'static sites'},
),
migrations.AlterField(
model_name='searchable',
name='desc_de',
field=models.CharField(default='Beschreibung DE', max_length=250),
),
migrations.AlterField(
model_name='searchable',
name='desc_en',
field=models.CharField(default='Description EN', max_length=250),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 4.2.6 on 2023-10-10 10:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('start', '0003_remove_link_status'),
]
operations = [
migrations.RemoveField(
model_name='link',
name='date',
),
migrations.RemoveField(
model_name='link',
name='subtitle_de',
),
migrations.RemoveField(
model_name='link',
name='subtitle_en',
),
migrations.RemoveField(
model_name='link',
name='suburl',
),
migrations.RemoveField(
model_name='link',
name='update',
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-05 16:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('start', '0004_auto_20230604_2312'),
]
operations = [
migrations.CreateModel(
name='Link',
fields=[
('searchable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='start.searchable')),
('url', models.URLField(primary_key=True, serialize=False, unique=True)),
],
options={
'verbose_name': 'Link',
'verbose_name_plural': 'Links',
},
bases=('start.searchable',),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-05 16:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('start', '0005_link'),
]
operations = [
migrations.AlterField(
model_name='searchable',
name='title_en',
field=models.CharField(default='title EN', max_length=50),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-05 17:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('start', '0006_alter_searchable_title_en'),
]
operations = [
migrations.AddField(
model_name='link',
name='favicon',
field=models.ImageField(blank=True, upload_to='img/links/favicons'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-05 20:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('start', '0007_link_favicon'),
]
operations = [
migrations.AddField(
model_name='link',
name='status',
field=models.BooleanField(default=False),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.19 on 2023-06-05 23:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('start', '0008_link_status'),
]
operations = [
migrations.AlterField(
model_name='link',
name='favicon',
field=models.ImageField(blank=True, null=True, upload_to='img/links/favicons'),
),
]

View File

@ -1,3 +1,11 @@
import random
import favicon
import requests
import toml
import ast
import re
import os
import pathlib
from django.db import models
from django.db.models.options import override
from django.utils.translation import gettext as _
@ -8,101 +16,58 @@ from django.conf import settings
import logging
logger = logging.getLogger(__name__)
import requests
import favicon
import random
class Keyword(models.Model):
"""
this is the model that should contain searchable keywords
"""
slug = models.SlugField(unique=True)
text_de = models.CharField(max_length=40)
text_en = models.CharField(max_length=40)
def __str__(self):
return f"{{<{self.__class__.__name__}>\"{self.text_en}\"}}"
return f"{{<{self.__class__.__name__}>\"{self.slug}\"}}"
class Meta:
verbose_name = _("Keyword")
verbose_name_plural = _("keywords")
class Searchable(models.Model):
"""
Abstract class for any model that should be searchable.
This model will be searched for by the searchbox that is in every upper part of the website.
This class is not a real abstract class, I need to query it in the main search, so thats impossible
"""
title_de = models.CharField(max_length=50, default="Titel DE")
title_en = models.CharField(max_length=50, default="title EN")
subtitle_de = models.CharField(max_length=50, blank=True)
subtitle_en = models.CharField(max_length=50, blank=True)
desc_de = models.CharField(max_length=250, unique=False, default="Beschreibung DE")
desc_en = models.CharField(max_length=250, unique=False, default="Description EN")
# may be empty/blank for some entries
date = models.DateField(blank=True, null=True)
keywords = models.ManyToManyField(Keyword)
suburl = models.CharField(max_length=200, blank=True, null=True)
public = models.BooleanField(default=True)
@classmethod
def regenerate_all_entries(cls):
"""
regenerate all searchable items
"""
logger.info(f"regenerating all {Searchable.__name__} entries")
for obj in cls.objects.all():
obj.regenerate()
def __str__(self):
return f"{{<{self.__class__.__name__}>\"{self.title_en}\"}}"
def regenerate(self):
"""
regenerate a object
"""
raise NotImplementedError(f"{self.__class__.__name__} does not implement regenerate")
class Meta:
verbose_name = _("Searchable")
verbose_name_plural = _("Searchables")
class StaticSite(Searchable):
"""
This model represents any static site, such as start:index,
that should show up in search.
Every searchable view should inherit from start.views.SearchableView.
# TODO automate scanning for SearchableView classes
"""
def regenerate(self):
"""
regenerate a object
"""
logger.info(f"regenerating {self.__class__.__name__} object: {self}")
logger.warning(f"{self.__class__.__name__} cannot regenerate.")
#self.save()
pass
class Meta:
verbose_name = _("static site")
verbose_name_plural = _("static sites")
class Link(Searchable):
class Link(models.Model):
"""
contains all my interesting links
"""
FAVICON_DIR: str = "img/links/favicons"
LINKS_FILE = "/app/start/data/links.toml"
# the actual link
url = models.URLField(unique=True, null=False, primary_key=True)
favicon_dir: str = "img/links/favicons"
favicon = models.ImageField(blank=True, upload_to=favicon_dir, null=True)
status = models.BooleanField(default=False)
favicon = models.ImageField(blank=True, upload_to=FAVICON_DIR, null=True)
personal = models.BooleanField(default=False)
title_de = models.CharField(max_length=50, default="Nicht übersetzt")
title_en = models.CharField(max_length=50, default="Not translated")
desc_de = models.TextField(
blank=True, max_length=250, unique=False, default="Keine Beschreibung")
desc_en = models.TextField(
blank=True, max_length=250, unique=False, default="no description")
keywords = models.ManyToManyField(Keyword, blank=True)
public = models.BooleanField(default=False)
def __str__(self):
return f"{{<{self.__class__.__name__}>\"{self.title_en}\"}}"
@ classmethod
def sync_with_fs(cls):
"""
Sync all Links with the filesystem.
Caution: Will delete all Links
"""
# delete all existing objects
cls.objects.all().delete()
# TODO: load links from links.toml
raise NotImplementedError("loading from links.toml not implemented")
def regenerate(self):
"""
regenerate a object
@ -120,11 +85,13 @@ class Link(Searchable):
icons = favicon.get(self.url, timeout=2)
except (ConnectionError) as ce:
# just keep whatever was stored if we cant get a new favicon
logger.warn(f"unable to download favicon for {self}: {ce.with_traceback(None)}")
logger.warn(
f"unable to download favicon for {self}: {ce.with_traceback(None)}")
self.status = False
except Exception as e:
logger.warn(f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
logger.warn(
f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
self.status = False
else:
@ -145,7 +112,8 @@ class Link(Searchable):
self.favicon = None
except Exception as e:
logger.warn(f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
logger.warn(
f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
self.favicon = None
self.save()

View File

@ -0,0 +1,6 @@
{% 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

@ -5,19 +5,32 @@
{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}">
<head>
<title>{% block title %}{% endblock title %}</title>
<title>
{% block title %}
{% endblock title %}
</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% compress css %}
<link type="text/x-scss" href="/static/bs5/scss/bootstrap.scss" rel="stylesheet" media="screen">
<link type="text/x-scss"
href="/static/bs5/scss/bootstrap.scss"
rel="stylesheet"
media="screen">
<link rel="stylesheet" href="/static/bsi1/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="/static/codehighlight.css">
<link rel="stylesheet" href="/static/custom.css">
{% block head_compress_css %}
{% endblock head_compress_css %}
<link href="/static/tagify/dist/tagify.css"
rel="stylesheet"
type="text/css" />
{% endcompress %}
{% compress js %}
<script src="/static/tagify/dist/tagify.min.js"></script>
<script src="/static/tagify/dist/tagify.polyfills.min.js"></script>
{% block head_compress_js %}
{% endblock head_compress_js %}
{% endcompress %}
<script>
const setTheme = theme => {
document.documentElement.setAttribute('data-bs-theme', theme)
}
setTheme(localStorage.getItem('theme'));
</script>
{% block headscripts %}
{% endblock headscripts %}
</head>
@ -25,8 +38,9 @@
{% block nav %}
{% include 'nav.html' %}
{% endblock nav %}
<main class="m-5" style="min-height: 80vh;">
{% block main %}{% endblock main %}
<main class="m-5" style="min-height: 80vh;" id="main">
{% block main %}
{% endblock main %}
</main>
<footer class="text-center text-lg-start bg-dark-subtle text-muted">
<div class="container overflow-hidden text-center">
@ -55,22 +69,20 @@
<div class="col-md-3 col-lg-4 col-xl-3 mx-auto mb-4">
<h6 class="text-uppercase fw-bold mb-4">Information</h6>
<p>
{# NOTE: If you are not me, then you should probably adjust this. #}
{% blocktranslate %}
Ich habe diese Website selbst programmiert und gehostet.
Falls Sie einen Fehler belibiger Art finden, würde ich mich freuen,
wenn Sie mich darüber benachrichtigen.
This website is developed by myself and is
<a href="https://git.cscherr.de/PlexSheep/gawa">OpenSource</a>.
<br><br>
Die Suche nach Schwachstellen und Fehlern auf dieser Website ist für
diesen Zweck ausdrücklich erlaubt.
I encourage you to search for and report usuability and security issues
on this site, aswell as server. For more information, see <a href="">Reporting</a>.
{% endblocktranslate %}
<br>
<a href="mailto:admin@cscherr.de">admin@cscherr.de</a>
</p>
</div>
<div class="col-md-2 col-lg-2 col-xl-2 mx-auto mb-4">
<h6 class="text-uppercase fw-bold mb-4">
Links
</h6>
<h6 class="text-uppercase fw-bold mb-4">Links</h6>
<p>
<a href="{% url 'start:legal' %}" class="text-reset">{% translate "Legal Info" %}</a>
</p>
@ -87,11 +99,15 @@
<div class="col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4">
<h6 class="text-uppercase fw-bold mb-4">{% translate "Contact" %}</h6>
<p>Christoph Johannes Scherr</p>
<p><i class="fas fa-home me-3 text-secondary">
Leininger Straße 20,<br>
Maxdorf 67133,<br>
<p>
<i class="fas fa-home me-3 text-secondary">
Leininger Straße 20,
<br>
Maxdorf 67133,
<br>
{% translate "Deutschland" %}
</i></p>
</i>
</p>
<p>
<a href="mailto:contact@cscherr.de">contact@cscherr.de</a>
</p>
@ -106,6 +122,7 @@
</div>
</footer>
{% compress js %}
<script src="/static/custom.js"></script>
<script src="/static/bs5/dist/js/bootstrap.bundle.min.js"></script>
{% endcompress %}
</body>

View File

@ -1,23 +0,0 @@
<button type="button" class="btn btn-dark" id="toggleThemeButton">
<i id="toggleThemeIcon" class="bi bi-sun"></i>
</button>
<script>
'use strict'
let i = document.getElementById("toggleThemeIcon").className = "bi bi-sun";
const storedTheme = localStorage.getItem('theme');
if (storedTheme == null) {
localStorage.setItem("theme", "light");
document.getElementById("toggleThemeIcon").className = "bi bi-sun-fill";
setTheme("light");
}
else if (storedTheme == "dark") {
localStorage.setItem("theme", "dark");
document.getElementById("toggleThemeIcon").className = "bi bi-sun";
setTheme("dark");
}
else if (storedTheme == "light") {
localStorage.setItem("theme", "light");
document.getElementById("toggleThemeIcon").className = "bi bi-sun-fill";
setTheme("light");
}
</script>

View File

@ -1,6 +0,0 @@
{% load i18n %}
<form class="d-flex" role="search" action="{% url 'start:search' %}" method="GET">
{#{ MainSearchForm }#}
<input type="search" name="search" class="form-control me-2" aria-label="Search" placeholder="Suchen" required id="id_search">
<button class="btn bg-primary fw-bold" type="submit">{% translate "Suchen" %}</button>
</form>

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