Commit 48aad4a0 authored by Thomas Kerkhof's avatar Thomas Kerkhof 🐍

init

parents
docker
!docker/requirements.txt
!docker/virtualhost.conf
.env
docker-compose.yml
DEBUG=True
SECRET_KEY=All hail the The Cheese (seriously: please change me)
TIME_ZONE=UTC
ADMIN_URL=admin/
STATIC_URL=/static/
STATIC_FOLDER=web/static/
UPLOAD_FOLDER=upload/
# Created by https://www.gitignore.io/api/osx,venv,django,python
# Edit at https://www.gitignore.io/?templates=osx,venv,django,python
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
media
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/
### Django.Python Stack ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
# Django stuff:
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don’t work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
### OSX ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs
# Unit test / coverage reports
# Translations
# Django stuff:
# Flask stuff:
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# Jupyter Notebook
# IPython
# pyenv
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don’t work, or not
# install all needed dependencies.
# celery beat schedule file
# SageMath parsed files
# Environments
# Spyder project settings
# Rope project settings
# mkdocs documentation
# mypy
# Pyre type checker
### venv ###
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
# End of https://www.gitignore.io/api/osx,venv,django,python
### IntelliJ ###
# Everything
.idea
web/static
node_modules
Copyright (c) 2019 Thomas Kerkhof - thomaskerkhof.nl
\ No newline at end of file
This diff is collapsed.
# Markdown Blog
## A blogging engine written in Django, no wysiwyg, just upload .md files!
While there are applications that convert your markdown files into a website, they don't do a very good job.
This is the solution.
This system will do a lot of fancy work like caching, image optimization
and every possible thing I can think of to bring a markdown based blog up to modern standards and maybe to the cutting edge of technology.
(I will be ignoring IE11 during development of my blog, if you want backwards compatability, it's up to you)
## Documentation will follow, this project is still in development
If you want to use this system for yourself, feel free to fork this project!
## Usage:
### Category creation:
Create a folder in your upload directory.
To keep things simple I advise using the slug as folder name (`upload/example_category`).
Create the metadata file (`upload/example_categoy/_meta.md`.
slug: example_category
title: Example category
description: Just an example
feature_image: _category.jpg
Upload the feature_image (`upload/example_category/_category.jpg`)
### Blog post creation:
Upload the markdown file:
author: your name here
slug: article_1
title: The first article
description: Some article has to go first, might as well be this one!
tags: first, tag, you're, it!
feature_image: some_image.jpg
draft: true
# This is a title
## The amazing subtitle to the title
### First paragraph
This is a block of text.
Line spacing
- un
- ordered
- list
1. ordered
2. list
head1 | head2
:---: | :---
item 1 | item2
**bold text**
- [ ] unchecked
- [x] checked
Upload the feature_image (`upload/example_category/some_image.jpg`)
from django.contrib import admin
from .models import TagCache, CategoryCache, ImageCache, ArticleCache
# Register your models here.
admin.site.register(TagCache)
admin.site.register(CategoryCache)
admin.site.register(ImageCache)
admin.site.register(ArticleCache)
from django.apps import AppConfig
class CachingConfig(AppConfig):
name = 'blog_cache'
import hashlib
def md5_file(filepath):
try:
md5 = hashlib.md5()
file = open(filepath, "rb")
with file as f:
for chunk in iter(lambda: f.read(4096), b""):
md5.update(chunk)
file.close()
except IOError:
return False
return md5.hexdigest()
def sha256_file(filepath):
try:
sha256 = hashlib.sha256()
file = open(filepath, "rb")
with file as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256.update(chunk)
file.close()
except IOError:
return False
return sha256.hexdigest()
import markdown, codecs
from markdown_blog import settings
from bs4 import BeautifulSoup
class Markdown:
def __init__(self, filepath):
text = codecs.open(filepath, mode="r", encoding="utf-8").read()
md = markdown.Markdown(extensions=settings.MARKDOWN_EXTENSIONS)
self.html = md.convert(text)
self.soup = BeautifulSoup(self.html, 'html.parser')
meta = md.Meta
try:
self.title = str(meta['title'][0])
except KeyError:
self.title = None
try:
self.slug = str(meta['slug'][0])
except KeyError:
self.slug = None
try:
self.description = str(meta['description'][0])
except KeyError:
self.description = None
try:
self.tags = list(map(str.strip, str(meta['tags'][0]).split(',')))
except KeyError:
self.tags = []
try:
self.featureImage = str(meta['feature_image'][0])
except KeyError:
self.featureImage = None
try:
self.author = str(meta['author'][0])
except KeyError:
self.author = None
self.isDraft = False
try:
if str(meta['draft'][0]):
self.isDraft = True
except KeyError:
pass
from django.core.management.base import BaseCommand, CommandError
from ...services import ArticleCacheService, CategoryCacheService, TagCacheService, ImageCacheService
class Command(BaseCommand):
help = 'Refresh blog cache'
def handle(self, *args, **options):
CategoryCacheService.check_existing()
CategoryCacheService.check_new()
ArticleCacheService.check_existing()
ArticleCacheService.check_new()
TagCacheService.kill_orphans()
ImageCacheService.check_existing()
from django.core.management.base import BaseCommand, CommandError
from ...services import ArticleCacheService, CategoryCacheService
class Command(BaseCommand):
help = 'Reload blog cache'
def handle(self, *args, **options):
print("Not yet implemented")
# Generated by Django 2.2.2 on 2019-08-12 18:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ImageCache',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FilePathField(path='/Users/htdutchy/PycharmProjects/markdown_blog/upload/', recursive=True, unique=True)),
('fileType', models.CharField(max_length=24)),
('altText', models.CharField(blank=True, max_length=120, null=True)),
('cachedSmall', models.FilePathField(path='/Users/htdutchy/PycharmProjects/markdown_blog/web/static/', recursive=True)),
('cachedMedium', models.FilePathField(path='/Users/htdutchy/PycharmProjects/markdown_blog/web/static/', recursive=True)),
('cachedLarge', models.FilePathField(path='/Users/htdutchy/PycharmProjects/markdown_blog/web/static/', recursive=True)),
('fileHash', models.CharField(max_length=160)),
('lastCached', models.DateTimeField()),
],
),
migrations.CreateModel(
name='TagCache',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=120, unique=True)),
('slug', models.CharField(max_length=120, unique=True)),
],
),
migrations.CreateModel(
name='CategoryCache',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('folder', models.FilePathField(allow_files=False, allow_folders=True, path='/Users/htdutchy/PycharmProjects/markdown_blog/upload/', unique=True)),
('file', models.FilePathField(path='/Users/htdutchy/PycharmProjects/markdown_blog/upload/', recursive=True)),
('slug', models.SlugField(max_length=120, unique=True)),
('title', models.CharField(max_length=120)),
('description', models.TextField(blank=True, null=True)),
('fileHash', models.CharField(max_length=160)),
('lastCached', models.DateTimeField()),
('featureImage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='blog_cache.ImageCache')),
],
),
migrations.CreateModel(
name='ArticleCache',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FilePathField(path='/Users/htdutchy/PycharmProjects/markdown_blog/upload/', recursive=True, unique=True)),
('fileHash', models.CharField(max_length=160)),
('slug', models.SlugField(max_length=240)),
('title', models.CharField(max_length=240)),
('description', models.TextField(blank=True, null=True)),
('published', models.DateTimeField()),
('author', models.CharField(blank=True, max_length=120, null=True)),
('cachedHeading', models.CharField(max_length=240)),
('cachedContent', models.TextField()),
('draft', models.BooleanField()),
('lastCached', models.DateTimeField()),
('category', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='blog_cache.CategoryCache')),
('featureImage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='blog_cache.ImageCache')),
('tags', models.ManyToManyField(to='blog_cache.TagCache')),
],
options={
'unique_together': {('slug', 'category')},
},
),
]
from markdown_blog import settings
from django.db import models
from os import path
# Image cache model
class ImageCache(models.Model):
file = models.FilePathField(path=settings.UPLOAD_FOLDER, recursive=True, unique=True)
fileType = models.CharField(max_length=24)
altText = models.CharField(max_length=120, blank=True, null=True)
cachedSmall = models.FilePathField(path=settings.STATIC_FOLDER, recursive=True)
cachedMedium = models.FilePathField(path=settings.STATIC_FOLDER, recursive=True)
cachedLarge = models.FilePathField(path=settings.STATIC_FOLDER, recursive=True)
fileHash = models.CharField(max_length=160)
lastCached = models.DateTimeField()
GENERIC_TYPE = 'generic'
CATEGORY_TYPE = 'category'
PAGE_TYPE = 'page'
SMALL = 200, 200
MEDIUM = 1000, 1000
LARGE = 2000, 2000
def __str__(self):
return self.altText
def url_small(self):
return path.join(settings.STATIC_IMAGE_CACHE_URL, path.basename(self.cachedSmall))
def url_medium(self):
return path.join(settings.STATIC_IMAGE_CACHE_URL, path.basename(self.cachedMedium))
def url_large(self):
return path.join(settings.STATIC_IMAGE_CACHE_URL, path.basename(self.cachedLarge))
# Category cache model
class CategoryCache(models.Model):
folder = models.FilePathField(path=settings.UPLOAD_FOLDER, allow_files=False, allow_folders=True, unique=True)
file = models.FilePathField(path=settings.UPLOAD_FOLDER, recursive=True)
slug = models.SlugField(max_length=120, unique=True)
title = models.CharField(max_length=120)
description = models.TextField(blank=True, null=True)
featureImage = models.ForeignKey('ImageCache', on_delete=models.DO_NOTHING, blank=True, null=True)
fileHash = models.CharField(max_length=160)
lastCached = models.DateTimeField()
def __str__(self):
return self.title
# TODO: Make dynamic subdir setting
def url(self):
return '/' + self.slug