e0b8f544ff
Fix draft inclusion in preview Enhance cache post content (avoid recomputing md5sum if present) Add generation duration time Add post only generation (for Dev) Remove Draft when it becomes Post Update blog Copyright Update TinyMCE plugins for inclusion Sort tags by name
589 lines
21 KiB
Python
Executable File
589 lines
21 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 re
|
|
import datetime
|
|
import hashlib
|
|
import xml
|
|
from xml.parsers.expat import *
|
|
import xml.parsers.expat
|
|
from xml.dom.minidom import parse, parseString
|
|
from dynastie.generators.generator import DynastieGenerator, StrictUTF8Writer
|
|
from django.db import models
|
|
from dynastie.generators import markdown2
|
|
|
|
class Index(DynastieGenerator):
|
|
|
|
def __init__(self, request=None, hash_posts={}, hash_posts_content={}):
|
|
DynastieGenerator.__init__(self, request, hash_posts, hash_posts_content)
|
|
|
|
self.hooks = {'posts' : self.createPosts,
|
|
'title' : self.createTitle,
|
|
'date' : self.createDate,
|
|
'navigation' : self.createNavigation,
|
|
'recents' : self.createRecents,
|
|
'tags' : self.createTags,
|
|
'replace' : self.createReplace,
|
|
'first_page_only' : self.createFirstPageOnly,
|
|
'ljdc_last' : self.createLJDCLast,
|
|
'comments_count' : self.createCommentsCount,
|
|
'category_name' : self.createCategoryName,
|
|
}
|
|
|
|
self.first_try = True
|
|
|
|
self.posts_per_page = 5
|
|
self.filename = 'index'
|
|
self.dirname = ''
|
|
self.blog = None
|
|
self.parent_posts = []
|
|
|
|
self.resetCounters()
|
|
|
|
def resetCounters(self):
|
|
self.nb_pages = 0
|
|
self.cur_page = 0
|
|
self.cur_post = 0
|
|
self.cur_post_obj = None
|
|
|
|
def createReplace(self, posts, dom, root, replace_elem):
|
|
if not replace_elem.hasAttribute('div_name'):
|
|
self.addError('No attribute div_name for a replace tag')
|
|
return
|
|
|
|
div_element = replace_elem.cloneNode(True)
|
|
div_element.tagName = replace_elem.getAttribute('div_name')
|
|
div_element.removeAttribute('div_name')
|
|
for key,value in replace_elem.attributes.items():
|
|
if key == 'div_name': continue
|
|
|
|
value = value.replace('dyn:blog_id', str(self.blog.id))
|
|
|
|
if self.cur_post_obj:
|
|
url = self.cur_post_obj.getPath()
|
|
full_url = self.cur_post_obj.blog.name + url
|
|
value = value.replace('dyn:post_url', url)
|
|
value = value.replace('dyn:post_full_url', full_url)
|
|
|
|
div_element.setAttribute(key, value)
|
|
|
|
root.replaceChild(div_element, replace_elem)
|
|
return div_element
|
|
|
|
def createCommentsCount(self, posts, dom, root, node):
|
|
from dynastie.models import Comment
|
|
if self.cur_post_obj is None:
|
|
count = 0
|
|
else:
|
|
count = Comment.objects.filter(post=self.cur_post_obj.id).count()
|
|
self.replaceByText(dom, root, node, str(count))
|
|
|
|
def createCategoryName(self, posts, dom, root, node):
|
|
from dynastie.models import Category
|
|
if self.cur_post_obj is None:
|
|
category = ''
|
|
else:
|
|
category = Category.objects.filter(id=self.cur_post_obj.category.id)[0].name
|
|
self.replaceByText(dom, root, node, category)
|
|
|
|
def createNavigation(self, posts, dom, root, node):
|
|
if 0 <= self.nb_pages <= 1:
|
|
return None
|
|
|
|
if self.dirname:
|
|
if self.dirname.startswith('/'):
|
|
href = '<a href="' + self.dirname + '/' + self.filename
|
|
else:
|
|
href = '<a href="/' + self.dirname + '/' + self.filename
|
|
else:
|
|
href = '<a href="/' + self.filename
|
|
|
|
nav = ''
|
|
if self.cur_page != 0:
|
|
nav = nav + href + '.html"><< First</a> '
|
|
if self.cur_page == 1:
|
|
nav = nav + href + '.html">< Prev</a> '
|
|
else:
|
|
nav = nav + href + str(self.cur_page-1) + '.html">< Prev</a> '
|
|
|
|
start = self.cur_page-5
|
|
if start < 0:
|
|
start = 0
|
|
end = start + 10
|
|
|
|
if end > self.nb_pages:
|
|
end = self.nb_pages
|
|
|
|
for i in range(start, end):
|
|
if i == self.cur_page:
|
|
nav = nav + str(i+1) + ' '
|
|
else:
|
|
if i == 0:
|
|
nav = nav + href + '.html">1</a> '
|
|
else:
|
|
nav = nav + href + str(i) + '.html">' + str(i+1) + '</a> '
|
|
|
|
if self.cur_page < self.nb_pages-1:
|
|
nav = nav + href + str(self.cur_page+1) + '.html">Next ></a> '
|
|
nav = nav + href + str(self.nb_pages-1) + '.html">Last >></a>'
|
|
|
|
s = '<div class="navigation">' + nav + '</div>'
|
|
new_dom = parseString(s.encode('utf-8'))
|
|
new_node = new_dom.getElementsByTagName('div')[0]
|
|
res = new_node.cloneNode(True)
|
|
root.replaceChild(res, node)
|
|
|
|
return res
|
|
|
|
def createTitle(self, posts, dom, root, title_elem):
|
|
create_link = (title_elem.getAttribute('link') == '1')
|
|
post = self.cur_post_obj
|
|
if create_link == True:
|
|
node = self.createElement(dom, 'title')
|
|
node.appendChild(self.createLinkElem(dom, post.getPath(), post.title))
|
|
else:
|
|
node = self.createElement(dom, 'title', post.title)
|
|
|
|
root.replaceChild(node, title_elem)
|
|
|
|
def createFirstPageOnly(self, posts, dom, root, node):
|
|
if self.cur_page == 0:
|
|
for n in node.childNodes:
|
|
root.insertBefore(n.cloneNode(True), node)
|
|
root.removeChild(node)
|
|
|
|
def createLJDCLast(self, posts, dom, root, node):
|
|
from dynastie.generators.ljdc import LJDC
|
|
|
|
l = LJDC()
|
|
img = l.getLast(dom, self.blog.src_path)
|
|
|
|
if not img is None:
|
|
root.replaceChild(img, node)
|
|
else:
|
|
root.removeChild(node)
|
|
|
|
def createDate(self, posts, dom, root, date_elem):
|
|
date_format = date_elem.getAttribute('format')
|
|
if date_format == '':
|
|
date_format = '%A, %d %B %Y %H:%m'
|
|
|
|
post = self.cur_post_obj
|
|
node = self.createElement(dom, 'date', post.creation_date.strftime(date_format))
|
|
|
|
root.replaceChild(node, date_elem)
|
|
|
|
def pygmentCode(self, code):
|
|
while True:
|
|
start = code.find('<dyn:code')
|
|
|
|
if start == -1: break
|
|
end = code.find('</dyn:code>')
|
|
|
|
if end < start:
|
|
self.addError('Invalid <dyn:code> tags in ' + self.filename)
|
|
break
|
|
|
|
try:
|
|
dom = parseString(code[start:end+11])
|
|
except xml.dom.DOMException as e:
|
|
self.addError('Error parsing ' + self.filename)
|
|
break
|
|
|
|
res = self.createCode(dom, dom.firstChild)
|
|
if res:
|
|
code = code.replace(code[start:end+11], res)
|
|
|
|
return code
|
|
|
|
def _have_I_right(self, user, post_id):
|
|
from dynastie.models import Post, Blog
|
|
|
|
p = Post.objects.get(pk=post_id)
|
|
|
|
if p is None: return None
|
|
|
|
blog_id = p.blog.id
|
|
|
|
if not user.is_superuser:
|
|
b = Blog.objects.filter(pk=blog_id, writers=user.id)
|
|
if not b: return None
|
|
b = b[0]
|
|
else:
|
|
b = Blog.objects.get(pk=blog_id)
|
|
if b is None: return None
|
|
|
|
return (b, p)
|
|
|
|
def _manageInternalPosts(self, post, text, user=None):
|
|
from dynastie.models import Post
|
|
if not user: user = post.author
|
|
# Markdown replace
|
|
if not post or (post and post.content_format == Post.CONTENT_TEXT):
|
|
internal_posts = re.search('\[\[([0-9]+)\]\]', text)
|
|
if internal_posts:
|
|
for post_id in internal_posts.groups():
|
|
post_id = int(post_id)
|
|
if post_id in self.parent_posts: continue
|
|
_,post = self._have_I_right(user, post_id)
|
|
if not post: continue
|
|
new_content = self._loadPostContent(post)
|
|
if new_content:
|
|
text = text.replace('[[' + str(post_id) + ']]', new_content)
|
|
if internal_posts: return text
|
|
# HTML replace
|
|
if not post or (post and post.content_format == Post.CONTENT_HTML):
|
|
while True:
|
|
start = text.find('<dyn:postinclude')
|
|
|
|
if start == -1: break
|
|
end = text.find('</dyn:postinclude>')
|
|
|
|
if end < start:
|
|
self.addError('Invalid <dyn:postinclude> tags in ' + self.filename)
|
|
break
|
|
|
|
internal_posts = re.search('post_id="([0-9]+)"', text[start:])
|
|
if not internal_posts: break
|
|
for post_id in internal_posts.groups():
|
|
_,post = self._have_I_right(user, post_id)
|
|
if not post: break
|
|
new_content = self._loadPostContent(post)
|
|
if new_content:
|
|
text = text.replace(text[start:end+18], new_content.encode('utf-8'))
|
|
return text
|
|
|
|
def _loadPostContent(self, post):
|
|
from dynastie.models import Post
|
|
|
|
blog = post.blog
|
|
blog.create_paths()
|
|
|
|
filename = blog.src_path + '/_post/' + str(post.id)
|
|
|
|
if not os.path.exists(filename):
|
|
filename2 = blog.src_path + '/_draft/' + str(post.id)
|
|
if not os.path.exists(filename2):
|
|
self.addError('File does not exists ' + filename)
|
|
return None
|
|
else:
|
|
filename = filename2
|
|
|
|
if not filename in self.hash_posts_content:
|
|
f = open(filename, 'rb')
|
|
post_content = f.read()
|
|
f.close()
|
|
self.parent_posts.append(post.id)
|
|
post_content = self._manageInternalPosts(post, post_content)
|
|
if post.content_format == Post.CONTENT_TEXT:
|
|
post_content = markdown2.markdown(post_content, extras=['fenced-code-blocks'])
|
|
self.hash_posts_content[filename] = post_content
|
|
else:
|
|
post_content = self.hash_posts_content[filename]
|
|
|
|
return post_content
|
|
|
|
def createPost(self, posts, dom, post_elem, root):
|
|
from dynastie.models import Post
|
|
post = self.cur_post_obj
|
|
|
|
if post.id in self.hash_posts and not self.first_try:
|
|
node,_ = self.hash_posts[post.id]
|
|
return node.cloneNode(0)
|
|
|
|
values = {'post_content': '', 'author': 'Unknown'}
|
|
try:
|
|
values['author'] = post.author.first_name + ' ' + post.author.last_name
|
|
except:
|
|
pass
|
|
|
|
self.parent_posts = []
|
|
post_content = self._loadPostContent(post)
|
|
if not post_content: return None
|
|
|
|
post_content = self.pygmentCode(post_content)
|
|
|
|
self.simpleTransform(values, dom, post_elem, root)
|
|
|
|
content_nodes = post_elem.getElementsByTagName('div')
|
|
post_transform = ['post_content']
|
|
for content_node in content_nodes:
|
|
the_class = content_node.getAttribute('class')
|
|
if not the_class in post_transform:
|
|
continue
|
|
new_node = dom.createTextNode(post_content)
|
|
content_node.appendChild(new_node)
|
|
|
|
writer = StrictUTF8Writer()
|
|
post_elem.writexml(writer)
|
|
content = writer.getvalue().encode('utf-8')
|
|
|
|
md5 = hashlib.md5()
|
|
md5.update(content)
|
|
|
|
if post.id in self.hash_posts:
|
|
# Here, we are in first_try, check that computed
|
|
# post has the same result than the one in cache
|
|
self.first_try = False
|
|
|
|
_,md5_2 = self.hash_posts[post.id]
|
|
|
|
# If not, clear cache
|
|
if md5.digest() != md5_2:
|
|
self.hash_posts = {}
|
|
self.hash_posts[post.id] = (post_elem.cloneNode(0), md5.digest())
|
|
else:
|
|
self.hash_posts[post.id] = (post_elem.cloneNode(0), md5.digest())
|
|
|
|
return post_elem
|
|
|
|
def createPosts(self, posts, dom, root, node):
|
|
posts_elem = self.createElement(dom, 'posts')
|
|
create_link = (node.getAttribute('link') == '1')
|
|
for i in range(0, self.posts_per_page):
|
|
post_elem = self.createElement(dom, 'post')
|
|
if len(posts) > self.cur_post:
|
|
self.cur_post_obj = posts[self.cur_post]
|
|
post_elem = self.createPost(posts, dom, post_elem, node)
|
|
if post_elem is None: continue
|
|
else:
|
|
post_elem = self.createElement(dom, '', '<b>No posts yet</b>')
|
|
self.cur_post_obj = None
|
|
if post_elem:
|
|
posts_elem.appendChild(post_elem)
|
|
|
|
# Parse inner HTML
|
|
self._parse(self.hooks, posts, dom, post_elem)
|
|
|
|
self.cur_post = self.cur_post + 1
|
|
if self.cur_post == len(posts):
|
|
break
|
|
root.replaceChild(posts_elem, node)
|
|
return posts_elem
|
|
|
|
def createRecents(self, posts, dom, root, node):
|
|
if self.cur_post == len(posts):
|
|
root.removeChild(node)
|
|
return
|
|
|
|
if node.hasAttribute("limit"):
|
|
nb_recents = int(node.getAttribute("limit"))
|
|
else:
|
|
nb_recents = 5
|
|
recents_elem = self.createElement(dom, 'recents')
|
|
for child in node.childNodes:
|
|
recents_elem.appendChild(child.cloneNode(True))
|
|
list_elem = dom.createElement('ul')
|
|
for i in range(0, nb_recents):
|
|
post_elem = dom.createElement('li')
|
|
if self.cur_post+i < len(posts):
|
|
post = posts[self.cur_post+i]
|
|
link_elem = self.createLinkElem(dom, post.getPath(), post.title)
|
|
post_elem.appendChild(link_elem)
|
|
else:
|
|
break
|
|
list_elem.appendChild(post_elem)
|
|
|
|
recents_elem.appendChild(list_elem)
|
|
root.replaceChild(recents_elem, node)
|
|
|
|
return recents_elem
|
|
|
|
def createTags(self, posts, dom, root, node):
|
|
from dynastie.models import Post
|
|
tags_elem = self.createElement(dom, 'tags')
|
|
create_link = (node.getAttribute('link') == '1')
|
|
if type(posts) == models.query.QuerySet or type(posts) == list:
|
|
if len(posts) > self.cur_post:
|
|
cur_post = posts[self.cur_post]
|
|
else:
|
|
cur_post = None
|
|
elif type(posts) == Post:
|
|
cur_post = posts
|
|
else:
|
|
cur_post = None
|
|
|
|
if cur_post:
|
|
for tag in cur_post.tags.all():
|
|
if create_link:
|
|
tag_elem = self.createElement(dom, 'tag')
|
|
link_elem = self.createLinkElem(dom, '/tag/' + tag.name_slug, '#' + tag.name)
|
|
tag_elem.appendChild(link_elem)
|
|
else:
|
|
tag_elem = self.createElement(dom, 'tag', '#' + tag.name)
|
|
tags_elem.appendChild(tag_elem)
|
|
|
|
if len(cur_post.tags.all()) == 0:
|
|
root.removeChild(node)
|
|
return None
|
|
else:
|
|
root.replaceChild(tags_elem, node)
|
|
else:
|
|
root.removeChild(node)
|
|
return None
|
|
|
|
return tags_elem
|
|
|
|
def createCode(self, dom, node):
|
|
try:
|
|
from pygments import highlight
|
|
from pygments.util import ClassNotFound
|
|
from pygments.lexers import get_lexer_by_name
|
|
from pygments.formatters import get_formatter_by_name
|
|
except ImportError:
|
|
self.addError('Pygments package is missing, please install it in order to use <dyn:code>')
|
|
return None
|
|
|
|
language = node.getAttribute('language')
|
|
|
|
if language is None:
|
|
self.addWarning('No language defined for <dyn:code> assuming C language')
|
|
language = "c"
|
|
else:
|
|
language = language.lower()
|
|
|
|
lexer_options = {}
|
|
try:
|
|
lexer = get_lexer_by_name(language, **lexer_options)
|
|
except ClassNotFound:
|
|
self.addWarning('Language ' + language + ' not supported by current version of pygments')
|
|
lexer = get_lexer_by_name('c', **lexer_options)
|
|
|
|
formatter_options = {'classprefix' : 'color_emacs_', 'style' : 'emacs'}
|
|
|
|
for k in node.attributes.keys():
|
|
attr = node.attributes[k]
|
|
if attr.prefix != '': continue
|
|
if attr.name == 'language': continue
|
|
name = attr.name
|
|
value = attr.value
|
|
if name == 'colouring':
|
|
name = 'style'
|
|
formatter_options['classprefix'] = 'color_' + value + '_'
|
|
formatter_options[name] = value
|
|
|
|
formatter = get_formatter_by_name('html', **formatter_options)
|
|
|
|
lexer.encoding = 'utf-8'
|
|
formatter.encoding = 'utf-8'
|
|
|
|
writer = StrictUTF8Writer()
|
|
node.firstChild.writexml(writer)
|
|
code = writer.getvalue().encode('utf-8')
|
|
|
|
r,w = os.pipe()
|
|
r,w=os.fdopen(r,'r',0), os.fdopen(w,'w',0)
|
|
highlight(code, lexer, formatter, w)
|
|
w.close()
|
|
|
|
code = r.read()
|
|
r.close()
|
|
|
|
# Remove <pre> after <div class="highlight">
|
|
code = code[28:-13]
|
|
code = '<div class="highlight">' + code + '</div>'
|
|
|
|
return code
|
|
|
|
def parseTemplate(self, blog, src, output, name, directory=None, parsePostsTag=True):
|
|
self.blog = blog
|
|
|
|
if not os.path.exists(src + '/_%s.html' % name):
|
|
self.addError('No _%s.html found, exiting' % name)
|
|
return None
|
|
|
|
try:
|
|
dom = parse(src + '/_%s.html' % name)
|
|
except xml.dom.DOMException as e:
|
|
self.addError('Error parsing _%s.html : ' + e)
|
|
return None
|
|
|
|
if directory and not os.path.exists(output + '/' + directory):
|
|
os.makedirs(output + '/' + directory)
|
|
|
|
if not parsePostsTag: return dom
|
|
|
|
post_nodes = dom.getElementsByTagNameNS(self.URI, "posts")
|
|
|
|
if post_nodes:
|
|
if post_nodes[0].hasAttribute("limit"):
|
|
self.posts_per_page = int(post_nodes[0].getAttribute("limit"))
|
|
else:
|
|
self.addWarning('No tag dyn:posts found')
|
|
|
|
return dom
|
|
|
|
def generatePages(self, dom, posts, src, output, name):
|
|
if len(posts) > self.posts_per_page:
|
|
self.nb_pages = self.computeNbPages(len(posts), self.posts_per_page)
|
|
|
|
if not os.path.exists(output + self.dirname):
|
|
os.mkdir(output + self.dirname)
|
|
|
|
filename = self.dirname + '/' + self.filename + '.html'
|
|
|
|
impl = xml.dom.getDOMImplementation()
|
|
|
|
while self.cur_page <= self.nb_pages:
|
|
#print 'Generate ' + filename
|
|
dom_ = impl.createDocument('', 'xml', None)
|
|
dom_.replaceChild(dom.firstChild.cloneNode(True), dom_.firstChild)
|
|
nodes = self.parse(src, self.hooks, posts, dom_, dom_.firstChild)
|
|
self.writeIfNotTheSame(output + filename, nodes)
|
|
self.cur_page = self.cur_page + 1
|
|
filename = self.dirname + '/' + self.filename + str(self.cur_page) + '.html'
|
|
|
|
filename = output + filename
|
|
|
|
while os.path.exists(filename):
|
|
self.addReport('Removing unused ' + filename)
|
|
os.unlink(filename)
|
|
filename = filename + '.gz'
|
|
if os.path.exists(filename):
|
|
self.addReport('Removing unused ' + filename)
|
|
os.unlink(filename)
|
|
self.cur_page = self.cur_page + 1
|
|
filename = self.dirname + '/' + self.filename + str(self.cur_page) + '.html'
|
|
filename = output + filename
|
|
|
|
def generate(self, blog, src, output):
|
|
from dynastie.models import Post, Blog
|
|
|
|
self.blog = blog
|
|
self.parent_posts = []
|
|
|
|
dom = self.parseTemplate(blog, src, output, 'index')
|
|
if dom is None: return self.report
|
|
|
|
now = datetime.datetime.now()
|
|
cur_year = now.year
|
|
posts = Post.objects.filter(creation_date__year=cur_year, published=True, front_page=True).order_by('-creation_date')
|
|
|
|
if posts.count() < self.posts_per_page:
|
|
posts = Post.objects.filter(published=True, front_page=True).order_by('-creation_date')[:self.posts_per_page]
|
|
|
|
self.dirname = ''
|
|
self.generatePages(dom, posts, src, output, 'index')
|
|
|
|
if not self.somethingWrote:
|
|
self.addReport('Nothing changed')
|
|
|
|
return self.report
|