Compare commits

...

50 Commits

Author SHA1 Message Date
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
2894 changed files with 1145 additions and 201859 deletions

4
.gitignore vendored
View File

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

3
.gitmodules vendored Normal file
View File

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

View File

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

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

26
docker/caddy/Caddyfile Normal file
View File

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

View File

@ -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/ COPY requirements.txt /app/
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
COPY . /app/ COPY . /app/
RUN mkdir -p /app/static
RUN pygmentize -S nord -f html -a .codehilite > /app/static/codehighlight.css

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,17 @@
{% 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 nav %}
{% include 'nav.html' %}
{% endblock nav %}
{% block main %}
<div class="container-xl">{{ category }}</div>
{% include 'blog/featured.html' %}
{% endblock main %}

View File

@ -1,40 +1,59 @@
{% load i18n %} {% load i18n %}
{% load helper_tags %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
<div class="container-lg mt-5"> <div class="container-lg mt-5">
<h4 class="">{% trans "Featured" %}</h4> <h4 class="">{% trans "Featured" %}</h4>
<div class="row row-cols-1 row-cols-md-5 my-4"> <div class="row row-cols-1 row-cols-md-5 my-4">
{% for post in featured_posts %} {% for post in featured_posts %}
<div class="card col m-2 p-0"> <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 %}"> <a class="text-reset link-offset-2 link-underline link-underline-opacity-0"
<img src="{{ post.thumbnail.url }}" class="card-img-top img-fluid" style="max-height: 150px;" alt="thumbnail"> href=" {% url 'blog:post' post.category.slug post.slug %}">
<div class="card-body" style="height: 100px;"> <img src="{{ post.thumbnail.url }}"
{% if LANGUAGE_CODE == "de" %} class="card-img-top img-fluid mx-auto d-block"
<h5 class="card-title">{{ post.title_de }}<small class="text-body-secondary">{{ post.subtitle }}</small></h5> style="max-height: 150px; width: auto; padding-top: 8px;"
<p class="card-text">{{ post.desc_de }}</p> alt="thumbnail" />
{% elif LANGUAGE_CODE == "en" %} <div class="card-body" style="height: 100px">
<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" %} {% if LANGUAGE_CODE == "de" %}
<li class="list-group-item">{{ keyword.text_de }}</li> <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" %} {% elif LANGUAGE_CODE == "en" %}
<li class="list-group-item">{{ keyword.text_en }}</li> <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 %} {% else %}
<li class="list-group-item">{{ keyword.text_en }}</li> <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 %} {% endif %}
{% endfor %} </div>
</ul> <div class="container pt-5">
</div> <ul class="list-group list-group-flush">
</a> <li class="list-group-item">
</div> {% 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>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

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

View File

@ -21,7 +21,6 @@ class Index(TemplateView, SearchableView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True, public=True) context['featured_posts'] = BlogPost.objects.filter(featured=True, public=True)
logger.debug(f"loaded featured posts: {context['featured_posts']}")
return context return context
class Post(DetailView): class Post(DetailView):
@ -36,49 +35,9 @@ class Post(DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True) context['featured_posts'] = BlogPost.objects.filter(featured=True)
logger.debug(f"loaded featured posts: {context['featured_posts']}")
return context return context
def get_object(self, queryset=None): class Browse(ListView):
obj = get_object_or_404(
BlogPost,
category__slug=self.kwargs['category'], # first slug is category
slug=self.kwargs['slug'] # second slug is article itself
)
match get_language():
case 'de':
logger.debug("setting language unspecific attributes for language: de")
obj.title = obj.title_de
obj.subtitle = obj.subtitle_de
obj.desc = obj.desc_de
case 'en':
logger.debug("setting language unspecific attributes for language: en")
obj.title = obj.title_en
obj.subtitle = obj.subtitle_en
obj.desc = obj.desc_en
case _:
# this should not happen, but who knows what dumb stuff users will come up with
logger.warning("article for unsupported language was requested")
return obj
class CategoryList(ListView):
"""
Scroll through a list of blog Categories
"""
model=Category
template_name = "blog/categories.html"
context_object_name = "categories"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True)
logger.debug(f"loaded featured posts: {context['featured_posts']}")
return context
class ArticleList(ListView):
""" """
Scroll through a list of blog posts Scroll through a list of blog posts
@ -87,11 +46,10 @@ class ArticleList(ListView):
""" """
model=BlogPost model=BlogPost
template_name = "blog/posts.html" template_name = "blog/browse.html"
context_object_name = "posts" context_object_name = "posts"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['featured_posts'] = BlogPost.objects.filter(featured=True) context['featured_posts'] = BlogPost.objects.filter(featured=True)
logger.debug(f"loaded featured posts: {context['featured_posts']}")
return context return context

View File

