Compare commits

..

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

2894 changed files with 201860 additions and 1146 deletions

4
.gitignore vendored
View File

@ -18,7 +18,3 @@ 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

3
.gitmodules vendored
View File

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

View File

@ -1,35 +1,8 @@
# 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` |
# gawa
Gawa is my personal website. I've personally written it using the django framework.
## License
Bootstrap: MIT Licensed
Django: BSD 3-Clause "New" or "Revised" 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)
###### Gawa: MIT Licensed, see LICENSE

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

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,62 +1,66 @@
services:
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
- ./docker/db/scripts:/docker-entrypoint-initdb.d/
- ./db/data:/var/lib/mysql
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
"
gawa:
build: ./web
container_name: gawa
command: 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
caddy:
image: caddy
restart: unless-stopped
ports:
- "80:80"
- "8081:8081"
# - "443:443"
# - "443:443/udp"
nginx:
# only for developement. Use dedicated static container in prod
image: nginx
container_name: gawa-web
volumes:
- $PWD/docker/caddy/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
- ./gawa/static:/srv/static
- ./gawa/media:/srv/media
db-admin:
image: phpmyadmin
- ./web/templates:/etc/nginx/templates
- ./gawa/static:/var/www/static
- ./gawa/media:/var/www/media
ports:
- 8080:80
- 80:80
environment:
- NGINX_HOST=0.0.0.0
- NGINX_PORT=80
networks:
- internal
depends_on:
- gawa
phpmyadmin:
image: phpmyadmin
container_name: gawa-db-admin
networks:
- internal
ports:
- "127.0.0.1:8082:80"
environment:
- PMA_HOST=db
- PMA_ABSOLUTE_URI=http://localhost:8080
depends_on:
- db
volumes:
caddy_data:
caddy_config:
# db_data:
networks:
internal:
driver: bridge

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
from django.contrib import admin
from django.urls import path
from django.utils.translation import gettext as _
from django.http.response import HttpResponseRedirect
from .models import *
@admin.register(Category)
@ -17,7 +15,6 @@ def regenerate(modeladmin, request, queryset):
for obj in queryset:
obj.regenerate()
@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
"""
@ -27,16 +24,3 @@ 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("../")

View File

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

View File

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

View File

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

View File

@ -1,47 +1,12 @@
# Generated by Django 3.2.21 on 2023-10-02 08:14
# Generated by Django 3.2.19 on 2023-06-03 12:03
import blog.models
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations
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',),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,35 +1,10 @@
import toml
import ast
import re
import os
import pathlib
import markdown
from bs4 import BeautifulSoup
from django.db import models
from django.utils.translation import gettext as _
from start.models import Keyword, Searchable
from start.models import 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
@ -37,8 +12,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(unique=True)
name= models.CharField(max_length=50)
slug = models.SlugField()
class Meta:
verbose_name = _("Category")
@ -47,54 +22,17 @@ 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
"""
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")
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)
langs = models.CharField(
default=DEFAULT_LANGS.__repr__(), max_length=64)
slug = models.SlugField(unique=True, blank=False)
markdown = models.BooleanField(default=False)
slug = models.SlugField()
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):
"""
@ -103,181 +41,9 @@ 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")

View File

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

View File

@ -1,96 +1,26 @@
{% extends 'base.html' %}
{% 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">
<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 %}
<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 %}

View File

@ -1,17 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% load helper_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block languagecode %}
{{ LANGUAGE_CODE }}
{% endblock languagecode %}
{% block title %}
{% translate "cscherr.de" %} - {% translate "Blog" %}
{% endblock title %}
{% block nav %}
{% include 'nav.html' %}
{% endblock nav %}
{% block main %}
<div class="container-xl">{{ category }}</div>
{% include 'blog/featured.html' %}
{% endblock main %}

View File

@ -1,59 +1,40 @@
{% load i18n %}
{% load helper_tags %}
{% get_current_language as LANGUAGE_CODE %}
<div class="container-lg mt-5">
<h4 class="">{% trans "Featured" %}</h4>
<div class="row row-cols-1 row-cols-md-5 my-4">
{% 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 mx-auto d-block"
style="max-height: 150px; width: auto; padding-top: 8px;"
alt="thumbnail" />
<div class="card-body" style="height: 100px">
<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 %}
{% 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>
<li class="list-group-item">{{ keyword.text_de }}</li>
{% 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>
<li class="list-group-item">{{ keyword.text_en }}</li>
{% 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>
<li class="list-group-item">{{ keyword.text_en }}</li>
{% endif %}
</div>
<div class="container pt-5">
<ul class="list-group list-group-flush">
<li class="list-group-item">
{% translate "category" %}: <b>{{ post.category.name }}</b>
</li>
{% for keyword in post.keywords.all %}
{% if LANGUAGE_CODE == "de" %}
<li class="list-group-item">{{ keyword.text_de }}</li>
{% elif LANGUAGE_CODE == "en" %}
<li class="list-group-item">{{ keyword.text_en }}</li>
{% else %}
<li class="list-group-item">{{ keyword.text_en }}</li>
{% endif %}
{% endfor %}
</ul>
</div>
<div class="container p-1 text-center" style="border-top: solid">
<li class="list-group-item">
{% format_time post.date "%F" as date %}
{% trans "published" %}: {{ date }}
</li>
</div>
</a>
</div>
{% endfor %}
</ul>
</div>
</a>
</div>
{% endfor %}
</div>
</div>

View File

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

View File

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

View File

@ -11,8 +11,6 @@ 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
@ -37,7 +35,7 @@ ALLOWED_HOSTS = ["*"]
# Allow inclusion of stuff from these origins
CORS_ALLOWED_ORIGINS = [
"https://static.cscherr.de",
"https://static.cscherr.de",
]
# Application definition
@ -63,6 +61,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
'start.middleware.LangBasedOnUrlMiddleware',
]
ROOT_URLCONF = 'gawa.urls'
@ -101,6 +100,7 @@ DATABASES = {
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
@ -123,6 +123,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
from django.utils.translation import gettext_lazy as _
LANGUAGES = [
("de", _("German")),
@ -154,10 +155,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'),
@ -172,6 +173,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Logging configs
import logging
myServerFormatter = ServerFormatter
myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S"
@ -292,7 +294,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"

View File

@ -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 = [
re_path('i18n/', include('django.conf.urls.i18n')),
url(r'^i18n/', include('django.conf.urls.i18n')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += i18n_patterns(

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -10,7 +10,6 @@ def regenerate(modeladmin, request, queryset):
for obj in queryset:
obj.regenerate()
@admin.register(Keyword)
class KeywordAdmin(admin.ModelAdmin):
"""
@ -18,25 +17,21 @@ 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]
@ -46,7 +41,6 @@ class LinkAdmin(admin.ModelAdmin):
"""
Admin Interface for Links
"""
list_display = ["title_en", "title_de", "url",
"suburl", "favicon", "status", "personal"]
list_display = ["title_en", "title_de", "url", "suburl", "favicon", "status"]
ordering = ['status', 'title_de', 'title_en']
actions = [regenerate]

