Merge branch 'sobol-templates-hotfix' into release
authorStephen Burrows <stephen.r.burrows@gmail.com>
Wed, 11 May 2011 21:21:35 +0000 (17:21 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Wed, 11 May 2011 21:21:35 +0000 (17:21 -0400)
125 files changed:
.gitignore
README
README.markdown
__init__.py [deleted file]
docs/Makefile [new file with mode: 0644]
docs/_ext/djangodocs.py [new file with mode: 0644]
docs/conf.py [new file with mode: 0644]
docs/dummy-settings.py [new file with mode: 0644]
docs/exceptions.rst [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/intro.rst [new file with mode: 0644]
docs/make.bat [new file with mode: 0644]
docs/middleware.rst [new file with mode: 0644]
docs/models/collections.rst [new file with mode: 0644]
docs/models/entities.rst [new file with mode: 0644]
docs/models/fields.rst [new file with mode: 0644]
docs/models/intro.rst [new file with mode: 0644]
docs/models/miscellaneous.rst [new file with mode: 0644]
docs/models/nodes-and-views.rst [new file with mode: 0644]
docs/signals.rst [new file with mode: 0644]
docs/validators.rst [new file with mode: 0644]
exceptions.py [deleted file]
philo/LICENSE [moved from LICENSE with 100% similarity]
philo/__init__.py [new file with mode: 0644]
philo/admin/__init__.py [moved from admin/__init__.py with 100% similarity]
philo/admin/base.py [moved from admin/base.py with 99% similarity]
philo/admin/collections.py [moved from admin/collections.py with 91% similarity]
philo/admin/forms/__init__.py [moved from admin/forms/__init__.py with 100% similarity]
philo/admin/forms/attributes.py [moved from admin/forms/attributes.py with 99% similarity]
philo/admin/forms/containers.py [moved from admin/forms/containers.py with 99% similarity]
philo/admin/nodes.py [moved from admin/nodes.py with 79% similarity]
philo/admin/pages.py [moved from admin/pages.py with 84% similarity]
philo/admin/widgets.py [moved from admin/widgets.py with 86% similarity]
philo/contrib/__init__.py [moved from contrib/__init__.py with 100% similarity]
philo/contrib/julian/__init__.py [moved from contrib/julian/__init__.py with 100% similarity]
philo/contrib/julian/admin.py [moved from contrib/julian/admin.py with 97% similarity]
philo/contrib/julian/feedgenerator.py [moved from contrib/julian/feedgenerator.py with 100% similarity]
philo/contrib/julian/migrations/0001_initial.py [moved from contrib/julian/migrations/0001_initial.py with 100% similarity]
philo/contrib/julian/migrations/__init__.py [moved from contrib/julian/migrations/__init__.py with 100% similarity]
philo/contrib/julian/models.py [moved from contrib/julian/models.py with 99% similarity]
philo/contrib/penfield/__init__.py [moved from contrib/penfield/__init__.py with 100% similarity]
philo/contrib/penfield/admin.py [moved from contrib/penfield/admin.py with 98% similarity]
philo/contrib/penfield/exceptions.py [moved from contrib/penfield/exceptions.py with 100% similarity]
philo/contrib/penfield/middleware.py [moved from contrib/penfield/middleware.py with 97% similarity]
philo/contrib/penfield/migrations/0001_initial.py [moved from contrib/penfield/migrations/0001_initial.py with 100% similarity]
philo/contrib/penfield/migrations/0002_auto.py [moved from contrib/penfield/migrations/0002_auto.py with 100% similarity]
philo/contrib/penfield/migrations/0003_auto__add_field_newsletterview_feed_type__add_field_newsletterview_ite.py [moved from contrib/penfield/migrations/0003_auto__add_field_newsletterview_feed_type__add_field_newsletterview_ite.py with 100% similarity]
philo/contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py [moved from contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py with 100% similarity]
philo/contrib/penfield/migrations/__init__.py [moved from contrib/penfield/migrations/__init__.py with 100% similarity]
philo/contrib/penfield/models.py [moved from contrib/penfield/models.py with 99% similarity]
philo/contrib/penfield/templatetags/__init__.py [moved from contrib/penfield/templatetags/__init__.py with 100% similarity]
philo/contrib/penfield/templatetags/penfield.py [moved from contrib/penfield/templatetags/penfield.py with 100% similarity]
philo/contrib/penfield/validators.py [moved from contrib/penfield/validators.py with 100% similarity]
philo/contrib/shipherd/__init__.py [moved from contrib/shipherd/__init__.py with 100% similarity]
philo/contrib/shipherd/admin.py [moved from contrib/shipherd/admin.py with 97% similarity]
philo/contrib/shipherd/migrations/0001_initial.py [moved from contrib/shipherd/migrations/0001_initial.py with 100% similarity]
philo/contrib/shipherd/migrations/0002_auto.py [moved from contrib/shipherd/migrations/0002_auto.py with 100% similarity]
philo/contrib/shipherd/migrations/__init__.py [moved from contrib/shipherd/migrations/__init__.py with 100% similarity]
philo/contrib/shipherd/models.py [moved from contrib/shipherd/models.py with 99% similarity]
philo/contrib/shipherd/templatetags/__init__.py [moved from contrib/shipherd/templatetags/__init__.py with 100% similarity]
philo/contrib/shipherd/templatetags/shipherd.py [moved from contrib/shipherd/templatetags/shipherd.py with 95% similarity]
philo/contrib/sobol/__init__.py [moved from contrib/sobol/__init__.py with 100% similarity]
philo/contrib/sobol/admin.py [moved from contrib/sobol/admin.py with 98% similarity]
philo/contrib/sobol/forms.py [moved from contrib/sobol/forms.py with 97% similarity]
philo/contrib/sobol/models.py [moved from contrib/sobol/models.py with 96% similarity]
philo/contrib/sobol/search.py [moved from contrib/sobol/search.py with 98% similarity]
philo/contrib/sobol/templates/admin/sobol/search/grappelli_results.html [moved from contrib/sobol/templates/admin/sobol/search/grappelli_results.html with 100% similarity]
philo/contrib/sobol/templates/admin/sobol/search/results.html [moved from contrib/sobol/templates/admin/sobol/search/results.html with 100% similarity]
philo/contrib/sobol/templates/search/googlesearch.html [moved from contrib/sobol/templates/search/googlesearch.html with 100% similarity]
philo/contrib/sobol/utils.py [moved from contrib/sobol/utils.py with 99% similarity]
philo/contrib/waldo/__init__.py [moved from contrib/waldo/__init__.py with 100% similarity]
philo/contrib/waldo/forms.py [moved from contrib/waldo/forms.py with 98% similarity]
philo/contrib/waldo/models.py [moved from contrib/waldo/models.py with 99% similarity]
philo/contrib/waldo/tokens.py [moved from contrib/waldo/tokens.py with 98% similarity]
philo/exceptions.py [new file with mode: 0644]
philo/fixtures/test_fixtures.json [moved from fixtures/test_fixtures.json with 100% similarity]
philo/forms/__init__.py [moved from forms/__init__.py with 100% similarity]
philo/forms/entities.py [moved from forms/entities.py with 99% similarity]
philo/forms/fields.py [moved from forms/fields.py with 88% similarity]
philo/loaders/__init__.py [moved from loaders/__init__.py with 100% similarity]
philo/loaders/database.py [moved from loaders/database.py with 89% similarity]
philo/middleware.py [moved from middleware.py with 88% similarity]
philo/migrations/0001_initial.py [moved from migrations/0001_initial.py with 100% similarity]
philo/migrations/0002_auto__add_field_attribute_value.py [moved from migrations/0002_auto__add_field_attribute_value.py with 100% similarity]
philo/migrations/0003_move_json.py [moved from migrations/0003_move_json.py with 100% similarity]
philo/migrations/0004_auto__del_field_attribute_json_value.py [moved from migrations/0004_auto__del_field_attribute_json_value.py with 100% similarity]
philo/migrations/0005_add_attribute_values.py [moved from migrations/0005_add_attribute_values.py with 100% similarity]
philo/migrations/0006_move_attribute_and_relationship_values.py [moved from migrations/0006_move_attribute_and_relationship_values.py with 100% similarity]
philo/migrations/0007_auto__del_relationship__del_field_attribute_value.py [moved from migrations/0007_auto__del_relationship__del_field_attribute_value.py with 100% similarity]
philo/migrations/0008_auto__del_field_manytomanyvalue_object_ids.py [moved from migrations/0008_auto__del_field_manytomanyvalue_object_ids.py with 100% similarity]
philo/migrations/0009_auto__add_field_node_lft__add_field_node_rght__add_field_node_tree_id_.py [moved from migrations/0009_auto__add_field_node_lft__add_field_node_rght__add_field_node_tree_id_.py with 100% similarity]
philo/migrations/0010_auto__add_field_redirect_target_node__add_field_redirect_url_or_subpat.py [moved from migrations/0010_auto__add_field_redirect_target_node__add_field_redirect_url_or_subpat.py with 100% similarity]
philo/migrations/0011_move_target_url.py [moved from migrations/0011_move_target_url.py with 100% similarity]
philo/migrations/0012_auto__del_field_redirect_target.py [moved from migrations/0012_auto__del_field_redirect_target.py with 100% similarity]
philo/migrations/0013_auto.py [moved from migrations/0013_auto.py with 100% similarity]
philo/migrations/0014_auto.py [moved from migrations/0014_auto.py with 100% similarity]
philo/migrations/__init__.py [moved from migrations/__init__.py with 100% similarity]
philo/models/__init__.py [moved from models/__init__.py with 100% similarity]
philo/models/base.py [moved from models/base.py with 71% similarity]
philo/models/collections.py [moved from models/collections.py with 58% similarity]
philo/models/fields/__init__.py [moved from models/fields/__init__.py with 96% similarity]
philo/models/fields/entities.py [moved from models/fields/entities.py with 90% similarity]
philo/models/nodes.py [moved from models/nodes.py with 54% similarity]
philo/models/pages.py [moved from models/pages.py with 63% similarity]
philo/signals.py [new file with mode: 0644]
philo/static/admin/js/TagCreation.js [moved from media/admin/js/TagCreation.js with 100% similarity]
philo/templates/admin/philo/edit_inline/grappelli_tabular_attribute.html [moved from templates/admin/philo/edit_inline/grappelli_tabular_attribute.html with 56% similarity]
philo/templates/admin/philo/edit_inline/grappelli_tabular_container.html [moved from templates/admin/philo/edit_inline/grappelli_tabular_container.html with 81% similarity]
philo/templates/admin/philo/edit_inline/tabular_attribute.html [moved from templates/admin/philo/edit_inline/tabular_attribute.html with 100% similarity]
philo/templates/admin/philo/edit_inline/tabular_container.html [moved from templates/admin/philo/edit_inline/tabular_container.html with 100% similarity]
philo/templates/admin/philo/page/add_form.html [new file with mode: 0644]
philo/templatetags/__init__.py [moved from templatetags/__init__.py with 100% similarity]
philo/templatetags/collections.py [moved from templatetags/collections.py with 100% similarity]
philo/templatetags/containers.py [moved from templatetags/containers.py with 100% similarity]
philo/templatetags/embed.py [moved from templatetags/embed.py with 99% similarity]
philo/templatetags/include_string.py [moved from templatetags/include_string.py with 100% similarity]
philo/templatetags/nodes.py [moved from templatetags/nodes.py with 98% similarity]
philo/tests.py [moved from tests.py with 94% similarity]
philo/urls.py [moved from urls.py with 99% similarity]
philo/utils.py [moved from utils.py with 100% similarity]
philo/validators.py [moved from validators.py with 77% similarity]
philo/views.py [moved from views.py with 97% similarity]
setup.py [new file with mode: 0644]
signals.py [deleted file]
templates/admin/philo/page/add_form.html [deleted file]

index 0d20b64..073067c 100644 (file)
@@ -1 +1,2 @@
 *.pyc
+docs/_build/
diff --git a/README b/README
index 4b1a6f7..6e47860 100644 (file)
--- a/README
+++ b/README
@@ -2,7 +2,7 @@ Philo is a foundation for developing web content management systems.
 
 Prerequisites:
        * Python 2.5.4+ <http://www.python.org/>
-       * Django 1.2+ <http://www.djangoproject.com/>
+       * Django 1.3+ <http://www.djangoproject.com/>
        * django-mptt e734079+ <https://github.com/django-mptt/django-mptt/> 
        * (Optional) django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>
        * (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
index 8060db8..349a727 100644 (file)
@@ -3,7 +3,7 @@ Philo is a foundation for developing web content management systems.
 Prerequisites:
 
  * [Python 2.5.4+ &lt;http://www.python.org&gt;](http://www.python.org/)
- * [Django 1.2+ &lt;http://www.djangoproject.com/&gt;](http://www.djangoproject.com/)
+ * [Django 1.3+ &lt;http://www.djangoproject.com/&gt;](http://www.djangoproject.com/)
  * [django-mptt e734079+ &lt;https://github.com/django-mptt/django-mptt/&gt;](https://github.com/django-mptt/django-mptt/)
  * (Optional) [django-grappelli 2.0+ &lt;http://code.google.com/p/django-grappelli/&gt;](http://code.google.com/p/django-grappelli/)
  * (Optional) [south 0.7.2+ &lt;http://south.aeracode.org/)](http://south.aeracode.org/)
diff --git a/__init__.py b/__init__.py
deleted file mode 100644 (file)
index ba78dda..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-from philo.loaders.database import Loader
-
-
-_loader = Loader()
-
-
-def load_template_source(template_name, template_dirs=None):
-    # For backwards compatibility
-    import warnings
-    warnings.warn(
-        "'philo.load_template_source' is deprecated; use 'philo.loaders.database.Loader' instead.",
-        PendingDeprecationWarning
-    )
-    return _loader.load_template_source(template_name, template_dirs)
-load_template_source.is_usable = True
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644 (file)
index 0000000..16c56a5
--- /dev/null
@@ -0,0 +1,130 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+       @echo "Please use \`make <target>' where <target> is one of"
+       @echo "  html       to make standalone HTML files"
+       @echo "  dirhtml    to make HTML files named index.html in directories"
+       @echo "  singlehtml to make a single large HTML file"
+       @echo "  pickle     to make pickle files"
+       @echo "  json       to make JSON files"
+       @echo "  htmlhelp   to make HTML files and a HTML help project"
+       @echo "  qthelp     to make HTML files and a qthelp project"
+       @echo "  devhelp    to make HTML files and a Devhelp project"
+       @echo "  epub       to make an epub"
+       @echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+       @echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+       @echo "  text       to make text files"
+       @echo "  man        to make manual pages"
+       @echo "  changes    to make an overview of all changed/added/deprecated items"
+       @echo "  linkcheck  to check all external links for integrity"
+       @echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+       -rm -rf $(BUILDDIR)/*
+
+html:
+       $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+       $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+       @echo
+       @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+       $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+       @echo
+       @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+       $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+       @echo
+       @echo "Build finished; now you can process the pickle files."
+
+json:
+       $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+       @echo
+       @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+       $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+       @echo
+       @echo "Build finished; now you can run HTML Help Workshop with the" \
+             ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+       $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+       @echo
+       @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+             ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+       @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Philo.qhcp"
+       @echo "To view the help file:"
+       @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Philo.qhc"
+
+devhelp:
+       $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+       @echo
+       @echo "Build finished."
+       @echo "To view the help file:"
+       @echo "# mkdir -p $$HOME/.local/share/devhelp/Philo"
+       @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Philo"
+       @echo "# devhelp"
+
+epub:
+       $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+       @echo
+       @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo
+       @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+       @echo "Run \`make' in that directory to run these through (pdf)latex" \
+             "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+       $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+       @echo "Running LaTeX files through pdflatex..."
+       make -C $(BUILDDIR)/latex all-pdf
+       @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+       $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+       @echo
+       @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+       $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+       @echo
+       @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+       $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+       @echo
+       @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+       $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+       @echo
+       @echo "Link check complete; look for any errors in the above output " \
+             "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+       $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+       @echo "Testing of doctests in the sources finished, look at the " \
+             "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py
new file mode 100644 (file)
index 0000000..7710786
--- /dev/null
@@ -0,0 +1,248 @@
+"""
+Sphinx plugins for Django documentation.
+"""
+import os
+import re
+
+from docutils import nodes, transforms
+try:
+    import json
+except ImportError:
+    try:
+        import simplejson as json
+    except ImportError:
+        try:
+            from django.utils import simplejson as json
+        except ImportError:
+            json = None
+
+from sphinx import addnodes, roles
+from sphinx.builders.html import StandaloneHTMLBuilder
+from sphinx.writers.html import SmartyPantsHTMLTranslator
+from sphinx.util.console import bold
+from sphinx.util.compat import Directive
+
+# RE for option descriptions without a '--' prefix
+simple_option_desc_re = re.compile(
+    r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)')
+
+def setup(app):
+    app.add_crossref_type(
+        directivename = "setting",
+        rolename      = "setting",
+        indextemplate = "pair: %s; setting",
+    )
+    app.add_crossref_type(
+        directivename = "templatetag",
+        rolename      = "ttag",
+        indextemplate = "pair: %s; template tag"
+    )
+    app.add_crossref_type(
+        directivename = "templatefilter",
+        rolename      = "tfilter",
+        indextemplate = "pair: %s; template filter"
+    )
+    app.add_crossref_type(
+        directivename = "fieldlookup",
+        rolename      = "lookup",
+        indextemplate = "pair: %s; field lookup type",
+    )
+    app.add_description_unit(
+        directivename = "django-admin",
+        rolename      = "djadmin",
+        indextemplate = "pair: %s; django-admin command",
+        parse_node    = parse_django_admin_node,
+    )
+    app.add_description_unit(
+        directivename = "django-admin-option",
+        rolename      = "djadminopt",
+        indextemplate = "pair: %s; django-admin command-line option",
+        parse_node    = parse_django_adminopt_node,
+    )
+    app.add_config_value('django_next_version', '0.0', True)
+    app.add_directive('versionadded', VersionDirective)
+    app.add_directive('versionchanged', VersionDirective)
+    app.add_transform(SuppressBlockquotes)
+    app.add_builder(DjangoStandaloneHTMLBuilder)
+
+
+class VersionDirective(Directive):
+    has_content = True
+    required_arguments = 1
+    optional_arguments = 1
+    final_argument_whitespace = True
+    option_spec = {}
+
+    def run(self):
+        env = self.state.document.settings.env
+        arg0 = self.arguments[0]
+        is_nextversion = env.config.django_next_version == arg0
+        ret = []
+        node = addnodes.versionmodified()
+        ret.append(node)
+        if not is_nextversion:
+            if len(self.arguments) == 1:
+                linktext = 'Please, see the release notes </releases/%s>' % (arg0)
+                xrefs = roles.XRefRole()('doc', linktext, linktext, self.lineno, self.state)
+                node.extend(xrefs[0])
+            node['version'] = arg0
+        else:
+            node['version'] = "Development version"
+        node['type'] = self.name
+        if len(self.arguments) == 2:
+            inodes, messages = self.state.inline_text(self.arguments[1], self.lineno+1)
+            node.extend(inodes)
+            if self.content:
+                self.state.nested_parse(self.content, self.content_offset, node)
+            ret = ret + messages
+        env.note_versionchange(node['type'], node['version'], node, self.lineno)
+        return ret
+
+
+class SuppressBlockquotes(transforms.Transform):
+    """
+    Remove the default blockquotes that encase indented list, tables, etc.
+    """
+    default_priority = 300
+
+    suppress_blockquote_child_nodes = (
+        nodes.bullet_list,
+        nodes.enumerated_list,
+        nodes.definition_list,
+        nodes.literal_block,
+        nodes.doctest_block,
+        nodes.line_block,
+        nodes.table
+    )
+
+    def apply(self):
+        for node in self.document.traverse(nodes.block_quote):
+            if len(node.children) == 1 and isinstance(node.children[0], self.suppress_blockquote_child_nodes):
+                node.replace_self(node.children[0])
+
+class DjangoHTMLTranslator(SmartyPantsHTMLTranslator):
+    """
+    Django-specific reST to HTML tweaks.
+    """
+
+    # Don't use border=1, which docutils does by default.
+    def visit_table(self, node):
+        self.body.append(self.starttag(node, 'table', CLASS='docutils'))
+
+    # <big>? Really?
+    def visit_desc_parameterlist(self, node):
+        self.body.append('(')
+        self.first_param = 1
+
+    def depart_desc_parameterlist(self, node):
+        self.body.append(')')
+
+    #
+    # Don't apply smartypants to literal blocks
+    #
+    def visit_literal_block(self, node):
+        self.no_smarty += 1
+        SmartyPantsHTMLTranslator.visit_literal_block(self, node)
+
+    def depart_literal_block(self, node):
+        SmartyPantsHTMLTranslator.depart_literal_block(self, node)
+        self.no_smarty -= 1
+
+    #
+    # Turn the "new in version" stuff (versionadded/versionchanged) into a
+    # better callout -- the Sphinx default is just a little span,
+    # which is a bit less obvious that I'd like.
+    #
+    # FIXME: these messages are all hardcoded in English. We need to change
+    # that to accomodate other language docs, but I can't work out how to make
+    # that work.
+    #
+    version_text = {
+        'deprecated':       'Deprecated in Django %s',
+        'versionchanged':   'Changed in Django %s',
+        'versionadded':     'New in Django %s',
+    }
+
+    def visit_versionmodified(self, node):
+        self.body.append(
+            self.starttag(node, 'div', CLASS=node['type'])
+        )
+        title = "%s%s" % (
+            self.version_text[node['type']] % node['version'],
+            len(node) and ":" or "."
+        )
+        self.body.append('<span class="title">%s</span> ' % title)
+
+    def depart_versionmodified(self, node):
+        self.body.append("</div>\n")
+
+    # Give each section a unique ID -- nice for custom CSS hooks
+    def visit_section(self, node):
+        old_ids = node.get('ids', [])
+        node['ids'] = ['s-' + i for i in old_ids]
+        node['ids'].extend(old_ids)
+        SmartyPantsHTMLTranslator.visit_section(self, node)
+        node['ids'] = old_ids
+
+def parse_django_admin_node(env, sig, signode):
+    command = sig.split(' ')[0]
+    env._django_curr_admin_command = command
+    title = "django-admin.py %s" % sig
+    signode += addnodes.desc_name(title, title)
+    return sig
+
+def parse_django_adminopt_node(env, sig, signode):
+    """A copy of sphinx.directives.CmdoptionDesc.parse_signature()"""
+    from sphinx.domains.std import option_desc_re
+    count = 0
+    firstname = ''
+    for m in option_desc_re.finditer(sig):
+        optname, args = m.groups()
+        if count:
+            signode += addnodes.desc_addname(', ', ', ')
+        signode += addnodes.desc_name(optname, optname)
+        signode += addnodes.desc_addname(args, args)
+        if not count:
+            firstname = optname
+        count += 1
+    if not count:
+        for m in simple_option_desc_re.finditer(sig):
+            optname, args = m.groups()
+            if count:
+                signode += addnodes.desc_addname(', ', ', ')
+            signode += addnodes.desc_name(optname, optname)
+            signode += addnodes.desc_addname(args, args)
+            if not count:
+                firstname = optname
+            count += 1
+    if not firstname:
+        raise ValueError
+    return firstname
+
+
+class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder):
+    """
+    Subclass to add some extra things we need.
+    """
+
+    name = 'djangohtml'
+
+    def finish(self):
+        super(DjangoStandaloneHTMLBuilder, self).finish()
+        if json is None:
+            self.warn("cannot create templatebuiltins.js due to missing simplejson dependency")
+            return
+        self.info(bold("writing templatebuiltins.js..."))
+        xrefs = self.env.domaindata["std"]["objects"]
+        templatebuiltins = {
+            "ttags": [n for ((t, n), (l, a)) in xrefs.items()
+                        if t == "templatetag" and l == "ref/templates/builtins"],
+            "tfilters": [n for ((t, n), (l, a)) in xrefs.items()
+                        if t == "templatefilter" and l == "ref/templates/builtins"],
+        }
+        outfilename = os.path.join(self.outdir, "templatebuiltins.js")
+        f = open(outfilename, 'wb')
+        f.write('var django_template_builtins = ')
+        json.dump(templatebuiltins, f)
+        f.write(';\n')
+        f.close();
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644 (file)
index 0000000..043219d
--- /dev/null
@@ -0,0 +1,220 @@
+# -*- coding: utf-8 -*-
+#
+# Philo documentation build configuration file, created by
+# sphinx-quickstart on Fri Jan 28 14:04:16 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext")))
+sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
+
+os.environ['DJANGO_SETTINGS_MODULE'] = 'dummy-settings'
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['djangodocs', 'sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Philo'
+copyright = u'2011, Joseph Spiros'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+from philo import VERSION
+version = '%s.%s' % (VERSION[0], VERSION[1])
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Philodoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'Philo.tex', u'Philo Documentation',
+   u'Stephen Burrows', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'philo', u'Philo Documentation',
+     [u'Stephen Burrows'], 1)
+]
diff --git a/docs/dummy-settings.py b/docs/dummy-settings.py
new file mode 100644 (file)
index 0000000..7e424ab
--- /dev/null
@@ -0,0 +1,6 @@
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': 'db.sl3'
+    }
+}
\ No newline at end of file
diff --git a/docs/exceptions.rst b/docs/exceptions.rst
new file mode 100644 (file)
index 0000000..679ac77
--- /dev/null
@@ -0,0 +1,5 @@
+Exceptions
+==========
+
+.. automodule:: philo.exceptions
+       :members: MIDDLEWARE_NOT_CONFIGURED, AncestorDoesNotExist, ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644 (file)
index 0000000..36470fb
--- /dev/null
@@ -0,0 +1,42 @@
+.. Philo documentation master file, created by
+   sphinx-quickstart on Fri Jan 28 14:04:16 2011.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to Philo's documentation!
+=================================
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+   
+   intro
+   models/intro
+   exceptions
+   middleware
+   signals
+   validators
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+What is Philo?
+==============
+
+Philo is a foundation for developing web content management systems.
+
+Prerequisites:
+
+* `Python 2.5.4+ <http://www.python.org>`_
+* `Django 1.2+ <http://www.djangoproject.com/>`_
+* `django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>`_
+* (Optional) `django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>`_
+* (Optional) `south 0.7.2+ <http://south.aeracode.org/>`_
+* (Optional) `recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>`_
+
+To contribute, please visit the `project website <http://philo.ithinksw.org/>`_ or make a fork of the `git repository <http://github.com/ithinksw/philo/>`_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo <irc://irc.oftc.net/#philo>`_.
diff --git a/docs/intro.rst b/docs/intro.rst
new file mode 100644 (file)
index 0000000..33d1a98
--- /dev/null
@@ -0,0 +1,35 @@
+How to get started with philo
+=============================
+
+After installing `philo`_ and `mptt`_ on your python path, make sure to complete the following steps:
+
+1. add :mod:`philo` and :mod:`mptt` to :setting:`settings.INSTALLED_APPS`::
+       
+       INSTALLED_APPS = (
+               ...
+               'philo',
+               'mptt',
+               ...
+       )
+       
+2. add :class:`philo.middleware.RequestNodeMiddleware` to :setting:`settings.MIDDLEWARE_CLASSES`::
+       
+       MIDDLEWARE_CLASSES = (
+               ...
+               'philo.middleware.RequestNodeMiddleware',
+               ...
+       )
+       
+3. include :mod:`philo.urls` somewhere in your urls.py file. For example::
+       
+       from django.conf.urls.defaults import patterns, include, url
+       urlpatterns = patterns('',
+               url(r'^', include('philo.urls')),
+       )
+       
+4. Optionally add a root :class:`node <philo.models.Node>` to your current :class:`Site` in the admin interface.
+
+Philo should be ready to go!
+
+.. _philo: http://github.com/ithinksw/philo
+.. _mptt: http://github.com/django-mptt/django-mptt
\ No newline at end of file
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644 (file)
index 0000000..25f0d2a
--- /dev/null
@@ -0,0 +1,170 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+       set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+       set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+       :help
+       echo.Please use `make ^<target^>` where ^<target^> is one of
+       echo.  html       to make standalone HTML files
+       echo.  dirhtml    to make HTML files named index.html in directories
+       echo.  singlehtml to make a single large HTML file
+       echo.  pickle     to make pickle files
+       echo.  json       to make JSON files
+       echo.  htmlhelp   to make HTML files and a HTML help project
+       echo.  qthelp     to make HTML files and a qthelp project
+       echo.  devhelp    to make HTML files and a Devhelp project
+       echo.  epub       to make an epub
+       echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+       echo.  text       to make text files
+       echo.  man        to make manual pages
+       echo.  changes    to make an overview over all changed/added/deprecated items
+       echo.  linkcheck  to check all external links for integrity
+       echo.  doctest    to run all doctests embedded in the documentation if enabled
+       goto end
+)
+
+if "%1" == "clean" (
+       for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+       del /q /s %BUILDDIR%\*
+       goto end
+)
+
+if "%1" == "html" (
+       %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+       goto end
+)
+
+if "%1" == "dirhtml" (
+       %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+       goto end
+)
+
+if "%1" == "singlehtml" (
+       %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+       goto end
+)
+
+if "%1" == "pickle" (
+       %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished; now you can process the pickle files.
+       goto end
+)
+
+if "%1" == "json" (
+       %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished; now you can process the JSON files.
+       goto end
+)
+
+if "%1" == "htmlhelp" (
+       %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+       goto end
+)
+
+if "%1" == "qthelp" (
+       %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+       echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Philo.qhcp
+       echo.To view the help file:
+       echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Philo.ghc
+       goto end
+)
+
+if "%1" == "devhelp" (
+       %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished.
+       goto end
+)
+
+if "%1" == "epub" (
+       %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished. The epub file is in %BUILDDIR%/epub.
+       goto end
+)
+
+if "%1" == "latex" (
+       %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+       goto end
+)
+
+if "%1" == "text" (
+       %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished. The text files are in %BUILDDIR%/text.
+       goto end
+)
+
+if "%1" == "man" (
+       %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Build finished. The manual pages are in %BUILDDIR%/man.
+       goto end
+)
+
+if "%1" == "changes" (
+       %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.The overview file is in %BUILDDIR%/changes.
+       goto end
+)
+
+if "%1" == "linkcheck" (
+       %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+       goto end
+)
+
+if "%1" == "doctest" (
+       %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+       if errorlevel 1 exit /b 1
+       echo.
+       echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+       goto end
+)
+
+:end
diff --git a/docs/middleware.rst b/docs/middleware.rst
new file mode 100644 (file)
index 0000000..4a5c05f
--- /dev/null
@@ -0,0 +1,5 @@
+Middleware
+==========
+
+.. automodule:: philo.middleware
+       :members:
diff --git a/docs/models/collections.rst b/docs/models/collections.rst
new file mode 100644 (file)
index 0000000..0519494
--- /dev/null
@@ -0,0 +1,8 @@
+Collections
+===========
+
+.. automodule:: philo.models.collections
+       :members: Collection, CollectionMember, CollectionMemberManager
+
+.. autoclass:: CollectionMemberManager
+       :members:
\ No newline at end of file
diff --git a/docs/models/entities.rst b/docs/models/entities.rst
new file mode 100644 (file)
index 0000000..4127f56
--- /dev/null
@@ -0,0 +1,56 @@
+Entities and Attributes
+=======================
+
+.. automodule:: philo.models.base
+
+One of the core concepts in Philo is the relationship between the :class:`Entity` and :class:`Attribute` classes. :class:`Attribute`\ s represent an arbitrary key/value pair by having one :class:`GenericForeignKey` to an :class:`Entity` and another to an :class:`AttributeValue`.
+
+
+Attributes
+----------
+
+.. autoclass:: Attribute
+       :members:
+
+.. autoclass:: AttributeValue
+       :members:
+
+.. automodule:: philo.models.base
+       :members: attribute_value_limiter
+
+.. autoclass:: JSONValue
+       :show-inheritance:
+
+.. autoclass:: ForeignKeyValue
+       :show-inheritance:
+
+.. autoclass:: ManyToManyValue
+       :show-inheritance:
+
+.. automodule:: philo.models.base
+       :noindex:
+       :members: value_content_type_limiter
+
+.. autofunction:: register_value_model(model)
+.. autofunction:: unregister_value_model(model)
+
+Entities
+--------
+
+.. autoclass:: Entity
+       :members:
+       :exclude-members: attribute_set
+
+.. autoclass:: TreeManager
+       :members:
+
+.. autoclass:: TreeEntity
+       :show-inheritance:
+       :members:
+       :exclude-members: attribute_set
+
+       .. attribute:: objects
+
+               An instance of :class:`TreeManager`.
+       
+       .. automethod:: get_path
\ No newline at end of file
diff --git a/docs/models/fields.rst b/docs/models/fields.rst
new file mode 100644 (file)
index 0000000..0b3d0f9
--- /dev/null
@@ -0,0 +1,11 @@
+Custom Fields
+=============
+
+.. automodule:: philo.models.fields
+       :members:
+
+EntityProxyFields
+-----------------
+
+.. automodule:: philo.models.fields.entities
+       :members:
\ No newline at end of file
diff --git a/docs/models/intro.rst b/docs/models/intro.rst
new file mode 100644 (file)
index 0000000..4f65585
--- /dev/null
@@ -0,0 +1,16 @@
+Philo's models
+==============
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+   
+   entities
+   nodes-and-views
+   collections
+   miscellaneous
+   fields
+
+
+.. automodule:: philo.models
diff --git a/docs/models/miscellaneous.rst b/docs/models/miscellaneous.rst
new file mode 100644 (file)
index 0000000..ea13db2
--- /dev/null
@@ -0,0 +1,10 @@
+Miscellaneous Models
+=============================
+.. currentmodule:: philo.models.nodes
+.. autoclass:: philo.models.nodes.TargetURLModel
+       :members:
+       :exclude-members: get_target_url
+
+.. currentmodule:: philo.models.base
+.. autoclass:: philo.models.base.Tag
+       :members:
\ No newline at end of file
diff --git a/docs/models/nodes-and-views.rst b/docs/models/nodes-and-views.rst
new file mode 100644 (file)
index 0000000..b78dbd9
--- /dev/null
@@ -0,0 +1,60 @@
+Nodes and Views: Building Website structure
+===========================================
+.. automodule:: philo.models.nodes
+
+Nodes
+-----
+
+.. autoclass:: Node
+       :show-inheritance:
+       :members:
+       :exclude-members: attribute_set
+
+Views
+-----
+
+Abstract View Models
+++++++++++++++++++++
+
+.. autoclass:: View
+       :show-inheritance:
+       :members:
+       :exclude-members: attribute_set
+
+.. autoclass:: MultiView
+       :show-inheritance:
+       :members:
+       :exclude-members: attribute_set
+
+Concrete View Subclasses
+++++++++++++++++++++++++
+
+.. autoclass:: Redirect
+       :show-inheritance:
+       :members:
+       :exclude-members: attribute_set
+
+.. autoclass:: File
+       :show-inheritance:
+       :members:
+       :exclude-members: attribute_set
+
+Pages
+*****
+
+.. automodule:: philo.models.pages
+
+.. autoclass:: Page
+       :members:
+       :exclude-members: attribute_set
+       :show-inheritance:
+
+.. autoclass:: Template
+       :members:
+       :show-inheritance:
+
+.. autoclass:: Contentlet
+       :members:
+
+.. autoclass:: ContentReference
+       :members:
\ No newline at end of file
diff --git a/docs/signals.rst b/docs/signals.rst
new file mode 100644 (file)
index 0000000..8b3da3c
--- /dev/null
@@ -0,0 +1,5 @@
+Signals
+=======
+
+.. automodule:: philo.signals
+       :members:
diff --git a/docs/validators.rst b/docs/validators.rst
new file mode 100644 (file)
index 0000000..f91818b
--- /dev/null
@@ -0,0 +1,5 @@
+Validators
+==========
+
+.. automodule:: philo.validators
+       :members:
diff --git a/exceptions.py b/exceptions.py
deleted file mode 100644 (file)
index f53083d..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-from django.core.exceptions import ImproperlyConfigured
-
-
-MIDDLEWARE_NOT_CONFIGURED = ImproperlyConfigured("""Philo requires the RequestNode middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'philo.middleware.RequestNodeMiddleware'.""")
-
-
-class ViewDoesNotProvideSubpaths(Exception):
-       """ Raised by View.reverse when the View does not provide subpaths (the default). """
-       silent_variable_failure = True
-
-
-class ViewCanNotProvideSubpath(Exception):
-       """ Raised by View.reverse when the View can not provide a subpath for the supplied arguments. """
-       silent_variable_failure = True
-
-
-class AncestorDoesNotExist(Exception):
-       """ Raised by get_path if the root model is not an ancestor of the current model """
-       pass
\ No newline at end of file
similarity index 100%
rename from LICENSE
rename to philo/LICENSE
diff --git a/philo/__init__.py b/philo/__init__.py
new file mode 100644 (file)
index 0000000..32297e0
--- /dev/null
@@ -0,0 +1 @@
+VERSION = (0, 0)
similarity index 100%
rename from admin/__init__.py
rename to philo/admin/__init__.py
similarity index 99%
rename from admin/base.py
rename to philo/admin/base.py
index 75fa336..3a9458e 100644 (file)
@@ -4,12 +4,13 @@ from django.contrib.contenttypes import generic
 from django.http import HttpResponse
 from django.utils import simplejson as json
 from django.utils.html import escape
+from mptt.admin import MPTTModelAdmin
+
 from philo.models import Tag, Attribute
 from philo.models.fields.entities import ForeignKeyAttribute, ManyToManyAttribute
 from philo.admin.forms.attributes import AttributeForm, AttributeInlineFormSet
 from philo.admin.widgets import TagFilteredSelectMultiple
 from philo.forms.entities import EntityForm, proxy_fields_for_entity_model
-from mptt.admin import MPTTModelAdmin
 
 
 COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
similarity index 91%
rename from admin/collections.py
rename to philo/admin/collections.py
index d422b74..c2a9034 100644 (file)
@@ -1,4 +1,5 @@
 from django.contrib import admin
+
 from philo.admin.base import COLLAPSE_CLASSES
 from philo.models import CollectionMember, Collection
 
similarity index 99%
rename from admin/forms/attributes.py
rename to philo/admin/forms/attributes.py
index fc77d0f..5372ab3 100644 (file)
@@ -1,6 +1,7 @@
 from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
 from django.contrib.contenttypes.models import ContentType
 from django.forms.models import ModelForm
+
 from philo.models import Attribute
 
 
similarity index 99%
rename from admin/forms/containers.py
rename to philo/admin/forms/containers.py
index 420ba17..246a954 100644 (file)
@@ -5,6 +5,7 @@ from django.db.models import Q
 from django.forms.models import ModelForm, BaseInlineFormSet, BaseModelFormSet
 from django.forms.formsets import TOTAL_FORM_COUNT
 from django.utils.datastructures import SortedDict
+
 from philo.admin.widgets import ModelLookupWidget
 from philo.models import Contentlet, ContentReference
 
similarity index 79%
rename from admin/nodes.py
rename to philo/admin/nodes.py
index 66be107..853ba25 100644 (file)
@@ -1,12 +1,15 @@
 from django.contrib import admin
+from mptt.admin import MPTTModelAdmin
+
 from philo.admin.base import EntityAdmin, TreeEntityAdmin, COLLAPSE_CLASSES
 from philo.models import Node, Redirect, File
 
 
 class NodeAdmin(TreeEntityAdmin):
        list_display = ('slug', 'view', 'accepts_subpath')
+       raw_id_fields = ('parent',)
        related_lookup_fields = {
-               'fk': [],
+               'fk': raw_id_fields,
                'm2m': [],
                'generic': [['view_content_type', 'view_object_id']]
        }
@@ -14,6 +17,9 @@ class NodeAdmin(TreeEntityAdmin):
        def accepts_subpath(self, obj):
                return obj.accepts_subpath
        accepts_subpath.boolean = True
+       
+       def formfield_for_foreignkey(self, db_field, request, **kwargs):
+               return super(MPTTModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
 
 
 class ViewAdmin(EntityAdmin):
similarity index 84%
rename from admin/pages.py
rename to philo/admin/pages.py
index 13d4098..fd8665b 100644 (file)
@@ -1,10 +1,11 @@
+from django import forms
 from django.conf import settings
 from django.contrib import admin
-from django import forms
+
 from philo.admin.base import COLLAPSE_CLASSES, TreeAdmin
+from philo.admin.forms.containers import *
 from philo.admin.nodes import ViewAdmin
 from philo.models.pages import Page, Template, Contentlet, ContentReference
-from philo.admin.forms.containers import *
 
 
 class ContentletInline(admin.StackedInline):
@@ -46,6 +47,12 @@ class PageAdmin(ViewAdmin):
        list_filter = ('template',)
        search_fields = ['title', 'contentlets__content']
        inlines = [ContentletInline, ContentReferenceInline] + ViewAdmin.inlines
+       
+       def response_add(self, request, obj, post_url_continue='../%s/'):
+               # Shamelessly cribbed from django/contrib/auth/admin.py:143
+               if '_addanother' not in request.POST and '_popup' not in request.POST:
+                       request.POST['_continue'] = 1
+               return super(PageAdmin, self).response_add(request, obj, post_url_continue)
 
 
 class TemplateAdmin(TreeAdmin):
similarity index 86%
rename from admin/widgets.py
rename to philo/admin/widgets.py
index aa0aa30..62a492b 100644 (file)
@@ -1,10 +1,10 @@
 from django import forms
 from django.conf import settings
 from django.contrib.admin.widgets import FilteredSelectMultiple, url_params_from_lookup_dict
-from django.utils.translation import ugettext as _
+from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.text import truncate_words
-from django.utils.html import escape
+from django.utils.translation import ugettext as _
 
 
 class ModelLookupWidget(forms.TextInput):
@@ -48,15 +48,12 @@ class TagFilteredSelectMultiple(FilteredSelectMultiple):
        catalog has been loaded in the page
        """
        class Media:
-               js = (settings.ADMIN_MEDIA_PREFIX + "js/core.js",
-                         settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js",
-                         settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js")
-               
-               if 'staticmedia' in settings.INSTALLED_APPS:
-                       import staticmedia
-                       js += (staticmedia.url('admin/js/TagCreation.js'),)
-               else:
-                       js += (settings.ADMIN_MEDIA_PREFIX + "js/TagCreation.js",)
+               js = (
+                       settings.ADMIN_MEDIA_PREFIX + "js/core.js",
+                       settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js",
+                       settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js",
+                       settings.ADMIN_MEDIA_PREFIX + "js/TagCreation.js",
+               )
 
        def render(self, name, value, attrs=None, choices=()):
                if attrs is None: attrs = {}
similarity index 97%
rename from contrib/julian/admin.py
rename to philo/contrib/julian/admin.py
index 8f104e2..cf72682 100644 (file)
@@ -1,4 +1,5 @@
 from django.contrib import admin
+
 from philo.admin import EntityAdmin, COLLAPSE_CLASSES
 from philo.contrib.julian.models import Location, Event, Calendar, CalendarView
 
similarity index 99%
rename from contrib/julian/models.py
rename to philo/contrib/julian/models.py
index 5c49c7e..550513c 100644 (file)
@@ -1,3 +1,6 @@
+import calendar
+import datetime
+
 from django.conf import settings
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib.auth.models import User
@@ -10,12 +13,12 @@ from django.db import models
 from django.db.models.query import QuerySet
 from django.http import HttpResponse, Http404
 from django.utils.encoding import force_unicode
+
 from philo.contrib.julian.feedgenerator import ICalendarFeed
 from philo.contrib.penfield.models import FeedView, FEEDS
 from philo.exceptions import ViewCanNotProvideSubpath
 from philo.models import Tag, Entity, Page, TemplateField
 from philo.utils import ContentTypeRegistryLimiter
-import datetime, calendar
 
 
 __all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
similarity index 98%
rename from contrib/penfield/admin.py
rename to philo/contrib/penfield/admin.py
index c70cf46..a897b97 100644 (file)
@@ -2,6 +2,7 @@ from django import forms
 from django.contrib import admin
 from django.core.urlresolvers import reverse
 from django.http import HttpResponseRedirect, QueryDict
+
 from philo.admin import EntityAdmin, AddTagAdmin, COLLAPSE_CLASSES
 from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
 
similarity index 97%
rename from contrib/penfield/middleware.py
rename to philo/contrib/penfield/middleware.py
index b25a28b..8bcff40 100644 (file)
@@ -1,5 +1,6 @@
 from django.http import HttpResponse
 from django.utils.decorators import decorator_from_middleware
+
 from philo.contrib.penfield.exceptions import HttpNotAcceptable
 
 
similarity index 99%
rename from contrib/penfield/models.py
rename to philo/contrib/penfield/models.py
index a03bed8..6955069 100644 (file)
@@ -1,3 +1,5 @@
+from datetime import date, datetime
+
 from django.conf import settings
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib.sites.models import Site, RequestSite
@@ -9,13 +11,14 @@ from django.utils import feedgenerator, tzinfo
 from django.utils.datastructures import SortedDict
 from django.utils.encoding import smart_unicode, force_unicode
 from django.utils.html import escape
-from datetime import date, datetime
+
 from philo.contrib.penfield.exceptions import HttpNotAcceptable
 from philo.contrib.penfield.middleware import http_not_acceptable
 from philo.contrib.penfield.validators import validate_pagination_count
 from philo.exceptions import ViewCanNotProvideSubpath
 from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField, Template
 from philo.utils import paginate
+
 try:
        import mimeparse
 except:
similarity index 97%
rename from contrib/shipherd/admin.py
rename to philo/contrib/shipherd/admin.py
index 93d21e5..be31a43 100644 (file)
@@ -1,4 +1,5 @@
 from django.contrib import admin
+
 from philo.admin import TreeEntityAdmin, COLLAPSE_CLASSES, NodeAdmin, EntityAdmin
 from philo.models import Node
 from philo.contrib.shipherd.models import NavigationItem, Navigation
similarity index 99%
rename from contrib/shipherd/models.py
rename to philo/contrib/shipherd/models.py
index 654f5f8..a09f385 100644 (file)
@@ -1,12 +1,13 @@
 #encoding: utf-8
+from UserDict import DictMixin
+
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import NoReverseMatch
 from django.core.validators import RegexValidator, MinValueValidator
 from django.db import models
 from django.forms.models import model_to_dict
+
 from philo.models import TreeEntity, Node, TreeManager, Entity, TargetURLModel
-from philo.validators import RedirectValidator
-from UserDict import DictMixin
 
 
 DEFAULT_NAVIGATION_DEPTH = 3
similarity index 95%
rename from contrib/shipherd/templatetags/shipherd.py
rename to philo/contrib/shipherd/templatetags/shipherd.py
index b05ff0f..c8ba4fd 100644 (file)
@@ -60,10 +60,6 @@ class LazyNavigationRecurser(object):
                        context['item'] = item
                        context['children'] = self.__class__(self.template_nodes, item.get_children(), context, request)
                        
-                       # Django 1.2.X compatibility - a lazy recurser will not be called if accessed as a template variable.
-                       if django_version < (1,3):
-                               context['children'] = context['children']()
-                       
                        # Then render the nodelist bit by bit.
                        for node in self.template_nodes:
                                bits.append(node.render(context))
@@ -131,7 +127,7 @@ def recursenavigation(parser, token):
        
        Example:
                <ul>
-                       {% recursenavigation node main %}
+                       {% recursenavigation node "main" %}
                                <li{% if navloop.active %} class='active'{% endif %}>
                                        {{ navloop.item.text }}
                                        {% if item.get_children %}
similarity index 98%
rename from contrib/sobol/admin.py
rename to philo/contrib/sobol/admin.py
index 87dd39a..f4636e7 100644 (file)
@@ -1,3 +1,5 @@
+from functools import update_wrapper
+
 from django.conf import settings
 from django.conf.urls.defaults import patterns, url
 from django.contrib import admin
@@ -7,9 +9,9 @@ from django.http import HttpResponseRedirect, Http404
 from django.shortcuts import render_to_response
 from django.template import RequestContext
 from django.utils.translation import ugettext_lazy as _
+
 from philo.admin import EntityAdmin
 from philo.contrib.sobol.models import Search, ResultURL, SearchView
-from functools import update_wrapper
 
 
 class ResultURLInline(admin.TabularInline):
similarity index 97%
rename from contrib/sobol/forms.py
rename to philo/contrib/sobol/forms.py
index e79d9e7..f9994a1 100644 (file)
@@ -1,4 +1,5 @@
 from django import forms
+
 from philo.contrib.sobol.utils import SEARCH_ARG_GET_KEY
 
 
similarity index 96%
rename from contrib/sobol/models.py
rename to philo/contrib/sobol/models.py
index ee8187d..dbf37ef 100644 (file)
@@ -1,22 +1,28 @@
+import datetime
+
+from django.conf import settings
 from django.conf.urls.defaults import patterns, url
 from django.contrib import messages
 from django.core.exceptions import ValidationError
+from django.core.validators import URLValidator
 from django.db import models
 from django.http import HttpResponseRedirect, Http404, HttpResponse
 from django.utils import simplejson as json
 from django.utils.datastructures import SortedDict
+
 from philo.contrib.sobol import registry
 from philo.contrib.sobol.forms import SearchForm
 from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KEY, SEARCH_ARG_GET_KEY, check_redirect_hash
 from philo.exceptions import ViewCanNotProvideSubpath
 from philo.models import MultiView, Page
 from philo.models.fields import SlugMultipleChoiceField
-from philo.validators import RedirectValidator
-import datetime
-try:
-       import eventlet
-except:
-       eventlet = False
+
+eventlet = None
+if getattr(settings, 'SOBOL_USE_EVENTLET', False):
+       try:
+               import eventlet
+       except:
+               pass
 
 
 class Search(models.Model):
@@ -77,7 +83,7 @@ class Search(models.Model):
 
 class ResultURL(models.Model):
        search = models.ForeignKey(Search, related_name='result_urls')
-       url = models.TextField(validators=[RedirectValidator()])
+       url = models.TextField(validators=[URLValidator()])
        
        def __unicode__(self):
                return self.url
similarity index 98%
rename from contrib/sobol/search.py
rename to philo/contrib/sobol/search.py
index 39b93c7..6cd577d 100644 (file)
@@ -1,4 +1,5 @@
 #encoding: utf-8
+import datetime
 
 from django.conf import settings
 from django.contrib.sites.models import Site
@@ -9,12 +10,16 @@ from django.utils.http import urlquote_plus
 from django.utils.safestring import mark_safe
 from django.utils.text import capfirst
 from django.template import loader, Context, Template
-import datetime
+
 from philo.contrib.sobol.utils import make_tracking_querydict
 
-try:
-       from eventlet.green import urllib2
-except:
+
+if getattr(settings, 'SOBOL_USE_EVENTLET', False):
+       try:
+               from eventlet.green import urllib2
+       except:
+               import urllib2
+else:
        import urllib2
 
 
similarity index 99%
rename from contrib/sobol/utils.py
rename to philo/contrib/sobol/utils.py
index 3c5e537..5c52c81 100644 (file)
@@ -1,8 +1,9 @@
+from hashlib import sha1
+
 from django.conf import settings
 from django.http import QueryDict
 from django.utils.encoding import smart_str
 from django.utils.http import urlquote_plus, urlquote
-from hashlib import sha1
 
 
 SEARCH_ARG_GET_KEY = 'q'
similarity index 98%
rename from contrib/waldo/forms.py
rename to philo/contrib/waldo/forms.py
index 2ee64d0..4cc9874 100644 (file)
@@ -1,4 +1,5 @@
 from datetime import date
+
 from django import forms
 from django.conf import settings
 from django.contrib.auth import authenticate
@@ -6,6 +7,7 @@ from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext_lazy as _
+
 from philo.contrib.waldo.tokens import REGISTRATION_TIMEOUT_DAYS
 
 
similarity index 99%
rename from contrib/waldo/models.py
rename to philo/contrib/waldo/models.py
index f63cdb1..87cfafe 100644 (file)
@@ -1,3 +1,5 @@
+import urlparse
+
 from django import forms
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib import messages
@@ -15,10 +17,10 @@ from django.utils.http import int_to_base36, base36_to_int
 from django.utils.translation import ugettext as _
 from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
+
 from philo.models import MultiView, Page
 from philo.contrib.waldo.forms import WaldoAuthenticationForm, RegistrationForm, UserAccountForm
 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
-import urlparse
 
 
 class LoginMultiView(MultiView):
similarity index 98%
rename from contrib/waldo/tokens.py
rename to philo/contrib/waldo/tokens.py
index 80f0b11..858b073 100644 (file)
@@ -2,12 +2,12 @@
 Based on django.contrib.auth.tokens
 """
 
-
+from hashlib import sha1
 from datetime import date
+
 from django.conf import settings
 from django.utils.http import int_to_base36, base36_to_int
 from django.contrib.auth.tokens import PasswordResetTokenGenerator
-from hashlib import sha1
 
 
 REGISTRATION_TIMEOUT_DAYS = getattr(settings, 'WALDO_REGISTRATION_TIMEOUT_DAYS', 1)
diff --git a/philo/exceptions.py b/philo/exceptions.py
new file mode 100644 (file)
index 0000000..9a8908e
--- /dev/null
@@ -0,0 +1,20 @@
+from django.core.exceptions import ImproperlyConfigured
+
+
+#: Raised if ``request.node`` is required but not present. For example, this can be raised by :func:`philo.views.node_view`. :data:`MIDDLEWARE_NOT_CONFIGURED` is an instance of :exc:`django.core.exceptions.ImproperlyConfigured`.
+MIDDLEWARE_NOT_CONFIGURED = ImproperlyConfigured("""Philo requires the RequestNode middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'philo.middleware.RequestNodeMiddleware'.""")
+
+
+class ViewDoesNotProvideSubpaths(Exception):
+       """Raised by :meth:`View.reverse` when the View does not provide subpaths (the default)."""
+       silent_variable_failure = True
+
+
+class ViewCanNotProvideSubpath(Exception):
+       """Raised by :meth:`View.reverse` when the :class:`View` can not provide a subpath for the supplied arguments."""
+       silent_variable_failure = True
+
+
+class AncestorDoesNotExist(Exception):
+       """Raised by :meth:`TreeModel.get_path` if the root instance is not an ancestor of the current instance."""
+       pass
\ No newline at end of file
similarity index 100%
rename from forms/__init__.py
rename to philo/forms/__init__.py
similarity index 99%
rename from forms/entities.py
rename to philo/forms/entities.py
index e781128..5d34cce 100644 (file)
@@ -1,5 +1,6 @@
 from django.forms.models import ModelFormMetaclass, ModelForm, ModelFormOptions
 from django.utils.datastructures import SortedDict
+
 from philo.utils import fattr
 
 
similarity index 88%
rename from forms/fields.py
rename to philo/forms/fields.py
index b148947..8bb5ce3 100644 (file)
@@ -1,6 +1,7 @@
 from django import forms
 from django.core.exceptions import ValidationError
 from django.utils import simplejson as json
+
 from philo.validators import json_validator
 
 
similarity index 89%
rename from loaders/database.py
rename to philo/loaders/database.py
index 141aedd..71b93a6 100644 (file)
@@ -1,6 +1,7 @@
 from django.template import TemplateDoesNotExist
 from django.template.loader import BaseLoader
 from django.utils.encoding import smart_unicode
+
 from philo.models import Template
 
 
similarity index 88%
rename from middleware.py
rename to philo/middleware.py
index 5ec3e77..b90067a 100644 (file)
@@ -1,6 +1,7 @@
 from django.conf import settings
 from django.contrib.sites.models import Site
 from django.http import Http404
+
 from philo.models import Node, View
 
 
@@ -43,7 +44,7 @@ class LazyNode(object):
 
 
 class RequestNodeMiddleware(object):
-       """Middleware to process the request's path and attach the closest ancestor node."""
+       """Adds a ``node`` attribute, representing the currently-viewed node, to every incoming :class:`HttpRequest` object. This is required by :func:`philo.views.node_view`."""
        def process_request(self, request):
                request.__class__.node = LazyNode()
        
similarity index 100%
rename from models/__init__.py
rename to philo/models/__init__.py
similarity index 71%
rename from models/base.py
rename to philo/models/base.py
index af1e880..1726d19 100644 (file)
@@ -1,25 +1,31 @@
+from UserDict import DictMixin
+
 from django import forms
-from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.validators import RegexValidator
+from django.db import models
 from django.utils import simplejson as json
 from django.utils.encoding import force_unicode
+from mptt.models import MPTTModel, MPTTModelBase, MPTTOptions
+
 from philo.exceptions import AncestorDoesNotExist
 from philo.models.fields import JSONField
-from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
 from philo.signals import entity_class_prepared
+from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
 from philo.validators import json_validator
-from UserDict import DictMixin
-from mptt.models import MPTTModel, MPTTModelBase, MPTTOptions
 
 
 class Tag(models.Model):
+       """A simple, generic model for tagging."""
+       #: A CharField (max length 255) which contains the name of the tag.
        name = models.CharField(max_length=255)
+       #: A CharField (max length 255) which contains the tag's unique slug.
        slug = models.SlugField(max_length=255, unique=True)
        
        def __unicode__(self):
+               """Returns the value of the :attr:`name` field"""
                return self.name
        
        class Meta:
@@ -38,10 +44,12 @@ class Titled(models.Model):
                abstract = True
 
 
+#: An instance of :class:`ContentTypeRegistryLimiter` which is used to track the content types which can be related to by :class:`ForeignKeyValue`\ s and :class:`ManyToManyValue`\ s.
 value_content_type_limiter = ContentTypeRegistryLimiter()
 
 
 def register_value_model(model):
+       """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
        value_content_type_limiter.register_class(model)
 
 
@@ -49,21 +57,37 @@ register_value_model(Tag)
 
 
 def unregister_value_model(model):
+       """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`."""
        value_content_type_limiter.unregister_class(model)
 
 
 class AttributeValue(models.Model):
+       """
+       This is an abstract base class for models that can be used as values for :class:`Attribute`\ s.
+       
+       AttributeValue subclasses are expected to supply access to a clean version of their value through an attribute called "value".
+       
+       """
+       
+       #: :class:`GenericRelation` to :class:`Attribute`
        attribute_set = generic.GenericRelation('Attribute', content_type_field='value_content_type', object_id_field='value_object_id')
        
        def set_value(self, value):
+               """Given a ``value``, sets the appropriate fields so that it can be correctly stored in the database."""
                raise NotImplementedError
        
        def value_formfields(self, **kwargs):
-               """Define any formfields that would be used to construct an instance of this value."""
+               """
+               Returns any formfields that would be used to construct an instance of this value.
+               
+               :returns: A dictionary mapping field names to formfields.
+               
+               """
+               
                raise NotImplementedError
        
        def construct_instance(self, **kwargs):
-               """Apply cleaned data from the formfields generated by valid_formfields to oneself."""
+               """Applies cleaned data from the formfields generated by valid_formfields to oneself."""
                raise NotImplementedError
        
        def __unicode__(self):
@@ -73,10 +97,12 @@ class AttributeValue(models.Model):
                abstract = True
 
 
+#: An instance of :class:`ContentTypeSubclassLimiter` which is used to track the content types which are considered valid value models for an :class:`Attribute`.
 attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
 
 
 class JSONValue(AttributeValue):
+       """Stores a python object as a json string."""
        value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null', db_index=True)
        
        def __unicode__(self):
@@ -99,6 +125,7 @@ class JSONValue(AttributeValue):
 
 
 class ForeignKeyValue(AttributeValue):
+       """Stores a generic relationship to an instance of any value content type (as defined by the :data:`value_content_type_limiter`)."""
        content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
        object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
        value = generic.GenericForeignKey()
@@ -136,6 +163,7 @@ class ForeignKeyValue(AttributeValue):
 
 
 class ManyToManyValue(AttributeValue):
+       """Stores a generic relationship to many instances of any value content type (as defined by the :data:`value_content_type_limiter`)."""
        content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
        values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True)
        
@@ -215,14 +243,20 @@ class ManyToManyValue(AttributeValue):
 
 
 class Attribute(models.Model):
+       """Represents an arbitrary key/value pair on an arbitrary :class:`Model` where the key consists of word characters and the value is a subclass of :class:`AttributeValue`."""
        entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID', db_index=True)
+       
+       #: :class:`GenericForeignKey` to anything (generally an instance of an Entity subclass).
        entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
        
        value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_limiter, verbose_name='Value type', null=True, blank=True)
        value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True, db_index=True)
+       
+       #: :class:`GenericForeignKey` to an instance of a subclass of :class:`AttributeValue` as determined by the :data:`attribute_value_limiter`.
        value = generic.GenericForeignKey('value_content_type', 'value_object_id')
        
+       #: :class:`CharField` containing a key (up to 255 characters) consisting of alphanumeric characters and underscores.
        key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
        
        def __unicode__(self):
@@ -279,12 +313,26 @@ class EntityBase(models.base.ModelBase):
 
 
 class Entity(models.Model):
+       """An abstract class that simplifies access to related attributes. Most models provided by Philo subclass Entity."""
        __metaclass__ = EntityBase
        
        attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
        
        @property
        def attributes(self):
+               """
+               Property that returns a dictionary-like object which can be used to retrieve related :class:`Attribute`\ s' values directly.
+
+               Example::
+
+                       >>> attr = entity.attribute_set.get(key='spam')
+                       >>> attr.value.value
+                       u'eggs'
+                       >>> entity.attributes['spam']
+                       u'eggs'
+               
+               """
+               
                return QuerySetMapper(self.attribute_set.all())
        
        class Meta:
@@ -296,19 +344,22 @@ class TreeManager(models.Manager):
        
        def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
                """
-               Returns the object with the path, unless absolute_result is set to False, in which
-               case it returns a tuple containing the deepest object found along the path, and the
-               remainder of the path after that object as a string (or None if there is no remaining
-               path). Raises a DoesNotExist exception if no object is found with the given path.
-               
-               If the path you're searching for is known to exist, it is always faster to use
-               absolute_result=True - unless the path depth is over ~40, in which case the high cost
-               of the absolute query makes a binary search (i.e. non-absolute) faster.
+               If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises an :class:`~django.core.exceptions.ObjectDoesNotExist` exception. Otherwise, returns a tuple containing the deepest object found along ``path`` (or ``root`` if no deeper object is found) and the remainder of the path after that object as a string (or None if there is no remaining path).
+               
+               .. note:: If you are looking for something with an exact path, it is faster to use absolute_result=True, unless the path depth is over ~40, in which case the high cost of the absolute query may make a binary search (i.e. non-absolute) faster.
+               
+               .. note:: SQLite allows max of 64 tables in one join. That means the binary search will only work on paths with a max depth of 127 and the absolute fetch will only work to a max depth of (surprise!) 63. Larger depths could be handled, but since the common use case will not have a tree structure that deep, they are not.
+               
+               :param path: The path of the object
+               :param root: The object which will be considered the root of the search
+               :param absolute_result: Whether to return an absolute result or do a binary search
+               :param pathsep: The path separator used in ``path``
+               :param field: The field on the model which should be queried for ``path`` segment matching.
+               :returns: An instance if ``absolute_result`` is ``True`` or an (instance, remaining_path) tuple otherwise.
+               :raises django.core.exceptions.ObjectDoesNotExist: if no object can be found matching the input parameters.
+               
                """
-               # Note: SQLite allows max of 64 tables in one join. That means the binary search will
-               # only work on paths with a max depth of 127 and the absolute fetch will only work
-               # to a max depth of (surprise!) 63. Although this could be handled, chances are your
-               # tree structure won't be that deep.
+               
                segments = path.split(pathsep)
                
                # Clean out blank segments. Handles multiple consecutive pathseps.
@@ -407,6 +458,14 @@ class TreeModel(MPTTModel):
        slug = models.SlugField(max_length=255)
        
        def get_path(self, root=None, pathsep='/', field='slug'):
+               """
+               :param root: Only return the path since this object.
+               :param pathsep: The path separator to use when constructing an instance's path
+               :param field: The field to pull path information from for each ancestor.
+               :returns: A string representation of an object's path.
+               
+               """
+               
                if root == self:
                        return ''
                
@@ -438,10 +497,27 @@ class TreeEntityBase(MPTTModelBase, EntityBase):
 
 
 class TreeEntity(Entity, TreeModel):
+       """An abstract subclass of Entity which represents a tree relationship."""
+       
        __metaclass__ = TreeEntityBase
        
        @property
        def attributes(self):
+               """
+               Property that returns a dictionary-like object which can be used to retrieve related :class:`Attribute`\ s' values directly. If an attribute with a given key is not related to the :class:`Entity`, then the object will check the parent's attributes.
+
+               Example::
+
+                       >>> attr = entity.attribute_set.get(key='spam')
+                       DoesNotExist: Attribute matching query does not exist.
+                       >>> attr = entity.parent.attribute_set.get(key='spam')
+                       >>> attr.value.value
+                       u'eggs'
+                       >>> entity.attributes['spam']
+                       u'eggs'
+               
+               """
+               
                if self.parent:
                        return QuerySetMapper(self.attribute_set.all(), passthrough=self.parent.attributes)
                return super(TreeEntity, self).attributes
similarity index 58%
rename from models/collections.py
rename to philo/models/collections.py
index 539ecdb..7c773b3 100644 (file)
@@ -1,17 +1,25 @@
-from django.db import models
-from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+from django.template import add_to_builtins as register_templatetags
+
 from philo.models.base import value_content_type_limiter, register_value_model
 from philo.utils import fattr
-from django.template import add_to_builtins as register_templatetags
 
 
 class Collection(models.Model):
+       """
+       Collections are curated ordered groupings of arbitrary models.
+       
+       """
+       #: :class:`CharField` with max_length 255
        name = models.CharField(max_length=255)
+       #: Optional :class:`TextField`
        description = models.TextField(blank=True, null=True)
        
        @fattr(short_description='Members')
        def get_count(self):
+               """Returns the number of items in the collection."""
                return self.members.count()
        
        def __unicode__(self):
@@ -25,15 +33,37 @@ class CollectionMemberManager(models.Manager):
        use_for_related_fields = True
 
        def with_model(self, model):
+               """
+               Given a model class or instance, returns a queryset of all instances of that model which have collection members in this manager's scope.
+               
+               Example::
+               
+                       >>> from philo.models import Collection
+                       >>> from django.contrib.auth.models import User
+                       >>> collection = Collection.objects.get(name="Foo")
+                       >>> collection.members.all()
+                       [<CollectionMember: Foo - user1>, <CollectionMember: Foo - user2>, <CollectionMember: Foo - Spam & Eggs>]
+                       >>> collection.members.with_model(User)
+                       [<User: user1>, <User: user2>]
+               
+               """
                return model._default_manager.filter(pk__in=self.filter(member_content_type=ContentType.objects.get_for_model(model)).values_list('member_object_id', flat=True))
 
 
 class CollectionMember(models.Model):
+       """
+       The collection member model represents a generic link from a :class:`Collection` to an arbitrary model instance with an attached order.
+       
+       """
+       #: A :class:`CollectionMemberManager` instance
        objects = CollectionMemberManager()
+       #: :class:`ForeignKey` to a :class:`Collection` instance.
        collection = models.ForeignKey(Collection, related_name='members')
+       #: The numerical index of the item within the collection (optional).
        index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
        member_content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Member type')
        member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
+       #: :class:`GenericForeignKey` to an arbitrary model instance.
        member = generic.GenericForeignKey('member_content_type', 'member_object_id')
        
        def __unicode__(self):
similarity index 96%
rename from models/fields/__init__.py
rename to philo/models/fields/__init__.py
index d900e31..eca3a12 100644 (file)
@@ -5,6 +5,7 @@ from django.db import models
 from django.utils import simplejson as json
 from django.utils.text import capfirst
 from django.utils.translation import ugettext_lazy as _
+
 from philo.forms.fields import JSONFormField
 from philo.validators import TemplateValidator, json_validator
 #from philo.models.fields.entities import *
@@ -109,8 +110,7 @@ class SlugMultipleChoiceField(models.Field):
                                del kwargs[k]
                
                defaults.update(kwargs)
-               # Django 1.2 does not supply MultipleChoiceField
-               form_class = getattr(forms, 'TypedMultipleChoiceField', forms.MultipleChoiceField)
+               form_class = forms.TypedMultipleChoiceField
                return form_class(**defaults)
        
        def validate(self, value, model_instance):
similarity index 90%
rename from models/fields/entities.py
rename to philo/models/fields/entities.py
index 6c407d0..c37d496 100644 (file)
@@ -1,29 +1,23 @@
 """
-The EntityProxyFields defined in this file can be assigned as fields on
-a subclass of philo.models.Entity. They act like any other model
-fields, but instead of saving their data to the database, they save it
-to attributes related to a model instance. Additionally, a new
-attribute will be created for an instance if and only if the field's
-value has been set. This is relevant i.e. for passthroughs, where the
-value of the field may be defined by some other instance's attributes.
+EntityProxyFields can be assigned as fields on a subclass of philo.models.Entity. They act like any other model fields, but instead of saving their data to the model's table, they save it to attributes related to a model instance. Additionally, a new attribute will be created for an instance if and only if the field's value has been set. This is relevant i.e. for :class:`QuerySetMapper` passthroughs, where even an Attribute with a value of ``None`` must prevent a passthrough.
 
 Example::
 
        class Thing(Entity):
                numbers = models.PositiveIntegerField()
-       
-       class ThingProxy(Thing):
                improvised = JSONAttribute(models.BooleanField)
 """
+import datetime
 from itertools import tee
+
 from django import forms
 from django.core.exceptions import FieldError
 from django.db import models
 from django.db.models.fields import NOT_PROVIDED
 from django.utils.text import capfirst
-from philo.signals import entity_class_prepared
+
 from philo.models import ManyToManyValue, JSONValue, ForeignKeyValue, Attribute, Entity
-import datetime
+from philo.signals import entity_class_prepared
 
 
 __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
@@ -124,6 +118,7 @@ class AttributeFieldDescriptor(object):
 
 
 def process_attribute_fields(sender, instance, created, **kwargs):
+       """This function is attached to each :class:`Entity` subclass's post_save signal. Any :class:`Attribute`\ s managed by EntityProxyFields which have been removed will be deleted, and any new attributes will be created """
        if ATTRIBUTE_REGISTRY in instance.__dict__:
                registry = instance.__dict__[ATTRIBUTE_REGISTRY]
                instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete()
@@ -180,6 +175,8 @@ class AttributeField(EntityProxyField):
 
 
 class JSONAttribute(AttributeField):
+       """Handles an :class:`Attribute` with a :class:`JSONValue`."""
+       
        value_class = JSONValue
        
        def __init__(self, field_template=None, **kwargs):
@@ -214,6 +211,7 @@ class JSONAttribute(AttributeField):
 
 
 class ForeignKeyAttribute(AttributeField):
+       """Handles an :class:`Attribute` with a :class:`ForeignKeyValue`."""
        value_class = ForeignKeyValue
        
        def __init__(self, model, limit_choices_to=None, **kwargs):
similarity index 54%
rename from models/nodes.py
rename to philo/models/nodes.py
index 99be196..a9b77fb 100644 (file)
@@ -1,20 +1,20 @@
-from django.db import models
-from django.contrib.contenttypes.models import ContentType
+from inspect import getargspec
+
 from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.models import Site, RequestSite
-from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect, Http404
 from django.core.exceptions import ValidationError
 from django.core.servers.basehttp import FileWrapper
 from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch
+from django.db import models
+from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect, Http404
 from django.template import add_to_builtins as register_templatetags
 from django.utils.encoding import smart_str
-from inspect import getargspec
-from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
+
+from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED, ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths
 from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
 from philo.models.fields import JSONField
 from philo.utils import ContentTypeSubclassLimiter
-from philo.validators import RedirectValidator
-from philo.exceptions import ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths, AncestorDoesNotExist
 from philo.signals import view_about_to_render, view_finished_rendering
 
 
@@ -22,12 +22,18 @@ _view_content_type_limiter = ContentTypeSubclassLimiter(None)
 
 
 class Node(TreeEntity):
+       """
+       :class:`Node`\ s are the basic building blocks of a website using Philo. They define the URL hierarchy and connect each URL to a :class:`View` subclass instance which is used to generate an HttpResponse.
+       
+       """
        view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter)
        view_object_id = models.PositiveIntegerField()
+       #: :class:`GenericForeignKey` to a non-abstract subclass of :class:`View`
        view = generic.GenericForeignKey('view_content_type', 'view_object_id')
        
        @property
        def accepts_subpath(self):
+               """A property shortcut for :attr:`self.view.accepts_subpath <View.accepts_subpath>`"""
                if self.view:
                        return self.view.accepts_subpath
                return False
@@ -36,21 +42,36 @@ class Node(TreeEntity):
                return self.view.handles_subpath(subpath)
        
        def render_to_response(self, request, extra_context=None):
+               """This is a shortcut method for :meth:`View.render_to_response`"""
                return self.view.render_to_response(request, extra_context)
        
        def get_absolute_url(self, request=None, with_domain=False, secure=False):
+               """
+               This is essentially a shortcut for calling :meth:`construct_url` without a subpath.
+               
+               :returns: The absolute url of the node on the current site.
+               
+               """
                return self.construct_url(request=request, with_domain=with_domain, secure=secure)
        
        def construct_url(self, subpath="/", request=None, with_domain=False, secure=False):
                """
-               This method will construct a URL based on the Node's location.
-               If a request is passed in, that will be used as a backup in case
-               the Site lookup fails. The Site lookup takes precedence because
-               it's what's used to find the root node. This will raise:
-               - NoReverseMatch if philo-root is not reverseable
-               - Site.DoesNotExist if a domain is requested but not buildable.
-               - AncestorDoesNotExist if the root node of the site isn't an
-                 ancestor of this instance.
+               This method will do its best to construct a URL based on the Node's location. If with_domain is True, that URL will include a domain and a protocol; if secure is True as well, the protocol will be https. The request will be used to construct a domain in cases where a call to :meth:`Site.objects.get_current` fails.
+               
+               Node urls will not contain a trailing slash unless a subpath is provided which ends with a trailing slash. Subpaths are expected to begin with a slash, as if returned by :func:`django.core.urlresolvers.reverse`.
+               
+               :meth:`construct_url` may raise the following exceptions:
+               
+               - :class:`NoReverseMatch` if "philo-root" is not reversable -- for example, if :mod:`philo.urls` is not included anywhere in your urlpatterns.
+               - :class:`Site.DoesNotExist <ObjectDoesNotExist>` if with_domain is True but no :class:`Site` or :class:`RequestSite` can be built.
+               - :class:`~philo.exceptions.AncestorDoesNotExist` if the root node of the site isn't an ancestor of the node constructing the URL.
+               
+               :param string subpath: The subpath to be constructed beyond beyond the node's URL.
+               :param request: :class:`HttpRequest` instance. Will be used to construct a :class:`RequestSite` if :meth:`Site.objects.get_current` fails.
+               :param with_domain: Whether the constructed URL should include a domain name and protocol.
+               :param secure: Whether the protocol, if included, should be http:// or https://.
+               :returns: A constructed url for accessing the given subpath of the current node instance.
+               
                """
                # Try reversing philo-root first, since we can't do anything if that fails.
                root_url = reverse('philo-root')
@@ -89,18 +110,38 @@ models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_
 
 
 class View(Entity):
+       """
+       :class:`View` is an abstract model that represents an item which can be "rendered", generally in response to an :class:`HttpRequest`.
+       
+       """
+       #: A generic relation back to nodes.
        nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id')
        
+       #: Property or attribute which defines whether this :class:`View` can handle subpaths. Default: ``False``
        accepts_subpath = False
        
        def handles_subpath(self, subpath):
+               """Returns True if the :class:`View` handles the given subpath, and False otherwise."""
                if not self.accepts_subpath and subpath != "/":
                        return False
                return True
        
        def reverse(self, view_name=None, args=None, kwargs=None, node=None, obj=None):
-               """Shortcut method to handle the common pattern of getting the
-               absolute url for a view's subpaths."""
+               """
+               If :attr:`accepts_subpath` is True, try to reverse a URL using the given parameters using ``self`` as the urlconf.
+               
+               If ``obj`` is provided, :meth:`get_reverse_params` will be called and the results will be combined with any ``view_name``, ``args``, and ``kwargs`` that may have been passed in.
+               
+               :param view_name: The name of the view to be reversed.
+               :param args: Extra args for reversing the view.
+               :param kwargs: A dictionary of arguments for reversing the view.
+               :param node: The node whose subpath this is.
+               :param obj: An object to be passed to :meth:`get_reverse_params` to generate a view_name, args, and kwargs for reversal.
+               :returns: A subpath beyond the node that reverses the view, or an absolute url that reverses the view if a node was passed in.
+               :except philo.exceptions.ViewDoesNotProvideSubpaths: if :attr:`accepts_subpath` is False
+               :except philo.exceptions.ViewCanNotProvideSubpath: if a reversal is not possible.
+               
+               """
                if not self.accepts_subpath:
                        raise ViewDoesNotProvideSubpaths
                
@@ -123,13 +164,26 @@ class View(Entity):
                return subpath
        
        def get_reverse_params(self, obj):
-               """This method should return a view_name, args, kwargs tuple suitable for reversing a url for the given obj using self as the urlconf."""
+               """
+               This method is not implemented on the base class. It should return a (``view_name``, ``args``, ``kwargs``) tuple suitable for reversing a url for the given ``obj`` using ``self`` as the urlconf. If a reversal will not be possible, this method should raise :class:`~philo.exceptions.ViewCanNotProvideSubpath`.
+               
+               """
                raise NotImplementedError("View subclasses must implement get_reverse_params to support subpaths.")
        
        def attributes_with_node(self, node):
+               """
+               Returns a :class:`~philo.models.base.QuerySetMapper` using the :class:`Node`'s attributes as a passthrough.
+               
+               """
                return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
        
        def render_to_response(self, request, extra_context=None):
+               """
+               Renders the :class:`View` as an :class:`HttpResponse`. This will raise :const:`~philo.exceptions.MIDDLEWARE_NOT_CONFIGURED` if the `request` doesn't have an attached :class:`Node`. This can happen if the :class:`~philo.middleware.RequestNodeMiddleware` is not in :setting:`settings.MIDDLEWARE_CLASSES` or if it is not functioning correctly.
+               
+               :meth:`render_to_response` will send the :data:`~philo.signals.view_about_to_render` signal, then call :meth:`actually_render_to_response`, and finally send the :data:`~philo.signals.view_finished_rendering` signal before returning the ``response``.
+
+               """
                if not hasattr(request, 'node'):
                        raise MIDDLEWARE_NOT_CONFIGURED
                
@@ -140,6 +194,7 @@ class View(Entity):
                return response
        
        def actually_render_to_response(self, request, extra_context=None):
+               """Concrete subclasses must override this method to provide the business logic for turning a ``request`` and ``extra_context`` into an :class:`HttpResponse`."""
                raise NotImplementedError('View subclasses must implement actually_render_to_response.')
        
        class Meta:
@@ -150,10 +205,16 @@ _view_content_type_limiter.cls = View
 
 
 class MultiView(View):
+       """
+       :class:`MultiView` is an abstract model which represents a section of related pages - for example, a :class:`~philo.contrib.penfield.BlogView` might have a foreign key to :class:`Page`\ s for an index, an entry detail, an entry archive by day, and so on. :class:`!MultiView` subclasses :class:`View`, and defines the following additional methods and attributes:
+       
+       """
+       #: Same as :attr:`View.accepts_subpath`. Default: ``True``
        accepts_subpath = True
        
        @property
        def urlpatterns(self):
+               """Returns urlpatterns that point to views (generally methods on the class). :class:`MultiView`\ s can be thought of as "managing" these subpaths."""
                raise NotImplementedError("MultiView subclasses must implement urlpatterns.")
        
        def handles_subpath(self, subpath):
@@ -166,6 +227,10 @@ class MultiView(View):
                return True
        
        def actually_render_to_response(self, request, extra_context=None):
+               """
+               Resolves the remaining subpath left after finding this :class:`View`'s node using :attr:`self.urlpatterns <urlpatterns>` and renders the view function (or method) found with the appropriate args and kwargs.
+               
+               """
                clear_url_caches()
                subpath = request.node.subpath
                view, args, kwargs = resolve(subpath, urlconf=self)
@@ -177,17 +242,33 @@ class MultiView(View):
                return view(request, *args, **kwargs)
        
        def get_context(self):
-               """Hook for providing instance-specific context - such as the value of a Field - to all views."""
+               """Hook for providing instance-specific context - such as the value of a Field - to any view methods on the instance."""
                return {}
        
        def basic_view(self, field_name):
                """
-               Given the name of a field on ``self``, accesses the value of
+               Given the name of a field on the class, accesses the value of
                that field and treats it as a ``View`` instance. Creates a
                basic context based on self.get_context() and any extra_context
                that was passed in, then calls the ``View`` instance's
                render_to_response() method. This method is meant to be called
                to return a view function appropriate for urlpatterns.
+               
+               :param field_name: The name of a field on the instance which contains a :class:`View` subclass instance.
+               :returns: A simple view function.
+               
+               Example::
+                       
+                       class Foo(Multiview):
+                               page = models.ForeignKey(Page)
+                               
+                               @property
+                               def urlpatterns(self):
+                                       urlpatterns = patterns('',
+                                               url(r'^$', self.basic_view('page'))
+                                       )
+                                       return urlpatterns
+               
                """
                field = self._meta.get_field(field_name)
                view = getattr(self, field.name, None)
@@ -206,8 +287,12 @@ class MultiView(View):
 
 
 class TargetURLModel(models.Model):
+       """An abstract parent class for models which deal in targeting a url."""
+       #: An optional :class:`ForeignKey` to a :class:`Node`. If provided, that node will be used as the basis for the redirect.
        target_node = models.ForeignKey(Node, blank=True, null=True, related_name="%(app_label)s_%(class)s_related")
-       url_or_subpath = models.CharField(max_length=200, validators=[RedirectValidator()], blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
+       #: A :class:`CharField` which may contain an absolute or relative URL, or the name of a node's subpath.
+       url_or_subpath = models.CharField(max_length=200, blank=True, help_text="Point to this url or, if a node is defined and accepts subpaths, this subpath of the node.")
+       #: A :class:`~philo.models.fields.JSONField` instance. If the value of :attr:`reversing_parameters` is not None, the :attr:`url_or_subpath` will be treated as the name of a view to be reversed. The value of :attr:`reversing_parameters` will be passed into the reversal as args if it is a list or as kwargs if it is a dictionary. Otherwise it will be ignored.
        reversing_parameters = JSONField(blank=True, help_text="If reversing parameters are defined, url_or_subpath will instead be interpreted as the view name to be reversed.")
        
        def clean(self):
@@ -236,6 +321,7 @@ class TargetURLModel(models.Model):
                return self.url_or_subpath, args, kwargs
        
        def get_target_url(self):
+               """Calculates and returns the target url based on the :attr:`target_node`, :attr:`url_or_subpath`, and :attr:`reversing_parameters`."""
                node = self.target_node
                if node is not None and node.accepts_subpath and self.url_or_subpath:
                        if self.reversing_parameters is not None:
@@ -260,13 +346,17 @@ class TargetURLModel(models.Model):
 
 
 class Redirect(TargetURLModel, View):
+       """Represents a 301 or 302 redirect to a different url on an absolute or relative path."""
+       #: A choices tuple of redirect status codes (temporary or permanent).
        STATUS_CODES = (
                (302, 'Temporary'),
                (301, 'Permanent'),
        )
+       #: An :class:`IntegerField` which uses :attr:`STATUS_CODES` as its choices. Determines whether the redirect is considered temporary or permanent.
        status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
        
        def actually_render_to_response(self, request, extra_context=None):
+               """Returns an :class:`HttpResponseRedirect` to :attr:`self.target_url`."""
                response = HttpResponseRedirect(self.target_url)
                response.status_code = self.status_code
                return response
@@ -276,9 +366,10 @@ class Redirect(TargetURLModel, View):
 
 
 class File(View):
-       """ For storing arbitrary files """
-       
+       """Stores an arbitrary file."""
+       #: Defines the mimetype of the uploaded file. This will not be validated.
        mimetype = models.CharField(max_length=255)
+       #: Contains the uploaded file. Files are uploaded to ``philo/files/%Y/%m/%d``.
        file = models.FileField(upload_to='philo/files/%Y/%m/%d')
        
        def actually_render_to_response(self, request, extra_context=None):
@@ -291,6 +382,7 @@ class File(View):
                app_label = 'philo'
        
        def __unicode__(self):
+               """Returns the path of the uploaded file."""
                return self.file.name
 
 
similarity index 63%
rename from models/pages.py
rename to philo/models/pages.py
index 2221ee4..3ad05d8 100644 (file)
@@ -1,4 +1,9 @@
 # encoding: utf-8
+"""
+:class:`Page`\ s are the most frequently used :class:`View` subclass. They define a basic HTML page and its associated content. Each :class:`Page` renders itself according to a :class:`Template`. The :class:`Template` may contain :ttag:`container <philo.templatetags.containers.do_container>` tags, which define related :class:`Contentlet`\ s and :class:`ContentReference`\ s for any page using that :class:`Template`.
+
+"""
+
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
@@ -8,13 +13,14 @@ from django.http import HttpResponse
 from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, add_to_builtins as register_templatetags, TextNode, VariableNode
 from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
 from django.utils.datastructures import SortedDict
+
 from philo.models.base import TreeModel, register_value_model
 from philo.models.fields import TemplateField
 from philo.models.nodes import View
+from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
 from philo.templatetags.containers import ContainerNode
 from philo.utils import fattr
 from philo.validators import LOADED_TEMPLATE_ATTR
-from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
 
 
 class LazyContainerFinder(object):
@@ -70,18 +76,21 @@ class LazyContainerFinder(object):
 
 
 class Template(TreeModel):
+       """Represents a database-driven django template."""
+       #: The name of the template. Used for organization and debugging.
        name = models.CharField(max_length=255)
+       #: Can be used to let users know what the template is meant to be used for.
        documentation = models.TextField(null=True, blank=True)
+       #: Defines the mimetype of the template. This is not validated. Default: ``text/html``.
        mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
+       #: An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template.
        code = TemplateField(secure=False, verbose_name='django template code')
        
        @property
        def containers(self):
                """
-               Returns a tuple where the first item is a list of names of contentlets referenced by containers,
-               and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
-               This will break if there is a recursive extends or includes in the template code.
-               Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
+               Returns a tuple where the first item is a list of names of contentlets referenced by containers, and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers. This will break if there is a recursive extends or includes in the template code. Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
+               
                """
                template = DjangoTemplate(self.code)
                
@@ -130,6 +139,7 @@ class Template(TreeModel):
                return contentlet_specs, contentreference_specs
        
        def __unicode__(self):
+               """Returns the value of the :attr:`name` field."""
                return self.name
        
        class Meta:
@@ -138,18 +148,29 @@ class Template(TreeModel):
 
 class Page(View):
        """
-       Represents a page - something which is rendered according to a template. The page will have a number of related Contentlets depending on the template selected - but these will appear only after the page has been saved with that template.
+       Represents a page - something which is rendered according to a :class:`Template`. The page will have a number of related :class:`Contentlet`\ s and :class:`ContentReference`\ s depending on the template selected - but these will appear only after the page has been saved with that template.
+       
        """
+       #: A :class:`ForeignKey` to the :class:`Template` used to render this :class:`Page`.
        template = models.ForeignKey(Template, related_name='pages')
+       #: The name of this page. Chances are this will be used for organization - i.e. finding the page in a list of pages - rather than for display.
        title = models.CharField(max_length=255)
        
        def get_containers(self):
+               """
+               Returns the results :attr:`~Template.containers` for the related template. This is a tuple containing the specs of all :ttag:`containers <philo.templatetags.containers.do_container>` in the :class:`Template`'s code. The value will be cached on the instance so that multiple accesses will be less expensive.
+               
+               """
                if not hasattr(self, '_containers'):
                        self._containers = self.template.containers
                return self._containers
        containers = property(get_containers)
        
        def render_to_string(self, request=None, extra_context=None):
+               """
+               In addition to rendering as an :class:`HttpResponse`, a :class:`Page` can also render as a string. This means, for example, that :class:`Page`\ s can be used to render emails or other non-HTML content with the same :ttag:`container <philo.templatetags.containers.do_container>`-based functionality as is used for HTML.
+               
+               """
                context = {}
                context.update(extra_context or {})
                context.update({'page': self, 'attributes': self.attributes})
@@ -165,12 +186,18 @@ class Page(View):
                return string
        
        def actually_render_to_response(self, request, extra_context=None):
+               """Returns an :class:`HttpResponse` with the content of the :meth:`render_to_string` method and the mimetype set to the :attr:`~Template.mimetype` of the related :class:`Template`."""
                return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
        
        def __unicode__(self):
+               """Returns the value of :attr:`title`"""
                return self.title
        
        def clean_fields(self, exclude=None):
+               """
+               This is an override of the default model clean_fields method. Essentially, in addition to validating the fields, this method validates the :class:`Template` instance that is used to render this :class:`Page`. This is useful for catching template errors before they show up as 500 errors on a live site.
+               
+               """
                if exclude is None:
                        exclude = []
                
@@ -196,11 +223,16 @@ class Page(View):
 
 
 class Contentlet(models.Model):
+       """Represents a piece of content on a page. This content is treated as a secure :class:`~philo.models.fields.TemplateField`."""
+       #: The page which this :class:`Contentlet` is related to.
        page = models.ForeignKey(Page, related_name='contentlets')
+       #: This represents the name of the container as defined by a :ttag:`container <philo.templatetags.containers.do_container>` tag.
        name = models.CharField(max_length=255, db_index=True)
+       #: A secure :class:`~philo.models.fields.TemplateField` holding the content for this :class:`Contentlet`. Note that actually using this field as a template requires use of the :ttag:`include_string <philo.templatetags.include_string.do_include_string>` template tag.
        content = TemplateField()
        
        def __unicode__(self):
+               """Returns the value of the :attr:`name` field."""
                return self.name
        
        class Meta:
@@ -208,13 +240,18 @@ class Contentlet(models.Model):
 
 
 class ContentReference(models.Model):
+       """Represents a model instance related to a page."""
+       #: The page which this :class:`ContentReference` is related to.
        page = models.ForeignKey(Page, related_name='contentreferences')
+       #: This represents the name of the container as defined by a :ttag:`container <philo.templatetags.containers.do_container>` tag.
        name = models.CharField(max_length=255, db_index=True)
        content_type = models.ForeignKey(ContentType, verbose_name='Content type')
        content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
+       #: A :class:`GenericForeignKey` to a model instance. The content type of this instance is defined by the :ttag:`container <philo.templatetags.containers.do_container>` tag which defines this :class:`ContentReference`.
        content = generic.GenericForeignKey('content_type', 'content_id')
        
        def __unicode__(self):
+               """Returns the value of the :attr:`name` field."""
                return self.name
        
        class Meta:
diff --git a/philo/signals.py b/philo/signals.py
new file mode 100644 (file)
index 0000000..558c6fe
--- /dev/null
@@ -0,0 +1,60 @@
+from django.dispatch import Signal
+
+
+#: Sent whenever an Entity subclass has been "prepared" -- that is, after the processing necessary to make :mod:`EntityProxyFields <philo.models.fields.entities>` work has been completed. This will fire after :obj:`django.db.models.signals.class_prepared`.
+#:
+#: Arguments that are sent with this signal:
+#: 
+#: ``sender``
+#:     The model class.
+entity_class_prepared = Signal(providing_args=['class'])
+
+#: Sent when a :class:`~philo.models.nodes.View` instance is about to render. This allows you, for example, to modify the ``extra_context`` dictionary used in rendering.
+#:
+#: Arguments that are sent with this signal:
+#:
+#: ``sender``
+#:     The :class:`~philo.models.nodes.View` instance
+#:
+#: ``request``
+#:     The :class:`HttpRequest` instance which the :class:`~philo.models.nodes.View` is rendering in response to.
+#:
+#: ``extra_context``
+#:     A dictionary which will be passed into :meth:`~philo.models.nodes.View.actually_render_to_response`.
+view_about_to_render = Signal(providing_args=['request', 'extra_context'])
+
+#: Sent when a view instance has finished rendering.
+#:
+#: Arguments that are sent with this signal:
+#:
+#: ``sender``
+#:     The :class:`~philo.models.nodes.View` instance
+#:
+#: ``response``
+#:     The :class:`HttpResponse` instance which :class:`~philo.models.nodes.View` view has rendered to.
+view_finished_rendering = Signal(providing_args=['response'])
+
+#: Sent when a :class:`~philo.models.pages.Page` instance is about to render as a string. If the :class:`~philo.models.pages.Page` is rendering as a response, this signal is sent after :obj:`view_about_to_render` and serves a similar function. However, there are situations where a :class:`~philo.models.pages.Page` may be rendered as a string without being rendered as a response afterwards.
+#:
+#: Arguments that are sent with this signal:
+#:
+#: ``sender``
+#:     The :class:`~philo.models.pages.Page` instance
+#:
+#: ``request``
+#:     The :class:`HttpRequest` instance which the :class:`~philo.models.pages.Page` is rendering in response to (if any).
+#:
+#: ``extra_context``
+#:     A dictionary which will be passed into the :class:`Template` context.
+page_about_to_render_to_string = Signal(providing_args=['request', 'extra_context'])
+
+#: Sent when a :class:`~philo.models.pages.Page` instance has just finished rendering as a string. If the :class:`~philo.models.pages.Page` is rendering as a response, this signal is sent before :obj:`view_finished_rendering` and serves a similar function. However, there are situations where a :class:`~philo.models.pages.Page` may be rendered as a string without being rendered as a response afterwards.
+#:
+#: Arguments that are sent with this signal:
+#:
+#: ``sender``
+#:     The :class:`~philo.models.pages.Page` instance
+#:
+#: ``string``
+#:     The string which the :class:`~philo.models.pages.Page` has rendered to.
+page_finished_rendering_to_string = Signal(providing_args=['string'])
\ No newline at end of file
@@ -1,4 +1,4 @@
-{% load i18n adminmedia %}
+{% load i18n adminmedia grp_tags %}
 
 <!-- group -->
 <div class="group tabular{% if inline_admin_formset.opts.classes %} {{ inline_admin_formset.opts.classes|join:" " }}{% endif %}"
 
 <script type="text/javascript">
 (function($) {
-    $(document).ready(function($) {
-        
-        $("#{{ inline_admin_formset.formset.prefix }}-group").grp_inline({
-            prefix: "{{ inline_admin_formset.formset.prefix }}",
-            onBeforeAdded: function(inline) {},
-            onAfterAdded: function(form) {
-                grappelli.reinitDateTimeFields(form);
-                grappelli.updateSelectFilter(form);
-                form.find("input.vForeignKeyRawIdAdminField").grp_related_fk({lookup_url:"{% url grp_related_lookup %}"});
-                form.find("input.vManyToManyRawIdAdminField").grp_related_m2m({lookup_url:"{% url grp_m2m_lookup %}"});
-                form.find("input[name*='object_id'][name$='id']").grp_related_generic({lookup_url:"{% url grp_related_lookup %}"});
-            },
-        });
-        
-        {% if inline_admin_formset.opts.sortable_field_name %}
-        $("#{{ inline_admin_formset.formset.prefix }}-group > div.table").sortable({
-            handle: "a.drag-handler",
-            items: "div.dynamic-form",
-            axis: "y",
-            appendTo: 'body',
-            forceHelperSize: true,
-            containment: '#{{ inline_admin_formset.formset.prefix }}-group > div.table',
-            tolerance: 'pointer',
-        });
-        $("#{{ opts.module_name }}_form").bind("submit", function(){
-            var sortable_field_name = "{{ inline_admin_formset.opts.sortable_field_name }}";
-            var i = 0;
-            $("#{{ inline_admin_formset.formset.prefix }}-group").find("div.dynamic-form").each(function(){
-                var fields = $(this).find("div.td :input[value]");
-                if (fields.serialize()) {
-                    $(this).find("input[name$='"+sortable_field_name+"']").val(i);
-                    i++;
-                }
-            });
-        });
-        {% endif %}
-        
-    });
+       $(document).ready(function($) {
+               
+               var prefix = "{{ inline_admin_formset.formset.prefix }}";
+               var related_lookup_fields_fk = {% get_related_lookup_fields_fk inline_admin_formset.opts %};
+               var related_lookup_fields_m2m = {% get_related_lookup_fields_m2m inline_admin_formset.opts %};
+               var related_lookup_fields_generic = {% get_related_lookup_fields_generic inline_admin_formset.opts %};
+               $.each(related_lookup_fields_fk, function() {
+                       $("#{{ inline_admin_formset.formset.prefix }}-group > div.table")
+                       .find("input[name^='" + prefix + "'][name$='" + this + "']")
+                       .grp_related_fk({lookup_url:"{% url grp_related_lookup %}"});
+               });
+               $.each(related_lookup_fields_m2m, function() {
+                       $("#{{ inline_admin_formset.formset.prefix }}-group > div.table")
+                       .find("input[name^='" + prefix + "'][name$='" + this + "']")
+                       .grp_related_m2m({lookup_url:"{% url grp_m2m_lookup %}"});
+               });
+               $.each(related_lookup_fields_generic, function() {
+                       var content_type = this[0],
+                               object_id = this[1];
+                       $("#{{ inline_admin_formset.formset.prefix }}-group > div.table")
+                       .find("input[name^='" + prefix + "'][name$='" + this[1] + "']")
+                       .each(function() {
+                               var i = $(this).attr("id").match(/-\d+-/);
+                               if (i) {
+                                       var ct_id = "#id_" + prefix + i[0] + content_type,
+                                               obj_id = "#id_" + prefix + i[0] + object_id;
+                                       $(this).grp_related_generic({content_type:ct_id, object_id:obj_id, lookup_url:"{% url grp_related_lookup %}"});
+                               }
+                       });
+               });
+               
+               $("#{{ inline_admin_formset.formset.prefix }}-group").grp_inline({
+                       prefix: "{{ inline_admin_formset.formset.prefix }}",
+                       onBeforeAdded: function(inline) {},
+                       onAfterAdded: function(form) {
+                               grappelli.reinitDateTimeFields(form);
+                               grappelli.updateSelectFilter(form);
+                               $.each(related_lookup_fields_fk, function() {
+                                       form.find("input[name^='" + prefix + "'][name$='" + this + "']")
+                                       .grp_related_fk({lookup_url:"{% url grp_related_lookup %}"});
+                               });
+                               $.each(related_lookup_fields_m2m, function() {
+                                       form.find("input[name^='" + prefix + "'][name$='" + this + "']")
+                                       .grp_related_m2m({lookup_url:"{% url grp_m2m_lookup %}"});
+                               });
+                               $.each(related_lookup_fields_generic, function() {
+                                       var content_type = this[0],
+                                               object_id = this[1];
+                                       form.find("input[name^='" + prefix + "'][name$='" + this[1] + "']")
+                                       .each(function() {
+                                               var i = $(this).attr("id").match(/-\d+-/);
+                                               if (i) {
+                                                       var ct_id = "#id_" + prefix + i[0] + content_type,
+                                                               obj_id = "#id_" + prefix + i[0] + object_id;
+                                                       $(this).grp_related_generic({content_type:ct_id, object_id:obj_id, lookup_url:"{% url grp_related_lookup %}"});
+                                               }
+                                       });
+                               });
+                       },
+               });
+               
+               {% if inline_admin_formset.opts.sortable_field_name %}
+               $("#{{ inline_admin_formset.formset.prefix }}-group > div.table").sortable({
+                       handle: "a.drag-handler",
+                       items: "div.dynamic-form",
+                       axis: "y",
+                       appendTo: 'body',
+                       forceHelperSize: true,
+                       containment: '#{{ inline_admin_formset.formset.prefix }}-group > div.table',
+                       tolerance: 'pointer',
+               });
+               $("#{{ opts.module_name }}_form").bind("submit", function(){
+                       var sortable_field_name = "{{ inline_admin_formset.opts.sortable_field_name }}";
+                       var i = 0;
+                       $("#{{ inline_admin_formset.formset.prefix }}-group").find("div.dynamic-form").each(function(){
+                               var fields = $(this).find("div.td :input[value]");
+                               if (fields.serialize()) {
+                                       $(this).find("input[name$='"+sortable_field_name+"']").val(i);
+                                       i++;
+                               }
+                       });
+               });
+               {% endif %}
+               
+       });
 })(django.jQuery);
 </script>
                        {% endfor %}{% endspaceless %}
                {% endfor %}
                {% for form in inline_admin_formset.formset.forms %}
-                       <div class="row cells-{{ form.fields.keys|length }}{% if not form.fields.keys|length_is:"2" %} cells{% endif %}{% if form.errors %} errors{% endif %} {% for field in form %}{{ field.field.name }} {% endfor %}{% comment %} {% if forloop.last %} empty-form{% endif %}{% endcomment %}">
+                       <div class="row cells-{{ form.fields|length }} cells{% if form.errors %} errors{% endif %}{% for field in form %} {{ field.field.name }}{% endfor %}">
                                {{ form.non_field_errors }}
-                               <div{% if not form.fields.keys|length_is:"2" %} class="cell"{% endif %}>
-                                       <div class="column span-4"><label class='required' for="{{ form.content.auto_id }}{{ form.content_id.auto_id }}">{{ form.verbose_name|capfirst }}:</label></div>
+                               <div>
                                {% for field in form %}
                                        {% if not field.is_hidden %}
+                                       {% comment %}This will be true for one field: the content/content reference{% endcomment %}
+                                       <div class="column span-4"><label class='required' for="{{ form.content.auto_id }}{{ form.content_id.auto_id }}">{{ form.verbose_name|capfirst }}:</label></div>
                                        <div class="column span-flexible">
                                                {{ field }}
                                                {{ field.errors }}
diff --git a/philo/templates/admin/philo/page/add_form.html b/philo/templates/admin/philo/page/add_form.html
new file mode 100644 (file)
index 0000000..b2a6358
--- /dev/null
@@ -0,0 +1,14 @@
+{% extends "admin/change_form.html" %}
+{% load i18n %}
+
+{% block form_top %}
+       {% if not is_popup %}
+               <p>{% trans "First, choose a template. After saving, you'll be able to provide additional content for containers." %}</p>
+       {% else %}
+               <p>{% trans "Choose a template" %}</p>
+       {% endif %}
+{% endblock %}
+
+{% block after_field_sets %}
+<script type="text/javascript">document.getElementById("id_name").focus();</script>
+{% endblock %}
\ No newline at end of file
similarity index 100%
rename from templatetags/containers.py
rename to philo/templatetags/containers.py
index c5fd445..f6def0a 100644 (file)
@@ -1,8 +1,8 @@
 from django import template
 from django.conf import settings
-from django.utils.safestring import SafeUnicode, mark_safe
-from django.core.exceptions import ObjectDoesNotExist
 from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.safestring import SafeUnicode, mark_safe
 
 
 register = template.Library()
similarity index 99%
rename from templatetags/embed.py
rename to philo/templatetags/embed.py
index eb4cd68..39b29e0 100644 (file)
@@ -1,7 +1,8 @@
 from django import template
-from django.contrib.contenttypes.models import ContentType
 from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
 from django.template.loader_tags import ExtendsNode, BlockContext, BLOCK_CONTEXT_KEY, TextNode, BlockNode
+
 from philo.utils import LOADED_TEMPLATE_ATTR
 
 
similarity index 98%
rename from templatetags/nodes.py
rename to philo/templatetags/nodes.py
index 5ae507d..00d9764 100644 (file)
@@ -4,6 +4,7 @@ from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse, NoReverseMatch
 from django.template.defaulttags import kwarg_re
 from django.utils.encoding import smart_str
+
 from philo.exceptions import ViewCanNotProvideSubpath
 
 
similarity index 94%
rename from tests.py
rename to philo/tests.py
index 96ac7b6..a0e0184 100644 (file)
--- a/tests.py
@@ -1,13 +1,17 @@
-from django.test import TestCase
+import sys
+import traceback
+
 from django import template
 from django.conf import settings
 from django.db import connection
 from django.template import loader
 from django.template.loaders import cached
+from django.test import TestCase
+from django.test.utils import setup_test_template_loader
+
+from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
 from philo.exceptions import AncestorDoesNotExist
 from philo.models import Node, Page, Template
-from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
-import sys, traceback
 
 
 class TemplateTestCase(TestCase):
@@ -17,19 +21,15 @@ class TemplateTestCase(TestCase):
                "Tests to make sure that embed behaves with complex includes and extends"
                template_tests = self.get_template_tests()
                
-               # Register our custom template loader. Shamelessly cribbed from django core regressiontests.
-               def test_template_loader(template_name, template_dirs=None):
-                       "A custom template loader that loads the unit-test templates."
-                       try:
-                               return (template_tests[template_name][0] , "test:%s" % template_name)
-                       except KeyError:
-                               raise template.TemplateDoesNotExist, template_name
-               
-               cache_loader = cached.Loader(('test_template_loader',))
-               cache_loader._cached_loaders = (test_template_loader,)
+               # Register our custom template loader. Shamelessly cribbed from django/tests/regressiontests/templates/tests.py:384.
+               cache_loader = setup_test_template_loader(
+                       dict([(name, t[0]) for name, t in template_tests.iteritems()]),
+                       use_cached_loader=True,
+               )
                
-               old_template_loaders = loader.template_source_loaders
-               loader.template_source_loaders = [cache_loader]
+               failures = []
+               tests = template_tests.items()
+               tests.sort()
                
                # Turn TEMPLATE_DEBUG off, because tests assume that.
                old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
@@ -38,9 +38,6 @@ class TemplateTestCase(TestCase):
                old_invalid = settings.TEMPLATE_STRING_IF_INVALID
                expected_invalid_str = 'INVALID'
                
-               failures = []
-               tests = template_tests.items()
-               tests.sort()
                # Run tests
                for name, vals in tests:
                        xx, context, result = vals
similarity index 99%
rename from urls.py
rename to philo/urls.py
index 0363224..d4dfc7b 100644 (file)
--- a/urls.py
@@ -1,4 +1,5 @@
 from django.conf.urls.defaults import patterns, url
+
 from philo.views import node_view
 
 
similarity index 100%
rename from utils.py
rename to philo/utils.py
similarity index 77%
rename from validators.py
rename to philo/validators.py
index 5ae9409..349dd56 100644 (file)
@@ -1,13 +1,15 @@
-from django.utils.translation import ugettext_lazy as _
-from django.core.validators import RegexValidator
+import re
+
 from django.core.exceptions import ValidationError
 from django.template import Template, Parser, Lexer, TOKEN_BLOCK, TOKEN_VAR, TemplateSyntaxError
 from django.utils import simplejson as json
 from django.utils.html import escape, mark_safe
-import re
+from django.utils.translation import ugettext_lazy as _
+
 from philo.utils import LOADED_TEMPLATE_ATTR
 
 
+#: Tags which are considered insecure and are therefore always disallowed by secure :class:`TemplateValidator` instances.
 INSECURE_TAGS = (
        'load',
        'extends',
@@ -16,34 +18,8 @@ INSECURE_TAGS = (
 )
 
 
-class RedirectValidator(RegexValidator):
-       """Based loosely on the URLValidator, but no option to verify_exists"""
-       regex = re.compile(
-               r'^(?:https?://' # http:// or https://
-               r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
-               r'localhost|' #localhost...
-               r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
-               r'(?::\d+)?' # optional port
-               r'(?:/?|[/?#]?\S+)|'
-               r'[^?#\s]\S*)$',
-               re.IGNORECASE)
-       message = _(u'Enter a valid absolute or relative redirect target')
-
-
-class URLLinkValidator(RegexValidator):
-       """Based loosely on the URLValidator, but no option to verify_exists"""
-       regex = re.compile(
-               r'^(?:https?://' # http:// or https://
-               r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
-               r'localhost|' #localhost...
-               r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
-               r'(?::\d+)?' # optional port
-               r'|)' # also allow internal links
-               r'(?:/?|[/?#]?\S+)$', re.IGNORECASE)
-       message = _(u'Enter a valid absolute or relative redirect target')
-
-
 def json_validator(value):
+       """Validates whether ``value`` is a valid json string."""
        try:
                json.loads(value)
        except Exception, e:
@@ -118,7 +94,7 @@ class TemplateValidationParser(Parser):
 
 
 def linebreak_iter(template_source):
-       # Cribbed from django/views/debug.py
+       # Cribbed from django/views/debug.py:18
        yield 0
        p = template_source.find('\n')
        while p >= 0:
@@ -128,6 +104,14 @@ def linebreak_iter(template_source):
 
 
 class TemplateValidator(object): 
+       """
+       Validates whether a string represents valid Django template code.
+       
+       :param allow: ``None`` or an iterable of tag names which are explicitly allowed. If provided, tags whose names are not in the iterable will cause a ValidationError to be raised if they are used in the template code.
+       :param disallow: ``None`` or an iterable of tag names which are explicitly allowed. If provided, tags whose names are in the iterable will cause a ValidationError to be raised if they are used in the template code. If a tag's name is in ``allow`` and ``disallow``, it will be disallowed.
+       :param secure: If the validator is set to secure, it will automatically disallow the tag names listed in :const:`INSECURE_TAGS`. Defaults to ``True``.
+       
+       """
        def __init__(self, allow=None, disallow=None, secure=True):
                self.allow = allow
                self.disallow = disallow
similarity index 97%
rename from views.py
rename to philo/views.py
index 598be36..28740fd 100644 (file)
--- a/views.py
@@ -2,6 +2,7 @@ from django.conf import settings
 from django.core.urlresolvers import resolve
 from django.http import Http404, HttpResponseRedirect
 from django.views.decorators.vary import vary_on_headers
+
 from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
 
 
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..3c18b16
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+import os
+
+
+# Shamelessly cribbed from django's setup.py file.
+def fullsplit(path, result=None):
+       """
+       Split a pathname into components (the opposite of os.path.join) in a
+       platform-neutral way.
+       """
+       if result is None:
+               result = []
+       head, tail = os.path.split(path)
+       if head == '':
+               return [tail] + result
+       if head == path:
+               return result
+       return fullsplit(head, [tail] + result)
+
+# Compile the list of packages available, because distutils doesn't have
+# an easy way to do this. Shamelessly cribbed from django's setup.py file.
+packages, data_files = [], []
+root_dir = os.path.dirname(__file__)
+if root_dir != '':
+    os.chdir(root_dir)
+philo_dir = 'philo'
+
+for dirpath, dirnames, filenames in os.walk(philo_dir):
+       # Ignore dirnames that start with '.'
+       for i, dirname in enumerate(dirnames):
+               if dirname.startswith('.'): del dirnames[i]
+       if '__init__.py' in filenames:
+               packages.append('.'.join(fullsplit(dirpath)))
+       elif filenames:
+               data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])
+
+
+version = __import__('philo').VERSION
+
+setup(
+       name = 'Philo',
+       version = '%s.%s' % (version[0], version[1]),
+       packages = packages,
+       data_files = data_files,
+)
\ No newline at end of file
diff --git a/signals.py b/signals.py
deleted file mode 100644 (file)
index 3653c54..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-from django.dispatch import Signal
-
-
-entity_class_prepared = Signal(providing_args=['class'])
-view_about_to_render = Signal(providing_args=['request', 'extra_context'])
-view_finished_rendering = Signal(providing_args=['response'])
-page_about_to_render_to_string = Signal(providing_args=['request', 'extra_context'])
-page_finished_rendering_to_string = Signal(providing_args=['string'])
\ No newline at end of file
diff --git a/templates/admin/philo/page/add_form.html b/templates/admin/philo/page/add_form.html
deleted file mode 100644 (file)
index 67f6ec4..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-{% extends "admin/change_form.html" %}
-{% load i18n %}
-
-{% block extrahead %}{{ block.super }}
-<!-- This will break if anything ever changes and may not work in all browsers. Sad face. -->
-<script type='text/javascript'>
-(function($){
-       $(function(){
-               $('#page_form input[type=submit]').click(function(e){
-                       if (e.target.name == '_addanother') {
-                               hidden = document.getElementById('page_form')._continue[0]
-                               hidden.parentNode.removeChild(hidden)
-                       }
-               })
-       })
-}(django.jQuery));
-</script>
-{% endblock %}
-
-{% block form_top %}
-       <p>{% trans "First, choose a template. After saving, you'll be able to provide additional content for containers." %}</p>
-       <input type="hidden" name="_continue" value="1" />
-{% endblock %}
-
-{% block content %}
-{% with 0 as save_on_top %}
-{{ block.super }}
-{% endwith %}
-{% endblock %}
\ No newline at end of file