@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/3.2/ref/settings/
""" """
# for getting envvars # for getting envvars
import logging
from django.utils.translation import gettext_lazy as _
import os import os
# default django # default django
@ -35,7 +37,7 @@ ALLOWED_HOSTS = ["*"]
# Allow inclusion of stuff from these origins # Allow inclusion of stuff from these origins
CORS_ALLOWED_ORIGINS = [ CORS_ALLOWED_ORIGINS = [
"https://static.cscherr.de", "https://static.cscherr.de",
] ]
# Application definition # Application definition
@ -61,7 +63,6 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'start.middleware.LangBasedOnUrlMiddleware',
] ]
ROOT_URLCONF = 'gawa.urls' ROOT_URLCONF = 'gawa.urls'
@ -100,7 +101,6 @@ DATABASES = {
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
@ -123,7 +123,6 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/ # https://docs.djangoproject.com/en/3.2/topics/i18n/
from django.utils.translation import gettext_lazy as _
LANGUAGES = [ LANGUAGES = [
("de", _("German")), ("de", _("German")),
@ -155,10 +154,10 @@ STATIC_URL = '/static/'
COMPRESS_ENABLED = True COMPRESS_ENABLED = True
STATICFILES_FINDERS = [ STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder', 'compressor.finders.CompressorFinder',
] ]
COMPRESS_PRECOMPILERS = ( COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'), ('text/x-scss', 'django_libsass.SassCompiler'),
@ -173,7 +172,6 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Logging configs # Logging configs
import logging
myServerFormatter = ServerFormatter myServerFormatter = ServerFormatter
myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S" myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S"
@ -294,7 +292,7 @@ LOGGING = {
# Media stuff # Media stuff
# this is where user uploaded files will go. # this is where user uploaded files will go.
# TODO change this for prod # TODO change this for prod
#MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media" # MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media"
MEDIA_ROOT = "/app/media" MEDIA_ROOT = "/app/media"
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload" # FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload"

View File

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

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

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

View File

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

View File

@ -1,10 +1,11 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
class MainSearchForm(forms.Form): class MainSearchForm(forms.Form):
search = forms.CharField( search = forms.CharField(
max_length=100, max_length=100,
label='' label=''
) )
search.widget = forms.TextInput( search.widget = forms.TextInput(
attrs={ attrs={

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,12 +1,75 @@
# Generated by Django 3.2.19 on 2023-06-03 12:03 # Generated by Django 3.2.21 on 2023-10-02 08:14
from django.db import migrations from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
] ]
operations = [ 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='Searchable',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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': 'Searchable',
'verbose_name_plural': 'Searchables',
},
),
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)),
('favicon', models.ImageField(blank=True, null=True, upload_to='img/links/favicons')),
('status', models.BooleanField(default=False)),
('personal', models.BooleanField(default=False)),
],
options={
'verbose_name': 'Link',
'verbose_name_plural': 'Links',
},
bases=('start.searchable',),
),
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')),
],
options={
'verbose_name': 'static site',
'verbose_name_plural': 'static sites',
},
bases=('start.searchable',),
),
] ]

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,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

@ -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,6 @@
import random
import favicon
import requests
from django.db import models from django.db import models
from django.db.models.options import override from django.db.models.options import override
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -8,24 +11,23 @@ from django.conf import settings
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import requests
import favicon
import random
class Keyword(models.Model): class Keyword(models.Model):
""" """
this is the model that should contain searchable keywords this is the model that should contain searchable keywords
""" """
slug = models.SlugField(unique=True)
text_de = models.CharField(max_length=40) text_de = models.CharField(max_length=40)
text_en = models.CharField(max_length=40) text_en = models.CharField(max_length=40)
def __str__(self): def __str__(self):
return f"{{<{self.__class__.__name__}>\"{self.text_en}\"}}" return f"{{<{self.__class__.__name__}>\"{self.slug}\"}}"
class Meta: class Meta:
verbose_name = _("Keyword") verbose_name = _("Keyword")
verbose_name_plural = _("keywords") verbose_name_plural = _("keywords")
class Searchable(models.Model): class Searchable(models.Model):
""" """
Abstract class for any model that should be searchable. Abstract class for any model that should be searchable.
@ -33,17 +35,19 @@ class Searchable(models.Model):
This class is not a real abstract class, I need to query it in the main search, so thats impossible 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_de = models.CharField(max_length=50, default="Nicht übersetzt")
title_en = models.CharField(max_length=50, default="title EN") title_en = models.CharField(max_length=50, default="Not translated")
subtitle_de = models.CharField(max_length=50, blank=True) subtitle_de = models.CharField(max_length=50, blank=True)
subtitle_en = 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_de = models.TextField(
desc_en = models.CharField(max_length=250, unique=False, default="Description EN") blank=True, max_length=250, unique=False, default="Keine Beschreibung")
# may be empty/blank for some entries desc_en = models.TextField(
date = models.DateField(blank=True, null=True) blank=True, max_length=250, unique=False, default="no description")
keywords = models.ManyToManyField(Keyword) 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) suburl = models.CharField(max_length=200, blank=True, null=True)
public = models.BooleanField(default=True) public = models.BooleanField(default=False)
@classmethod @classmethod
def regenerate_all_entries(cls): def regenerate_all_entries(cls):
@ -61,12 +65,14 @@ class Searchable(models.Model):
""" """
regenerate a object regenerate a object
""" """
raise NotImplementedError(f"{self.__class__.__name__} does not implement regenerate") raise NotImplementedError(
f"{self.__class__.__name__} does not implement regenerate")
class Meta: class Meta:
verbose_name = _("Searchable") verbose_name = _("Searchable")
verbose_name_plural = _("Searchables") verbose_name_plural = _("Searchables")
class StaticSite(Searchable): class StaticSite(Searchable):
""" """
This model represents any static site, such as start:index, This model represents any static site, such as start:index,
@ -82,13 +88,14 @@ class StaticSite(Searchable):
""" """
logger.info(f"regenerating {self.__class__.__name__} object: {self}") logger.info(f"regenerating {self.__class__.__name__} object: {self}")
logger.warning(f"{self.__class__.__name__} cannot regenerate.") logger.warning(f"{self.__class__.__name__} cannot regenerate.")
#self.save() # self.save()
pass pass
class Meta: class Meta:
verbose_name = _("static site") verbose_name = _("static site")
verbose_name_plural = _("static sites") verbose_name_plural = _("static sites")
class Link(Searchable): class Link(Searchable):
""" """
contains all my interesting links contains all my interesting links
@ -99,6 +106,7 @@ class Link(Searchable):
favicon_dir: str = "img/links/favicons" favicon_dir: str = "img/links/favicons"
favicon = models.ImageField(blank=True, upload_to=favicon_dir, null=True) favicon = models.ImageField(blank=True, upload_to=favicon_dir, null=True)
status = models.BooleanField(default=False) status = models.BooleanField(default=False)
personal = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return f"{{<{self.__class__.__name__}>\"{self.title_en}\"}}" return f"{{<{self.__class__.__name__}>\"{self.title_en}\"}}"
@ -120,11 +128,13 @@ class Link(Searchable):
icons = favicon.get(self.url, timeout=2) icons = favicon.get(self.url, timeout=2)
except (ConnectionError) as ce: except (ConnectionError) as ce:
# just keep whatever was stored if we cant get a new favicon # 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 self.status = False
except Exception as e: 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 self.status = False
else: else:
@ -145,7 +155,8 @@ class Link(Searchable):
self.favicon = None self.favicon = None
except Exception as e: 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.favicon = None
self.save() self.save()

