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")