501 lines
17 KiB
Python
Executable File
501 lines
17 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2012-2014 Grégory Soutadé
|
|
|
|
This file is part of Dynastie.
|
|
|
|
Dynastie is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Dynastie is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Dynastie. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import hashlib
|
|
import inspect
|
|
import gzip
|
|
from unicodedata import normalize
|
|
from re import sub
|
|
from datetime import datetime
|
|
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
from django.db.models.signals import pre_init, post_init, pre_delete, post_delete
|
|
from django.db.models.signals import pre_save, post_save
|
|
from django.dispatch import receiver
|
|
from dynastie.generators import *
|
|
|
|
def slugify(name):
|
|
name = name.strip()
|
|
name = normalize('NFKD', name).replace(' ', '-').lower()
|
|
#remove `other` characters
|
|
name = sub('[^a-zA-Z0-9_-]', '', name)
|
|
#nomalize dashes
|
|
name = sub('-+', '-', name)
|
|
return name
|
|
|
|
class Blog(models.Model):
|
|
name = models.CharField(max_length=255, unique=True)
|
|
title = models.CharField(max_length=255)
|
|
description = models.TextField(max_length=255, blank=True)
|
|
keywords = models.TextField(blank=True)
|
|
writers = models.ManyToManyField(User)
|
|
|
|
engines = list()
|
|
|
|
src_path = ''
|
|
output_path = ''
|
|
report = ''
|
|
|
|
def create_paths(self):
|
|
self.src_path = os.environ['DYNASTIE_ROOT'] + 'sites/' + self.name
|
|
self.output_path = os.environ['DYNASTIE_ROOT'] + 'sites/' + self.name + '_output'
|
|
|
|
def create(self):
|
|
self.create_paths()
|
|
if not os.path.exists(os.environ['DYNASTIE_ROOT'] + 'sites'):
|
|
os.mkdir(os.environ['DYNASTIE_ROOT'] + 'sites')
|
|
|
|
if not os.path.exists(self.src_path):
|
|
os.mkdir(self.src_path)
|
|
|
|
if not os.path.exists(self.output_path):
|
|
os.mkdir(self.output_path)
|
|
|
|
def remove(self):
|
|
if os.path.exists(self.src_path):
|
|
shutil.rmtree(self.src_path)
|
|
if os.path.exists(self.output_path):
|
|
shutil.rmtree(self.output_path)
|
|
|
|
def load_generators(self):
|
|
if os.path.exists(self.src_path + '/_generators'):
|
|
f = open(self.src_path + '/_generators', 'r')
|
|
for line in f:
|
|
if line.startswith("#"):
|
|
continue
|
|
engine = line.strip()
|
|
if not engine in globals():
|
|
print('Engine ' + engine + ' doesn\'t exists')
|
|
else:
|
|
self.engines.append(globals()[engine])
|
|
f.close()
|
|
else:
|
|
self.engines.append(globals()['post'])
|
|
self.engines.append(globals()['index'])
|
|
self.engines.append(globals()['category'])
|
|
self.engines.append(globals()['tag'])
|
|
self.engines.append(globals()['archive'])
|
|
self.engines.append(globals()['atom'])
|
|
self.engines.append(globals()['rss'])
|
|
self.engines.append(globals()['all_posts'])
|
|
|
|
def get_engines(self):
|
|
return self.engines
|
|
|
|
def copytree(self, src, dst):
|
|
names = os.listdir(src)
|
|
|
|
errors = []
|
|
for name in names:
|
|
if name.startswith('_') or name.endswith('~'):
|
|
continue
|
|
srcname = os.path.join(src, name)
|
|
dstname = os.path.join(dst, name)
|
|
try:
|
|
if os.path.islink(srcname) and not os.path.exists(dstname):
|
|
linkto = os.readlink(srcname)
|
|
os.symlink(linkto, dstname)
|
|
if os.path.isdir(srcname):
|
|
if not os.path.exists(dstname):
|
|
os.makedirs(dstname)
|
|
self.copytree(srcname, dstname, ignore=True)
|
|
else:
|
|
return self.copytree(srcname, dstname)
|
|
else:
|
|
copied = False
|
|
if os.path.exists(dstname):
|
|
src_md5 = hashlib.md5()
|
|
f = open(srcname,'rb')
|
|
src_md5.update(f.read())
|
|
f.close()
|
|
|
|
dst_md5 = hashlib.md5()
|
|
f = open(dstname,'rb')
|
|
dst_md5.update(f.read())
|
|
f.close()
|
|
|
|
if src_md5.digest() != dst_md5.digest():
|
|
self.report = self.report + 'Update ' + dstname + '<br/>\n'
|
|
shutil.copy2(srcname, dstname)
|
|
copied = True
|
|
|
|
else:
|
|
self.report = self.report + 'Add ' + dstname + '<br/>\n'
|
|
shutil.copy2(srcname, dstname)
|
|
copied = True
|
|
|
|
if copied:
|
|
exts = ('css', 'html', 'htm', 'xhtml', 'js')
|
|
found = False
|
|
for ext in exts:
|
|
if srcname.endswith(ext):
|
|
found = True
|
|
break
|
|
if found:
|
|
dstname = dstname + '.gz'
|
|
if os.path.exists(dstname):
|
|
os.unlink(dstname)
|
|
f = open(srcname)
|
|
content = f.read()
|
|
f.close()
|
|
f = gzip.open(dstname, 'wb')
|
|
f.write(content)
|
|
f.close()
|
|
|
|
|
|
# XXX What about devices, sockets etc.?
|
|
except (IOError, os.error) as why:
|
|
errors.append((srcname, dstname, str(why)))
|
|
# catch the Error from the recursive copytree so that we can
|
|
# continue with other files
|
|
except Exception as err:
|
|
errors.extend(err.args[0])
|
|
if errors:
|
|
raise Exception(errors)
|
|
|
|
def generate(self, request):
|
|
start_time = datetime.now()
|
|
self.report = ''
|
|
self.load_generators()
|
|
self.copytree(self.src_path, self.output_path)
|
|
generated = []
|
|
hash_posts = {}
|
|
hash_posts_content = {}
|
|
for engine in self.engines:
|
|
if not inspect.ismodule(engine):
|
|
continue
|
|
for name, obj in inspect.getmembers(engine):
|
|
if inspect.isclass(obj) and obj.__module__.startswith("dynastie.generators"):
|
|
if obj.__module__ in generated: continue
|
|
e = obj(hash_posts, hash_posts_content)
|
|
print('Go for {}'.format(e))
|
|
r = e.generate(self, self.src_path, self.output_path)
|
|
generated.append(obj.__module__)
|
|
if not r is None:
|
|
self.report = self.report + '<br/>\n' + r
|
|
|
|
duration = datetime.now() - start_time
|
|
t = '<br/><br/>Generation of ' + start_time.strftime("%d/%m/%Y at %H:%M:%S") + '<br/>\n'
|
|
t = t + 'Duration ' + str(duration) + '<br/><br/>\n'
|
|
return t + self.report
|
|
|
|
def generate_post(self, request, post):
|
|
from dynastie.generators import post as PostGenerator
|
|
start_time = datetime.now()
|
|
self.report = ''
|
|
self.load_generators()
|
|
self.copytree(self.src_path, self.output_path)
|
|
post_list = [post]
|
|
hash_posts = {}
|
|
hash_posts_content = {}
|
|
engine = globals()['post']
|
|
for name, obj in inspect.getmembers(engine):
|
|
if inspect.isclass(obj) and obj.__module__.startswith("dynastie.generators") \
|
|
and obj.__module__.endswith("post"):
|
|
e = obj(request, hash_posts, hash_posts_content)
|
|
self.report = e._generate(self, self.src_path, self.output_path, post_list)
|
|
break
|
|
# report = PostGenerator()._generate(self, self.src_path, self.output_path, post_list)
|
|
|
|
duration = datetime.now() - start_time
|
|
t = '<br/><br/>Generation of ' + start_time.strftime("%d/%m/%Y at %H:%M:%S") + ' post ' + str(post.id) + '<br/>\n'
|
|
t = t + 'Duration ' + str(duration) + '<br/><br/>\n'
|
|
return t + self.report
|
|
|
|
class Editor(models.Model):
|
|
name = models.CharField(max_length=255, unique=True)
|
|
|
|
class Category(models.Model):
|
|
name = models.CharField(max_length=255, unique=True)
|
|
name_slug = models.CharField(max_length=255)
|
|
parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.CASCADE)
|
|
description = models.TextField(max_length=255, blank=True)
|
|
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
|
|
|
|
def save(self):
|
|
self.name_slug = slugify(self.name)
|
|
super(Category, self).save()
|
|
|
|
def remove(self):
|
|
blog = Blog.objects.get(pk=self.blog.id)
|
|
output = blog.output_path + '/category/' + self.name_slug
|
|
if os.path.exists(output):
|
|
shutil.rmtree(output)
|
|
|
|
|
|
class Tag(models.Model):
|
|
name = models.CharField(max_length=255, unique=True)
|
|
name_slug = models.CharField(max_length=255)
|
|
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
|
|
|
|
def save(self):
|
|
self.name_slug = slugify(self.name)
|
|
super(Tag, self).save()
|
|
|
|
def remove(self):
|
|
blog = Blog.objects.get(pk=self.blog.id)
|
|
output = blog.output_path + '/tag/' + self.name_slug
|
|
if os.path.exists(output):
|
|
shutil.rmtree(output)
|
|
|
|
class PostOnlyManager(models.Manager):
|
|
|
|
def get_queryset(self):
|
|
return super(PostOnlyManager, self).get_queryset().filter(post_type='P')
|
|
|
|
class Post(models.Model):
|
|
|
|
objects = PostOnlyManager()
|
|
|
|
title = models.CharField(max_length=255)
|
|
title_slug = models.CharField(max_length=255)
|
|
category = models.ForeignKey(Category, blank=True, null=True, on_delete=models.SET_NULL)
|
|
published = models.BooleanField()
|
|
creation_date = models.DateTimeField()
|
|
modification_date = models.DateTimeField()
|
|
front_page = models.BooleanField()
|
|
author = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
|
|
description = models.TextField(max_length=255, blank=True)
|
|
keywords = models.TextField(blank=True)
|
|
tags = models.ManyToManyField(Tag, blank=True, null=True)
|
|
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
|
|
CONTENT_HTML = 0
|
|
CONTENT_TEXT = 1
|
|
CONTENT_FORMAT = (
|
|
(CONTENT_HTML, 'HTML'),
|
|
(CONTENT_TEXT, 'Text'))
|
|
content_format = models.IntegerField(choices=CONTENT_FORMAT, default=CONTENT_HTML, blank=False, null=False)
|
|
post_type = models.CharField(max_length=1, default='P')
|
|
|
|
def getPath(self):
|
|
filename = '/post/'
|
|
filename = filename + self.creation_date.strftime("%Y") + '/' + self.creation_date.strftime("%m") + '/'
|
|
filename = filename + self.title_slug + '.html'
|
|
return filename
|
|
|
|
def save(self):
|
|
self.title = self.title.strip()
|
|
self.title_slug = slugify(self.title)
|
|
self.modification_date=datetime.now()
|
|
super(Post, self).save()
|
|
|
|
def manageTags(self, tags):
|
|
tags_list = Tag.objects.filter(blog_id=self.blog.id)
|
|
my_tags = []
|
|
# Create new tags
|
|
for tag in tags.split(','):
|
|
if tag == '': continue
|
|
tag_slug = slugify(tag)
|
|
found = False
|
|
for t in tags_list:
|
|
if t.name_slug == tag_slug:
|
|
found = True
|
|
break
|
|
if not found and not tag in my_tags:
|
|
t = Tag(blog=self.blog, name=tag.strip(), name_slug=tag_slug)
|
|
t.save()
|
|
# print 'Create ' + tag_slug
|
|
my_tags.append(tag)
|
|
|
|
# Add new tags
|
|
post_tags_list = Tag.objects.filter(post=self.id)
|
|
for tag in tags.split(','):
|
|
if tag == '': continue
|
|
tag_slug = slugify(tag)
|
|
found = False
|
|
for t in post_tags_list:
|
|
if t.name_slug == tag_slug:
|
|
found = True
|
|
break
|
|
if not found:
|
|
for t in tags_list:
|
|
if t.name_slug == tag_slug:
|
|
self.tags.add(t)
|
|
# print 'Add ' + tag_slug
|
|
break
|
|
|
|
# Remove old tags
|
|
if tags == '':
|
|
for t in post_tags_list:
|
|
self.tags.remove(t)
|
|
else:
|
|
for t in post_tags_list:
|
|
found = False
|
|
for tag in tags.split(','):
|
|
tag_slug = slugify(tag)
|
|
if t.name_slug == tag_slug:
|
|
found = True
|
|
break
|
|
if not found:
|
|
# print 'Remove ' + t.name_slug
|
|
self.tags.remove(t)
|
|
|
|
def createPost(self, content, tags):
|
|
output = self.blog.src_path
|
|
if not os.path.exists(output + '/_post'):
|
|
os.mkdir(output + '/_post')
|
|
|
|
filename = output + '/_post/' + str(self.pk)
|
|
content = content.encode('utf-8')
|
|
|
|
f = open(filename, 'wb')
|
|
f.write(content)
|
|
f.close()
|
|
|
|
self.manageTags(tags)
|
|
|
|
def remove(self):
|
|
b = self.blog
|
|
|
|
output = b.src_path
|
|
filename = output + '/_post/' + str(self.pk)
|
|
if os.path.exists(filename):
|
|
os.unlink(filename)
|
|
|
|
output = b.output_path + self.getPath()
|
|
if os.path.exists(filename):
|
|
os.unlink(filename)
|
|
filename = filename + '.gz'
|
|
if os.path.exists(filename):
|
|
os.unlink(filename)
|
|
|
|
filename = b.src_path + '/post/'
|
|
filename = filename + self.creation_date.strftime("%Y") + '/' + self.creation_date.strftime("%m") + '/'
|
|
if os.path.exists(filename) and len(os.listdir(filename)) == 0:
|
|
os.rmdir(filename)
|
|
|
|
filename = b.output_path + '/post/'
|
|
filename = filename + self.creation_date.strftime("%Y") + '/'
|
|
if os.path.exists(filename) and len(os.listdir(filename)) == 0:
|
|
os.rmdir(filename)
|
|
|
|
|
|
def get_editor(self):
|
|
if self.content_format == Post.CONTENT_HTML:
|
|
return 'html'
|
|
else:
|
|
return 'text'
|
|
|
|
class Draft(Post):
|
|
|
|
objects = models.Manager()
|
|
|
|
def createDraft(self, content, tags):
|
|
b = self.blog
|
|
output = b.src_path
|
|
if not os.path.exists(output + '/_draft'):
|
|
os.mkdir(output + '/_draft')
|
|
|
|
filename = output + '/_draft/' + str(self.pk)
|
|
content = content.encode('utf-8')
|
|
modif = True
|
|
|
|
if os.path.exists(filename):
|
|
f = open(filename, 'rb')
|
|
src_md5 = hashlib.md5()
|
|
src_md5.update(f.read())
|
|
f.close()
|
|
|
|
dst_md5 = hashlib.md5()
|
|
dst_md5.update(content)
|
|
|
|
if src_md5.digest() == dst_md5.digest():
|
|
modif = False
|
|
else:
|
|
os.unlink(filename)
|
|
|
|
if modif:
|
|
f = open(filename, 'wb')
|
|
f.write(content)
|
|
f.close()
|
|
|
|
self.manageTags(tags)
|
|
self.save()
|
|
|
|
def remove(self):
|
|
b = self.blog
|
|
|
|
output = b.src_path
|
|
filename = output + '/_draft/' + str(self.pk)
|
|
if os.path.exists(filename):
|
|
os.unlink(filename)
|
|
|
|
def save(self):
|
|
self.published = False
|
|
super(Draft, self).save()
|
|
|
|
class Comment(models.Model):
|
|
post = models.ForeignKey(Post, on_delete=models.CASCADE)
|
|
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)
|
|
date = models.DateTimeField()
|
|
author = models.CharField(max_length=255)
|
|
email = models.EmailField(max_length=255, blank=True)
|
|
the_comment = models.TextField(max_length=255)
|
|
ip = models.GenericIPAddressField()
|
|
|
|
def _update_line_returns(self):
|
|
self.the_comment = self.the_comment.replace('\n', '<br />')
|
|
|
|
def _remove_br(self):
|
|
self.the_comment = self.the_comment.replace('<br />', '\n')
|
|
|
|
class FileOutputCache(models.Model):
|
|
name = models.CharField(max_length=512)
|
|
hash = models.CharField(max_length=512)
|
|
|
|
@receiver(post_init, sender=Blog)
|
|
def init_blog_signal(sender, **kwargs):
|
|
kwargs['instance'].create_paths()
|
|
|
|
@receiver(post_delete, sender=Blog)
|
|
def delete_blog_signal(sender, **kwargs):
|
|
kwargs['instance'].remove()
|
|
|
|
@receiver(pre_delete, sender=Category)
|
|
def delete_category_signal(sender, **kwargs):
|
|
kwargs['instance'].remove()
|
|
|
|
@receiver(pre_delete, sender=Tag)
|
|
def delete_tag_signal(sender, **kwargs):
|
|
kwargs['instance'].remove()
|
|
|
|
@receiver(post_delete, sender=Post)
|
|
def delete_post_signal(sender, **kwargs):
|
|
kwargs['instance'].remove()
|
|
|
|
@receiver(post_delete, sender=Draft)
|
|
def delete_draft_signal(sender, **kwargs):
|
|
kwargs['instance'].remove()
|
|
|
|
@receiver(pre_delete, sender=Post)
|
|
def pre_delete_post_signal(sender, **kwargs):
|
|
post = kwargs['instance']
|
|
comments = Comment.objects.filter(post=post.id).delete()
|
|
|
|
# Replace line returns by <br /> for generation
|
|
@receiver(pre_save, sender=Comment)
|
|
def pre_save_comment_signal(sender, **kwargs):
|
|
kwargs['instance']._update_line_returns()
|
|
|
|
@receiver(pre_save, sender=Draft)
|
|
def pre_save_draft_signal(sender, **kwargs):
|
|
kwargs['instance'].post_type = 'D'
|