View File

@ -11,13 +11,9 @@
{% compress css %} {% 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/bsi1/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="/static/codehighlight.css">
<link rel="stylesheet" href="/static/custom.css">
{% endcompress %} {% endcompress %}
<script>
const setTheme = theme => {
document.documentElement.setAttribute('data-bs-theme', theme)
}
setTheme(localStorage.getItem('theme'));
</script>
{% block headscripts %} {% block headscripts %}
{% endblock headscripts %} {% endblock headscripts %}
</head> </head>
@ -106,6 +102,7 @@
</div> </div>
</footer> </footer>
{% compress js %} {% compress js %}
<script src="/static/custom.js"></script>
<script src="/static/bs5/dist/js/bootstrap.bundle.min.js"></script> <script src="/static/bs5/dist/js/bootstrap.bundle.min.js"></script>
{% endcompress %} {% endcompress %}
</body> </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,89 +1,116 @@
{% load i18n %} {% load i18n %}
{% load helper_tags %}
<nav class="sticky-top navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="sticky-top navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{% url 'start:index' %}"> <a class="navbar-brand" href="{% url 'start:index' %}">
<img src="https://static.cscherr.de/images/profile/profile-margin.png" alt="Logo" height="30" class="d-inline-block align-text-top"> <img src="https://static.cscherr.de/images/profile/profile-margin.png"
alt="Logo"
height="30"
class="d-inline-block align-text-top">
{% translate "cscherr.de" noop %} {% translate "cscherr.de" noop %}
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle"
{% translate "Start" %} href="#"
</a> role="button"
data-bs-toggle="dropdown"
aria-expanded="false">{% translate "Start" %}</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'start:index' %}">{% translate "Start" %}</a></li> <li>
<li><a class="dropdown-item" href="{% url 'start:professional' %}">{% translate "Professionell" %}</a></li> <a class="dropdown-item" href="{% url 'start:index' %}">{% translate "Start" %}</a>
<li><a class="dropdown-item" href="{% url 'start:links' %}">{% translate "Links" %}</a></li> </li>
<li><hr class="dropdown-divider"></li> <li>
<li><a class="dropdown-item" href="{% url 'start:legal' %}">{% translate "Legal Info" %}</a></li> <a class="dropdown-item" href="{% url 'start:professional' %}">{% translate "Professionell" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'start:links' %}">{% translate "Links" %}</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item" href="{% url 'start:legal' %}">{% translate "Legal Info" %}</a>
</li>
</ul> </ul>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle"
{% translate "Blog" %} href="#"
</a> role="button"
data-bs-toggle="dropdown"
aria-expanded="false">{% translate "Blog" %}</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'blog:index' %}">{% translate "Start" %}</a></li> <li>
<li><a class="dropdown-item" href="{% url 'blog:category_list' %}">{% translate "category list" %}</a></li> <a class="dropdown-item" href="{% url 'blog:index' %}">{% translate "Start" %}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'blog:browse' %}">{% translate "Browse" %}</a>
</li>
</ul> </ul>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle"
Debug href="#"
</a> role="button"
data-bs-toggle="dropdown"
aria-expanded="false">Debug</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="http://localhost:8082" target="_blank">DB</a></li> <li>
<li><a class="dropdown-item" href="{% url 'admin:index' %}" target="_blank">Admin</a></li> <a class="dropdown-item" href="http://localhost:8080" target="_blank">DB</a>
<li><hr class="dropdown-divider"></li> </li>
<li><a class="dropdown-item" href="#">Something else here</a></li> <li>
<a class="dropdown-item" href="{% url 'admin:index' %}" target="_blank">Admin</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item" href="#">Something else here</a>
</li>
</ul> </ul>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
{% get_available_languages as languages %} {% get_current_language as LANGUAGE_CODE %}
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <form action="{% url 'set_language' %}" method="post">
{{ LANGUAGE_CODE }} {% csrf_token %}
</a> {% get_current_language as LANGUAGE_CODE %}
<ul class="dropdown-menu"> {% get_available_languages as LANGUAGES %}
{% for lang_code, lang_name in languages %} {% get_language_info_list for LANGUAGES as languages %}
<li><a class="dropdown-item" <input name="next" type="hidden" value="{{ redirect_to }}">
href="{% change_lang lang_code %}"> <a class="nav-link dropdown-toggle"
{{ lang_name }}</a></li> href="#"
{% endfor %} role="button"
<li><hr class="dropdown-divider"></li> data-bs-toggle="dropdown"
<li><a class="dropdown-item" href="#">Something else here</a></li> aria-expanded="false">{{ LANGUAGE_CODE }}</a>
</ul> <ul class="dropdown-menu">
{% for language in languages %}
<li>
<input class="dropdown-item"
type="submit"
name="language"
value="{{ language.code }}">
</li>
{% endfor %}
</ul>
</form>
</li> </li>
<li class="nav-item"> <li class="nav-item">
{% include 'dark_light_switch.html' %} <button type="button" class="btn btn-dark" id="toggleThemeButton">
<i id="toggleThemeIcon" class="bi bi-sun"></i>
</button>
</li> </li>
</ul> </ul>
{% include 'main_search_form.html' %} {% include 'main_search_form.html' %}
</div> </div>
</div> </div>
<script>
'use strict'
document.getElementById("toggleThemeButton").onclick = function () {
const storedTheme = localStorage.getItem('theme');
if (storedTheme == null) {
localStorage.setItem("theme", "dark");
document.getElementById("toggleThemeIcon").className = "bi bi-sun";
setTheme("dark");
}
else if (storedTheme == "dark") {
localStorage.setItem("theme", "light");
document.getElementById("toggleThemeIcon").className = "bi bi-sun-fill";
setTheme("light");
}
else if (storedTheme == "light") {
localStorage.setItem("theme", "dark");
document.getElementById("toggleThemeIcon").className = "bi bi-sun";
setTheme("dark");
}
};
</script>
</nav> </nav>

