Compare commits

..

No commits in common. "9a4f535ca2581ba9f9ebca733e6f4fcac96ebdb3" and "f8c3577a5458564b1cff419054fa9175b409c1cc" have entirely different histories.

13 changed files with 73 additions and 145 deletions

View file

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

View file

@ -1,8 +1,3 @@
import ast
import re
import os
import pathlib
import markdown
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from start.models import Searchable from start.models import Searchable
@ -10,6 +5,7 @@ from start.models import Searchable
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import markdown
EXTENSIONS = [ EXTENSIONS = [
"extra", "extra",
"admonition", "admonition",
@ -24,9 +20,11 @@ EXTENSION_CONFIGS = {
}, },
} }
MD = markdown.Markdown(extensions=EXTENSIONS, MD = markdown.Markdown(extensions=EXTENSIONS, extension_configs=EXTENSION_CONFIGS)
extension_configs=EXTENSION_CONFIGS)
import pathlib
import os
import re
class Category(models.Model): class Category(models.Model):
""" """
@ -35,8 +33,8 @@ class Category(models.Model):
Name not translated because it would make i18n in urls and Searchables specifically a pain. Name not translated because it would make i18n in urls and Searchables specifically a pain.
Maybe some day it would be cool if these were Searchable Maybe some day it would be cool if these were Searchable
""" """
name = models.CharField(max_length=50) name= models.CharField(max_length=50)
slug = models.SlugField(unique=True) slug = models.SlugField()
class Meta: class Meta:
verbose_name = _("Category") verbose_name = _("Category")
@ -45,19 +43,16 @@ class Category(models.Model):
def __str__(self): def __str__(self):
return f"{{<{self.__class__.__name__}>\"{self.name}\"}}" return f"{{<{self.__class__.__name__}>\"{self.name}\"}}"
class BlogPost(Searchable): class BlogPost(Searchable):
""" """
Should contain a blogpost Should contain a blogpost
""" """
body_en = models.TextField(blank=True, default="") body_en = models.TextField(blank=True, default="")
body_de = models.TextField(blank=True, default="") body_de = models.TextField(blank=True, default="")
category = models.ForeignKey( category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
Category, on_delete=models.SET_NULL, null=True)
thumbnail = models.ImageField(blank=True, upload_to="img/thumbnails") thumbnail = models.ImageField(blank=True, upload_to="img/thumbnails")
featured = models.BooleanField(default=False) featured = models.BooleanField(default=False)
langs = models.CharField( langs = models.CharField(default="['en': False, 'de': False]", max_length=64)
default="['en': False, 'de': False]", max_length=64)
slug = models.SlugField() slug = models.SlugField()
# TODO autodiscover new blog posts based on markdown files? # TODO autodiscover new blog posts based on markdown files?
@ -93,38 +88,21 @@ class BlogPost(Searchable):
html_en: str = MD.convert(body_en) html_en: str = MD.convert(body_en)
try: try:
# NOTE: MD.Meta is generated after MD.convert() by the meta
# extension.
if not hasattr(MD, 'Meta'):
logger.error("Metadata extension for markdown\
not loaded")
raise ValueError("Metadata extension for markdown\
not loaded")
meta_en = MD.Meta meta_en = MD.Meta
self.title_en = meta_en["title"][0] self.title_en = meta_en["title"][0]
self.subtitle_en = meta_en["subtitle"][0] self.subtitle_en = meta_en["subtitle"][0]
self.desc_en = meta_en["desc"][0] self.desc_en = meta_en["desc"][0]
self.date = meta_en["date"][0] # TODO: parse date from markdown
self.featured = meta_en["featured"][0] == "True" self.featured = meta_en["featured"][0] == "True"
self.public = meta_en["public"][0] == "True" self.public = meta_en["public"][0] == "True"
# self.thumbnail = meta_en["thumbnail"] # self.thumbnail = meta_en["thumbnail"]
# TODO: parse keywords from markdown # TODO: parse keywords from markdown
# TODO: parse category from markdown # TODO: parse category from markdown
try:
category: Category = Category.objects.get(
slug=meta_en['category'][0])
except Category.DoesNotExist:
category = Category.objects.create(
name=meta_en['category'], slug=meta_en['category'])
logger.debug(f"category of {self}: {category}")
self.category = category
# if keyword or category do not exist, create them # if keyword or category do not exist, create them
# I suppose # I suppose
except Exception as e: except Exception as e:
logger.warning( logger.warning(f"could not generate metadata {self.slug} from markdown: {e}")
f"could not generate metadata {self.slug} from markdown: {e}")
self.body_en = "" self.body_en = ""
self.body_en = html_en self.body_en = html_en
@ -132,8 +110,7 @@ class BlogPost(Searchable):
# TODO: mark as untranslated # TODO: mark as untranslated
pass pass
except Exception as e: except Exception as e:
logger.warning( logger.warning(f"could not generate article {self.slug} from markdown: {e}")
f"could not generate article {self.slug} from markdown: {e}")
try: try:
MD.reset() MD.reset()
with open(f"{self.DATA_DIR}/de-{self.slug}.md") as f_de: with open(f"{self.DATA_DIR}/de-{self.slug}.md") as f_de:
@ -142,13 +119,6 @@ class BlogPost(Searchable):
html_de: str = MD.convert(body_de) html_de: str = MD.convert(body_de)
try: try:
# NOTE: MD.Meta is generated after MD.convert() by the meta
# extension.
if not hasattr(MD, 'Meta'):
logger.error("Metadata extension for markdown\
not loaded")
raise ValueError("Metadata extension for markdown\
not loaded")
meta_de = MD.Meta meta_de = MD.Meta
self.title_de = meta_de["title"][0] self.title_de = meta_de["title"][0]
self.subtitle_de = meta_de["subtitle"][0] self.subtitle_de = meta_de["subtitle"][0]
@ -163,8 +133,7 @@ class BlogPost(Searchable):
# if keyword or category do not exist, create them # if keyword or category do not exist, create them
# I suppose # I suppose
except Exception as e: except Exception as e:
logger.warning( logger.warning(f"could not generate metadata {self.slug} from markdown: {e}")
f"could not generate metadata {self.slug} from markdown: {e}")
self.body_de = "" self.body_de = ""
self.body_de = html_de self.body_de = html_de
@ -172,10 +141,9 @@ class BlogPost(Searchable):
# TODO: mark as untranslated # TODO: mark as untranslated
pass pass
except Exception as e: except Exception as e:
logger.warning( logger.warning(f"could not generate article {self.slug} from markdown: {e}")
f"could not generate article {self.slug} from markdown: {e}")
def get_langs(self) -> dict[str, bool] | None: def get_langs(self) -> dict[str, bool]:
""" """
get available languages get available languages
""" """
@ -184,13 +152,7 @@ class BlogPost(Searchable):
# SECURITY: # SECURITY:
# If someone could inject the langs field, arbitrary python code might # If someone could inject the langs field, arbitrary python code might
# run, Potentially ending in a critical RCE vulnerability # run, Potentially ending in a critical RCE vulnerability
try: return eval(str(self.langs))
langs = ast.literal_eval(str(self.langs))
return langs
except ValueError as e:
logger.error(
f"could not safely evaluate 'langs' for '{self}': {e}")
return None
def set_langs(self, langs: dict[str, bool]): def set_langs(self, langs: dict[str, bool]):
""" """
@ -217,8 +179,7 @@ class BlogPost(Searchable):
if not data_dir.is_dir(): if not data_dir.is_dir():
logger.error(f"'{cls.DATA_DIR} is not a directory'") logger.error(f"'{cls.DATA_DIR} is not a directory'")
files = [f for f in os.listdir(data_dir) if ( files = [f for f in os.listdir(data_dir) if (data_dir.joinpath(f)).is_file()]
data_dir.joinpath(f)).is_file()]
logger.debug(f"discovered files: {files}") logger.debug(f"discovered files: {files}")
# finding lang and title # finding lang and title
@ -230,12 +191,6 @@ class BlogPost(Searchable):
# parse file name # parse file name
try: try:
matches = re.match(regex, file[0]) matches = re.match(regex, file[0])
if matches is None:
logger.warning(
f"Data file '{file[0]}' does not fit to the filename\
regex")
files.remove(file)
else:
current_lang = matches.group(1) current_lang = matches.group(1)
file[1][current_lang] = True file[1][current_lang] = True
file[2] = matches.group(2) file[2] = matches.group(2)
@ -260,14 +215,12 @@ class BlogPost(Searchable):
# only a single version of this file # only a single version of this file
continue continue
except Exception as e: except Exception as e:
logger.error( logger.error(f"Could not combine BlogPosts for '{file[0]}': {e}")
f"Could not combine BlogPosts for '{file[0]}': {e}")
try: try:
# deduplicate # deduplicate
_files = [] _files = []
for f in [[_f[1], _f[2]] for f in [[_f[1],_f[2]] for _f in files]: # dont care about fname
for _f in files]: # dont care about fname
if f not in _files: if f not in _files:
_files.append(f) _files.append(f)
files = _files files = _files
@ -283,6 +236,8 @@ class BlogPost(Searchable):
except Exception as e: except Exception as e:
logger.error(f"Could not create BlogPost for '{file[1]}': {e}") logger.error(f"Could not create BlogPost for '{file[1]}': {e}")
class Meta: class Meta:
verbose_name = _("blog post") verbose_name = _("blog post")
verbose_name_plural = _("blog posts") verbose_name_plural = _("blog posts")

