Compare commits
50 Commits
master
...
blog-style
Author | SHA1 | Date |
---|---|---|
Christoph J. Scherr | 64f84d58bc | |
Christoph J. Scherr | 9b7c73a977 | |
Christoph J. Scherr | 83d75823a4 | |
Christoph J. Scherr | c22807921d | |
Christoph J. Scherr | 3534f5399e | |
Christoph J. Scherr | f7f0675d26 | |
Christoph J. Scherr | 48a0d26745 | |
Christoph J. Scherr | c0750fdb30 | |
Christoph J. Scherr | 1ce0d52302 | |
Christoph J. Scherr | 7e3f33a824 | |
Christoph J. Scherr | c59d243cc0 | |
Christoph J. Scherr | d287b3291d | |
Christoph J. Scherr | e8c332fce4 | |
Christoph J. Scherr | 6f2267e18a | |
Christoph J. Scherr | 50db49911f | |
Christoph J. Scherr | 05497e10e1 | |
Christoph J. Scherr | a9a90e9bd4 | |
Christoph J. Scherr | 3bf0dd42c7 | |
Christoph J. Scherr | 3be2f095c0 | |
Christoph J. Scherr | b269b452fd | |
Christoph J. Scherr | adac986018 | |
Christoph J. Scherr | a2fc4eb8b8 | |
Christoph J. Scherr | 6ad7f0cfbd | |
Christoph J. Scherr | c42653df80 | |
Christoph J. Scherr | 37c3104bc5 | |
Christoph J. Scherr | 6d9660f44c | |
Christoph J. Scherr | f8c3577a54 | |
Christoph J. Scherr | 05ade38f2f | |
Christoph J. Scherr | ed75a20862 | |
Christoph J. Scherr | a027fd7582 | |
Christoph J. Scherr | 42ea8d14d4 | |
Christoph J. Scherr | d81b20e391 | |
Christoph J. Scherr | a687700c85 | |
Christoph J. Scherr | 2c8b562601 | |
Christoph J. Scherr | 56cd5943d2 | |
Christoph J. Scherr | ff231dfbc1 | |
Christoph J. Scherr | 7e7db5a480 | |
Christoph J. Scherr | 7d03b80f7a | |
Christoph J. Scherr | 50d17032af | |
Christoph J. Scherr | eb1d4e85ed | |
Christoph J. Scherr | d3c2c22e82 | |
Christoph J. Scherr | 8a99e38c74 | |
Christoph J. Scherr | e561153284 | |
Christoph J. Scherr | ab7ea1b17b | |
Christoph J. Scherr | 6de4f905d8 | |
Christoph J. Scherr | e87f6648cf | |
Christoph J. Scherr | edf5f7e190 | |
Christoph J. Scherr | 002656ea72 | |
Christoph J. Scherr | 6016a864d9 | |
Christoph J. Scherr | f844fab25e |
|
@ -18,3 +18,7 @@ db/data/ibtmp1
|
|||
db/data/multi-master.info
|
||||
db/data/mysql_upgrade_info
|
||||
gawa/media/CACHE
|
||||
gawa/static/CACHE
|
||||
site/static/CACHE
|
||||
gawa/media/img/links/favicons
|
||||
gawa/static
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "blog"]
|
||||
path = site/blog
|
||||
url = https://github.com/xenocrat/chyrp-lite
|
37
README.md
37
README.md
|
@ -1,8 +1,35 @@
|
|||
# gawa
|
||||
Gawa is my personal website. I've personally written it using the django framework.
|
||||
# Gawa
|
||||
|
||||
Gawa is my personal website. I've personally written it using the Django framework.
|
||||
|
||||
## Credentials
|
||||
|
||||
These are the Credentials for logging into the admin panel:
|
||||
| Username | Password |
|
||||
|----------|----------|
|
||||
| `root` | `root` |
|
||||
|
||||
### Blog
|
||||
|
||||
| Username | Password |
|
||||
|--------------------------------------------------|----------------|
|
||||
| [`contact@cscherr.de`](mailto:contact@cscherr.de) | `hrCcDa0jBspG` |
|
||||
|
||||
## License
|
||||
Bootstrap: MIT Licensed
|
||||
Django: BSD 3-Clause "New" or "Revised" License
|
||||
|
||||
###### Gawa: MIT Licensed, see LICENSE
|
||||
Bootstrap: MIT Licensed
|
||||
Django: BSD 3-Clause "New" or "Revised" 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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.
Binary file not shown.
Binary file not shown.
|
@ -1,66 +1,62 @@
|
|||
services:
|
||||
db:
|
||||
image: mariadb
|
||||
container_name: gawa-db
|
||||
networks:
|
||||
- internal
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
MYSQL_DATABASE: gawa
|
||||
MYSQL_USER: gawa
|
||||
MYSQL_PASSWORD: changethisforprod
|
||||
# MYSQL_DATABASE: gawa
|
||||
# MYSQL_USER: gawa
|
||||
# MYSQL_PASSWORD: changethisforprod
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
volumes:
|
||||
- ./db/data:/var/lib/mysql
|
||||
# - db_data:/var/lib/mysql
|
||||
- ./docker/db/scripts:/docker-entrypoint-initdb.d/
|
||||
|
||||
gawa:
|
||||
build: ./web
|
||||
container_name: gawa
|
||||
command: python manage.py runserver 0.0.0.0:80
|
||||
main:
|
||||
build: ./docker/main
|
||||
command: bash -c "echo 'setting django up'
|
||||
&& python manage.py 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:
|
||||
- ./gawa:/app
|
||||
ports:
|
||||
- 8080:80
|
||||
environment:
|
||||
- MARIADB_NAME=gawa
|
||||
- MARIADB_USER=gawa
|
||||
- MARIADB_PASSWORD=changethisforprod
|
||||
networks:
|
||||
- internal
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
nginx:
|
||||
# only for developement. Use dedicated static container in prod
|
||||
image: nginx
|
||||
container_name: gawa-web
|
||||
caddy:
|
||||
image: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8081:8081"
|
||||
# - "443:443"
|
||||
# - "443:443/udp"
|
||||
volumes:
|
||||
- ./web/templates:/etc/nginx/templates
|
||||
- ./gawa/static:/var/www/static
|
||||
- ./gawa/media:/var/www/media
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- NGINX_HOST=0.0.0.0
|
||||
- NGINX_PORT=80
|
||||
networks:
|
||||
- internal
|
||||
depends_on:
|
||||
- gawa
|
||||
- $PWD/docker/caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
- ./gawa/static:/srv/static
|
||||
- ./gawa/media:/srv/media
|
||||
|
||||
phpmyadmin:
|
||||
db-admin:
|
||||
image: phpmyadmin
|
||||
container_name: gawa-db-admin
|
||||
networks:
|
||||
- internal
|
||||
ports:
|
||||
- "127.0.0.1:8082:80"
|
||||
- 8080:80
|
||||
environment:
|
||||
- PMA_HOST=db
|
||||
- PMA_ABSOLUTE_URI=http://localhost:8080
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
|
||||
networks:
|
||||
internal:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
# db_data:
|
||||
|
|
|
@ -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'
|
|
@ -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
|
||||
}
|
|
@ -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'@'%';
|
|
@ -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`;
|
|
@ -8,3 +8,5 @@ RUN apt update && apt install -y gettext && rm -rf /var/lib/apt/lists/*
|
|||
COPY requirements.txt /app/
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . /app/
|
||||
RUN mkdir -p /app/static
|
||||
RUN pygmentize -S nord -f html -a .codehilite > /app/static/codehighlight.css
|
|
@ -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
|
|
@ -1,5 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.utils.translation import gettext as _
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from .models import *
|
||||
|
||||
@admin.register(Category)
|
||||
|
@ -15,6 +17,7 @@ def regenerate(modeladmin, request, queryset):
|
|||
for obj in queryset:
|
||||
obj.regenerate()
|
||||
|
||||
|
||||
@admin.register(BlogPost)
|
||||
class BlogPostAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
|
@ -24,3 +27,16 @@ class BlogPostAdmin(admin.ModelAdmin):
|
|||
date_hierarchy = "date"
|
||||
ordering = ['title_de', 'title_en']
|
||||
actions = [regenerate]
|
||||
|
||||
change_list_template = "admin/blogpost.html"
|
||||
|
||||
def get_urls(self):
|
||||
urls = super().get_urls()
|
||||
my_urls = [
|
||||
path('sync/', self.sync_with_fs),
|
||||
]
|
||||
return my_urls + urls
|
||||
|
||||
def sync_with_fs(self, request):
|
||||
BlogPost.sync_with_fs()
|
||||
return HttpResponseRedirect("../")
|
||||
|
|
|
@ -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.
|
||||
"""
|
|
@ -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
|
|
@ -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
|
|
@ -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):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('start', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Category',
|
||||
'verbose_name_plural': 'Categories',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogPost',
|
||||
fields=[
|
||||
('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',),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -6,14 +6,13 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0003_blogpost_public'),
|
||||
('blog', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
migrations.AlterField(
|
||||
model_name='blogpost',
|
||||
name='slug',
|
||||
field=models.SlugField(default='test'),
|
||||
preserve_default=False,
|
||||
field=models.SlugField(unique=True),
|
||||
),
|
||||
]
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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'},
|
||||
),
|
||||
]
|
|
@ -1,10 +1,35 @@
|
|||
import toml
|
||||
import ast
|
||||
import re
|
||||
import os
|
||||
import pathlib
|
||||
import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from start.models import Searchable
|
||||
from start.models import Keyword, Searchable
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXTENSIONS = [
|
||||
"extra",
|
||||
"admonition",
|
||||
"codehilite",
|
||||
"meta",
|
||||
"toc"
|
||||
]
|
||||
EXTENSION_CONFIGS = {
|
||||
'codehilite': {
|
||||
'linenums': True,
|
||||
'pygments_style': 'monokai'
|
||||
},
|
||||
}
|
||||
|
||||
MD = markdown.Markdown(extensions=EXTENSIONS,
|
||||
extension_configs=EXTENSION_CONFIGS)
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
"""
|
||||
A category of blog posts
|
||||
|
@ -12,8 +37,8 @@ class Category(models.Model):
|
|||
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
|
||||
"""
|
||||
name= models.CharField(max_length=50)
|
||||
slug = models.SlugField()
|
||||
name = models.CharField(max_length=50)
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Category")
|
||||
|
@ -22,17 +47,54 @@ class Category(models.Model):
|
|||
def __str__(self):
|
||||
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):
|
||||
"""
|
||||
Should contain a blogpost
|
||||
"""
|
||||
body = models.TextField()
|
||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
|
||||
thumbnail = models.ImageField(blank=True, upload_to="img/thumbnails")
|
||||
featured = models.BooleanField(default=False)
|
||||
markdown = models.BooleanField(default=False)
|
||||
slug = models.SlugField()
|
||||
DATA_DIR = "/app/blog/data/articles"
|
||||
DEFAULT_LANGS = {'en': False, 'de': False}
|
||||
META_TOP_KEYS = [
|
||||
"date",
|
||||
"update",
|
||||
"keywords",
|
||||
"category",
|
||||
"featured",
|
||||
"public",
|
||||
"lang"]
|
||||
META_LANG_KEYS = ["title", "subtitle", "desc"]
|
||||
|
||||
body_en = models.TextField(blank=True,
|
||||
default="""Dieser Artikel ist nicht auf deutsch verfügbar.""")
|
||||
body_de = models.TextField(blank=True,
|
||||
default="""This aritcle is not available in english.""")
|
||||
category = models.ForeignKey(
|
||||
Category, on_delete=models.SET_DEFAULT, blank=False,
|
||||
default=Category.get_or_create_uncategorized)
|
||||
thumbnail = models.ImageField(
|
||||
blank=True,
|
||||
upload_to="img/thumbnails",
|
||||
default="img/thumbnails/default.jpg")
|
||||
featured = models.BooleanField(default=False)
|
||||
langs = models.CharField(
|
||||
default=DEFAULT_LANGS.__repr__(), max_length=64)
|
||||
slug = models.SlugField(unique=True, blank=False)
|
||||
|
||||
def save(self):
|
||||
# check if the slug is empty if we remove whitespaces
|
||||
if len(self.slug.strip()) == 0:
|
||||
logger.error(
|
||||
f"trying to save '{self.__class__}' with empty slug: {self}")
|
||||
raise ValueError(f"trying to save '{self.__class__}' with empty slug: {self}")
|
||||
super().save()
|
||||
|
||||
def regenerate(self):
|
||||
"""
|
||||
|
@ -41,9 +103,181 @@ class BlogPost(Searchable):
|
|||
Implements the abstract method of Searchable
|
||||
"""
|
||||
logger.info(f"regenerating {self.__class__.__name__} object: {self}")
|
||||
# url stuff
|
||||
self.suburl = f"/blog/{self.category.name}/{self.slug}"
|
||||
|
||||
# load from markdown
|
||||
# self.sync_file()
|
||||
|
||||
# redundand vvvv
|
||||
self.save()
|
||||
|
||||
@staticmethod
|
||||
def __patch_html(html: str) -> str:
|
||||
soup = BeautifulSoup(html)
|
||||
|
||||
# add bootstrap classes to regular tables
|
||||
tables = soup.select("table")
|
||||
for table in tables:
|
||||
if 'class' in table.attrs and "codehilitetable" in table.attrs['class']:
|
||||
# table has at least one class already AND
|
||||
# this table is a codehighlighting table,
|
||||
# not a regular one
|
||||
continue
|
||||
# set the bootstrap classes for tables
|
||||
table.attrs['class'] = 'table table-striped border'
|
||||
return soup.prettify()
|
||||
|
||||
def sync_file(self):
|
||||
"""
|
||||
generate an article fromm it's original markdown file
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"syncing article to markdown for: {self}")
|
||||
# read metadata
|
||||
try:
|
||||
fmeta = open(f"{self.DATA_DIR}/{self.slug}.toml", "r")
|
||||
except Exception as e:
|
||||
logger.error(f"could not find meta file for '{self}'")
|
||||
return
|
||||
data = toml.load(fmeta)
|
||||
langs = self.get_langs()
|
||||
for key in self.META_TOP_KEYS:
|
||||
if key not in data:
|
||||
logger.error(f"Key '{key}' missing in meta file for '{self}'")
|
||||
raise ValueError(
|
||||
f"Key '{key}' missing in meta file for '{self}'")
|
||||
for lang in self.DEFAULT_LANGS.keys():
|
||||
if lang not in data['lang']:
|
||||
langs[lang] = False
|
||||
else:
|
||||
langs[lang] = True
|
||||
self.set_langs(langs)
|
||||
for lang in data['lang']:
|
||||
for key in self.META_LANG_KEYS:
|
||||
if key not in data['lang'][lang]:
|
||||
logger.warning(
|
||||
f"Key '{key}' ('{lang}') missing in meta file for '{self}'")
|
||||
langs[lang] = False
|
||||
else:
|
||||
langs[lang] = True
|
||||
if not langs[lang]:
|
||||
# no translation for this language
|
||||
continue
|
||||
with open(f"{self.DATA_DIR}/{lang}-{self.slug}.md") as f_en:
|
||||
MD.reset()
|
||||
body: str = f_en.read()
|
||||
match lang:
|
||||
case "en":
|
||||
self.title_en = data['lang'][lang]["title"]
|
||||
self.subtitle_en = data['lang'][lang]["subtitle"]
|
||||
self.desc_en = data['lang'][lang]["desc"]
|
||||
self.body_en = BlogPost.__patch_html(MD.convert(body))
|
||||
case "de":
|
||||
self.title_de = data['lang'][lang]["title"]
|
||||
self.subtitle_de = data['lang'][lang]["subtitle"]
|
||||
self.desc_de = data['lang'][lang]["desc"]
|
||||
self.body_de = BlogPost.__patch_html(MD.convert(body))
|
||||
case _:
|
||||
logger.error(
|
||||
f"unknown language '{lang}' in meta file for '{self}'")
|
||||
self.date = data["date"]
|
||||
self.update = data["update"]
|
||||
self.featured = data["featured"]
|
||||
self.public = data["public"]
|
||||
# NOTE: thumbnail is optional
|
||||
if "thumbnail" in data:
|
||||
self.thumbnail = data["thumbnail"]
|
||||
# NOTE: category is optional
|
||||
if "category" in data:
|
||||
try:
|
||||
category: Category = Category.objects.get(
|
||||
slug=data['category'])
|
||||
except Category.DoesNotExist:
|
||||
category = Category.objects.create(
|
||||
name=data['category'], slug=data['category'])
|
||||
self.category = category
|
||||
self.save()
|
||||
for keyword in data["keywords"]:
|
||||
try:
|
||||
self.keywords.add(Keyword.objects.get(slug=keyword))
|
||||
except Keyword.DoesNotExist:
|
||||
self.keywords.create(
|
||||
slug=keyword, text_en=keyword, text_de=keyword)
|
||||
self.save()
|
||||
|
||||
def get_langs(self) -> dict[str, bool]:
|
||||
"""
|
||||
get available languages
|
||||
"""
|
||||
# SECURITY:
|
||||
# If someone could inject the langs field, literal_eval will evaluate to
|
||||
# any literal Python value. This should not be a concern.
|
||||
# If we would use a regular eval here, an attacker who controls one of
|
||||
# the `langs` of a BlogPost could run arbitrary Python code.
|
||||
try:
|
||||
# literal_eval is safe because it only parses literals, not any kind
|
||||
# of python expression. If the given str is not a valid literal,
|
||||
# ValueError will be raised
|
||||
langs = ast.literal_eval(str(self.langs))
|
||||
return langs
|
||||
except ValueError as e:
|
||||
logger.error(
|
||||
f"could not safely evaluate 'langs' for '{self}': {e}")
|
||||
raise e
|
||||
|
||||
def set_langs(self, langs: dict[str, bool]):
|
||||
"""
|
||||
set available languages
|
||||
"""
|
||||
self.langs = langs.__repr__()
|
||||
|
||||
@ classmethod
|
||||
def sync_with_fs(cls):
|
||||
"""
|
||||
Sync all Blog Posts with the filesystem.
|
||||
|
||||
Caution: Will delete all Blog Posts
|
||||
"""
|
||||
# logger.name = logger.name + ".sync_with_fs"
|
||||
|
||||
# delete all existing objects
|
||||
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:
|
||||
verbose_name = _("blog post")
|
||||
verbose_name_plural = _("blog posts")
|
||||
|
|
|
@ -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 %}
|
|
@ -1,26 +1,96 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load helper_tags %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% block languagecode %}{{ LANGUAGE_CODE }}{% endblock languagecode %}
|
||||
{% block title %}{% translate "cscherr.de" %} - {% translate "Blog" %}{% endblock title %}
|
||||
{% block languagecode %}
|
||||
{{ LANGUAGE_CODE }}
|
||||
{% endblock languagecode %}
|
||||
{% block title %}
|
||||
{% translate "cscherr.de" %} - {% translate "Blog" %}
|
||||
{% endblock title %}
|
||||
{% block nav %}
|
||||
{% include 'nav.html' %}
|
||||
{% include 'nav.html' %}
|
||||
{% endblock nav %}
|
||||
{% block headscripts %}
|
||||
<script type="text/javascript" id="MathJax-script" async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3.0.0/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
<script type="text/javascript"
|
||||
id="MathJax-script"
|
||||
async
|
||||
src="https://cdn.jsdelivr.net/npm/mathjax@3.0.0/es5/tex-mml-chtml.js"></script>
|
||||
{% endblock headscripts %}
|
||||
{% block main %}
|
||||
<div class="container-xl">
|
||||
<div class="jumbotron text-center">
|
||||
<h1>{{ post.title }} <small class="">{{ post.subtitle }}</small></h1>
|
||||
<img src="{{ post.thumbnail.url }}" alt="thumbnail">
|
||||
</div>
|
||||
<div class="container">
|
||||
$$x=\frac{-b+\sqrt{b^2-4ac}}{2a}$$
|
||||
{{ post.body | safe }}
|
||||
</div>
|
||||
{% include 'blog/featured.html' %}
|
||||
</div>
|
||||
{% endblock main %}
|
||||
<div class="container-xl">
|
||||
<article>
|
||||
<div class="jumbotron my-5">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<picture>
|
||||
<img src="{{ post.thumbnail.url }}" alt="thumbnail" class="img-fluid">
|
||||
</picture>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row py-5">
|
||||
{% if LANGUAGE_CODE == "de" %}
|
||||
<h1 class="">
|
||||
{{ post.title_de }}
|
||||
<br>
|
||||
<small class="fs-4">{{ post.subtitle_de }}</small>
|
||||
</h1>
|
||||
<br>
|
||||
{% elif LANGUAGE_CODE == "en" %}
|
||||
<h1 class="">
|
||||
{{ post.title_en }}
|
||||
<br>
|
||||
<small class="fs-4">{{ post.subtitle_en }}</small>
|
||||
</h1>
|
||||
<br>
|
||||
{% else %}
|
||||
<h1 class="">
|
||||
{{ post.title_en }}
|
||||
<br>
|
||||
<small class="fs-4">{{ post.subtitle_en }}</small>
|
||||
</h1>
|
||||
<br>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row py-5">
|
||||
{% if LANGUAGE_CODE == "de" %}
|
||||
<p class="lead">{{ post.desc_de }}</p>
|
||||
{% elif LANGUAGE_CODE == "en" %}
|
||||
<p class="lead">{{ post.desc_en }}</p>
|
||||
{% else %}
|
||||
<p class="lead">{{ post.desc_en }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row py-5">
|
||||
<p>
|
||||
<b>{{ post.category.name }}<b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="my-5">
|
||||
{% if LANGUAGE_CODE == "de" %}
|
||||
{{ post.body_de | safe }}
|
||||
{% elif LANGUAGE_CODE == "en" %}
|
||||
{{ post.body_en | safe }}
|
||||
{% else %}
|
||||
{{ post.body_en | safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
{% format_time post.date as date %}
|
||||
<p>{% trans "published" %}: {{ date }}</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
{% format_time post.update as update %}
|
||||
<p>{% trans "updated" %}: {{ update }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% include 'blog/featured.html' %}
|
||||
{% endblock main %}
|
||||
|
|
|
@ -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 %}
|
|
@ -1,40 +1,59 @@
|
|||
{% load i18n %}
|
||||
{% load helper_tags %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<div class="container-lg mt-5">
|
||||
<h4 class="">{% trans "Featured" %}</h4>
|
||||
<div class="row row-cols-1 row-cols-md-5 my-4">
|
||||
{% for post in featured_posts %}
|
||||
<div class="card col m-2 p-0">
|
||||
<a class="text-reset link-offset-2 link-underline link-underline-opacity-0" href=" {% url 'blog:post' post.category.slug post.slug %}">
|
||||
<img src="{{ post.thumbnail.url }}" class="card-img-top img-fluid" style="max-height: 150px;" alt="thumbnail">
|
||||
<div class="card-body" style="height: 100px;">
|
||||
{% if LANGUAGE_CODE == "de" %}
|
||||
<h5 class="card-title">{{ post.title_de }}<small class="text-body-secondary">{{ post.subtitle }}</small></h5>
|
||||
<p class="card-text">{{ post.desc_de }}</p>
|
||||
{% elif LANGUAGE_CODE == "en" %}
|
||||
<h5 class="card-title">{{ post.title_en }}<small class="text-body-secondary">{{ post.subtitle }}</small></h5>
|
||||
<p class="card-text">{{ post.desc_en }}</p>
|
||||
{% else %}
|
||||
<h5 class="card-title">{{ post.title_en }}<small class="text-body-secondary">{{ post.subtitle }}</small></h5>
|
||||
<p class="card-text">{{ post.desc_en }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="container pt-5">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">{% translate "category" %}: <b>{{ post.category.name }}</b></li>
|
||||
{% for keyword in post.keywords.all %}
|
||||
<div class="card col m-2 p-0">
|
||||
<a class="text-reset link-offset-2 link-underline link-underline-opacity-0"
|
||||
href=" {% url 'blog:post' post.category.slug post.slug %}">
|
||||
<img src="{{ post.thumbnail.url }}"
|
||||
class="card-img-top img-fluid mx-auto d-block"
|
||||
style="max-height: 150px; width: auto; padding-top: 8px;"
|
||||
alt="thumbnail" />
|
||||
<div class="card-body" style="height: 100px">
|
||||
{% if LANGUAGE_CODE == "de" %}
|
||||
<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" %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container pt-5">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
{% translate "category" %}: <b>{{ post.category.name }}</b>
|
||||
</li>
|
||||
{% for keyword in post.keywords.all %}
|
||||
{% if LANGUAGE_CODE == "de" %}
|
||||
<li class="list-group-item">{{ keyword.text_de }}</li>
|
||||
{% elif LANGUAGE_CODE == "en" %}
|
||||
<li class="list-group-item">{{ keyword.text_en }}</li>
|
||||
{% else %}
|
||||
<li class="list-group-item">{{ keyword.text_en }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="container p-1 text-center" style="border-top: solid">
|
||||
<li class="list-group-item">
|
||||
{% format_time post.date "%F" as date %}
|
||||
{% trans "published" %}: {{ date }}
|
||||
</li>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,6 @@ from . import views
|
|||
app_name: str = "blog"
|
||||
urlpatterns = [
|
||||
path("", views.Index.as_view(), name="index"),
|
||||
path("categories", views.CategoryList.as_view(), name="category_list"),
|
||||
path("<slug:slug>", views.ArticleList.as_view(), name="article_list"),
|
||||
path("browse", views.Browse.as_view(), name="browse"),
|
||||
path("<slug:category>/<slug:slug>", views.Post.as_view(), name="post"),
|
||||
]
|
||||
|
|
|
@ -12,7 +12,7 @@ class Index(TemplateView, SearchableView):
|
|||
"""
|
||||
The index page of the gawa/blog app.
|
||||
|
||||
Utilizes a generic view, because I will do so for all views,
|
||||
Utilizes a generic view, because I will do so for all views,
|
||||
a regular view function would suffice.
|
||||
"""
|
||||
|
||||
|
@ -21,7 +21,6 @@ class Index(TemplateView, SearchableView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['featured_posts'] = BlogPost.objects.filter(featured=True, public=True)
|
||||
logger.debug(f"loaded featured posts: {context['featured_posts']}")
|
||||
return context
|
||||
|
||||
class Post(DetailView):
|
||||
|
@ -36,49 +35,9 @@ class Post(DetailView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['featured_posts'] = BlogPost.objects.filter(featured=True)
|
||||
logger.debug(f"loaded featured posts: {context['featured_posts']}")
|
||||
return context
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
obj = get_object_or_404(
|
||||
BlogPost,
|
||||
category__slug=self.kwargs['category'], # first slug is category
|
||||
slug=self.kwargs['slug'] # second slug is article itself
|
||||
)
|
||||
match get_language():
|
||||
case 'de':
|
||||
logger.debug("setting language unspecific attributes for language: de")
|
||||
obj.title = obj.title_de
|
||||
obj.subtitle = obj.subtitle_de
|
||||
obj.desc = obj.desc_de
|
||||
|
||||
case 'en':
|
||||
logger.debug("setting language unspecific attributes for language: en")
|
||||
obj.title = obj.title_en
|
||||
obj.subtitle = obj.subtitle_en
|
||||
obj.desc = obj.desc_en
|
||||
|
||||
case _:
|
||||
# this should not happen, but who knows what dumb stuff users will come up with
|
||||
logger.warning("article for unsupported language was requested")
|
||||
return obj
|
||||
|
||||
class CategoryList(ListView):
|
||||
"""
|
||||
Scroll through a list of blog Categories
|
||||
"""
|
||||
|
||||
model=Category
|
||||
template_name = "blog/categories.html"
|
||||
context_object_name = "categories"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['featured_posts'] = BlogPost.objects.filter(featured=True)
|
||||
logger.debug(f"loaded featured posts: {context['featured_posts']}")
|
||||
return context
|
||||
|
||||
class ArticleList(ListView):
|
||||
class Browse(ListView):
|
||||
"""
|
||||
Scroll through a list of blog posts
|
||||
|
||||
|
@ -87,11 +46,10 @@ class ArticleList(ListView):
|
|||
"""
|
||||
|
||||
model=BlogPost
|
||||
template_name = "blog/posts.html"
|
||||
template_name = "blog/browse.html"
|
||||
context_object_name = "posts"
|
||||
|
||||
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
|
||||
|
|
|
@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/3.2/ref/settings/
|
|||
"""
|
||||
|
||||
# for getting envvars
|
||||
import logging
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import os
|
||||
|
||||
# default django
|
||||
|
@ -35,7 +37,7 @@ ALLOWED_HOSTS = ["*"]
|
|||
|
||||
# Allow inclusion of stuff from these origins
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"https://static.cscherr.de",
|
||||
"https://static.cscherr.de",
|
||||
]
|
||||
|
||||
# Application definition
|
||||
|
@ -61,7 +63,6 @@ MIDDLEWARE = [
|
|||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'start.middleware.LangBasedOnUrlMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'gawa.urls'
|
||||
|
@ -100,7 +101,6 @@ DATABASES = {
|
|||
}
|
||||
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||
|
||||
|
@ -123,7 +123,6 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
LANGUAGES = [
|
||||
("de", _("German")),
|
||||
|
@ -155,10 +154,10 @@ STATIC_URL = '/static/'
|
|||
COMPRESS_ENABLED = True
|
||||
|
||||
STATICFILES_FINDERS = [
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'compressor.finders.CompressorFinder',
|
||||
]
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'compressor.finders.CompressorFinder',
|
||||
]
|
||||
|
||||
COMPRESS_PRECOMPILERS = (
|
||||
('text/x-scss', 'django_libsass.SassCompiler'),
|
||||
|
@ -173,7 +172,6 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||
|
||||
# Logging configs
|
||||
|
||||
import logging
|
||||
|
||||
myServerFormatter = ServerFormatter
|
||||
myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S"
|
||||
|
@ -294,7 +292,7 @@ LOGGING = {
|
|||
# Media stuff
|
||||
# this is where user uploaded files will go.
|
||||
# TODO change this for prod
|
||||
#MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media"
|
||||
# MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media"
|
||||
MEDIA_ROOT = "/app/media"
|
||||
MEDIA_URL = "/media/"
|
||||
FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload"
|
||||
# FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload"
|
||||
|
|
|
@ -14,14 +14,14 @@ Including another URLconf
|
|||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.urls import include, re_path
|
||||
from django.urls import include, path
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^i18n/', include('django.conf.urls.i18n')),
|
||||
re_path('i18n/', include('django.conf.urls.i18n')),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
|
|
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 |
|
@ -10,6 +10,7 @@ def regenerate(modeladmin, request, queryset):
|
|||
for obj in queryset:
|
||||
obj.regenerate()
|
||||
|
||||
|
||||
@admin.register(Keyword)
|
||||
class KeywordAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
|
@ -17,21 +18,25 @@ class KeywordAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
list_display = ["text_en", "text_de"]
|
||||
|
||||
|
||||
@admin.register(StaticSite)
|
||||
class StaticSiteAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin Interface for StaticSite
|
||||
"""
|
||||
list_display = ["title_en", "subtitle_en", "title_de", "subtitle_de", "suburl"]
|
||||
list_display = ["title_en", "subtitle_en",
|
||||
"title_de", "subtitle_de", "suburl"]
|
||||
ordering = ['title_de', 'title_en']
|
||||
actions = [regenerate]
|
||||
|
||||
|
||||
@admin.register(Searchable)
|
||||
class SearchableAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Abstract Admin Interface for all Searchables
|
||||
"""
|
||||
list_display = ["title_en", "subtitle_en", "title_de", "subtitle_de", "suburl"]
|
||||
list_display = ["title_en", "subtitle_en",
|
||||
"title_de", "subtitle_de", "suburl"]
|
||||
ordering = ['title_de', 'title_en']
|
||||
actions = [regenerate]
|
||||
|
||||
|
@ -41,6 +46,7 @@ class LinkAdmin(admin.ModelAdmin):
|
|||
"""
|
||||
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']
|
||||
actions = [regenerate]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StartConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'start'
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class MainSearchForm(forms.Form):
|
||||
search = forms.CharField(
|
||||
max_length=100,
|
||||
label=''
|
||||
max_length=100,
|
||||
label=''
|
||||
)
|
||||
search.widget = forms.TextInput(
|
||||
attrs={
|
||||
|
|
|
@ -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
|
|
@ -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):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
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',),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -1,3 +1,6 @@
|
|||
import random
|
||||
import favicon
|
||||
import requests
|
||||
from django.db import models
|
||||
from django.db.models.options import override
|
||||
from django.utils.translation import gettext as _
|
||||
|
@ -8,24 +11,23 @@ from django.conf import settings
|
|||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import requests
|
||||
import favicon
|
||||
import random
|
||||
|
||||
class Keyword(models.Model):
|
||||
"""
|
||||
this is the model that should contain searchable keywords
|
||||
"""
|
||||
slug = models.SlugField(unique=True)
|
||||
text_de = models.CharField(max_length=40)
|
||||
text_en = models.CharField(max_length=40)
|
||||
|
||||
def __str__(self):
|
||||
return f"{{<{self.__class__.__name__}>\"{self.text_en}\"}}"
|
||||
return f"{{<{self.__class__.__name__}>\"{self.slug}\"}}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Keyword")
|
||||
verbose_name_plural = _("keywords")
|
||||
|
||||
|
||||
class Searchable(models.Model):
|
||||
"""
|
||||
Abstract class for any model that should be searchable.
|
||||
|
@ -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
|
||||
"""
|
||||
title_de = models.CharField(max_length=50, default="Titel DE")
|
||||
title_en = models.CharField(max_length=50, default="title EN")
|
||||
title_de = models.CharField(max_length=50, default="Nicht übersetzt")
|
||||
title_en = models.CharField(max_length=50, default="Not translated")
|
||||
subtitle_de = models.CharField(max_length=50, blank=True)
|
||||
subtitle_en = models.CharField(max_length=50, blank=True)
|
||||
desc_de = models.CharField(max_length=250, unique=False, default="Beschreibung DE")
|
||||
desc_en = models.CharField(max_length=250, unique=False, default="Description EN")
|
||||
# may be empty/blank for some entries
|
||||
date = models.DateField(blank=True, null=True)
|
||||
keywords = models.ManyToManyField(Keyword)
|
||||
desc_de = models.TextField(
|
||||
blank=True, max_length=250, unique=False, default="Keine Beschreibung")
|
||||
desc_en = models.TextField(
|
||||
blank=True, max_length=250, unique=False, default="no description")
|
||||
date = models.DateTimeField(blank=True, null=True)
|
||||
update = models.DateTimeField(blank=True, null=True)
|
||||
keywords = models.ManyToManyField(Keyword, blank=True)
|
||||
suburl = models.CharField(max_length=200, blank=True, null=True)
|
||||
public = models.BooleanField(default=True)
|
||||
public = models.BooleanField(default=False)
|
||||
|
||||
@classmethod
|
||||
def regenerate_all_entries(cls):
|
||||
|
@ -61,34 +65,37 @@ class Searchable(models.Model):
|
|||
"""
|
||||
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:
|
||||
verbose_name = _("Searchable")
|
||||
verbose_name_plural = _("Searchables")
|
||||
|
||||
|
||||
class StaticSite(Searchable):
|
||||
"""
|
||||
This model represents any static site, such as start:index,
|
||||
This model represents any static site, such as start:index,
|
||||
that should show up in search.
|
||||
|
||||
Every searchable view should inherit from start.views.SearchableView.
|
||||
# TODO automate scanning for SearchableView classes
|
||||
"""
|
||||
|
||||
|
||||
def regenerate(self):
|
||||
"""
|
||||
regenerate a object
|
||||
"""
|
||||
logger.info(f"regenerating {self.__class__.__name__} object: {self}")
|
||||
logger.warning(f"{self.__class__.__name__} cannot regenerate.")
|
||||
#self.save()
|
||||
# self.save()
|
||||
pass
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("static site")
|
||||
verbose_name_plural = _("static sites")
|
||||
|
||||
|
||||
class Link(Searchable):
|
||||
"""
|
||||
contains all my interesting links
|
||||
|
@ -99,6 +106,7 @@ class Link(Searchable):
|
|||
favicon_dir: str = "img/links/favicons"
|
||||
favicon = models.ImageField(blank=True, upload_to=favicon_dir, null=True)
|
||||
status = models.BooleanField(default=False)
|
||||
personal = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"{{<{self.__class__.__name__}>\"{self.title_en}\"}}"
|
||||
|
@ -120,11 +128,13 @@ class Link(Searchable):
|
|||
icons = favicon.get(self.url, timeout=2)
|
||||
except (ConnectionError) as ce:
|
||||
# just keep whatever was stored if we cant get a new favicon
|
||||
logger.warn(f"unable to download favicon for {self}: {ce.with_traceback(None)}")
|
||||
logger.warn(
|
||||
f"unable to download favicon for {self}: {ce.with_traceback(None)}")
|
||||
self.status = False
|
||||
|
||||
except Exception as e:
|
||||
logger.warn(f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
|
||||
logger.warn(
|
||||
f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
|
||||
self.status = False
|
||||
|
||||
else:
|
||||
|
@ -143,9 +153,10 @@ class Link(Searchable):
|
|||
except FileNotFoundError as fe:
|
||||
logger.error(f"cant write favicon to file for {self}: {fe}")
|
||||
self.favicon = None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warn(f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
|
||||
logger.warn(
|
||||
f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
|
||||
self.favicon = None
|
||||
|
||||
self.save()
|
||||
|
|
|
@ -11,13 +11,9 @@
|
|||
{% compress css %}
|
||||
<link type="text/x-scss" href="/static/bs5/scss/bootstrap.scss" rel="stylesheet" media="screen">
|
||||
<link rel="stylesheet" href="/static/bsi1/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/static/codehighlight.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
{% endcompress %}
|
||||
<script>
|
||||
const setTheme = theme => {
|
||||
document.documentElement.setAttribute('data-bs-theme', theme)
|
||||
}
|
||||
setTheme(localStorage.getItem('theme'));
|
||||
</script>
|
||||
{% block headscripts %}
|
||||
{% endblock headscripts %}
|
||||
</head>
|
||||
|
@ -106,6 +102,7 @@
|
|||
</div>
|
||||
</footer>
|
||||
{% compress js %}
|
||||
<script src="/static/custom.js"></script>
|
||||
<script src="/static/bs5/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% endcompress %}
|
||||
</body>
|
||||
|
|
|
@ -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>
|
|
@ -1,89 +1,116 @@
|
|||
{% load i18n %}
|
||||
{% load helper_tags %}
|
||||
<nav class="sticky-top navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<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 %}
|
||||
</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>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% translate "Start" %}
|
||||
</a>
|
||||
<a class="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">{% translate "Start" %}</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'start:index' %}">{% translate "Start" %}</a></li>
|
||||
<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>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'start:index' %}">{% translate "Start" %}</a>
|
||||
</li>
|
||||
<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>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% translate "Blog" %}
|
||||
</a>
|
||||
<a class="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">{% translate "Blog" %}</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'blog:index' %}">{% translate "Start" %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'blog:category_list' %}">{% translate "category list" %}</a></li>
|
||||
<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>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Debug
|
||||
</a>
|
||||
<a class="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">Debug</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="http://localhost:8082" target="_blank">DB</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>
|
||||
<li>
|
||||
<a class="dropdown-item" href="http://localhost:8080" target="_blank">DB</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>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
{% get_available_languages as languages %}
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ LANGUAGE_CODE }}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{% for lang_code, lang_name in languages %}
|
||||
<li><a class="dropdown-item"
|
||||
href="{% change_lang lang_code %}">
|
||||
{{ lang_name }}</a></li>
|
||||
{% endfor %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||
</ul>
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<form action="{% url 'set_language' %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
<input name="next" type="hidden" value="{{ redirect_to }}">
|
||||
<a class="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">{{ LANGUAGE_CODE }}</a>
|
||||
<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 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>
|
||||
</ul>
|
||||
{% include 'main_search_form.html' %}
|
||||
</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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -5,11 +5,55 @@
|
|||
{% block title %}{% translate "cscherr.de" %} - {% translate "Links" %}{% endblock title %}
|
||||
{% block main %}
|
||||
<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">
|
||||
{% for link in 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 }}">
|
||||
<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" %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.template import Library
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.translation import activate, get_language
|
||||
from datetime import datetime
|
||||
|
||||
import re
|
||||
|
||||
|
@ -9,29 +10,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def change_lang(context, lang="de", *args, **kwargs):
|
||||
"""
|
||||
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
|
||||
|
||||
@register.simple_tag
|
||||
def format_time(timestamp: datetime, format: str = "%F %H:%M:%S %Z") -> str:
|
||||
return timestamp.strftime(format)
|
||||
|
|
|
@ -9,5 +9,4 @@ urlpatterns = [
|
|||
path("legal/", views.LegalInfo.as_view(), name="legal"),
|
||||
path("links/", views.Links.as_view(), name="links"),
|
||||
path("professional/", views.LegalInfo.as_view(), name="professional"),
|
||||
path('language/activate/<language_code>/', views.ActivateLanguage.as_view(), name='activate_language'),
|
||||
]
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.shortcuts import render
|
|||
from django.views.generic.list import QuerySet
|
||||
from django.db.models import Q
|
||||
from django.views.static import loader
|
||||
from django.views import i18n
|
||||
from requests import request
|
||||
|
||||
from .forms import MainSearchForm
|
||||
|
@ -18,6 +19,7 @@ from abc import ABC
|
|||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SearchableView(View, ABC):
|
||||
"""
|
||||
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"
|
||||
|
||||
|
||||
class Professional(TemplateView, SearchableView):
|
||||
"""
|
||||
Professional informations that might interest a professional employer
|
||||
|
@ -47,6 +50,7 @@ class Professional(TemplateView, SearchableView):
|
|||
# TODO
|
||||
template_name: str = "start/legalinfo.html"
|
||||
|
||||
|
||||
class LegalInfo(TemplateView, SearchableView):
|
||||
"""
|
||||
Legal info that the german authorities want.
|
||||
|
@ -54,20 +58,6 @@ class LegalInfo(TemplateView, SearchableView):
|
|||
# TODO
|
||||
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):
|
||||
"""
|
||||
Search for anything.
|
||||
|
@ -94,6 +84,7 @@ class MainSearch(ListView):
|
|||
return render(request, "errors/bad_request.html")
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class Links(ListView):
|
||||
"""
|
||||
This View contains links to various interesting sites.
|
||||
|
@ -104,7 +95,17 @@ class Links(ListView):
|
|||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
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
|
||||
|
||||
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
|
@ -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
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue