blog-markdown #31

Merged
PlexSheep merged 27 commits from blog-markdown into devel 2023-10-02 11:31:41 +02:00
3 changed files with 90 additions and 40 deletions
Showing only changes of commit 37c3104bc5 - Show all commits

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 | hrCcDa0jBspG | | [`contact@cscherr.de`](mailto:contact@cscherr.de) | `hrCcDa0jBspG` |
## License ## License
@ -22,6 +22,13 @@ 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,3 +1,8 @@
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
@ -5,7 +10,6 @@ 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",
@ -20,11 +24,9 @@ EXTENSION_CONFIGS = {
}, },
} }
MD = markdown.Markdown(extensions=EXTENSIONS, extension_configs=EXTENSION_CONFIGS) MD = markdown.Markdown(extensions=EXTENSIONS,
extension_configs=EXTENSION_CONFIGS)
import pathlib
import os
import re
class Category(models.Model): class Category(models.Model):
""" """
@ -33,8 +35,8 @@ class Category(models.Model):
Name not translated because it would make i18n in urls and Searchables specifically a pain. Name not translated because it would make i18n in urls and Searchables specifically a pain.
Maybe some day it would be cool if these were Searchable Maybe some day it would be cool if these were Searchable
""" """
name= models.CharField(max_length=50) name = models.CharField(max_length=50)
slug = models.SlugField() slug = models.SlugField(unique=True)
class Meta: class Meta:
verbose_name = _("Category") verbose_name = _("Category")
@ -43,16 +45,19 @@ 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, on_delete=models.SET_NULL, null=True) category = models.ForeignKey(
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(default="['en': False, 'de': False]", max_length=64) langs = models.CharField(
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?
@ -88,21 +93,38 @@ 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]
# TODO: parse date from markdown self.date = meta_en["date"][0]
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(f"could not generate metadata {self.slug} from markdown: {e}") logger.warning(
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
@ -110,7 +132,8 @@ class BlogPost(Searchable):
# TODO: mark as untranslated # TODO: mark as untranslated
pass pass
except Exception as e: except Exception as e:
logger.warning(f"could not generate article {self.slug} from markdown: {e}") logger.warning(
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:
@ -119,6 +142,13 @@ 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]
@ -133,7 +163,8 @@ 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(f"could not generate metadata {self.slug} from markdown: {e}") logger.warning(
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
@ -141,9 +172,10 @@ class BlogPost(Searchable):
# TODO: mark as untranslated # TODO: mark as untranslated
pass pass
except Exception as e: except Exception as e:
logger.warning(f"could not generate article {self.slug} from markdown: {e}") logger.warning(
f"could not generate article {self.slug} from markdown: {e}")
def get_langs(self) -> dict[str, bool]: def get_langs(self) -> dict[str, bool] | None:
""" """
get available languages get available languages
""" """
@ -152,7 +184,13 @@ 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
return eval(str(self.langs)) try:
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]):
""" """
@ -179,7 +217,8 @@ 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 (data_dir.joinpath(f)).is_file()] files = [f for f in os.listdir(data_dir) if (
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
@ -191,6 +230,12 @@ 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)
@ -215,12 +260,14 @@ 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(f"Could not combine BlogPosts for '{file[0]}': {e}") logger.error(
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 files]: # dont care about fname for f in [[_f[1], _f[2]]
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
@ -232,12 +279,11 @@ class BlogPost(Searchable):
try: try:
obj = BlogPost(langs=file[0], slug=file[1]) obj = BlogPost(langs=file[0], slug=file[1])
obj.sync_file() obj.sync_file()
obj.regenerate()
obj.save() obj.save()
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,9 +1,6 @@
{% extends 'admin/change_list.html' %} {% extends 'admin/change_list.html' %} {% block object-tools %}
<form action="sync" method="POST">
{% block object-tools %}
<form action="sync" method="POST">
{% csrf_token %} {% csrf_token %}
<button type="submit">Sync with FS</button> <button type="submit" class="button" style="padding: 4px">Sync with FS</button>
</form> </form>
{{ block.super }} {{ block.super }} {% endblock %}
{% endblock %}