View File

@ -1 +1,40 @@
TODO {% extends 'base.html' %}
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% block languagecode %}
{{ LANGUAGE_CODE }}
{% endblock languagecode %}
{% block title %}
{% translate "cscherr.de" %} - {% translate "Links" %}
{% endblock title %}
{% block main %}
<div class="container-small p-5 m-5">
<h1 class="my-5">{% trans "Legal Info" %}</h1>
<div class="container-small text-center">
<div class="row border-bottom py-4">
<h2 class="mb-4">
<b>{% trans "Angaben gemäß § 5 TMG" %}</b>
</h2>
<div class="container-sm" style="width: 50%;">
<ul class="list-group">
<li class="list-group-item">Christoph Johannes Scherr</li>
<li class="list-group-item">Leininger Straße 20</li>
<li class="list-group-item">67133 Maxdorf</li>
<li class="list-group-item">{% trans "Germany" %}</li>
<li class="list-group-item">
{% trans "Email: " %}<a href="mailto:contact@cscherr.de">contact@cscherr.de</a>
</li>
</ul>
</div>
</div>
<div class="row border-bottom m-2 p-2">
<div class="col">A</div>
<div class="col">B</div>
</div>
<div class="row border-bottom m-2 p-2">
<div class="col">A</div>
<div class="col">B</div>
</div>
</div>
</div>
{% endblock main %}

View File

@ -5,11 +5,55 @@
{% block title %}{% translate "cscherr.de" %} - {% translate "Links" %}{% endblock title %} {% block title %}{% translate "cscherr.de" %} - {% translate "Links" %}{% endblock title %}
{% block main %} {% block main %}
<div class="container-fluid mt-5"> <div class="container-fluid mt-5">
<h4 class="">{% trans "Links" %}</h4> <h4 class="">{% trans "Personal" %}</h4>
<div class="row row-cols-1 row-cols-md-6 my-4">
{% for link in personal_links %}
<div class="card col m-2 p-0">
<a class="text-reset link-offset-2 link-underline link-underline-opacity-0" href="{{ link.url }}" target="_blank">
<div class="card-body" style="height: 150px;">
<img src="{{ link.favicon.url }}" class="card-img-top rounded float-end" style="max-height: 32px; max-width: 32px;" alt="">
{% if LANGUAGE_CODE == "de" %}
<h5 class="card-title" style="height: 35px;">{{ link.title_de }}<small class="text-body-secondary">{{ link.subtitle }}</small></h5>
<hr>
<p class="card-text">{{ link.desc_de }}</p>
{% elif LANGUAGE_CODE == "en" %}
<h5 class="card-title" style="height: 35px;">{{ link.title_en }}<small class="text-body-secondary">{{ link.subtitle }}</small></h5>
<hr>
<p class="card-text">{{ link.desc_en }}</p>
{% else %}
<h5 class="card-title" style="height: 35px;">{{ link.title_en }}<small class="text-body-secondary">{{ link.subtitle }}</small></h5>
<hr>
<p class="card-text">{{ link.desc_en }}</p>
{% endif %}
</div>
<div class="container pt-5 mt-3">
<ul class="list-group list-group-flush">
{% for keyword in link.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 %}
<li class="list-group-item">
<i class="bi-box-arrow-up-right"></i>
{% translate "visit" %}
</li>
</ul>
</div>
</a>
</div>
{% endfor %}
</div>
</div>
<div class="container-fluid mt-5">
<h4 class="">{% trans "Others" %}</h4>
<div class="row row-cols-1 row-cols-md-6 my-4"> <div class="row row-cols-1 row-cols-md-6 my-4">
{% for link in links %} {% for link in links %}
<div class="card col m-2 p-0"> <div class="card col m-2 p-0">
<a class="text-reset link-offset-2 link-underline link-underline-opacity-0" href="{{ link.url }}"> <a class="text-reset link-offset-2 link-underline link-underline-opacity-0" href="{{ link.url }}" target="_blank">
<div class="card-body" style="height: 150px;"> <div class="card-body" style="height: 150px;">
<img src="{{ link.favicon.url }}" class="card-img-top rounded float-end" style="max-height: 32px; max-width: 32px;" alt=""> <img src="{{ link.favicon.url }}" class="card-img-top rounded float-end" style="max-height: 32px; max-width: 32px;" alt="">
{% if LANGUAGE_CODE == "de" %} {% if LANGUAGE_CODE == "de" %}

View File

@ -1,6 +1,7 @@
from django.template import Library from django.template import Library
from django.urls import resolve, reverse from django.urls import resolve, reverse
from django.utils.translation import activate, get_language from django.utils.translation import activate, get_language
from datetime import datetime
import re import re
@ -9,29 +10,6 @@ logger = logging.getLogger(__name__)
register = Library() register = Library()
@register.simple_tag
@register.simple_tag(takes_context=True) def format_time(timestamp: datetime, format: str = "%F %H:%M:%S %Z") -> str:
def change_lang(context, lang="de", *args, **kwargs): return timestamp.strftime(format)
"""
Get active page's url by a specified language
Usage: {% change_lang 'en' %}
shamelessly stolen from stackoverflow:
https://stackoverflow.com/a/41147772
"""
path = context['request'].get_raw_uri()
logger.debug(f"requestdir: {dir(context['request'])}")
url = path
try:
cur_lang: str = get_language()
url = re.sub(f"/{cur_lang}/", f"/{lang}/", url)
except Exception as e:
logger.error(f"exception while building language switcher form: {e}")
logger.debug(f"this is the context: {context}")
finally:
activate(lang)
return "%s" % url

View File

@ -9,5 +9,4 @@ urlpatterns = [
path("legal/", views.LegalInfo.as_view(), name="legal"), path("legal/", views.LegalInfo.as_view(), name="legal"),
path("links/", views.Links.as_view(), name="links"), path("links/", views.Links.as_view(), name="links"),
path("professional/", views.LegalInfo.as_view(), name="professional"), path("professional/", views.LegalInfo.as_view(), name="professional"),
path('language/activate/<language_code>/', views.ActivateLanguage.as_view(), name='activate_language'),
] ]