View file

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

View file

@ -11,8 +11,6 @@ https://docs.djangoproject.com/en/3.2/ref/settings/
""" """
# for getting envvars # for getting envvars
import logging
from django.utils.translation import gettext_lazy as _
import os import os
# default django # default django
@ -102,6 +100,7 @@ DATABASES = {
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
@ -124,6 +123,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/ # https://docs.djangoproject.com/en/3.2/topics/i18n/
from django.utils.translation import gettext_lazy as _
LANGUAGES = [ LANGUAGES = [
("de", _("German")), ("de", _("German")),
@ -158,7 +158,7 @@ STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder', 'compressor.finders.CompressorFinder',
] ]
COMPRESS_PRECOMPILERS = ( COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'), ('text/x-scss', 'django_libsass.SassCompiler'),
@ -173,6 +173,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Logging configs # Logging configs
import logging
myServerFormatter = ServerFormatter myServerFormatter = ServerFormatter
myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S" myServerFormatter.default_time_format = "%Y-%M-%d %H:%M:%S"
@ -293,7 +294,7 @@ LOGGING = {
# Media stuff # Media stuff
# this is where user uploaded files will go. # this is where user uploaded files will go.
# TODO change this for prod # TODO change this for prod
# MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media" #MEDIA_ROOT = "/home/plex/Documents/code/python/gawa/media"
MEDIA_ROOT = "/app/media" MEDIA_ROOT = "/app/media"
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
# FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload" # FILE_UPLOAD_TEMP_DIR = "/tmp/gawa/upload"

View file

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

View file

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

View file

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

View file

@ -15,8 +15,7 @@ class LangBasedOnUrlMiddleware(MiddlewareMixin):
def process_request(request): def process_request(request):
if hasattr(request, 'session'): if hasattr(request, 'session'):
active_session_lang = request.session.get( active_session_lang = request.session.get(translation.LANGUAGE_SESSION_KEY)
translation.LANGUAGE_SESSION_KEY)
if active_session_lang == request.LANGUAGE_CODE: if active_session_lang == request.LANGUAGE_CODE:
return return

View file

@ -1,6 +1,3 @@
import random
import favicon
import requests
from django.db import models from django.db import models
from django.db.models.options import override from django.db.models.options import override
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -11,6 +8,9 @@ from django.conf import settings
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import requests
import favicon
import random
class Keyword(models.Model): class Keyword(models.Model):
""" """
@ -26,7 +26,6 @@ class Keyword(models.Model):
verbose_name = _("Keyword") verbose_name = _("Keyword")
verbose_name_plural = _("keywords") verbose_name_plural = _("keywords")
class Searchable(models.Model): class Searchable(models.Model):
""" """
Abstract class for any model that should be searchable. Abstract class for any model that should be searchable.
@ -38,10 +37,8 @@ class Searchable(models.Model):
title_en = models.CharField(max_length=50, default="title EN") title_en = models.CharField(max_length=50, default="title EN")
subtitle_de = models.CharField(max_length=50, blank=True) subtitle_de = models.CharField(max_length=50, blank=True)
subtitle_en = models.CharField(max_length=50, blank=True) subtitle_en = models.CharField(max_length=50, blank=True)
desc_de = models.TextField( desc_de = models.TextField(blank=True, max_length=250, unique=False, default="Beschreibung DE")
blank=True, max_length=250, unique=False, default="Beschreibung DE") desc_en = models.TextField(blank=True, max_length=250, unique=False, default="Description EN")
desc_en = models.TextField(
blank=True, max_length=250, unique=False, default="Description EN")
# may be empty/blank for some entries # may be empty/blank for some entries
date = models.DateField(blank=True, null=True) date = models.DateField(blank=True, null=True)
keywords = models.ManyToManyField(Keyword) keywords = models.ManyToManyField(Keyword)
@ -64,14 +61,12 @@ class Searchable(models.Model):
""" """
regenerate a object regenerate a object
""" """
raise NotImplementedError( raise NotImplementedError(f"{self.__class__.__name__} does not implement regenerate")
f"{self.__class__.__name__} does not implement regenerate")
class Meta: class Meta:
verbose_name = _("Searchable") verbose_name = _("Searchable")
verbose_name_plural = _("Searchables") verbose_name_plural = _("Searchables")
class StaticSite(Searchable): class StaticSite(Searchable):
""" """
This model represents any static site, such as start:index, This model represents any static site, such as start:index,
@ -87,14 +82,13 @@ class StaticSite(Searchable):
""" """
logger.info(f"regenerating {self.__class__.__name__} object: {self}") logger.info(f"regenerating {self.__class__.__name__} object: {self}")
logger.warning(f"{self.__class__.__name__} cannot regenerate.") logger.warning(f"{self.__class__.__name__} cannot regenerate.")
# self.save() #self.save()
pass pass
class Meta: class Meta:
verbose_name = _("static site") verbose_name = _("static site")
verbose_name_plural = _("static sites") verbose_name_plural = _("static sites")
class Link(Searchable): class Link(Searchable):
""" """
contains all my interesting links contains all my interesting links
@ -127,13 +121,11 @@ class Link(Searchable):
icons = favicon.get(self.url, timeout=2) icons = favicon.get(self.url, timeout=2)
except (ConnectionError) as ce: except (ConnectionError) as ce:
# just keep whatever was stored if we cant get a new favicon # just keep whatever was stored if we cant get a new favicon
logger.warn( logger.warn(f"unable to download favicon for {self}: {ce.with_traceback(None)}")
f"unable to download favicon for {self}: {ce.with_traceback(None)}")
self.status = False self.status = False
except Exception as e: except Exception as e:
logger.warn( logger.warn(f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
self.status = False self.status = False
else: else:
@ -154,8 +146,7 @@ class Link(Searchable):
self.favicon = None self.favicon = None
except Exception as e: except Exception as e:
logger.warn( logger.warn(f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
f"Unexpected Exception while downloading {self}: {e.with_traceback(None)}")
self.favicon = None self.favicon = None
self.save() self.save()

View file

@ -36,3 +36,4 @@ def change_lang(context, lang="de", *args, **kwargs):
finally: finally:
activate(lang) activate(lang)
return "%s" % url return "%s" % url

View file

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

View file

@ -18,7 +18,6 @@ from abc import ABC
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SearchableView(View, ABC): class SearchableView(View, ABC):
""" """
This abstract view implements some traits of views that should show up This abstract view implements some traits of views that should show up
@ -41,7 +40,6 @@ class Index(TemplateView, SearchableView):
template_name: str = "start/index.html" template_name: str = "start/index.html"
class Professional(TemplateView, SearchableView): class Professional(TemplateView, SearchableView):
""" """
Professional informations that might interest a professional employer Professional informations that might interest a professional employer
@ -49,7 +47,6 @@ class Professional(TemplateView, SearchableView):
# TODO # TODO
template_name: str = "start/legalinfo.html" template_name: str = "start/legalinfo.html"
class LegalInfo(TemplateView, SearchableView): class LegalInfo(TemplateView, SearchableView):
""" """
Legal info that the german authorities want. Legal info that the german authorities want.
@ -57,7 +54,6 @@ class LegalInfo(TemplateView, SearchableView):
# TODO # TODO
template_name: str = "start/legalinfo.html" template_name: str = "start/legalinfo.html"
class ActivateLanguage(View): class ActivateLanguage(View):
""" """
Set the language to whatever Set the language to whatever
@ -72,7 +68,6 @@ class ActivateLanguage(View):
request.session[translation.LANGUAGE_SESSION_KEY] = self.language_code request.session[translation.LANGUAGE_SESSION_KEY] = self.language_code
return redirect(self.redirect_to) return redirect(self.redirect_to)
class MainSearch(ListView): class MainSearch(ListView):
""" """
Search for anything. Search for anything.
@ -99,7 +94,6 @@ class MainSearch(ListView):
return render(request, "errors/bad_request.html") return render(request, "errors/bad_request.html")
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
class Links(ListView): class Links(ListView):
""" """
This View contains links to various interesting sites. This View contains links to various interesting sites.