Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'lollysite'

Conflicts:
	README
	app.yaml
	index.yaml
	webbase.py
  • Loading branch information...
commit b98f9f98e8744071eb482e32adafbafeeff0d70f 2 parents 08ddb8e + 001962e
@chilts chilts authored
Showing with 5,508 additions and 18 deletions.
  1. +28 −0 .cil
  2. +4 −0 .gitignore
  3. +13 −0 Makefile
  4. +5 −0 README
  5. +93 −0 admin.py
  6. +35 −18 app.yaml
  7. +49 −0 comment.py
  8. +37 −0 config.py
  9. +55 −0 deploy.sh
  10. +60 −0 formbase.py
  11. +82 −0 image.py
  12. +59 −0 index.yaml
  13. +3 −0  issues/README.txt
  14. +8 −0 issues/c_11ec6a41.cil
  15. +6 −0 issues/c_266be84e.cil
  16. +6 −0 issues/c_3478283b.cil
  17. +9 −0 issues/c_4212371e.cil
  18. +7 −0 issues/c_45ce395f.cil
  19. +9 −0 issues/c_49febb07.cil
  20. +8 −0 issues/c_72026994.cil
  21. +7 −0 issues/c_8aec3b49.cil
  22. +7 −0 issues/c_8c04ba86.cil
  23. +6 −0 issues/c_8c913e7d.cil
  24. +8 −0 issues/c_96e7fff2.cil
  25. +7 −0 issues/c_aaeb75a5.cil
  26. +8 −0 issues/c_c29602a7.cil
  27. +6 −0 issues/c_d16a43ef.cil
  28. +11 −0 issues/c_d888b9f3.cil
  29. +6 −0 issues/c_de398b30.cil
  30. +8 −0 issues/c_e914e17d.cil
  31. +9 −0 issues/c_f17e8af3.cil
  32. +7 −0 issues/c_f31a14c7.cil
  33. +9 −0 issues/c_f3c6fe7f.cil
  34. +13 −0 issues/i_0085f7a8.cil
  35. +31 −0 issues/i_017be0b1.cil
  36. +19 −0 issues/i_13be3dea.cil
  37. +13 −0 issues/i_1a5632a5.cil
  38. +16 −0 issues/i_1eb22241.cil
  39. +10 −0 issues/i_215f3669.cil
  40. +10 −0 issues/i_22b3cae7.cil
  41. +13 −0 issues/i_25945b54.cil
  42. +17 −0 issues/i_2967c411.cil
  43. +10 −0 issues/i_2d45b464.cil
  44. +10 −0 issues/i_45ff7274.cil
  45. +20 −0 issues/i_475f1a9b.cil
  46. +17 −0 issues/i_5cc7cb70.cil
  47. +18 −0 issues/i_61708bd4.cil
  48. +16 −0 issues/i_65d73568.cil
  49. +15 −0 issues/i_7c9e24ce.cil
  50. +10 −0 issues/i_a07f8c6b.cil
  51. +38 −0 issues/i_a4c0e80f.cil
  52. +11 −0 issues/i_aa20208b.cil
  53. +14 −0 issues/i_ac70e68c.cil
  54. +13 −0 issues/i_ae338c79.cil
  55. +13 −0 issues/i_af6d0b4a.cil
  56. +31 −0 issues/i_b9f21e3d.cil
  57. +12 −0 issues/i_beba51d7.cil
  58. +14 −0 issues/i_c4aa4a14.cil
  59. +10 −0 issues/i_cf2c65d5.cil
  60. +17 −0 issues/i_d8fd5c23.cil
  61. +25 −0 issues/i_db0edf7f.cil
  62. +13 −0 issues/i_e08ba70d.cil
  63. +11 −0 issues/i_fba16979.cil
  64. +303 −0 lollysite.py
  65. +87 −0 migrate.py
  66. +173 −0 models.py
  67. +51 −0 node.py
  68. +89 −0 page.py
  69. +162 −0 phliky.py
  70. +235 −0 properties.py
  71. +67 −0 property.py
  72. +127 −0 queue.py
  73. +5 −0 queue.yaml
  74. +85 −0 section.py
  75. +71 −0 tags.py
  76. +177 −0 test.py
  77. +23 −0 theme/admin/admin-edit.html
  78. +46 −0 theme/admin/admin-image-index.html
  79. +30 −0 theme/admin/admin-index.html
  80. +46 −0 theme/admin/admin-page-index.html
  81. +57 −0 theme/admin/comment-index.html
  82. +24 −0 theme/admin/delete.html
  83. +23 −0 theme/admin/edit.html
  84. +78 −0 theme/admin/image-form.html
  85. +15 −0 theme/admin/migrate.html
  86. +75 −0 theme/admin/page-form.html
  87. +36 −0 theme/admin/property-form.html
  88. +44 −0 theme/admin/property-list.html
  89. +70 −0 theme/admin/section-form.html
  90. +49 −0 theme/admin/section-list.html
  91. +614 −0 theme/admin/static/960.min.css
  92. BIN  theme/admin/static/i/accept.png
  93. BIN  theme/admin/static/i/add.png
  94. BIN  theme/admin/static/i/brick_delete.png
  95. BIN  theme/admin/static/i/cancel.png
  96. BIN  theme/admin/static/i/delete.png
  97. BIN  theme/admin/static/i/error.png
  98. BIN  theme/admin/static/i/exclamation.png
  99. BIN  theme/admin/static/i/feed.png
  100. BIN  theme/admin/static/i/image_add.png
  101. BIN  theme/admin/static/i/layout_edit.png
  102. BIN  theme/admin/static/i/rss.png
  103. +15 −0 theme/admin/static/ready.js
  104. +53 −0 theme/admin/static/reset.min.css
  105. +126 −0 theme/admin/static/style.css
  106. +84 −0 theme/admin/static/text.min.css
  107. +78 −0 theme/admin/wrapper.html
  108. +26 −0 theme/rss/rss20.xml
  109. +9 −0 theme/sitemaps/sitemapindex.xml
  110. +17 −0 theme/sitemaps/urlset.xml
  111. +3 −0  theme/verticals/README
  112. +13 −0 theme/verticals/archive-index.html
  113. +36 −0 theme/verticals/blog-index.html
  114. +40 −0 theme/verticals/comment.html
  115. +30 −0 theme/verticals/contact.html
  116. +20 −0 theme/verticals/content-index.html
  117. +14 −0 theme/verticals/content.html
  118. +27 −0 theme/verticals/faq-index.html
  119. +13 −0 theme/verticals/label-index.html
  120. +243 −0 theme/verticals/license.txt
  121. +8 −0 theme/verticals/minimal.html
  122. +19 −0 theme/verticals/node-skel.frag
  123. +30 −0 theme/verticals/node.frag
  124. +39 −0 theme/verticals/node.html
  125. BIN  theme/verticals/static/button-active.gif
  126. BIN  theme/verticals/static/button-hover.gif
  127. BIN  theme/verticals/static/button.gif
  128. BIN  theme/verticals/static/footer.gif
  129. BIN  theme/verticals/static/li.gif
  130. BIN  theme/verticals/static/li.png
  131. BIN  theme/verticals/static/sidebar-extra.gif
  132. BIN  theme/verticals/static/sidebar-title.gif
  133. BIN  theme/verticals/static/sidebar.gif
  134. +363 −0 theme/verticals/static/style.css
  135. BIN  theme/verticals/static/underline.gif
  136. +9 −0 theme/verticals/static/verticals.js
  137. +104 −0 theme/verticals/wrapper.html
  138. +68 −0 util.py
  139. +4 −0 webbase.py