View File

@ -8,6 +8,7 @@ from django.shortcuts import render
from django.views.generic.list import QuerySet from django.views.generic.list import QuerySet
from django.db.models import Q from django.db.models import Q
from django.views.static import loader from django.views.static import loader
from django.views import i18n
from requests import request from requests import request
from .forms import MainSearchForm from .forms import MainSearchForm
@ -18,6 +19,7 @@ from abc import ABC
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SearchableView(View, ABC): class SearchableView(View, ABC):
""" """
This abstract view implements some traits of views that should show up This abstract view implements some traits of views that should show up
@ -40,6 +42,7 @@ class Index(TemplateView, SearchableView):
template_name: str = "start/index.html" template_name: str = "start/index.html"
class Professional(TemplateView, SearchableView): class Professional(TemplateView, SearchableView):
""" """
Professional informations that might interest a professional employer Professional informations that might interest a professional employer
@ -47,6 +50,7 @@ class Professional(TemplateView, SearchableView):
# TODO # TODO
template_name: str = "start/legalinfo.html" template_name: str = "start/legalinfo.html"
class LegalInfo(TemplateView, SearchableView): class LegalInfo(TemplateView, SearchableView):
""" """
Legal info that the german authorities want. Legal info that the german authorities want.
@ -54,20 +58,6 @@ class LegalInfo(TemplateView, SearchableView):
# TODO # TODO
template_name: str = "start/legalinfo.html" template_name: str = "start/legalinfo.html"
class ActivateLanguage(View):
"""
Set the language to whatever
"""
language_code = ''
redirect_to = ''
def get(self, request, *args, **kwargs):
self.redirect_to = request.META.get('HTTP_REFERER')
self.language_code = kwargs.get('language_code')
translation.activate(self.language_code)
request.session[translation.LANGUAGE_SESSION_KEY] = self.language_code
return redirect(self.redirect_to)
class MainSearch(ListView): class MainSearch(ListView):
""" """
Search for anything. Search for anything.
@ -94,6 +84,7 @@ class MainSearch(ListView):
return render(request, "errors/bad_request.html") return render(request, "errors/bad_request.html")
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
class Links(ListView): class Links(ListView):
""" """
This View contains links to various interesting sites. This View contains links to various interesting sites.
@ -104,7 +95,17 @@ class Links(ListView):
def get_queryset(self) -> QuerySet: def get_queryset(self) -> QuerySet:
object_list = Link.objects.filter( object_list = Link.objects.filter(
status=True, public=True status=True, public=True, personal=False
) )
object_list = object_list.filter(public=True)
return object_list return object_list
def get_queryset_personal_links(self) -> QuerySet:
object_list = Link.objects.filter(
status=True, public=True, personal=True
)
return object_list
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context['personal_links'] = self.get_queryset_personal_links()
return context

File diff suppressed because one or more lines are too long

View File

