164 lines
5.3 KiB
Python
164 lines
5.3 KiB
Python
from django.db import models
|
|
from django.utils.translation import gettext as _
|
|
from start.models import Searchable
|
|
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
import markdown
|
|
EXTENSIONS = [
|
|
"extra",
|
|
"admonition",
|
|
"codehilite",
|
|
"meta",
|
|
"toc"
|
|
]
|
|
EXTENSION_CONFIGS = {
|
|
'codehilite': {
|
|
'linenums': True,
|
|
'pygments_style': 'monokai'
|
|
},
|
|
}
|
|
|
|
MD = markdown.Markdown(extensions=EXTENSIONS, extension_configs=EXTENSION_CONFIGS)
|
|
|
|
import pathlib
|
|
import os
|
|
|
|
class Category(models.Model):
|
|
"""
|
|
A category of blog posts
|
|
|
|
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()
|
|
|
|
class Meta:
|
|
verbose_name = _("Category")
|
|
verbose_name_plural = _("Categories")
|
|
|
|
def __str__(self):
|
|
return f"{{<{self.__class__.__name__}>\"{self.name}\"}}"
|
|
|
|
class BlogPost(Searchable):
|
|
"""
|
|
Should contain a blogpost
|
|
"""
|
|
body_en = models.TextField(default="No english translation yet.")
|
|
body_de = models.TextField(default="Bis jetzt keine deutsche Übersetzung.")
|
|
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)
|
|
slug = models.SlugField()
|
|
|
|
# TODO autodiscover new blog posts based on markdown files?
|
|
|
|
DATA_DIR = "/app/blog/data/articles"
|
|
|
|
def regenerate(self):
|
|
"""
|
|
regenerate a object
|
|
|
|
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()
|
|
|
|
self.save()
|
|
|
|
def sync_file(self):
|
|
"""
|
|
generate an article fromm it's original markdown file
|
|
"""
|
|
logger.info(f"regenerating article from markdown for: {self}")
|
|
try:
|
|
MD.reset()
|
|
with open(f"{self.DATA_DIR}/en-{self.slug}.md") as f_en:
|
|
|
|
body_en: str = f_en.read()
|
|
|
|
html_en: str = MD.convert(body_en)
|
|
try:
|
|
meta_en = MD.Meta
|
|
self.title_en = meta_en["title"][0]
|
|
self.subtitle_en = meta_en["subtitle"][0]
|
|
self.desc_en = meta_en["desc"][0]
|
|
# TODO: parse date from markdown
|
|
self.featured = meta_en["featured"][0] == "True"
|
|
self.public = meta_en["public"][0] == "True"
|
|
# TODO: parse keywords from markdown
|
|
# TODO: parse category from markdown
|
|
|
|
# if keyword or category do not exist, create them
|
|
# I suppose
|
|
except Exception as e:
|
|
logger.warning(f"could not generate metadata {self.slug} from markdown: {e}")
|
|
|
|
self.body_en = ""
|
|
self.body_en = html_en
|
|
except FileNotFoundError as e:
|
|
# TODO: mark as untranslated
|
|
pass
|
|
except Exception as e:
|
|
logger.warning(f"could not generate article {self.slug} from markdown: {e}")
|
|
try:
|
|
MD.reset()
|
|
with open(f"{self.DATA_DIR}/de-{self.slug}.md") as f_de:
|
|
|
|
body_de: str = f_de.read()
|
|
|
|
html_de: str = MD.convert(body_de)
|
|
try:
|
|
meta_de = MD.Meta
|
|
self.title_de = meta_de["title"][0]
|
|
self.subtitle_de = meta_de["subtitle"][0]
|
|
self.desc_de = meta_de["desc"][0]
|
|
# TODO: parse date from markdown
|
|
self.featured = meta_de["featured"][0] == "True"
|
|
self.public = meta_de["public"][0] == "True"
|
|
# TODO: parse keywords from markdown
|
|
# TODO: parse category from markdown
|
|
|
|
# if keyword or category do not exist, create them
|
|
# I suppose
|
|
except Exception as e:
|
|
logger.warning(f"could not generate metadata {self.slug} from markdown: {e}")
|
|
|
|
self.body_de = ""
|
|
self.body_de = html_de
|
|
except FileNotFoundError as e:
|
|
# TODO: mark as untranslated
|
|
pass
|
|
except Exception as e:
|
|
logger.warning(f"could not generate article {self.slug} from markdown: {e}")
|
|
|
|
@classmethod
|
|
def sync_all(cls):
|
|
"""
|
|
Sync all Blog Posts with the filesystem.
|
|
|
|
Caution: Will delete all Blog Posts
|
|
"""
|
|
# 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 os.path.isfile(f)]
|
|
logger.debug(f"discovered files: {files}")
|
|
|
|
|
|
class Meta:
|
|
verbose_name = _("blog post")
|
|
verbose_name_plural = _("blog posts")
|