Permalink
Browse files

Working comment system using thread strings. TODO - better CSS. Some …

…clean-up with a little more python experience.
  • Loading branch information...
1 parent 60038c4 commit 4920029fd9033eff1662e01efda9737dda547623 @DocSavage committed Aug 7, 2008
Showing with 138 additions and 98 deletions.
  1. +7 −0 authorized.py
  2. +33 −26 blog.py
  3. +15 −23 main.py
  4. +20 −16 model.py
  5. +9 −3 restful.py
  6. +18 −4 static/js/bloog_comments.js
  7. +5 −0 static/style.css
  8. +6 −3 view.py
  9. +5 −5 views/_shared/form_comment.html
  10. +3 −3 views/_shared/form_editor.html
  11. +1 −15 views/blog/article.html
  12. +16 −0 views/blog/comment.html
View
@@ -30,6 +30,8 @@
from google.appengine.api import users
+import logging
+
def role(role):
"""
A decorator to enforce user roles, currently 'user' (logged in)
@@ -56,16 +58,21 @@ def check_login(self, *args, **kwargs):
user = users.get_current_user()
if not user:
if self.request.method != 'GET':
+ logging.debug("Not user - aborting")
self.error(403)
else:
+ logging.debug("User not logged in -- force login")
self.redirect(users.create_login_url(self.request.uri))
elif role == "user" or (role == "admin" and
users.is_current_user_admin()):
+ logging.debug("Role is %s so will allow handler", role)
handler_method(self, *args, **kwargs)
else:
if self.request.method == 'GET':
+ logging.debug("Unknown role (%s) on GET", role)
self.redirect("/403.html")
else:
+ logging.debug("Unknown role: %s", role)
self.error(403) # User didn't meet role.
# TODO: Give better feedback/status code.
return check_login
View
59 blog.py
@@ -48,6 +48,7 @@
from google.appengine.ext import webapp
from google.appengine.api import users
from google.appengine.ext import db
+from google.appengine.ext.webapp import template
import restful
import authorized
@@ -160,10 +161,6 @@ def process_article_submission(handler, article_type):
handler.error(400)
def process_comment_submission(handler, article):
- if not article:
- handler.error(404)
- return
-
# Get and store some pieces of information from parent article.
# TODO: See if this overhead can be avoided
if not article.num_comments:
@@ -179,29 +176,36 @@ def process_comment_submission(handler, article):
'title',
'body',
'key',
+ 'thread', # If it's given, use it. Else generate it.
('published', get_datetime)])
-
+ logging.debug("Got body: %s", property_hash['body'])
# Generate a thread string.
- matchobj = re.match(r'[^#]+#comment-(\w+)', property_hash['key'])
- if matchobj:
- logging.debug("Comment has parent: %s", matchobj.group(1))
- comment_key = matchobj.group(1)
- thread_string = "%03d" % article.num_comments
- else:
- logging.debug("Comment is off main article")
- comment_key = None
- thread_string = "%03d" % article.num_comments
-
- comment = model.Comment(
- name = property_hash['name'],
- email = property_hash['email'],
- homepage = property_hash['homepage'],
- title = property_hash['title'],
- body = property_hash['body'],
- article = article_key,
- thread = thread_string)
+ if 'thread' not in property_hash:
+ matchobj = re.match(r'[^#]+#comment-(?P<key>\w+)', property_hash['key'])
+ if matchobj:
+ logging.debug("Comment has parent: %s", matchobj.group('key'))
+ comment_key = matchobj.group('key')
+ # TODO -- Think about GQL injection security issue since comes from public
+ parent = model.Comment.get(db.Key(comment_key))
+ thread_string = parent.next_child_thread_string()
+ else:
+ logging.debug("Comment is off main article")
+ comment_key = None
+ thread_string = article.next_comment_thread_string()
+ if not thread_string:
+ handler.error(400)
+ return
+ property_hash['thread'] = thread_string
+
+ property_hash['article'] = article_key
+
+ comment = model.Comment(**property_hash)
comment.put()
- restful.send_successful_response(handler, '/' + article.permalink)
+ # Render just this comment and send it to client
+ response = template.render("views/blog/comment.html",
+ { 'comment': comment },
+ debug=config.DEBUG)
+ handler.response.out.write(response)
view.invalidate_cache()
def render_article(handler, article):
@@ -261,7 +265,6 @@ def get(self, path):
if not article:
article = db.Query(model.Article). \
filter('permalink =', path).get()
-
render_article(self, article)
@restful.methods_via_query_allowed
@@ -327,7 +330,11 @@ def post(self, year, month, perm_stem):
permalink = year + '/' + month + '/' + perm_stem
article = db.Query(model.Article). \
filter('permalink =', permalink).get()
- process_comment_submission(self, article)
+ if article:
+ process_comment_submission(self, article)
+ else:
+ logging.debug("No article attached to submitted comment")
+ self.error(400)
@authorized.role("admin")
def put(self, year, month, perm_stem):
View
38 main.py
@@ -36,33 +36,25 @@
import config
import timings
-# TODO: Global that stores cached regexs for routing
-# Each imported handler can check if its routes are already present,
-# and if not, it adds them.
-# This might already be done by webapp.WSGIApplication,
-# in which case we should look for hook to add routes within modules.
-ROUTES = []
+ROUTES = [
+ ('/*$', blog.RootHandler),
+ ('/403.html', blog.UnauthorizedHandler),
+ ('/404.html', blog.NotFoundHandler),
+ ('/([12]\d\d\d)/*$', blog.YearHandler),
+ ('/([12]\d\d\d)/(\d|[01]\d)/*$', blog.MonthHandler),
+ ('/([12]\d\d\d)/(\d|[01]\d)/([-\w]+)/*$', blog.BlogEntryHandler),
+ ('/admin/cache_stats/*$', cache_stats.CacheStatsHandler),
+ ('/admin/timings/*$', timings.TimingHandler),
+ ('/search', blog.SearchHandler),
+ ('/contact/*$', contact.ContactHandler),
+ ('/tag/(.*)', blog.TagHandler),
+ (config.blog['master_atom_url'] + '/*$', blog.AtomHandler),
+ ('/(.*)', blog.ArticleHandler)]
def main():
path = timings.start_run()
logging.debug("Received request with path %s", path)
- application = webapp.WSGIApplication(
- [('/*$', blog.RootHandler),
- ('/403.html', blog.UnauthorizedHandler),
- ('/404.html', blog.NotFoundHandler),
- ('/([12]\d\d\d)/*$', blog.YearHandler),
- ('/([12]\d\d\d)/(\d|[01]\d)/*$', blog.MonthHandler),
- ('/([12]\d\d\d)/(\d|[01]\d)/([-\w]+)/*$',
- blog.BlogEntryHandler),
- ('/admin/cache_stats/*$', cache_stats.CacheStatsHandler),
- ('/admin/timings/*$', timings.TimingHandler),
- ('/search', blog.SearchHandler),
- ('/contact/*$', contact.ContactHandler),
- ('/tag/(.*)', blog.TagHandler),
- (config.blog['master_atom_url'] + '/*$',
- blog.AtomHandler),
- ('/(.*)', blog.ArticleHandler)],
- debug=True)
+ application = webapp.WSGIApplication(ROUTES, debug=True)
wsgiref.handlers.CGIHandler().run(application)
timings.stop_run(path)
View
@@ -24,6 +24,19 @@
import logging
import config
+# Handle generation of thread strings
+def get_thread_string(article, cur_thread_string):
+ min_str = cur_thread_string + '000'
+ max_str = cur_thread_string + '999'
+ q = db.GqlQuery("SELECT * FROM Comment " +
+ "WHERE article = :1 " +
+ "AND thread >= :2 AND thread <= :3",
+ article, min_str, max_str)
+ num_comments = q.count(999)
+ if num_comments > 998:
+ return None # Only allow 999 comments on each tree level
+ return cur_thread_string + "%03d" % (num_comments + 1)
+
# The below was pulled due to computational quota issues on large posts.
# Works with dev server but not after uploading to cloud.
@@ -32,38 +45,29 @@
class Article(db.Model):
permalink = db.StringProperty(required=True)
-
# Useful for aliasing of old urls
legacy_id = db.StringProperty()
-
title = db.StringProperty(required=True)
article_type = db.StringProperty(
required=True,
choices=set(["article", "blog entry"]))
-
# Body can be in any format supported by Bloog (e.g. textile)
body = db.TextProperty(required=True)
-
# If available, we use 'excerpt' to summarize instead of
# extracting the first 68 words of 'body'.
excerpt = db.TextProperty()
-
# The html property is generated from body
html = db.TextProperty()
-
published = db.DateTimeProperty(auto_now_add=True)
updated = db.DateTimeProperty(auto_now_add=True)
format = db.StringProperty(
required=True,
choices=set(["html", "textile",
"markdown", "text"]))
-
# Picked dict for sidelinks, associated Amazon items, etc.
assoc_dict = db.BlobProperty()
-
# To prevent full query when just showing article headlines
num_comments = db.IntegerProperty(default=0)
-
tags = db.ListProperty(db.Category)
def get_comments(self):
@@ -104,6 +108,10 @@ def is_big(self):
else:
return False
+ def next_comment_thread_string(self):
+ 'Returns thread string for next comment for this article'
+ return get_thread_string(self, '')
+
class Comment(db.Model):
name = db.StringProperty()
email = db.EmailProperty()
@@ -134,10 +142,6 @@ def get_indentation(self):
return min([len(nesting_str_array), 10])
def next_child_thread_string(self):
- """Returns thread string for next child of this comment"""
- q = db.GqlQuery("SELECT * FROM Comment " +
- "WHERE article = :1 " +
- "AND thread >= :2 AND thread <= :3",
- self.article,
- thread + '.000', thread + '.999')
- return self.thread + "." + "%03d" % q.count(999)
+ 'Returns thread string for next child of this comment'
+ return get_thread_string(self.article, self.thread + '.')
+
View
@@ -56,22 +56,28 @@ def get_sent_properties(request_func, propname_list):
3) tuple (key, func, additional keys...) -> Get the request
values for the additional keys and pass them through func
before setting the key's value with the output.
+ If a key is not present in the request, then we do not insert a key
+ with None or empty string. The key is simply absent, therefore allowing
+ you to use the returned hash to initial a Model instance.
"""
prop_hash = {}
for item in propname_list:
if type(item) == str:
- prop_hash[item] = request_func(item)
+ key = item
+ value = request_func(item)
elif type(item == tuple):
key = item[0]
prop_func = item[1]
if len(item) <= 2:
- prop_hash[key] = prop_func(request_func(key))
+ value = prop_func(request_func(key))
else:
try:
addl_keys = map(prop_hash.get, item[2:])
- prop_hash[key] = prop_func(*addl_keys)
+ value = prop_func(*addl_keys)
except:
return None
+ if value:
+ prop_hash[key] = value
return prop_hash
def methods_via_query_allowed(handler_method):
@@ -29,7 +29,18 @@ YAHOO.bloog.initComments = function() {
YAHOO.bloog.commentDialog.show();
}
+ var handleSuccess = function(o) {
+ var response = o.responseText;
+ // Insert the comment into the appropriate place then hide dialog.
+ parent_id = '#' + YAHOO.bloog.action.split('#')[1];
+ Ojay(parent_id).insert(response, 'after');
+ YAHOO.bloog.commentDialog.hide();
+ }
+ var handleFailure = function(o) {
+ alert("Sorry, could not save comment!");
+ }
var handleSubmit = function() {
+ YAHOO.bloog.commentEditor.saveHTML();
var html = YAHOO.bloog.commentEditor.get('element').value;
var name = YAHOO.util.Dom.get('commentName').value;
var email = YAHOO.util.Dom.get('commentEmail').value;
@@ -46,11 +57,11 @@ YAHOO.bloog.initComments = function() {
var cObj = YAHOO.util.Connect.asyncRequest(
'POST',
YAHOO.bloog.action,
- { success: YAHOO.bloog.handleSuccess,
- failure: YAHOO.bloog.handleFailure },
+ { success: handleSuccess,
+ failure: handleFailure },
postData);
}
-
+
YAHOO.bloog.commentDialog = new YAHOO.widget.Dialog(
"commentDialog", {
width: "550px",
@@ -70,7 +81,10 @@ YAHOO.bloog.initComments = function() {
}
return true;
}
- YAHOO.bloog.commentDialog.callback = { success: YAHOO.bloog.handleSuccess,
+ var handleDialogSuccess = function() {
+ alert("We are having success from commentDialog");
+ }
+ YAHOO.bloog.commentDialog.callback = { success: handleDialogSuccess,
failure: YAHOO.bloog.handleFailure };
YAHOO.bloog.commentDialog.render();
View
@@ -32,6 +32,11 @@ a:hover{color:#666;}
#wrapper pre, #wrapper p { clear: none; }
th,td{padding:3px 20px 3px 0px;}
+.popupdialog form{text-align: center;}
+.popupdialog form label{width: 400px;}
+.popupdialog form input{width: 400px;}
+.popupdialog form textarea{width: 400px;}
+
/* masthead / footer - navigation and categories */
#masthead{padding:5px 0;}
#masthead h1 a{text-transform:uppercase;width:400px;float:left;margin:12px 0 0 0;}
View
@@ -42,7 +42,9 @@
def invalidate_cache():
memcache.flush_all()
-HANDLER_PATTERN = re.compile("<class '([^\.]*)\.(\w+)Handler'>")
+HANDLER_PATTERN = re.compile(r"<class '(?P<module_name>[^\.]*)"
+ r"\."
+ r"(?P<handler_name>\w+)Handler'>")
def to_filename(camelcase_handler_str):
filename = camelcase_handler_str[0].lower()
@@ -75,8 +77,8 @@ def get_view_file(handler, params={}):
class_name = str(handler.__class__)
nmatch = re.match(HANDLER_PATTERN, class_name)
if nmatch:
- module_name = to_filename(nmatch.group(1))
- handler_name = to_filename(nmatch.group(2))
+ module_name = to_filename(nmatch.group('module_name'))
+ handler_name = to_filename(nmatch.group('handler_name'))
else:
module_name = None
handler_name = None
@@ -116,6 +118,7 @@ def __init__(self, cache_time=None):
def full_render(self, handler, template_file, more_params):
"""Render a dynamic page from scatch."""
+ logging.debug("Doing full render using template_file: %s", template_file)
url = handler.request.uri
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
Oops, something went wrong.

0 comments on commit 4920029

Please sign in to comment.