@ -1,275 +0,0 @@
select.admin-autocomplete {
width: 20em;
}
.select2-container--admin-autocomplete.select2-container {
min-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single,
.select2-container--admin-autocomplete .select2-selection--multiple {
min-height: 30px;
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
border-color: var(--body-quiet-color);
min-height: 30px;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
padding: 0;
}
.select2-container--admin-autocomplete .select2-selection--single {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
color: var(--body-fg);
line-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
.select2-container--admin-autocomplete .select2-selection--multiple {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: text;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 10px 5px 5px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
list-style: none;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
color: var(--body-quiet-color);
margin-top: 5px;
float: left;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin: 5px;
position: absolute;
right: 0;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
background-color: var(--darkened-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
color: var(--body-quiet-color);
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
color: var(--body-fg);
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
border: solid var(--body-quiet-color) 1px;
outline: 0;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--admin-autocomplete .select2-search--dropdown {
background: var(--darkened-bg);
}
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
background: var(--body-bg);
color: var(--body-fg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
background: transparent;
color: var(--body-fg);
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield;
}
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
color: var(--body-fg);
background: var(--body-bg);
}
.select2-container--admin-autocomplete .select2-results__option[role=group] {
padding: 0;
}
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
background-color: var(--selected-bg);
color: var(--body-fg);
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
padding-left: 1em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em;
}
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary);
color: var(--primary-fg);
}
.select2-container--admin-autocomplete .select2-results__group {
cursor: default;
display: block;
padding: 6px;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,351 +0,0 @@
/* CHANGELISTS */
#changelist {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
#changelist .changelist-form-container {
flex: 1 1 auto;
min-width: 0;
}
#changelist table {
width: 100%;
}
.change-list .hiddenfields { display:none; }
.change-list .filtered table {
border-right: none;
}
.change-list .filtered {
min-height: 400px;
}
.change-list .filtered .results, .change-list .filtered .paginator,
.filtered #toolbar, .filtered div.xfull {
width: auto;
}
.change-list .filtered table tbody th {
padding-right: 1em;
}
#changelist-form .results {
overflow-x: auto;
width: 100%;
}
#changelist .toplinks {
border-bottom: 1px solid var(--hairline-color);
}
#changelist .paginator {
color: var(--body-quiet-color);
border-bottom: 1px solid var(--hairline-color);
background: var(--body-bg);
overflow: hidden;
}
/* CHANGELIST TABLES */
#changelist table thead th {
padding: 0;
white-space: nowrap;
vertical-align: middle;
}
#changelist table thead th.action-checkbox-column {
width: 1.5em;
text-align: center;
}
#changelist table tbody td.action-checkbox {
text-align: center;
}
#changelist table tfoot {
color: var(--body-quiet-color);
}
/* TOOLBAR */
#toolbar {
padding: 8px 10px;
margin-bottom: 15px;
border-top: 1px solid var(--hairline-color);
border-bottom: 1px solid var(--hairline-color);
background: var(--darkened-bg);
color: var(--body-quiet-color);
}
#toolbar form input {
border-radius: 4px;
font-size: 14px;
padding: 5px;
color: var(--body-fg);
}
#toolbar #searchbar {
height: 19px;
border: 1px solid var(--border-color);
padding: 2px 5px;
margin: 0;
vertical-align: top;
font-size: 13px;
max-width: 100%;
}
#toolbar #searchbar:focus {
border-color: var(--body-quiet-color);
}
#toolbar form input[type="submit"] {
border: 1px solid var(--border-color);
font-size: 13px;
padding: 4px 8px;
margin: 0;
vertical-align: middle;
background: var(--body-bg);
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
cursor: pointer;
color: var(--body-fg);
}
#toolbar form input[type="submit"]:focus,
#toolbar form input[type="submit"]:hover {
border-color: var(--body-quiet-color);
}
#changelist-search img {
vertical-align: middle;
margin-right: 4px;
}
/* FILTER COLUMN */
#changelist-filter {
flex: 0 0 240px;
order: 1;
background: var(--darkened-bg);
border-left: none;
margin: 0 0 0 30px;
}
#changelist-filter h2 {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 5px 15px;
margin-bottom: 12px;
border-bottom: none;
}
#changelist-filter h3 {
font-weight: 400;
padding: 0 15px;
margin-bottom: 10px;
}
#changelist-filter ul {
margin: 5px 0;
padding: 0 15px 15px;
border-bottom: 1px solid var(--hairline-color);
}
#changelist-filter ul:last-child {
border-bottom: none;
}
#changelist-filter li {
list-style-type: none;
margin-left: 0;
padding-left: 0;
}
#changelist-filter a {
display: block;
color: var(--body-quiet-color);
text-overflow: ellipsis;
overflow-x: hidden;
}
#changelist-filter li.selected {
border-left: 5px solid var(--hairline-color);
padding-left: 10px;
margin-left: -15px;
}
#changelist-filter li.selected a {
color: var(--link-selected-fg);
}
#changelist-filter a:focus, #changelist-filter a:hover,
#changelist-filter li.selected a:focus,
#changelist-filter li.selected a:hover {
color: var(--link-hover-color);
}
#changelist-filter #changelist-filter-clear a {
font-size: 13px;
padding-bottom: 10px;
border-bottom: 1px solid var(--hairline-color);
}
/* DATE DRILLDOWN */
.change-list ul.toplinks {
display: block;
float: left;
padding: 0;
margin: 0;
width: 100%;
}
.change-list ul.toplinks li {
padding: 3px 6px;
font-weight: bold;
list-style-type: none;
display: inline-block;
}
.change-list ul.toplinks .date-back a {
color: var(--body-quiet-color);
}
.change-list ul.toplinks .date-back a:focus,
.change-list ul.toplinks .date-back a:hover {
color: var(--link-hover-color);
}
/* PAGINATOR */
.paginator {
font-size: 13px;
padding-top: 10px;
padding-bottom: 10px;
line-height: 22px;
margin: 0;
border-top: 1px solid var(--hairline-color);
width: 100%;
}
.paginator a:link, .paginator a:visited {
padding: 2px 6px;
background: var(--button-bg);
text-decoration: none;
color: var(--button-fg);
}
.paginator a.showall {
border: none;
background: none;
color: var(--link-fg);
}
.paginator a.showall:focus, .paginator a.showall:hover {
background: none;
color: var(--link-hover-color);
}
.paginator .end {
margin-right: 6px;
}
.paginator .this-page {
padding: 2px 6px;
font-weight: bold;
font-size: 13px;
vertical-align: top;
}
.paginator a:focus, .paginator a:hover {
color: white;
background: var(--link-hover-color);
}
/* ACTIONS */
.filtered .actions {
border-right: none;
}
#changelist table input {
margin: 0;
vertical-align: baseline;
}
#changelist table tbody tr.selected {
background-color: var(--selected-row);
}
#changelist .actions {
padding: 10px;
background: var(--body-bg);
border-top: none;
border-bottom: none;
line-height: 24px;
color: var(--body-quiet-color);
width: 100%;
}
#changelist .actions.selected { /* XXX Probably unused? */
background: var(--body-bg);
border-top: 1px solid var(--body-bg);
border-bottom: 1px solid #edecd6;
}
#changelist .actions span.all,
#changelist .actions span.action-counter,
#changelist .actions span.clear,
#changelist .actions span.question {
font-size: 13px;
margin: 0 0.5em;
}
#changelist .actions:last-child {
border-bottom: none;
}
#changelist .actions select {
vertical-align: top;
height: 24px;
color: var(--body-fg);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
padding: 0 0 0 4px;
margin: 0;
margin-left: 10px;
}
#changelist .actions select:focus {
border-color: var(--body-quiet-color);
}
#changelist .actions label {
display: inline-block;
vertical-align: middle;
font-size: 13px;
}
#changelist .actions .button {
font-size: 13px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--body-bg);
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
cursor: pointer;
height: 24px;
line-height: 1;
padding: 4px 8px;
margin: 0;
color: var(--body-fg);
}
#changelist .actions .button:focus, #changelist .actions .button:hover {
border-color: var(--body-quiet-color);
}

View File

@ -1,26 +0,0 @@
/* DASHBOARD */
.dashboard .module table th {
width: 100%;
}
.dashboard .module table td {
white-space: nowrap;
}
.dashboard .module table td a {
display: block;
padding-right: .6em;
}
/* RECENT ACTIONS MODULE */
.module ul.actionlist {
margin-left: 0;
}
ul.actionlist li {
list-style-type: none;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -1,20 +0,0 @@
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Bold-webfont.woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Regular-webfont.woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Light-webfont.woff');
font-weight: 300;
font-style: normal;
}

View File

