Skip to content
Browse files

Content generation and dependency system.

  • Loading branch information...
1 parent c94ebfe commit 17e64516f435f61ef864f53621d447123f941e9e Nicholas Johnson committed Oct 9, 2009
Showing with 154 additions and 21 deletions.
  1. +6 −0 config.py
  2. +98 −0 generators.py
  3. +1 −5 handlers.py
  4. +1 −1 lib/aetycoon
  5. +31 −13 models.py
  6. +1 −1 static.py
  7. +12 −0 themes/default/listing.html
  8. +1 −1 themes/default/post.html
  9. +3 −0 utils.py
View
6 config.py
@@ -30,3 +30,9 @@
('Schneier on Security', 'http://www.schneier.com/blog/'),
]),
]
+
+# Number of entries per page in indexes.
+posts_per_page = 10
+
+# The mime type to serve HTML files as.
+html_mime_type = "text/html; charset=utf-8"
View
98 generators.py
@@ -0,0 +1,98 @@
+import hashlib
+from google.appengine.ext import db
+
+import fix_path
+import config
+import static
+import utils
+
+
+generator_list = []
+
+
+class ContentGenerator(object):
+ """A class that generates content and dependency lists for blog posts."""
+
+ @classmethod
+ def name(cls):
+ return cls.__name__
+
+ @classmethod
+ def get_resource_list(cls, post):
+ """Returns a list of resources for the given post.
+
+ Args:
+ post: A BlogPost entity.
+ Returns:
+ A list of resource strings representing resources affected by this post.
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def get_etag(cls, post):
+ """Returns a string that changes if the resource requires regenerating.
+
+ Args:
+ post: A BlogPost entity.
+ Returns:
+ A string representing the current state of the entity, as relevant to this
+ ContentGenerator.
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def generate_resource(cls, post, resource):
+ """(Re)generates a resource for the provided post.
+
+ Args:
+ post: A BlogPost entity.
+ resource: A resource string as returned by get_resource_list.
+ """
+ raise NotImplementedError()
+
+
+class PostContentGenerator(ContentGenerator):
+ """ContentGenerator for the actual blog post itself."""
+
+ @classmethod
+ def get_resource_list(cls, post):
+ return [post.path]
+
+ @classmethod
+ def get_etag(cls, post):
+ return hashlib.sha1(db.model_to_protobuf(post).Encode()).hexdigest()
+
+ @classmethod
+ def generate_resource(cls, post, resource):
+ assert resource == post.path
+ template_vals = {
+ 'post': post,
+ }
+ rendered = utils.render_template("post.html", template_vals)
+ static.set(post.path, rendered, config.html_mime_type)
+generator_list.append(PostContentGenerator)
+
+
+class IndexContentGenerator(ContentGenerator):
+ """ContentGenerator for the homepage of the blog and archive pages."""
+
+ @classmethod
+ def get_resource_list(cls, post):
+ return ["index"]
+
+ @classmethod
+ def get_etag(cls, post):
+ return hashlib.sha1(post.title + post.summary).hexdigest()
+
+ @classmethod
+ def generate_resource(cls, post, resource):
+ assert resource == "index"
+ import models
+ q = models.BlogPost.all().order('-published')
+ posts = q.fetch(config.posts_per_page)
+ template_vals = {
+ 'posts': posts,
+ }
+ rendered = utils.render_template("listing.html", template_vals)
+ static.set('/', rendered, config.html_mime_type)
+generator_list.append(IndexContentGenerator)
View
6 handlers.py
@@ -2,7 +2,6 @@
from google.appengine.ext import webapp
-import config
import models
import utils
@@ -13,7 +12,7 @@
class PostForm(djangoforms.ModelForm):
class Meta:
model = models.BlogPost
- exclude = [ 'path', 'published', 'updated' ]
+ exclude = [ 'path', 'published', 'updated', 'deps' ]
def with_post(fun):
@@ -30,9 +29,6 @@ def decorate(self, post_id=None):
class BaseHandler(webapp.RequestHandler):
def render_to_response(self, template_name, template_vals=None, theme=None):
- template_vals.update({
- 'config': config,
- })
template_name = os.path.join("admin", template_name)
self.response.out.write(utils.render_template(template_name, template_vals,
theme))
2 lib/aetycoon
@@ -1 +1 @@
-Subproject commit 4c6d3ce50199d8c267d39a99996fd238c4c6bbe2
+Subproject commit 316e36aa5e6792bdc5c010454aa45a04c814eb34
View
44 models.py
@@ -1,37 +1,55 @@
+import aetycoon
+import re
from google.appengine.ext import db
+from google.appengine.ext import deferred
import config
+import generators
import static
import utils
class BlogPost(db.Model):
- MIME_TYPE = "text/html; charset=utf-8"
-
# The URL path to the blog post. Posts have a path iff they are published.
path = db.StringProperty()
title = db.StringProperty(required=True, indexed=False)
body = db.TextProperty(required=True)
published = db.DateTimeProperty(auto_now_add=True)
updated = db.DateTimeProperty(auto_now=True)
+ deps = aetycoon.PickleProperty()
- def render(self):
- template_vals = {
- 'config': config,
- 'post': self,
- }
- return utils.render_template("post.html", template_vals)
+ @property
+ def summary(self):
+ """Returns a summary of the blog post."""
+ match = re.search("<!--.*cut.*-->", self.body)
+ if match:
+ return self.body[:match.start(0)]
+ else:
+ return self.body
def publish(self):
- rendered = self.render()
if not self.path:
num = 0
content = None
while not content:
path = utils.format_post_path(self, num)
- content = static.add(path, rendered, self.MIME_TYPE)
+ content = static.add(path, '', config.html_mime_type)
num += 1
self.path = path
- self.put()
- else:
- static.set(self.path, rendered, self.MIME_TYPE)
+ if not self.deps:
+ self.deps = {}
+ self.put()
+ for generator_class in generators.generator_list:
+ new_deps = set(generator_class.get_resource_list(self))
+ new_etag = generator_class.get_etag(self)
+ old_deps, old_etag = self.deps.get(generator_class.name(), (set(), None))
+ if new_etag != old_etag:
+ # If the etag has changed, regenerate everything
+ to_regenerate = new_deps | old_deps
+ else:
+ # Otherwise just regenerate the changes
+ to_regenerate = new_deps ^ old_deps
+ for dep in to_regenerate:
+ generator_class.generate_resource(self, dep)
+ self.deps[generator_class.name()] = (new_deps, new_etag)
+ self.put()
View
2 static.py
@@ -18,7 +18,7 @@ class StaticContent(db.Model):
The serving path for content is provided in the key name.
"""
- body = db.BlobProperty(required=True)
+ body = db.BlobProperty()
content_type = db.StringProperty(required=True)
last_modified = db.DateTimeProperty(required=True, auto_now=True)
etag = aetycoon.DerivedProperty(lambda x: hashlib.sha1(x.body).hexdigest())
View
12 themes/default/listing.html
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+{% block title %}{{config.blog_name}}{% endblock %}
+{% block body %}
+ {% for post in posts %}
+ <h2><a href="{{post.path}}">{{post.title}}</a></h2>
+ {{post.summary|linebreaks}}
+ <p class="postmeta">
+ <a href="{{post.path}}" class="readmore">Read more</a> |
+ <span class="date">{{post.published|date:"d F, Y"}}</span>
+ </p>
+ {% endfor %}
+{% endblock %}
View
2 themes/default/post.html
@@ -2,5 +2,5 @@
{% block title %}{{post.title}} - {{config.blog_name}}{% endblock %}
{% block body %}
<h2>{{post.title}}</h2>
- {{post.body}}
+ {{post.body|linebreaks}}
{% endblock %}
View
3 utils.py
@@ -26,5 +26,8 @@ def format_post_path(post, num):
def render_template(template_name, template_vals=None, theme=None):
+ template_vals.update({
+ 'config': config,
+ })
template_path = os.path.join("themes", theme or config.theme, template_name)
return template.render(template_path, template_vals or {})

0 comments on commit 17e6451

Please sign in to comment.
Something went wrong with that request. Please try again.