View File

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

View File

@ -1,11 +1,10 @@
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={

25
gawa/start/middleware.py Normal file
View File

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

View File

@ -1,75 +1,12 @@
# Generated by Django 3.2.21 on 2023-10-02 08:14
# Generated by Django 3.2.19 on 2023-06-03 12:03
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations
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',),
),
]

View File

@ -0,0 +1,46 @@
# Generated by Django 3.2.19 on 2023-06-03 12:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('start', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Keyword',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text_de', models.CharField(max_length=40)),
('text_en', models.CharField(max_length=40)),
],
),
migrations.CreateModel(
name='Searchable',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title_de', models.CharField(default='Titel DE', max_length=50)),
('title_en', models.CharField(default='title en', max_length=50)),
('subtitle_de', models.CharField(blank=True, max_length=50)),
('subtitle_en', models.CharField(blank=True, max_length=50)),
('desc_de', models.CharField(default='Beschreibung DE', max_length=250, unique=True)),
('desc_en', models.CharField(default='Description EN', max_length=250, unique=True)),
('date', models.DateField(blank=True, null=True)),
('suburl', models.CharField(blank=True, max_length=200, null=True)),
('keywords', models.ManyToManyField(to='start.Keyword')),
],
),
migrations.CreateModel(
name='StaticSite',
fields=[
('searchable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='start.searchable')),
],
bases=('start.searchable',),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,3 @@
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 _
@ -11,23 +8,24 @@ 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.slug}\"}}"
return f"{{<{self.__class__.__name__}>\"{self.text_en}\"}}"
class Meta:
verbose_name = _("Keyword")
verbose_name_plural = _("keywords")
class Searchable(models.Model):
"""
Abstract class for any model that should be searchable.
@ -35,19 +33,17 @@ 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="Nicht übersetzt")
title_en = models.CharField(max_length=50, default="Not translated")
title_de = models.CharField(max_length=50, default="Titel DE")
title_en = models.CharField(max_length=50, default="title EN")
subtitle_de = models.CharField(max_length=50, blank=True)
subtitle_en = models.CharField(max_length=50, blank=True)
desc_de = models.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)
desc_de = models.CharField(max_length=250, unique=False, default="Beschreibung DE")
desc_en = models.CharField(max_length=250, unique=False, default="Description EN")
# may be empty/blank for some entries
date = models.DateField(blank=True, null=True)
keywords = models.ManyToManyField(Keyword)
suburl = models.CharField(max_length=200, blank=True, null=True)
public = models.BooleanField(default=False)
public = models.BooleanField(default=True)
@classmethod
def regenerate_all_entries(cls):
@ -65,37 +61,34 @@ 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
@ -106,7 +99,6 @@ 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}\"}}"
@ -128,13 +120,11 @@ 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:
@ -153,10 +143,9 @@ 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()

View File

@ -11,9 +11,13 @@
{% 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>
@ -102,7 +106,6 @@
</div>
</footer>
{% compress js %}
<script src="/static/custom.js"></script>
<script src="/static/bs5/dist/js/bootstrap.bundle.min.js"></script>
{% endcompress %}
</body>

View File

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

View File

@ -1,116 +1,89 @@
{% 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:browse' %}">{% translate "Browse" %}</a>
</li>
<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>
</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: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>
<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>
</ul>
</li>
<li class="nav-item dropdown">
{% 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>
{% 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>
</li>
<li class="nav-item">
<button type="button" class="btn btn-dark" id="toggleThemeButton">
<i id="toggleThemeIcon" class="bi bi-sun"></i>
</button>
{% include 'dark_light_switch.html' %}
</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>

View File

@ -1,40 +1 @@
{% 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 %}
TODO

View File

@ -5,55 +5,11 @@
{% block title %}{% translate "cscherr.de" %} - {% translate "Links" %}{% endblock title %}
{% block main %}
<div class="container-fluid mt-5">
<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>
<h4 class="">{% trans "Links" %}</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 }}" target="_blank">
<a class="text-reset link-offset-2 link-underline link-underline-opacity-0" href="{{ link.url }}">
<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" %}

View File

@ -1,7 +1,6 @@
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
@ -10,6 +9,29 @@ logger = logging.getLogger(__name__)
register = Library()
@register.simple_tag
def format_time(timestamp: datetime, format: str = "%F %H:%M:%S %Z") -> str:
return timestamp.strftime(format)
@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

View File

@ -9,4 +9,5 @@ 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'),
]

View File

@ -8,7 +8,6 @@ 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
@ -19,7 +18,6 @@ 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
@ -42,7 +40,6 @@ class Index(TemplateView, SearchableView):
template_name: str = "start/index.html"
class Professional(TemplateView, SearchableView):
"""
Professional informations that might interest a professional employer
@ -50,7 +47,6 @@ class Professional(TemplateView, SearchableView):
# TODO
template_name: str = "start/legalinfo.html"
class LegalInfo(TemplateView, SearchableView):
"""
Legal info that the german authorities want.
@ -58,6 +54,20 @@ 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.
@ -84,7 +94,6 @@ 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.
@ -95,17 +104,7 @@ class Links(ListView):
def get_queryset(self) -> QuerySet:
object_list = Link.objects.filter(
status=True, public=True, personal=False
status=True, public=True
)
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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,80 @@
/* TABLETS */
@media (max-width: 1024px) {
[dir="rtl"] .colMS {
margin-right: 0;
}
[dir="rtl"] #user-tools {
text-align: right;
}
[dir="rtl"] #changelist .actions label {
padding-left: 10px;
padding-right: 0;
}
[dir="rtl"] #changelist .actions select {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .change-list .filtered .results,
[dir="rtl"] .change-list .filtered .paginator,
[dir="rtl"] .filtered #toolbar,
[dir="rtl"] .filtered div.xfull,
[dir="rtl"] .filtered .actions,
[dir="rtl"] #changelist-filter {
margin-left: 0;
}
[dir="rtl"] .inline-group ul.tools a.add,
[dir="rtl"] .inline-group div.add-row a,
[dir="rtl"] .inline-group .tabular tr.add-row td a {
padding: 8px 26px 8px 10px;
background-position: calc(100% - 8px) 9px;
}
[dir="rtl"] .related-widget-wrapper-link + .selector {
margin-right: 0;
margin-left: 15px;
}
[dir="rtl"] .selector .selector-filter label {
margin-right: 0;
margin-left: 8px;
}
[dir="rtl"] .object-tools li {
float: right;
}
[dir="rtl"] .object-tools li + li {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .dashboard .module table td a {
padding-left: 0;
padding-right: 16px;
}
}
/* MOBILE */
@media (max-width: 767px) {
[dir="rtl"] .aligned .related-lookup,
[dir="rtl"] .aligned .datetimeshortcuts {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .aligned ul {
margin-right: 0;
}
[dir="rtl"] #changelist-filter {
margin-left: 0;
margin-right: 0;
}
}

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