@ -1,523 +0,0 @@
@import url('widgets.css');
/* FORM ROWS */
.form-row {
overflow: hidden;
padding: 10px;
font-size: 13px;
border-bottom: 1px solid var(--hairline-color);
}
.form-row img, .form-row input {
vertical-align: middle;
}
.form-row label input[type="checkbox"] {
margin-top: 0;
vertical-align: 0;
}
form .form-row p {
padding-left: 0;
}
/* FORM LABELS */
label {
font-weight: normal;
color: var(--body-quiet-color);
font-size: 13px;
}
.required label, label.required {
font-weight: bold;
color: var(--body-fg);
}
/* RADIO BUTTONS */
form ul.radiolist li {
list-style-type: none;
}
form ul.radiolist label {
float: none;
display: inline;
}
form ul.radiolist input[type="radio"] {
margin: -2px 4px 0 0;
padding: 0;
}
form ul.inline {
margin-left: 0;
padding: 0;
}
form ul.inline li {
float: left;
padding-right: 7px;
}
/* ALIGNED FIELDSETS */
.aligned label {
display: block;
padding: 4px 10px 0 0;
float: left;
width: 160px;
word-wrap: break-word;
line-height: 1;
}
.aligned label:not(.vCheckboxLabel):after {
content: '';
display: inline-block;
vertical-align: middle;
height: 26px;
}
.aligned label + p, .aligned label + div.help, .aligned label + div.readonly {
padding: 6px 0;
margin-top: 0;
margin-bottom: 0;
margin-left: 170px;
}
.aligned ul label {
display: inline;
float: none;
width: auto;
}
.aligned .form-row input {
margin-bottom: 0;
}
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
width: 350px;
}
form .aligned ul {
margin-left: 160px;
padding-left: 10px;
}
form .aligned ul.radiolist {
display: inline-block;
margin: 0;
padding: 0;
}
form .aligned p.help,
form .aligned div.help {
clear: left;
margin-top: 0;
margin-left: 160px;
padding-left: 10px;
}
form .aligned label + p.help,
form .aligned label + div.help {
margin-left: 0;
padding-left: 0;
}
form .aligned p.help:last-child,
form .aligned div.help:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
form .aligned input + p.help,
form .aligned textarea + p.help,
form .aligned select + p.help,
form .aligned input + div.help,
form .aligned textarea + div.help,
form .aligned select + div.help {
margin-left: 160px;
padding-left: 10px;
}
form .aligned ul li {
list-style: none;
}
form .aligned table p {
margin-left: 0;
padding-left: 0;
}
.aligned .vCheckboxLabel {
float: none;
width: auto;
display: inline-block;
vertical-align: -3px;
padding: 0 0 5px 5px;
}
.aligned .vCheckboxLabel + p.help,
.aligned .vCheckboxLabel + div.help {
margin-top: -4px;
}
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
width: 610px;
}
.checkbox-row p.help,
.checkbox-row div.help {
margin-left: 0;
padding-left: 0;
}
fieldset .fieldBox {
float: left;
margin-right: 20px;
}
/* WIDE FIELDSETS */
.wide label {
width: 200px;
}
form .wide p,
form .wide input + p.help,
form .wide input + div.help {
margin-left: 200px;
}
form .wide p.help,
form .wide div.help {
padding-left: 38px;
}
form div.help ul {
padding-left: 0;
margin-left: 0;
}
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
width: 450px;
}
/* COLLAPSED FIELDSETS */
fieldset.collapsed * {
display: none;
}
fieldset.collapsed h2, fieldset.collapsed {
display: block;
}
fieldset.collapsed {
border: 1px solid var(--hairline-color);
border-radius: 4px;
overflow: hidden;
}
fieldset.collapsed h2 {
background: var(--darkened-bg);
color: var(--body-quiet-color);
}
fieldset .collapse-toggle {
color: var(--header-link-color);
}
fieldset.collapsed .collapse-toggle {
background: transparent;
display: inline;
color: var(--link-fg);
}
/* MONOSPACE TEXTAREAS */
fieldset.monospace textarea {
font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace;
}
/* SUBMIT ROW */
.submit-row {
padding: 12px 14px;
margin: 0 0 20px;
background: var(--darkened-bg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
text-align: right;
overflow: hidden;
}
body.popup .submit-row {
overflow: auto;
}
.submit-row input {
height: 35px;
line-height: 15px;
margin: 0 0 0 5px;
}
.submit-row input.default {
margin: 0 0 0 8px;
text-transform: uppercase;
}
.submit-row p {
margin: 0.3em;
}
.submit-row p.deletelink-box {
float: left;
margin: 0;
}
.submit-row a.deletelink {
display: block;
background: var(--delete-button-bg);
border-radius: 4px;
padding: 10px 15px;
height: 15px;
line-height: 15px;
color: var(--button-fg);
}
.submit-row a.closelink {
display: inline-block;
background: var(--close-button-bg);
border-radius: 4px;
padding: 10px 15px;
height: 15px;
line-height: 15px;
margin: 0 0 0 5px;
color: var(--button-fg);
}
.submit-row a.deletelink:focus,
.submit-row a.deletelink:hover,
.submit-row a.deletelink:active {
background: var(--delete-button-hover-bg);
}
.submit-row a.closelink:focus,
.submit-row a.closelink:hover,
.submit-row a.closelink:active {
background: var(--close-button-hover-bg);
}
/* CUSTOM FORM FIELDS */
.vSelectMultipleField {
vertical-align: top;
}
.vCheckboxField {
border: none;
}
.vDateField, .vTimeField {
margin-right: 2px;
margin-bottom: 4px;
}
.vDateField {
min-width: 6.85em;
}
.vTimeField {
min-width: 4.7em;
}
.vURLField {
width: 30em;
}
.vLargeTextField, .vXMLLargeTextField {
width: 48em;
}
.flatpages-flatpage #id_content {
height: 40.2em;
}
.module table .vPositiveSmallIntegerField {
width: 2.2em;
}
.vTextField, .vUUIDField {
width: 20em;
}
.vIntegerField {
width: 5em;
}
.vBigIntegerField {
width: 10em;
}
.vForeignKeyRawIdAdminField {
width: 5em;
}
/* INLINES */
.inline-group {
padding: 0;
margin: 0 0 30px;
}
.inline-group thead th {
padding: 8px 10px;
}
.inline-group .aligned label {
width: 160px;
}
.inline-related {
position: relative;
}
.inline-related h3 {
margin: 0;
color: var(--body-quiet-color);
padding: 5px;
font-size: 13px;
background: var(--darkened-bg);
border-top: 1px solid var(--hairline-color);
border-bottom: 1px solid var(--hairline-color);
}
.inline-related h3 span.delete {
float: right;
}
.inline-related h3 span.delete label {
margin-left: 2px;
font-size: 11px;
}
.inline-related fieldset {
margin: 0;
background: var(--body-bg);
border: none;
width: 100%;
}
.inline-related fieldset.module h3 {
margin: 0;
padding: 2px 5px 3px 5px;
font-size: 11px;
text-align: left;
font-weight: bold;
background: #bcd;
color: var(--body-bg);
}
.inline-group .tabular fieldset.module {
border: none;
}
.inline-related.tabular fieldset.module table {
width: 100%;
overflow-x: scroll;
}
.last-related fieldset {
border: none;
}
.inline-group .tabular tr.has_original td {
padding-top: 2em;
}
.inline-group .tabular tr td.original {
padding: 2px 0 0 0;
width: 0;
_position: relative;
}
.inline-group .tabular th.original {
width: 0px;
padding: 0;
}
.inline-group .tabular td.original p {
position: absolute;
left: 0;
height: 1.1em;
padding: 2px 9px;
overflow: hidden;
font-size: 9px;
font-weight: bold;
color: var(--body-quiet-color);
_width: 700px;
}
.inline-group ul.tools {
padding: 0;
margin: 0;
list-style: none;
}
.inline-group ul.tools li {
display: inline;
padding: 0 5px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
color: var(--body-quiet-color);
background: var(--darkened-bg);
padding: 8px 10px;
border-bottom: 1px solid var(--hairline-color);
}
.inline-group .tabular tr.add-row td {
padding: 8px 10px;
border-bottom: 1px solid var(--hairline-color);
}
.inline-group ul.tools a.add,
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
padding-left: 16px;
font-size: 12px;
}
.empty-form {
display: none;
}
/* RELATED FIELD ADD ONE / LOOKUP */
.related-lookup {
margin-left: 5px;
display: inline-block;
vertical-align: middle;
background-repeat: no-repeat;
background-size: 14px;
}
.related-lookup {
width: 16px;
height: 16px;
background-image: url(../img/search.svg);
}
form .related-widget-wrapper ul {
display: inline-block;
margin-left: 0;
padding-left: 0;
}
.clearable-file-input input {
margin-top: 0;
}