View
28 .cil
@@ -0,0 +1,28 @@
+VCS: Git
+StatusStrict: 1
+StatusAllowedList: New
+StatusAllowedList: Reviewed
+StatusAllowedList: Assigned
+StatusAllowedList: InProgress
+StatusAllowedList: Finished
+StatusAllowedList: WontFix
+StatusAllowedList: Cancelled
+StatusOpenList: New
+StatusOpenList: Reviewed
+StatusOpenList: Assigned
+StatusOpenList: InProgress
+StatusClosedList: Finished
+StatusClosedList: WontFix
+StatusClosedList: Cancelled
+DefaultNewStatus: New
+LabelStrict: 1
+LabelAllowedList: Type-Enhancement
+LabelAllowedList: Type-Defect
+LabelAllowedList: Priority-High
+LabelAllowedList: Priority-Medium
+LabelAllowedList: Priority-Low
+LabelAllowedList: Release-v0.1.0
+LabelAllowedList: Release-v0.2.0
+LabelAllowedList: Milestone-v0.1
+LabelAllowedList: Milestone-v0.2
+LabelAllowedList: Milestone-Future
View
4 .gitignore
@@ -0,0 +1,4 @@
+*.pyc
+app.yaml.*
+google_appengine
+store
View
13 Makefile
@@ -0,0 +1,13 @@
+start-server:
+ google_appengine/dev_appserver.py --datastore_path=store/data.db --history_path=store/data.db.history ./
+
+issue-summary:
+ cil summary --is-open --label=Milestone-v0.2
+
+issue-list:
+ cil list --is-open --label=Milestone-v0.2
+
+clean:
+ find . -name '*~' -exec rm {} ';'
+
+.PHONY: start-server upload update-indexes issue-summary issue-list
View
5 README
@@ -1,2 +1,7 @@
This is a repository for holding the app used to run the Kohacon website
+This application is based on Lollysite:
+
+* http://www.chilts.org/project/lollysite/
+* http://gitorious.org/lollysite
+* git clone git://gitorious.org/lollysite/lollysite.git
View
93 admin.py
@@ -0,0 +1,93 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import cgi
+import os
+from types import *
+
+# Google specific modules
+from google.appengine.api import users
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import run_wsgi_app
+from google.appengine.ext import db
+
+# local modules
+import webbase
+import property
+import section
+import page
+import image
+import comment
+
+import migrate
+
+## ----------------------------------------------------------------------------
+
+class Home(webbase.WebBase):
+ def get(self):
+ self.redirect('/admin/section/')
+
+class Delete(webbase.WebBase):
+ def get(self):
+ entity = db.get( self.request.get('key') )
+ action = self.request.get('_act')
+ if action == 'rem':
+ pass
+ elif action == 'del':
+ entity.delete()
+ else:
+ pass
+ vals = {
+ 'entity' : entity
+ }
+ self.template( 'delete.html', vals, 'admin' )
+
+application = webapp.WSGIApplication(
+ [
+ ('/admin/', Home),
+
+ # properties
+ ('/admin/property/', property.List),
+ ('/admin/property/new.html', property.FormHandler),
+ ('/admin/property/edit.html', property.FormHandler),
+ ('/admin/property/uncache.html', property.UnCache),
+
+ # sections
+ ('/admin/section/', section.List),
+ ('/admin/section/new.html', section.FormHandler),
+ ('/admin/section/edit.html', section.FormHandler),
+
+ # pages
+ ('/admin/page/', page.List),
+ ('/admin/page/new.html', page.FormHandler),
+ ('/admin/page/edit.html', page.FormHandler),
+
+ # images
+ ('/admin/image/', image.List),
+ ('/admin/image/new.html', image.FormHandler),
+ ('/admin/image/edit.html', image.FormHandler),
+
+ # files
+ #('file/', file.FileList),
+ #('file/edit.html', file.FileEdit),
+
+ # comments
+ ('/admin/comment/', comment.Index),
+
+ # delete any entity
+ ('/admin/del.html', Delete),
+
+ # migrations
+ ('/admin/migrate/', migrate.Migrate),
+ ],
+ debug = True
+)
+
+## ----------------------------------------------------------------------------
+
+def main():
+ run_wsgi_app(application)
+
+if __name__ == "__main__":
+ main()
+
+## ----------------------------------------------------------------------------
View
53 app.yaml
@@ -1,5 +1,5 @@
application: kohacon
-version: v0-1
+version: v0-02
runtime: python
api_version: 1
default_expiration: '1d'
@@ -13,24 +13,41 @@ handlers:
script: admin.py
login: admin
-- upload: theme/[^/]+/s/.* # all the files to upload
- url: /s/([^/]+)/(.*) # the paths this handler will serve
- static_files: theme/\1/s/\2 # where the files actually live (on the server)
+- url: /_ah/queue/.*
+ script: queue.py
+ login: admin
+
+- upload: theme/[^/]+/static/.* # all the files to upload
+ url: /static/([^/]+)/(.*) # the paths this handler will serve
+ static_files: theme/\1/static/\2 # where the files actually live (on the server)
+
+- url: /test/.*
+ script: test.py
+ login: admin
+
+- url: /node/.*
+ script: node.py
- url: /.*
- script: kohacon.py
+ script: lollysite.py
skip_files:
-- ^(.*/)?app\.yaml
-- ^(.*/)?app\.yml
-- ^(.*/)?index\.yaml
-- ^(.*/)?index\.yml
-- ^(.*/)?#.*#
-- ^(.*/)?.*~
-- ^(.*/)?.*\.py[co]
-- ^(.*/)?.*/RCS/.*
-- ^(.*/)?\..*
-- store/.*
-- Makefile
-- README
-
+- ^(.*/)?app\.yaml$
+- ^(.*/)?app\.yml$
+- ^(.*/)?index\.yaml$
+- ^(.*/)?index\.yml$
+- ^(.*/)?.*\.log$
+- ^(.*/)?.*\.dat$
+- ^(.*/)?.*\.py[co]$ # compiled Python
+- ^(.*/)?.*/RCS/.* # any RCS files
+- ^(.*/)?\..* # dotfiles
+- ^(.*/)?#.*# # emacs autosave
+- ^(.*/)?.*~$ # backup files
+- ^issues/.* # cil issues
+- ^store/.* # local datastore
+- ^logs/.* # downloaded logs
+- ^docs/.* # all the docs
+- ^google_appengine/.* # the SDK
+- ^Makefile
+- ^README
+- ^LICENSE
View
49 comment.py
@@ -0,0 +1,49 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import re
+
+# Google specific modules
+from google.appengine.ext.webapp import template
+from google.appengine.ext import db
+
+# local modules
+import webbase
+import formbase
+from models import Comment
+
+## ----------------------------------------------------------------------------
+
+status_map = {
+ 'approve' : 'approved',
+ 'reject' : 'rejected',
+}
+
+## ----------------------------------------------------------------------------
+
+# Forms
+class Index(webbase.WebBase):
+ def get(self):
+ # see if there is a key
+ comment = None
+ status = None
+ try:
+ comment = Comment.get( self.request.get('key') )
+ status = self.request.get('status')
+ except db.BadKeyError:
+ pass
+
+ if comment is None:
+ # show all the 'new' comments
+ comments = Comment.all().filter('status =', 'new').order('inserted')
+ vals = {
+ 'comments' : comments,
+ }
+ self.template( 'comment-index.html', vals, 'admin' )
+ else:
+ comment.status = status_map[status]
+ comment.put()
+ comment.node.regenerate()
+ self.redirect('./')
+ return
+
+## ----------------------------------------------------------------------------
View
37 config.py
@@ -0,0 +1,37 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import logging
+
+# Google specific modules
+from google.appengine.api import memcache
+from google.appengine.ext.webapp import template
+
+# local modules
+from models import Property
+
+## ----------------------------------------------------------------------------
+
+register = template.create_template_register()
+
+# utility functions
+def value(title):
+ # see if this is in Memcache
+ value = memcache.get(title, 'property')
+ if value is not None:
+ return value
+
+ # not in Memcached, so ask the datastore
+ data = Property.all().filter("title =", title)
+ if not data.count():
+ return ''
+
+ # if this property has no value
+ value = data[0].value
+ if value is None:
+ return ''
+
+ # set the new value in memcache and return it
+ memcache.set(title, value, namespace='property')
+ return value
+
+## ----------------------------------------------------------------------------
View
55 deploy.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+if [ -z "$1" ]; then
+ echo 'Please provide an app to deploy'
+ exit 2
+fi
+
+if [ -z "$2" ]; then
+ echo 'Please specify a command:'
+ echo ' - update'
+ echo ' - update_indexes'
+ echo ' - vacuum_indexes'
+ echo ' - update_queues'
+ echo ' - update_cron'
+ exit 2
+fi
+
+APP="$1"
+COMMAND="$2"
+
+echo "Performing '$COMMAND' on '$APP'"
+
+case "$APP" in
+ chilts)
+ echo "Hi Chilts"
+ APP=website-chilts-org
+ ;;
+ lollysite)
+ echo "Main Lollysite site"
+ ;;
+ *)
+ echo "Unknown site"
+ exit 2
+ ;;
+esac
+
+# setup the app.yaml for this application
+sed -i.orig "1,1s/^application: lollysite$/application: $APP/" app.yaml
+
+case "$COMMAND" in
+ update|update_indexes|update_queues|update_cron)
+ echo "Doing $COMMAND"
+ google_appengine/appcfg.py $COMMAND ./
+ ;;
+ request-logs)
+ echo "Doing update"
+ google_appengine/appcfg.py request_logs ./ logs/access-$APP.log
+ ;;
+ *)
+ echo "Unknown command"
+ ;;
+esac
+
+# put the normal app.yaml back
+sed -i.last "1,1s/^application: .*$/application: lollysite/" app.yaml
View
60 formbase.py
@@ -0,0 +1,60 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import logging
+
+# Google specific modules
+from google.appengine.ext import db
+
+# local modules
+import webbase
+
+## ----------------------------------------------------------------------------
+
+# FormBaseHandler
+class FormBaseHandler(webbase.WebBase):
+ def get(self):
+ item = None
+ if self.request.get('key'):
+ item = db.get( self.request.get('key') )
+ logging.info('item=' + item.__class__.__name__)
+ form = self.form(instance=item)
+ logging.info('form.instance=' + form.instance.__class__.__name__)
+ else:
+ form = self.form()
+
+ # logging.info('Doing get() for ' + self.type() + ' with ' + (str(item.key()) or '[None]'))
+ vals = {
+ 'type' : self.type(),
+ 'form' : form,
+ 'item' : item,
+ }
+ self.template( 'admin-edit.html', vals, 'admin' );
+
+ def post(self):
+ item = None
+ form = None
+ if self.request.get('key'):
+ item = db.get( self.request.get('key') )
+ form = self.form( self.request.POST, instance=item )
+ else:
+ form = self.form( self.request.POST )
+
+ # logging.info('Doing get() for ' + self.type() + ' with ' + (str(item.key()) or '[None]'))
+
+ if form.is_valid():
+ item = form.save( commit = False )
+ item.put()
+ self.redirect('.')
+ else:
+ vals = {
+ 'type' : self.type(),
+ 'form' : form,
+ 'item' : item,
+ }
+ self.template( 'admin-edit.html', vals, 'admin' );
+
+class FormDelBase(webbase.WebBase):
+ def get(self):
+ return ''
+
+## ----------------------------------------------------------------------------
View
82 image.py
@@ -0,0 +1,82 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import logging
+import re
+
+# Google specific modules
+from google.appengine.ext import db
+
+# local modules
+import models
+import webbase
+import formbase
+import util
+
+## ----------------------------------------------------------------------------
+
+# list
+class List(webbase.WebBase):
+ def get(self):
+ images = models.Image.all().order('-inserted')
+ vals = {
+ 'title' : ' List',
+ 'images' : images
+ }
+ self.template( 'admin-image-index.html', vals, 'admin' );
+
+# form
+class FormHandler(webbase.WebBase):
+ def get(self):
+ item = None
+ if self.request.get('key'):
+ item = db.get( self.request.get('key') )
+ logging.info('item=' + item.__class__.__name__)
+
+ vals = {
+ 'item' : item,
+ 'sections' : models.Section.all(),
+ }
+ self.template( 'image-form.html', vals, 'admin' );
+
+ def post(self):
+ item = None
+ if self.request.get('key'):
+ item = db.get( self.request.get('key') )
+
+ # do some preprocessing of the input params
+ name = self.request.get('name')
+ if name == '':
+ name = util.urlify(self.request.get('title'))
+
+ # save the image
+ file = self.request.POST['image']
+ section = db.get( self.request.POST['section'] )
+ imagedata = models.ImageData(
+ data = self.request.POST.get('image').file.read()
+ )
+ imagedata.put()
+
+ # put the item to the stastore
+ item = models.Image(
+ section = section,
+ name = name,
+ title = self.request.POST['title'],
+ filename = file.filename,
+ mimetype = file.type,
+ imagedata = imagedata,
+ caption = self.request.POST['caption'],
+ credit = self.request.POST['credit'],
+ credit_link = self.request.POST['credit_link'],
+ label_raw = self.request.get('label_raw'),
+ )
+
+ # update and save this page
+ item.set_derivatives()
+ item.put()
+
+ # once saved, regenerate certain section properties
+ section.regenerate()
+
+ self.redirect('.')
+
+## ----------------------------------------------------------------------------
View
59 index.yaml
@@ -9,3 +9,62 @@ indexes:
# manually, move them above the marker line. The index.yaml file is
# automatically uploaded to the admin console when you next deploy
# your application using appcfg.py.
+
+- kind: Comment
+ properties:
+ - name: node
+ - name: inserted
+
+- kind: Comment
+ properties:
+ - name: node
+ - name: inserted
+ direction: desc
+
+- kind: Comment
+ properties:
+ - name: node
+ - name: status
+ - name: inserted
+
+- kind: Comment
+ properties:
+ - name: status
+ - name: inserted
+
+- kind: Node
+ properties:
+ - name: archive
+ - name: section
+ - name: inserted
+
+- kind: Node
+ properties:
+ - name: archive
+ - name: section
+ - name: inserted
+ direction: desc
+
+- kind: Node
+ properties:
+ - name: class
+ - name: inserted
+ direction: desc
+
+- kind: Node
+ properties:
+ - name: label
+ - name: section
+ - name: inserted
+ direction: desc
+
+- kind: Node
+ properties:
+ - name: section
+ - name: inserted
+
+- kind: Node
+ properties:
+ - name: section
+ - name: inserted
+ direction: desc
View
3  issues/README.txt
@@ -0,0 +1,3 @@
+This directory is used by CIL to track issues and feature requests.
+
+The home page for CIL is at http://kapiti.geek.nz/software/cil.html
View
8 issues/c_11ec6a41.cil
@@ -0,0 +1,8 @@
+Issue: 1a5632a5
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-07T08:52:39
+Updated: 2009-12-07T08:52:59
+
+Added quick links to both add node and add image.
+
+Also quick link to section edit.
View
6 issues/c_266be84e.cil
@@ -0,0 +1,6 @@
+Issue: af6d0b4a
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-12T05:05:01
+Updated: 2009-12-12T05:05:17
+
+Done. Needs to be tested on live.
View
6 issues/c_3478283b.cil
@@ -0,0 +1,6 @@
+Issue: aa20208b
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-07T09:30:53
+Updated: 2009-12-07T09:31:05
+
+Heh, funny story. I was using self.key instead of self.key().
View
9 issues/c_4212371e.cil
@@ -0,0 +1,9 @@
+Issue: fba16979
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-05T01:25:38
+Updated: 2009-12-05T01:26:23
+
+Unfortunately, I can't make the comment inherit from the layout of the section.
+This is annoying since it's like Mason all over again.
+
+There must be a better way.
View
7 issues/c_45ce395f.cil
@@ -0,0 +1,7 @@
+Issue: b9f21e3d
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-24T09:03:26
+Updated: 2009-11-24T09:03:58
+
+All of these things should be called 'Attributes' and not 'Properties' since
+property is already used for the general site.
View
9 issues/c_49febb07.cil
@@ -0,0 +1,9 @@
+Issue: 61708bd4
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-22T08:43:35
+Updated: 2009-11-22T08:44:20
+
+Archive pages have been added. A new 'archive' StringProperty() was added to
+'Node'.
+
+This was then used like 'archive:2009.html'.
View
8 issues/c_72026994.cil
@@ -0,0 +1,8 @@
+Issue: 25945b54
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-10T09:50:47
+Updated: 2009-12-10T09:51:30
+
+To actually remove the value from memcached upon save, it is worth moving away
+from DjangoForms (since they are overcomplicated and have given me lots of
+trouble) to something a little simpler.
View
7 issues/c_8aec3b49.cil
@@ -0,0 +1,7 @@
+Issue: 017be0b1
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-05T00:33:37
+Updated: 2009-12-05T00:34:24
+
+Menu overriding has now been updated to include the information regenerated in
+cil-5cc7cb70.
View
7 issues/c_8c04ba86.cil
@@ -0,0 +1,7 @@
+Issue: af6d0b4a
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-12T02:00:26
+Updated: 2009-12-12T02:01:02
+
+The contact form is now showing. It uses a 'contact-form' attribute (not
+'has-contact-form') since the method itself is called has().
View
6 issues/c_8c913e7d.cil
@@ -0,0 +1,6 @@
+Issue: 475f1a9b
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-07T08:56:05
+Updated: 2009-12-07T08:56:28
+
+Initial work for this has been done in cil-1a5632a5.
View
8 issues/c_96e7fff2.cil
@@ -0,0 +1,8 @@
+Issue: beba51d7
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-23T09:10:05
+Updated: 2009-11-23T09:10:56
+
+Make the labels a text input and split it when saving: from label_raw -> label.
+
+Also, tidied up some rendering of images and suchlike.
View
7 issues/c_aaeb75a5.cil
@@ -0,0 +1,7 @@
+Issue: 25945b54
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-10T09:19:35
+Updated: 2009-12-10T09:19:58
+
+Might as well have a button which can delete it from memcached from the admin
+interface too. Just in case.
View
8 issues/c_c29602a7.cil
@@ -0,0 +1,8 @@
+Issue: 017be0b1
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-30T09:06:13
+Updated: 2009-11-30T09:06:47
+
+Initial part done. Menus now overridden correctly for each section.
+
+THe next part to do is to make the menus actually mean something :)
View
6 issues/c_d16a43ef.cil
@@ -0,0 +1,6 @@
+Issue: ae338c79
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-05T01:33:48
+Updated: 2009-12-05T01:33:56
+
+Remove SectionLayout stuff too.
View
11 issues/c_d888b9f3.cil
@@ -0,0 +1,11 @@
+Issue: 475f1a9b
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-07T09:03:21
+Updated: 2009-12-07T09:04:50
+
+Also, instead of passing the 'item' object, we should take the fields from that
+_OR_ the input params (whether that is from the initial GET params or if the
+item has been edited and validation failed.
+
+This is easier since then we don't have things like {{ item.section.key.str }}
+in the template and we can just do {{ item.section }}.
View
6 issues/c_de398b30.cil
@@ -0,0 +1,6 @@
+Issue: b9f21e3d
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-24T09:16:47
+Updated: 2009-11-24T09:17:10
+
+Fields like 'allow_comment' cann now be removed since they are superceded.
View
8 issues/c_e914e17d.cil
@@ -0,0 +1,8 @@
+Issue: db0edf7f
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-23T09:23:37
+Updated: 2009-11-23T09:24:25
+
+Added a quick and easy migration policy, but doesn't yet save which migrations
+have already been done. Merging into master such that we can make use of it,
+but still InProgress.
View
9 issues/c_f17e8af3.cil
@@ -0,0 +1,9 @@
+Issue: 5cc7cb70
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-05T00:26:11
+Updated: 2009-12-05T00:27:51
+
+Finished the Section regeneration code. Archives and labels are now regenerated
+30s after a node has been saved underneath them.
+
+Will add the regeneration for comments into a separate issue.
View
7 issues/c_f31a14c7.cil
@@ -0,0 +1,7 @@
+Issue: b9f21e3d
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-25T06:49:32
+Updated: 2009-11-25T06:50:08
+
+All done. The next part might be something so that it's easier to add or remove
+attributes in the admin section.
View
9 issues/c_f3c6fe7f.cil
@@ -0,0 +1,9 @@
+Issue: d8fd5c23
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-05T01:05:42
+Updated: 2009-12-05T01:06:10
+
+This has been added.
+
+Some interesting bits with not fetching the comments (if none there), has also
+been added.
View
13 issues/i_0085f7a8.cil
@@ -0,0 +1,13 @@
+Summary: Add ability to edit comments
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-07T23:41:50
+Updated: 2009-12-07T23:43:23
+
+Mostly, we don't want to edit comments, but sometimes people make mistakes (and
+don't realise and don't correct them). Having the ability to edit comments from
+the 'New Comments' list would be good.
+
+Editing and saving a comment, wouldn't change it's status though, so we'd leave
+that to the nice clicky buttons on the new comment list.
View
31 issues/i_017be0b1.cil
@@ -0,0 +1,31 @@
+Summary: Make the 'verticals' theme allow menu overriding
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.1
+Comment: 8aec3b49
+Comment: c29602a7
+Inserted: 2009-11-22T05:24:22
+Updated: 2009-12-05T00:34:24
+
+The menu on the left should change from the top level one to a smaller but
+section dependent menu when drilling down through the site.
+
+e.g. the top level might be:
+
+* /blog/
+* /wiki/
+* /faq/
+
+But when you go into the blog section, it might look like:
+
+* << Home
+* Archives
+** 2009
+** 2008
+* Labels
+** perl
+** wellington
+** database-design
+
+Not quite sure how it fits together yet, but go for it and see what happens.
View
19 issues/i_13be3dea.cil
@@ -0,0 +1,19 @@
+Summary: Make the link to the RSS files absolute
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.2
+Label: Type-Defect
+Inserted: 2009-11-23T08:03:47
+Updated: 2009-12-07T09:00:27
+
+Currently the link to the RSS files are:
+
+ <link rel="alternate" type="application/rss+xml" title="RSS" href="rss20.xml" />
+
+The href="" really ought to be absolute:
+
+ e.g. http://www.chilts.org/rss20.xml
+
+This is easy enough since the Properties and Section object contain all the
+info needed.
View
13 issues/i_1a5632a5.cil
@@ -0,0 +1,13 @@
+Summary: Quick Links from Section Listing
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.2
+Label: Type-Enhancement
+Comment: 11ec6a41
+Inserted: 2009-12-06T04:51:25
+Updated: 2009-12-07T08:54:46
+
+Allow ability to click 'New Post' from the section listing page.
+
+i.e. /admin/page/new.html?section=...
View
16 issues/i_1eb22241.cil
@@ -0,0 +1,16 @@
+Summary: Move FamFamFam icons to a common top-level static section
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-07T07:52:19
+Updated: 2009-12-07T07:54:12
+
+Currently, FamFamFam icons are served from /s/admin/i/*.png, but if we move
+them somewhere else, they can be used by all themes.
+
+e.g. /cdn/i/*.png
+
+Also, whilst doing this, put things like JQuery in there too:
+
+* /cdn/css/
+* /cdn/js/
View
10 issues/i_215f3669.cil
@@ -0,0 +1,10 @@
+Summary: Add table support to Phliky
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-21T10:18:34
+Updated: 2009-11-21T10:19:37
+
+Table support needs to be added.
+
+See: http://github.com/andychilton/phliky/blob/master/lib/Text/Phliky.pm
View
10 issues/i_22b3cae7.cil
@@ -0,0 +1,10 @@
+Summary: Deleting a Property value doesn't work
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Type-Defect
+Inserted: 2009-12-10T10:05:29
+Updated: 2009-12-10T10:06:44
+
+Deleting a property value should both remove it from memcache and delete it
+from the datastore. I suspect nothing is working.
View
13 issues/i_25945b54.cil
@@ -0,0 +1,13 @@
+Summary: When changing a Property, it should be deleted from MemCached
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.2
+Label: Type-Defect
+Comment: 72026994
+Comment: aaeb75a5
+Inserted: 2009-12-07T08:32:22
+Updated: 2009-12-10T10:03:20
+
+Otherwise, it'll stay in Memcached and the old value will end up being used
+instead of the new value.
View
17 issues/i_2967c411.cil
@@ -0,0 +1,17 @@
+Summary: Add a recipe Node entity
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-21T10:35:49
+Updated: 2009-11-21T10:37:13
+
+The recipe node could have the following fields:
+
+* serves (optional)
+* intro (type)
+* ingredients_raw (line separated list of ingredients)
+* method
+* etc
+* go look at other sites
+
+Might also have a variations bit at the bottom.
View
10 issues/i_2d45b464.cil
@@ -0,0 +1,10 @@
+Summary: Allow the Page/Image/Node listing filter on Section
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-07T09:09:53
+Updated: 2009-12-07T09:11:39
+
+Currently each Node listing page shows all the nodes spread across all
+sections. By being able to filter on one section means it'll make nodes easier
+to find.
View
10 issues/i_45ff7274.cil
@@ -0,0 +1,10 @@
+Summary: Add proper license information to all source files
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-06T19:33:56
+Updated: 2009-12-06T19:35:01
+
+At the top of each source file should be a copyright statement and information
+about the license of that particular file. Also, check what needs to happen for
+a changelog, README, LICENSE, COPYING and all that.
View
20 issues/i_475f1a9b.cil
@@ -0,0 +1,20 @@
+Summary: Implement error checking when adding/editing sections/nodes
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.2
+Label: Type-Defect
+Comment: 8c913e7d
+Comment: d888b9f3
+Inserted: 2009-11-21T10:21:14
+Updated: 2009-12-07T09:04:50
+
+Currently if the datastore rejects a value when saving, you just see the Python
+exception and hit the back button. Proper error checking should be added to be
+shown on the form.
+
+As an example, the errors could be passed in in an 'errors' dictionary which can be displayed at the relevant field.
+
+ vals = {
+ 'errors' = { 'title' = 'Title must be specified' }
+ }
View
17 issues/i_5cc7cb70.cil
@@ -0,0 +1,17 @@
+Summary: Add tasks to task queue to rebuild section/node information
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.1
+Comment: f17e8af3
+Inserted: 2009-11-21T10:27:03
+Updated: 2009-12-05T00:27:51
+
+When a section is added/updated, one or more tasks need to be added to rebuild
+various bits of section information. e.g. the archive counts, or the label
+counts.
+
+The same with nodes when comments are approved. A task should be added so that
+it counts the number of comments. Maybe we do this when a comment is added too
+(so we can see the unapproved list, but I doubt that is very useful. Instead,
+just do it when a comment is approved.
View
18 issues/i_61708bd4.cil
@@ -0,0 +1,18 @@
+Summary: Add archive pages
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.1
+Comment: 49febb07
+Inserted: 2009-11-21T04:50:27
+Updated: 2009-11-22T08:44:20
+
+Currently the site doesn't allow archive pages. Use the example of labels:
+
+ /blog/archive:2009.html
+ /blog/archive:2009-01.html
+ /blog/archive:2009-01-15.html
+ /blog/archive:2009.rss
+ /blog/archive:2009-01.rss
+
+Each section should contain all the _indexed_ nodes in ascending order.
View
16 issues/i_65d73568.cil
@@ -0,0 +1,16 @@
+Summary: Add new section layouts - Featured Node (or equivalent)
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-23T01:43:33
+Updated: 2009-11-23T01:48:56
+
+Have two new section layouts which find the top 1 or the top 4 nodes from a
+particular section. Maybe these have a property 'is-featured'. Other nodes then
+render as per usual underneath.
+
+The first layout will just have one node at the top (ie. the Node rendered in a
+particular way).
+
+The second layout will have a similar image at the top, then a block of 3
+nodes side-by-side underneath, before the rest of the nodes show underneath.
View
15 issues/i_7c9e24ce.cil
@@ -0,0 +1,15 @@
+Summary: Add a menu component for each section
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-21T04:54:06
+Updated: 2009-11-21T04:57:24
+
+This might be separate entities (like a Menu model) or it could be just nodes
+added to the section. e.g. URL nodes but with a particular label. For example, maybe a URL node with the label 'menu-item'.
+
+In most cases, the 'in-index' property would be absent.
+
+e.g. menu-item label
+
+There must be some way to rank those nodes, otherwise won't work.
View
10 issues/i_a07f8c6b.cil
@@ -0,0 +1,10 @@
+Summary: Add ability to view Messages in the admin interface
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-12-12T05:20:28
+Updated: 2009-12-12T05:21:26
+
+Currently contact messages are sent to the user. They are also saved (might as
+well) but there is not way to see them in the admin interface. Something should
+be added to make this available.
View
38 issues/i_a4c0e80f.cil
@@ -0,0 +1,38 @@
+Summary: Add a 'FlickrImage' node type
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-22T05:40:18
+Updated: 2009-11-22T06:06:39
+
+Have a new node type which has the normal 'Node' properties plus the following
+new ones:
+
+* url
+* realname
+* username
+* photoid
+* license
+* imageurl
+
+The form only allows input of the URL and the usual node properties but that
+URL is fetched and parsed to extract the other information.
+
+e.g. http://www.flickr.com/photos/edwinylee/2811299532/
+
+realname = Ed-meister
+username = edwinylee
+photoid = 2811299532
+imageurl = http://farm4.static.flickr.com/3267/2811299532_4251728015.jpg
+license = by-nd 2.0
+
+Not sure how to get that last one but we'll see.
+
+Also, once the url has been fetched, the image is also fetched. In this case,
+the image resides at:
+
+* http://farm4.static.flickr.com/3267/2811299532_4251728015.jpg
+
+Much like the 'Image' node, you'll also have to have a 'FlickrImageData' to
+save it in. Also, when downloading the image, make sure to save the mimetype
+given from Flickr.
View
11 issues/i_aa20208b.cil
@@ -0,0 +1,11 @@
+Summary: Looks like labels and archive are not being regenerated on live
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.2
+Label: Type-Defect
+Comment: 3478283b
+Inserted: 2009-12-06T19:21:06
+Updated: 2009-12-07T09:31:16
+
+On /blog/, there is one entry. But neither the labels or archive have been regenerated.
View
14 issues/i_ac70e68c.cil
@@ -0,0 +1,14 @@
+Summary: Add new ways to render each node
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-21T10:15:30
+Updated: 2009-11-21T10:16:50
+
+For example, we already have a skeleton render for FAQs, so how about:
+
+* thumbnail (eg. images)
+* URL link
+* MenuItem
+
+Let's see what happens.
View
13 issues/i_ae338c79.cil
@@ -0,0 +1,13 @@
+Summary: Remove NodeLayout stuff since it isn't used
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.1
+Comment: d16a43ef
+Inserted: 2009-12-05T00:36:59
+Updated: 2009-12-05T01:36:40
+
+In models.py, there is still a NodeLayout class. This isn't used, nor is the
+menu item in the admin interface.
+
+This should all be removed.
View
13 issues/i_af6d0b4a.cil
@@ -0,0 +1,13 @@
+Summary: Add a contact form
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.2
+Label: Type-Enhancement
+Comment: 266be84e
+Comment: 8c04ba86
+Inserted: 2009-12-07T07:39:22
+Updated: 2009-12-12T05:21:53
+
+Add the ability to add a contact.html page to any section. This should use the
+'has-contact-form' attribute from the section object.
View
31 issues/i_b9f21e3d.cil
@@ -0,0 +1,31 @@
+Summary: Add some boolean properties to each section/node
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.1
+Comment: 45ce395f
+Comment: de398b30
+Comment: f31a14c7
+Inserted: 2009-11-22T04:44:52
+Updated: 2009-11-25T06:50:08
+
+Then each template can check things like:
+
+* {% if section|has:"comments-open" %}
+* {% if node|has:"comments-open" %}
+
+Some example properties for sections would be:
+
+* is-in-index (shows whether to appear in sitemapindex.xml)
+* has-filter-page (responds to filter.html)
+
+Some example properties for nodes would be:
+
+* does-allow-comment
+* are-comments-open
+* is-in-index (shows in section, archive, rss and sitemap.xml pages)
+* is-menu-item
+* is-feature
+
+For example, is-menu-item means it can show up in that section's menu.
+'is-feature' might be used by 'project-index' pages.
View
12 issues/i_beba51d7.cil
@@ -0,0 +1,12 @@
+Summary: Save labels as 'label_raw' and have 'label' generated from it
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.1
+Comment: 96e7fff2
+Inserted: 2009-11-21T10:33:30
+Updated: 2009-11-23T09:13:09
+
+Having the original field means we can present that same input back to the
+user. This also means it's a good opportunity to let them input in a text input
+using comma separated labels rather than a textarea.
View
14 issues/i_c4aa4a14.cil
@@ -0,0 +1,14 @@
+Summary: Fix up deletions
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.2
+Label: Type-Defect
+Inserted: 2009-11-21T10:16:57
+Updated: 2009-12-07T07:46:02
+
+When deleting a node, it redirects back to itself rather than back to the page
+you came from.
+
+Also, when the node is shown, it just shows the common fields. Instead, it
+should render from something like 'node-readonly.html' such that it looks nice.
View
10 issues/i_cf2c65d5.cil
@@ -0,0 +1,10 @@
+Summary: After a node has been inserted or updated, check for duplicates
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-22T04:51:45
+Updated: 2009-11-22T04:54:14
+
+After a node has been inserted or updated, checks to make sure a duplicate node
+name hasn't been used. If a clash has been found, the author of the newer
+article is emailed saying so.
View
17 issues/i_d8fd5c23.cil
@@ -0,0 +1,17 @@
+Summary: Add task to regenerate certain fields on Nodes (eg. comment count)
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-v0.1
+Comment: f3c6fe7f
+Inserted: 2009-12-05T00:29:19
+Updated: 2009-12-05T01:06:16
+
+When a comment is added (or in fact, approved), there should be a field on that
+particular node to save the comment count.
+
+New field required on Node:
+
+ comment_count = db.IntegerProperty( required=True, default=0 )
+
+Please see issue cil-5cc7cb70 for other information.
View
25 issues/i_db0edf7f.cil
@@ -0,0 +1,25 @@
+Summary: Complete migration policy
+Status: InProgress
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Label: Milestone-Future
+Comment: e914e17d
+Inserted: 2009-11-21T10:23:55
+Updated: 2009-11-25T07:01:57
+
+The migrations are a bit weird at the moment, so kinda do like what Rails does.
+The migrations should be named something like:
+
+* 20091119a_AddNewLayoutToSection_Forward
+* 20091119a_AddNewLayoutToSection_Reverse
+
+We need to somehow store in the database _what_ migrations have been performed.
+Maybe this could be a migration object which contains:
+
+* name = the name of the migration (e.g. 20091119a_AddNewLayoutToSection)
+* the datetime it started
+* the datetime it finished
+* how many entities were looked at
+* how many entities were updated (and saved)
+
+All of this information could be interesting.
View
13 issues/i_e08ba70d.cil
@@ -0,0 +1,13 @@
+Summary: Add a filter.html page (dependent on section properties)
+Status: New
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Inserted: 2009-11-21T10:30:35
+Updated: 2009-11-21T10:32:40
+
+Having a filter.html page which just looks at labels would be interesting.
+Maybe in the future it could also look at inserted, updated, owner and other
+fields to be a bit more flexible.
+
+The use-case for this is looking for recipes that contain both asparagus _and_
+sour cream. Maybe we allow 2, 3 or many labels to be checked.
View
11 issues/i_fba16979.cil
@@ -0,0 +1,11 @@
+Summary: Make the comment.html MainMenu block override it
+Status: Finished
+CreatedBy: Andrew Chilton <andychilton@gmail.com>
+AssignedTo: Andrew Chilton <andychilton@gmail.com>
+Comment: 4212371e
+Inserted: 2009-12-05T00:50:26
+Updated: 2009-12-05T01:26:23
+
+Currently the comment page doesn't have an overriden mainmenu block. This
+should either be overridden by itself or the template should inherit from one
+which does.
View
303 lollysite.py
@@ -0,0 +1,303 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import cgi
+import os
+import logging
+import re
+import urllib
+
+# Google specific modules
+from google.appengine.api import users
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import run_wsgi_app
+from google.appengine.ext import db
+from google.appengine.api import mail
+
+# local modules
+import webbase
+from models import Section
+from models import Node
+from models import Comment
+from models import Message
+import config
+import util
+
+## ----------------------------------------------------------------------------
+# regexes
+
+# matches:
+# - (/)
+# - (/)(intro).(html)
+# - (/blog/)(this).(html)
+# - (/something/here/)
+# - (/something/here/)(hello).(html)
+# - (/software/cil/)(cil_v1.3.2.tar.gz).(html)
+# - (/software/cil/)(label:this).(html)
+parts = re.compile('^(.*/)(([\w\._:-]*)\.(\w+))?$')
+
+label_page = re.compile('^label:(.+)$', re.DOTALL | re.VERBOSE)
+archive_page = re.compile('^archive:(\d\d\d\d(-\d\d(-\d\d)?)?)$', re.DOTALL | re.VERBOSE)
+
+## ----------------------------------------------------------------------------
+
+class LollySite(webbase.WebBase):
+ def get(self):
+ path = urllib.unquote(self.request.path)
+ m = parts.search(path)
+
+ if m is None:
+ self.error(404)
+ return
+
+ this_path = m.group(1)
+ this_page = m.group(3) or 'index'
+ this_ext = m.group(4) or 'html'
+
+ if this_page is None:
+ this_page = 'index'
+ this_ext = 'html'
+
+ section = Section.all().filter('path =', this_path).get()
+ if section is None:
+ logging.info('404: This section not found')
+ self.error(404)
+ return
+
+ # if this is an index, call a different template
+ if this_page == 'index' and this_ext == 'html':
+ # index.html
+ vals = {
+ 'section' : section,
+ }
+ self.template( section.layout + '-index.html', vals, config.value('Theme') );
+
+ elif this_page == 'contact' and this_ext == 'html' and section.has('contact-form'):
+ logging.info('right here')
+ # contact.html
+ node = Node.all().filter('section =', section.key()).filter('name =', 'contact').get()
+ vals = {
+ 'section' : section,
+ 'node' : node,
+ }
+ self.template( 'contact.html', vals, config.value('Theme') );
+
+ elif this_page == 'rss20' and this_ext == 'xml':
+ # rss20.xml
+ nodes = self.latest_nodes(section, 10)
+ vals = {
+ 'section' : section,
+ 'nodes' : nodes,
+ }
+ self.response.headers['Content-Type'] = 'application/rss+xml'
+ self.template( 'rss20.xml', vals, 'rss' );
+
+ elif this_page == 'sitemapindex' and this_ext == 'xml':
+ # sitemapindex.xml
+ sections = Section.all()
+ vals = {
+ 'sections' : sections,
+ }
+ self.response.headers['Content-Type'] = 'text/xml'
+ self.template( 'sitemapindex.xml', vals, 'sitemaps' );
+
+ elif this_page == 'urlset' and this_ext == 'xml':
+ # urlset.xml
+ vals = {
+ 'section' : section,
+ 'nodes' : Node.all().filter('section =', section.key())
+ }
+ self.response.headers['Content-Type'] = 'text/xml'
+ self.template( 'urlset.xml', vals, 'sitemaps' );
+
+ elif label_page.search(this_page) and this_ext == 'html':
+ # path =~ 'label:something.html'
+ m = label_page.search(this_page)
+ label = m.group(1)
+ logging.info('m=' + repr(m))
+ logging.info('Inside a label (%s) page' % label)
+ vals = {
+ 'section' : section,
+ 'nodes' : Node.all().filter('section =', section.key()).filter('label =', label).order('-inserted'),
+ 'label' : label
+ }
+ self.template( 'label-index.html', vals, config.value('Theme') );
+
+ elif archive_page.search(this_page) and this_ext == 'html':
+ # path =~ 'archive:2009.html'
+ m = archive_page.search(this_page)
+ archive = m.group(1)
+ logging.info('archive=' + archive)
+ vals = {
+ 'section' : section,
+ 'nodes' : Node.all().filter('section =', section.key()).filter('archive =', archive).order('-inserted'),
+ 'archive' : archive
+ }
+ self.template( 'archive-index.html', vals, config.value('Theme') );
+
+ elif this_page == 'comment' and this_ext == 'html':
+ # get the comment if it exists
+ try:
+ comment = Comment.get( self.request.get('key') )
+ except db.BadKeyError:
+ self.error(404)
+ return
+
+ if comment is None:
+ self.error(404)
+ return
+
+ vals = {
+ 'section' : section,
+ 'node' : comment.node,
+ 'comment' : comment,
+ }
+ self.template( 'comment.html', vals, config.value('Theme') );
+
+ else:
+ # get the node itself
+ node_query = Node.all().filter('section =', section.key()).filter('name =', this_page)
+ if node_query.count() == 0:
+ logging.info('404: no nodes in this section (%s) of that name (%s.%s)' % (section.path, this_page, this_ext))
+ self.error(404)
+ return
+ node = node_query.fetch(1)[0]
+ logging.info('node.name=' + node.name)
+ logging.info('node.title=' + node.title)
+
+ # get the approved comments (but only if we know some are there, save a trip to the datastore)
+ comments = None
+ if node.comment_count:
+ comments = Comment.all().filter('node =', node.key()).filter('status =', 'approved').order('inserted')
+
+ vals = {
+ 'section' : section,
+ 'node' : node,
+ 'comments' : comments,
+ }
+ self.template( 'node.html', vals, config.value('Theme') );
+
+ def post(self):
+ path = urllib.unquote(self.request.path)
+ m = parts.search(path)
+
+ if m is None:
+ self.error(404)
+ return
+
+ this_path = m.group(1)
+ this_page = m.group(3) or 'index'
+ this_ext = m.group(4) or 'html'
+
+ # get section and node
+ section = Section.all().filter('path =', this_path).get()
+ node = Node.all().filter('section =', section).get()
+
+ if section is None or node is None:
+ self.error(404)
+ return
+
+ self.request.charset = 'utf8'
+
+ # remove the horribleness from comment
+ if this_page == 'comment' and this_ext == 'html':
+ # comment submission for each section
+ node = db.get( self.request.POST['node'] )
+ name = self.request.POST['name']
+ email = self.request.POST['email']
+ website = self.request.POST['website']
+ comment_text = re.sub('\r', '', self.request.POST['comment']);
+
+ # now create the comment
+ comment = Comment(
+ node = node,
+ name = name,
+ email = email,
+ website = website,
+ comment = comment_text,
+ comment_html = util.render(comment_text, 'text'),
+ )
+ comment.put()
+
+ # send a mail to the admin
+ admin_email = config.value('Admin Email')
+ if mail.is_email_valid(admin_email):
+ url_mod = 'http://www.' + config.value('Naked Domain') + '/admin/comment/?key=' + str(comment.key()) + ';status='
+ url_del = 'http://www.' + config.value('Naked Domain') + '/admin/del.html?key='+ str(comment.key())
+
+ body = 'Comment from ' + name + '<' + email + '>\n'
+ body = body + website + '\n\n'
+ body = body + comment_text + '\n\n'
+ body = body + '---\n\nActions\n\n'
+ body = body + 'Approve = ' + url_mod + 'approve\n'
+ body = body + 'Reject = ' + url_mod + 'reject\n'
+ body = body + 'Delete = ' + url_del + '\n'
+ mail.send_mail(admin_email, admin_email, 'New comment on ' + section.path + node.name + '.html', body)
+ else:
+ # don't do anything
+ logging.info('No valid email set, skipping sending admin an email for new comment')
+
+ # redirect to the comment page
+ self.redirect('comment.html?key=' + str(comment.key()))
+ return
+
+ elif this_page == 'contact' and this_ext == 'html':
+ # comment submission for each section
+ name = self.request.POST['name']
+ email = self.request.POST['email']
+ website = self.request.POST['website']
+ subject = self.request.POST['subject']
+ message = re.sub('\r', '', self.request.POST['message']);
+
+ # now create the message
+ msg = Message(
+ name = name,
+ email = email,
+ website = website,
+ subject = subject,
+ message = message,
+ )
+ msg.put()
+
+ # send a mail to the admin
+ admin_email = config.value('Admin Email')
+ if mail.is_email_valid(admin_email):
+ body = 'name : ' + name + '\n'
+ body = body + 'email : ' + email + '\n'
+ body = body + 'website : ' + website + '\n'
+ body = body + 'subject : ' + subject + '\n'
+ body = body + message + '\n'
+ mail.send_mail(admin_email, admin_email, '[Contact] ' + subject, body)
+ else:
+ # don't do anything
+ logging.info('No valid email set, skipping sending admin an email for new contact message')
+
+ self.redirect('.')
+ return
+ else:
+ # not found
+ self.error(404)
+ return
+
+ def latest_nodes(self, section, limit):
+ logging.info('Getting latest nodes for ' + section.path)
+ nodes = Node.all().filter('section =', section.key()).order('-inserted').fetch(limit)
+ return nodes
+
+## ----------------------------------------------------------------------------
+
+application = webapp.WSGIApplication(
+ [
+ ('/.*', LollySite)
+ ],
+ debug = True
+)
+
+## ----------------------------------------------------------------------------
+
+def main():
+ run_wsgi_app(application)
+
+if __name__ == "__main__":
+ main()
+
+## ----------------------------------------------------------------------------
View
87 migrate.py
@@ -0,0 +1,87 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import logging
+
+# Google specific modules
+from google.appengine.ext import db
+
+# local modules
+import webbase
+from models import Section
+from models import Node
+
+## ----------------------------------------------------------------------------
+# define all the entity types we have
+
+# migration list
+migrations = [
+ '20091122a_RemoveUnusedArchive',
+ '20091123a_GenerateLabelRaw',
+ '20091124a_MakeSectionRightAgain',
+ ]
+
+## ----------------------------------------------------------------------------
+
+# do all migrations in short steps (< 30 secs)
+
+# Forms
+class Migrate(webbase.WebBase):
+ def get(self):
+ logging.info('Right here')
+ vals = {
+ 'migrations' : migrations
+ }
+
+ m = self.request.get('m')
+ if m == '20091122a_RemoveUnusedArchive':
+ self.m_20091122a_RemoveUnusedArchive()
+ elif m == '20091123a_GenerateLabelRaw':
+ self.m_20091123a_GenerateLabelRaw()
+ elif m == '20091124a_MakeSectionRightAgain':
+ self.m_20091124a_MakeSectionRightAgain()
+ else:
+ logging.info('Not doing any migration')
+
+ self.template( 'migrate.html', vals, 'admin' )
+
+ def m_20091122a_RemoveUnusedArchive(self):
+ logging.info('20091122a_RemoveUnusedArchive')
+ nodes = db.GqlQuery("SELECT * FROM Node")
+ new = []
+ for n in nodes:
+ changed = False
+ for attr in ['archive_day', 'archive_month', 'archive_year']:
+ if hasattr(n, attr):
+ logging.info('This entity %s has %s' % (str(n.key()), attr))
+ delattr(n, attr)
+ changed = True
+ if changed:
+ new.append(n)
+ db.put(new)
+ logging.info('new.count()' + str(new.count(new)))
+
+ def m_20091123a_GenerateLabelRaw(self):
+ logging.info('20091123a_GenerateLabelRaw')
+ nodes = db.GqlQuery("SELECT * FROM Node")
+ new = []
+ for n in nodes:
+ if hasattr(n, 'label'):
+ n.label_raw = ','.join(n.label)
+ else:
+ n.label_raw = ''
+ logging.info('label_raw=' + n.label_raw)
+ new.append(n)
+ db.put(new)
+
+ def m_20091124a_MakeSectionRightAgain(self):
+ logging.info('20091124a_MakeSectionRightAgain')
+ sections = db.GqlQuery("SELECT * FROM Section")
+ new = []
+ for s in sections:
+ s.attribute_raw = ''
+ s.attribute = s.attribute_raw.split()
+ new.append(s)
+ logging.info('Putting...')
+ db.put(new)
+
+## ----------------------------------------------------------------------------
View
173 models.py
@@ -0,0 +1,173 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import re
+import logging
+
+# Google specific modules
+from google.appengine.ext import db
+from google.appengine.ext.db import polymodel
+from google.appengine.api.labs.taskqueue import Task
+
+# local stuff
+import util
+import properties
+
+## ----------------------------------------------------------------------------
+
+# all models should have the following properties:
+# * owner = db.UserProperty( auto_current_user_add = True )
+# * editor = db.UserProperty( auto_current_user = True )
+# * inserted = db.DateTimeProperty( auto_now_add = True )
+# * updated = db.DateTimeProperty( auto_now = True )
+
+type_choices = ["text", "code", "phliky", "html"]
+layout_choices = ['content', 'blog', 'faq']
+
+## ----------------------------------------------------------------------------
+# normal models
+
+# BaseModel
+class BaseModel(db.Model):
+ # common properties for _every_ datastore object
+ owner = db.UserProperty( auto_current_user_add=True )
+ editor = db.UserProperty( auto_current_user=True )
+ inserted = db.DateTimeProperty( auto_now_add=True )
+ updated = db.DateTimeProperty( auto_now=True )
+
+# Property class so the application can be configured
+class Property(BaseModel):
+ title = db.StringProperty( required=True )
+ value = db.StringProperty( required=True )
+
+# Section: to group Nodes together
+class Section(BaseModel):
+ # path := usually something starting with '/', like '/', '/blog/', '/article/' and '/path/to/'
+ # path := m{ / (a-z[a-z0-9]*/)* }xms # intial / followed by 0 or more sections
+ path = db.StringProperty( required=True )
+ title = db.StringProperty( required=True )
+ description = db.TextProperty()
+ type = db.StringProperty(required=True, choices=set(type_choices))
+ layout = db.StringProperty(
+ required=False,
+ default='content',
+ choices=layout_choices
+ )
+ attribute_raw = db.StringProperty( multiline=False )
+
+ def has(self, attr):
+ return [ x for x in self.attribute if x == attr ]
+
+ # Derivative Properties
+ description_html = db.TextProperty()
+ attribute = db.StringListProperty()
+ def set_derivatives(self):
+ # set the description_html
+ self.description_html = util.render(self.description, self.type)
+ # set the lists from their raw values
+ self.attribute = self.attribute_raw.split()
+
+ # Generated Properties (and the tasks to kick them off)
+ archive_json = properties.JsonProperty()
+ label_json = properties.JsonProperty()
+ def regenerate(self):
+ Task( params={ 'key': str(self.key()) }, countdown=30, ).add( queue_name='section-regenerate' )
+
+## ----------------------------------------------------------------------------
+# polymodels
+
+class Node(polymodel.PolyModel):
+ # common properties for _every_ datastore object
+ owner = db.UserProperty( auto_current_user_add=True )
+ editor = db.UserProperty( auto_current_user=True )
+ inserted = db.DateTimeProperty( auto_now_add=True )
+ updated = db.DateTimeProperty( auto_now=True )
+
+ # common properties for every Node
+ section = db.ReferenceProperty( Section, required=True, collection_name='nodes' )
+ # name := a-z[a-z0-9_-.]*
+ name = db.StringProperty( required=True, multiline=False )
+ title = db.StringProperty( required=True, multiline=False )
+ label_raw = db.StringProperty( multiline=False )
+ attribute_raw = db.StringProperty( multiline=False )
+
+ # Derivative Properties
+ label = db.StringListProperty()
+ archive = db.StringListProperty()
+ attribute = db.StringListProperty()
+ def set_derivatives(self):
+ # set the archive dates
+ datetime = str(self.inserted)
+ y = re.search(r'^(\d\d\d\d)', datetime, re.DOTALL | re.VERBOSE).group(1)
+ m = re.search(r'^(\d\d\d\d-\d\d)', datetime, re.DOTALL | re.VERBOSE).group(1)
+ d = re.search(r'^(\d\d\d\d-\d\d-\d\d)', datetime, re.DOTALL | re.VERBOSE).group(1)
+ self.archive = [y, m, d]
+
+ # set the lists from their raw values
+ self.label = self.label_raw.split()
+ self.attribute = self.attribute_raw.split()
+
+ # Generated Properties (and the tasks to kick them off)
+ comment_count = db.IntegerProperty( required=True, default=0 )
+ def regenerate(self):
+ Task( params={ 'key': str(self.key()) }, countdown=30, ).add( queue_name='node-regenerate' )
+
+# Page
+class Page(Node):
+ content = db.TextProperty( required=True )
+ content_html = db.TextProperty()
+ type = db.StringProperty(required=True, choices=set(type_choices))
+
+ def set_derivatives(self):
+ Node.set_derivatives(self)
+ self.content_html = util.render(self.content, self.type)
+
+# Files: See - http://blog.notdot.net/2009/9/Handling-file-uploads-in-App-Engine
+
+# Image
+class ImageData(db.Model):
+ data = db.BlobProperty( required=True )
+
+class Image(Node):
+ caption = db.TextProperty()
+ credit = db.TextProperty()
+ credit_link = db.LinkProperty()
+ imagedata = db.ReferenceProperty( ImageData, required=True, collection_name='image' )
+ filename = db.StringProperty( required=True )
+ mimetype = db.StringProperty( required=True )
+
+# File
+class FileData(db.Model):
+ data = db.BlobProperty( required=True )
+
+class File(Node):
+ filedata = db.ReferenceProperty( FileData, required=True, collection_name='file' )
+ filename = db.StringProperty( required=True )
+ mimetype = db.StringProperty( required=True )
+
+## ----------------------------------------------------------------------------
+# comments
+
+class Comment(BaseModel):
+ node = db.ReferenceProperty( Node, required=True, collection_name='comments' )
+ name = db.StringProperty(multiline=False)
+ email = db.StringProperty(multiline=False)
+ website = db.StringProperty(multiline=False)
+ comment = db.TextProperty()
+ comment_html = db.TextProperty()
+ status = db.StringProperty(
+ required=True,
+ default='new',
+ choices=['new', 'approved', 'rejected']
+ )
+
+## ----------------------------------------------------------------------------
+# messages
+
+class Message(BaseModel):
+ name = db.StringProperty(multiline=False)
+ email = db.StringProperty(multiline=False)
+ website = db.StringProperty(multiline=False)
+ subject = db.StringProperty(multiline=False)
+ message = db.TextProperty()
+
+## ----------------------------------------------------------------------------
View
51 node.py
@@ -0,0 +1,51 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import logging
+import urllib
+
+# Google specific modules
+from google.appengine.ext import webapp
+from google.appengine.ext import db
+from google.appengine.ext.webapp.util import run_wsgi_app
+
+# local modules
+import models
+import webbase
+
+## ----------------------------------------------------------------------------
+
+class Image(webbase.WebBase):
+ def get(self, name):
+ name = urllib.unquote(name)
+ image = models.Image.all().filter('name =', name).fetch(1)[0]
+
+ self.response.headers['Content-Type'] = image.mimetype
+ self.response.out.write(image.imagedata.data)
+
+class File(webbase.WebBase):
+ def get(self):
+ path = urllib.unquote(self.request.path)
+ file = models.File.all().filter('name =', name).fetch(1)[0]
+
+ self.response.headers['Content-Type'] = file.mimetype
+ self.response.out.write(file.filedata.data)
+
+## ----------------------------------------------------------------------------
+
+application = webapp.WSGIApplication(
+ [
+ ('/node/image/(.*)', Image),
+ ('/node/file/(.*)', File),
+ ],
+ debug = True
+)
+
+## ----------------------------------------------------------------------------
+
+def main():
+ run_wsgi_app(application)
+
+if __name__ == "__main__":
+ main()
+
+## ----------------------------------------------------------------------------
View
89 page.py
@@ -0,0 +1,89 @@
+## ----------------------------------------------------------------------------
+# import standard modules
+import re
+import logging
+
+# Google specific modules
+from google.appengine.ext import db
+
+# local modules
+from models import Section, Page
+import webbase
+import formbase
+import util
+
+## ----------------------------------------------------------------------------
+
+# list
+class List(webbase.WebBase):
+ def get(self):
+ pages = Page.all().order('-inserted')
+ vals = {
+ 'title' : 'Section Layout List',
+ 'pages' : pages
+ }
+ self.template( 'admin-page-index.html', vals, 'admin' );
+
+# form
+class FormHandler(webbase.WebBase):
+ def get(self):
+ item = None
+ if self.request.get('key'):
+ item = db.get( self.request.get('key') )
+ else:
+ item = {}
+ for arg in self.request.arguments():
+ if arg == 'section':
+ item['section'] = { 'key' : str(self.request.get(arg)) }
+ else:
+ item[arg] = str(self.request.get(arg))
+
+ vals = {
+ 'item' : item,
+ 'sections' : Section.all(),
+ 'types' : [ 'rst', 'phliky', 'text', 'code', 'html' ]
+ }
+ self.template( 'page-form.html', vals, 'admin' );
+
+ def post(self):
+ # some initial setup
+ section = db.get( self.request.get('section') )
+
+ # do some preprocessing of the input params
+ name = self.request.get('name')
+ if name == '':
+ name = util.urlify(self.request.get('title'))
+
+ attribute_raw = self.request.get('attribute_raw')
+
+ item = None
+ if self.request.get('key'):
+ item = db.get( self.request.get('key') )
+ item.section = section
+ item.name = name
+ item.title = self.request.get('title')
+ item.content = self.request.get('content')
+ item.type = self.request.get('type')
+ item.label_raw = self.request.get('label_raw')
+ item.attribute_raw = attribute_raw
+ else:
+ item = Page(
+ section = section,
+ name = name,
+ title = self.request.get('title'),
+ content = self.request.get('content'),
+ type = self.request.get('type'),
+ label_raw = self.request.get('label_raw'),
+ attribute_raw = attribute_raw,
+ )
+
+ # update and save this page
+ item.set_derivatives()
+ item.put()
+
+ # once saved, regenerate certain section properties
+ section.regenerate()
+
+ self.redirect('.')
+
+## ----------------------------------------------------------------------------
View
162 phliky.py
@@ -0,0 +1,162 @@
+## ----------------------------------------------------------------------------
+import cgi
+import re
+
+## ----------------------------------------------------------------------------
+# regexes
+
+heading = re.compile('^!([123456])\s+(.*)', re.DOTALL)
+pre = re.compile('^\s')
+html = re.compile('^<\s(.*)', re.DOTALL)
+blockquote = re.compile('^"\s+(.*)', re.DOTALL)
+list = re.compile('^([\#\*]+)\s+(.*)', re.DOTALL)
+centered = re.compile('^\^\s+(.*)', re.DOTALL)
+
+# inline regexes
+balanced = re.compile(r'\\(\w+){([^{}]*?)}', re.DOTALL | re.VERBOSE)
+
+## ----------------------------------------------------------------------------
+
+def esc(text):
+ return cgi.escape(text, True)
+
+def text2html(text):
+ html = ''
+ # these \r's were doing my head in, so get rid of them
+ text = re.sub('\r', '', text)
+ for chunk in text.split('\n\n'):
+ html = html + do_chunk(chunk) + '\n'
+
+ return html
+
+def do_chunk(chunk):
+ m = heading.search(chunk)
+ if m:
+ return '<h%s>%s</h%s>' % (m.group(1), do_inline(esc(m.group(2))), m.group(1))
+
+ m = pre.search(chunk)
+ if m:
+ return '<pre>' + esc(chunk) + '</pre>'
+
+ m = html.search(chunk)
+ if m:
+ return m.group(1)
+
+ m = blockquote.search(chunk)
+ if m:
+ return '<blockquote>' + do_inline(esc(m.group(1))) + '</blockquote>'
+
+ m = list.search(chunk)
+ if m:
+ return do_list(chunk)
+
+ m = centered.search(chunk)
+ if m:
+ return '<p class="c">' + do_inline(esc(m.group(1))) + '</p>'
+
+ # just a normal paragraph
+ return '<p>' + do_inline(esc(chunk)) + '</p>'
+
+def do_inline(line):
+ finished = False
+ while not finished:
+ m = balanced.search(line)
+ if m:
+ type = m.group(1)
+ str = m.group(2)
+
+ if type == 'b':
+ line = balanced.sub(r'<strong>\2</strong>', line, 1)
+ # line[m.start():m.end()] = '<strong>%s</strong' % str
+
+ elif type == 'i':
+ line = balanced.sub(r'<em>\2</em>', line, 1)
+
+ elif type == 'u':
+ line = balanced.sub(r'<span style="text-decoration: underline;">\2</span>', line, 1)
+
+ elif type == 'c':
+ line = balanced.sub(r'<code>\2</code>', line, 1)
+
+ elif type == 'l':
+ (text, href) = str.split(r'|')
+ line = balanced.sub('<a href="%s">%s</a>' % (href, text), line, 1)
+
+ elif type == 'h':
+ line = balanced.sub('<a href="%s">%s</a>' % (str, str), line, 1)
+
+ elif type == 'w':
+ line = balanced.sub('<a href="%s.html">%s</a>' % (str, str), line, 1)
+
+ elif type == 'img':
+ (title, src) = str.split(r'|')
+ line = balanced.sub('<img src="%s" title="%s" />' % (src, title), line, 1)
+
+ elif type == 'a':
+ (abbr, abbreviation) = str.split(r'|')
+ line = balanced.sub('<acronym title="%s">%s</acronym>' % (abbreviation, abbr), line, 1)
+
+ elif type == 'br':
+ line = balanced.sub('<br />', line, 1)
+
+ elif type == 'copy':
+ line = balanced.sub('&copy;', line, 1)
+
+ else:
+ line = balanced.sub(r'\[\1]{\2}', line, 1)
+ else:
+ finished = True
+
+ return line
+
+def do_list(chunk):
+ # make the para into lines
+ lines = chunk.split('\n')
+
+ if len(lines) == 0:
+ return ''
+
+ html = ''
+ indent = []
+
+ for line in lines:
+ (type, length, content) = list_line_info( line )
+
+ if length == len(indent):
+ # same indent
+ html = html + '</li><li>' + do_inline(esc(content))
+ elif length == len(indent)+1:
+ # new indent
+ indent.append(type)
+ html = html + '<%s><li>' % (type) + do_inline(esc(content))
+ elif length < len(indent):
+ while len(indent) != length:
+ # new outdent
+ html = html + '</li></%s>' % (indent.pop())
+ html = html + '</li><li>' + do_inline(esc(content))
+ else:
+ while len(indent) != length:
+ # indenting multiple times
+ indent.append(type)
+ html = html + '<%s><li>' % (type)
+ html = html + do_inline(esc(content))
+
+ while len(indent) > 0:
+ # final outdenting
+ html = html + '</li></%s>' % (indent.pop())
+
+ return html
+
+def list_line_info(line):
+ m = list.search(line)
+ if m:
+ type = m.group(1)
+ content = m.group(2)
+ length = len(type)
+ type = 'ol' if type[0] == '#' else 'ul'
+ return (type, length, content)
+
+ return (None, None, None)
+
+
+## ----------------------------------------------------------------------------
View
235 properties.py
@@ -0,0 +1,235 @@
+# -*- coding: utf-8 -*-
+"""
+ gaefy.db.properties
+ ~~~~~~~~~~~~~~~~~~~
+
+ Extra properties for App Engine Models.
+
+ :copyright: 2009 by tipfy.org.
+ :license: BSD, see LICENSE.txt for more details.
+"""
+import csv, pickle
+from django.utils import simplejson
+from cStringIO import StringIO
+
+from google.appengine.ext import db
+from google.appengine.api import datastore_errors
+
+class SerialProperty(db.Property):
+ """Overrides make_value_from_form() provided in GAE, which casts the
+ property value to a string. This is not desired for some properties, and
+ enables the use of CsvProperty in MultiValueField's.
+ """
+ def make_value_from_form(self, value):
+ return value
+
+ def get_value_for_form(self, instance):
+ return getattr(instance, self.name)
+
+
+class CsvProperty(SerialProperty):
+ """A special property that accepts a list or tuple as value, and stores the
+ data in CSV format using the db.Text data_type. Each item in the list must
+ be a iterable representing fields in a CSV row. The value is converted back
+ to a list of tuples when read.
+ """
+ data_type = db.Text
+
+ def __init__(self, csv_params={}, field_count=None, default=None, **kwargs):
+ ""