View File

@ -1,60 +0,0 @@
/* LOGIN FORM */
.login {
background: var(--darkened-bg);
height: auto;
}
.login #header {
height: auto;
padding: 15px 16px;
justify-content: center;
}
.login #header h1 {
font-size: 18px;
}
.login #header h1 a {
color: var(--header-link-color);
}
.login #content {
padding: 20px 20px 0;
}
.login #container {
background: var(--body-bg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
overflow: hidden;
width: 28em;
min-width: 300px;
margin: 100px auto;
height: auto;
}
.login .form-row {
padding: 4px 0;
}
.login .form-row label {
display: block;
line-height: 2em;
}
.login .form-row #id_username, .login .form-row #id_password {
padding: 8px;
width: 100%;
box-sizing: border-box;
}
.login .submit-row {
padding: 1em 0 0 0;
margin: 0;
text-align: center;
}
.login .password-reset-link {
text-align: center;
}

View File

@ -1,120 +0,0 @@
.sticky {
position: sticky;
top: 0;
max-height: 100vh;
}
.toggle-nav-sidebar {
z-index: 20;
left: 0;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 23px;
width: 23px;
border: 0;
border-right: 1px solid var(--hairline-color);
background-color: var(--body-bg);
cursor: pointer;
font-size: 20px;
color: var(--link-fg);
padding: 0;
}
[dir="rtl"] .toggle-nav-sidebar {
border-left: 1px solid var(--hairline-color);
border-right: 0;
}
.toggle-nav-sidebar:hover,
.toggle-nav-sidebar:focus {
background-color: var(--darkened-bg);
}
#nav-sidebar {
z-index: 15;
flex: 0 0 275px;
left: -276px;
margin-left: -276px;
border-top: 1px solid transparent;
border-right: 1px solid var(--hairline-color);
background-color: var(--body-bg);
overflow: auto;
}
[dir="rtl"] #nav-sidebar {
border-left: 1px solid var(--hairline-color);
border-right: 0;
left: 0;
margin-left: 0;
right: -276px;
margin-right: -276px;
}
.toggle-nav-sidebar::before {
content: '\00BB';
}
.main.shifted .toggle-nav-sidebar::before {
content: '\00AB';
}
.main.shifted > #nav-sidebar {
left: 24px;
margin-left: 0;
}
[dir="rtl"] .main.shifted > #nav-sidebar {
left: 0;
right: 24px;
margin-right: 0;
}
#nav-sidebar .module th {
width: 100%;
overflow-wrap: anywhere;
}
#nav-sidebar .module th,
#nav-sidebar .module caption {
padding-left: 16px;
}
#nav-sidebar .module td {
white-space: nowrap;
}
[dir="rtl"] #nav-sidebar .module th,
[dir="rtl"] #nav-sidebar .module caption {
padding-left: 8px;
padding-right: 16px;
}
#nav-sidebar .current-app .section:link,
#nav-sidebar .current-app .section:visited {
color: var(--header-color);
font-weight: bold;
}
#nav-sidebar .current-model {
background: var(--selected-row);
}
.main > #nav-sidebar + .content {
max-width: calc(100% - 23px);
}
.main.shifted > #nav-sidebar + .content {
max-width: calc(100% - 299px);
}
@media (max-width: 767px) {
#nav-sidebar, #toggle-nav-sidebar {
display: none;
}
.main > #nav-sidebar + .content,
.main.shifted > #nav-sidebar + .content {
max-width: 100%;
}
}

File diff suppressed because it is too large Load Diff

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