From: Joseph Spiros
Date: Fri, 4 Nov 2011 23:05:56 +0000 (-0400)
Subject: Merge branch 'develop' into gilbert-ext4
X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/4a170a70ed8171fc66d9d139df5f7be5208d838c?hp=b1187c750167bfbdb0c50f62923d11ad77d26a34
Merge branch 'develop' into gilbert-ext4
* develop: (137 commits)
Added include_package_data option to setup.py to ensure MANIFEST.in is actually heeded.
Edited the READMEs and documentation to be cleaner and more consistent.
Bumped version number to 0.9.1 in preparation for release.
Tweaked manifest file. Removed a bunch of hackery from setup.py in favor of setuptools.find_packages, since we're using setuptools anyway. Addresses bug #177.
Abstracted email resetting during the email change process onto the AccountForm. Addresses bug #173.
This will include non *.py files in our distribution for setup.py, easy_install, pip, &c. purposes.
Removed python requirement line from setup.py, since easy_install and pip can't do anything with it and will choke on it for e.g. pypy.
Correction to waldo login view to pass the request into the login_form for GET requests as well.
Added release notes for 0.9.1.
Merged the contribution information from the ithinksw.org philo wiki into the documentation.
Removed philo.models.Tag entry from the docs.
Incremented version number to 0.9.1rc and cleaned up README a tiny bit.
Tweaked AttributeMapper._fill_cache to also store values on the Attribute instance itself. Made everything about AttributeMapper._fill_cache lazier.
Lazy-eval the values of AttributeValues instead of loading them all during AttributeMapper._fill_cache.
Removed Node.render_to_response select_related call since it is not clearly more efficient. Delayed page evaluation in FeedView.page_view to the end of the inner function.
Added a 'contributing' page to the philo docs. This is meant primarily as an initial effort, which can be expanded upon more later.
Corrections to Blog.entry_tags to use taggit APIs. Tweaks to penfield migration 0005 because of South issue 428 (and some incorrect filters obscured by that issue.)
Minor correction to EmbedWidget.js to handle window names with dashes.
Overrides TemplateField widget on admin container forms instead of on the ModelAdmin.
Reverted TemplateField parent to models.TextField and moved EmbedWidget into philo.admin.widgets. Added EmbedWidget use on the appropriate ModelAdmins. Cleaned up ContentletAdmin and ContentReferenceAdmin.
...
---
diff --git a/philo/LICENSE b/LICENSE
similarity index 94%
rename from philo/LICENSE
rename to LICENSE
index 61eafbd..78171e9 100644
--- a/philo/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2009-2010, iThink Software.
+Copyright (c) 2009-2011, iThink Software.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..0e076d9
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,12 @@
+include README
+include README.markdown
+include LICENSE
+include MANIFEST.in
+recursive-include philo/templates *.html
+recursive-include philo/contrib/sobol/templates *.html
+recursive-include philo/fixtures *.json
+recursive-include philo/static *.css *.js
+recursive-include philo/contrib/sobol/static *.css *.js
+recursive-include docs *.py *.rst *.bat *.txt Makefile
+global-exclude *~
+prune docs/_build
\ No newline at end of file
diff --git a/README b/README
index cb5f47a..abf3a35 100644
--- a/README
+++ b/README
@@ -1,27 +1,20 @@
-Philo is a foundation for developing web content management systems.
+Philo is a foundation for developing web content management systems.
Prerequisites:
* Python 2.5.4+
* Django 1.3+
* django-mptt e734079+
- * (Optional) django-grappelli 2.0+
- * (Optional) south 0.7.2+
- * (Optional) recaptcha-django r6
+ * (optional) django-grappelli 2.0+
+ * (optional) south 0.7.2+
+ * (philo.contrib.penfield) django-taggit 0.9.3+
+ * (philo.contrib.waldo, optional) recaptcha-django r6+
-To contribute, please visit the project website . Feel free to join us on IRC at irc://irc.oftc.net/#philo.
+After installing philo and mptt on your PYTHONPATH, make sure to complete the following steps:
-====
-Using philo
-====
-After installing philo and mptt on your python path, make sure to complete the following steps:
-
-1. add 'philo.middleware.RequestNodeMiddleware' to settings.MIDDLEWARE_CLASSES.
-2. add 'philo' and 'mptt' to settings.INSTALLED_APPS.
-3. include 'philo.urls' somewhere in your urls.py file.
+1. Add 'philo.middleware.RequestNodeMiddleware' to settings.MIDDLEWARE_CLASSES.
+2. Add 'philo' and 'mptt' to settings.INSTALLED_APPS.
+3. Include 'philo.urls' somewhere in your urls.py file.
4. Optionally add a root node to your current Site.
+5. (philo.contrib.gilbert) Add 'django.core.context_processors.request' to settings.TEMPLATE_CONTEXT_PROCESSORS.
-Philo should be ready to go!
-
-If you are using philo.contrib.gilbert, you will additionally need to complete the following steps:
-
-1. add 'django.core.context_processors.request' to settings.TEMPLATE_CONTEXT_PROCESSORS
+Philo should be ready to go! All that's left is to learn more and contribute .
diff --git a/README.markdown b/README.markdown
index b529ca2..30856dc 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,28 +1,21 @@
-Philo is a foundation for developing web content management systems.
+[Philo](http://philocms.org/) is a foundation for developing web content management systems.
Prerequisites:
- * [Python 2.5.4+ <http://www.python.org>](http://www.python.org/)
- * [Django 1.3+ <http://www.djangoproject.com/>](http://www.djangoproject.com/)
- * [django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>](https://github.com/django-mptt/django-mptt/)
- * (Optional) [django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>](http://code.google.com/p/django-grappelli/)
- * (Optional) [south 0.7.2+ <http://south.aeracode.org/)](http://south.aeracode.org/)
- * (Optional) [recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>](http://code.google.com/p/recaptcha-django/)
+ * [Python 2.5.4+](http://www.python.org/)
+ * [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) [south 0.7.2+](http://south.aeracode.org/)
+ * (philo.contrib.penfield) [django-taggit 0.9.3+](https://github.com/alex/django-taggit/)
+ * (philo.contrib.waldo, optional) [recaptcha-django r6+](http://code.google.com/p/recaptcha-django/)
-To contribute, please visit the [project website <http://philo.ithinksw.org/<](http://philo.ithinksw.org/). Feel free to join us on IRC at [irc://irc.oftc.net/#philo](irc://irc.oftc.net/#philo).
+After installing philo and mptt on your PYTHONPATH, make sure to complete the following steps:
-Using philo
-===========
-
-After installing philo and mptt on your python path, make sure to complete the following steps:
-
-1. add 'philo.middleware.RequestNodeMiddleware' to settings.MIDDLEWARE_CLASSES.
-2. add 'philo' and 'mptt' to settings.INSTALLED_APPS.
-3. include 'philo.urls' somewhere in your urls.py file.
+1. Add 'philo.middleware.RequestNodeMiddleware' to settings.MIDDLEWARE_CLASSES.
+2. Add 'philo' and 'mptt' to settings.INSTALLED_APPS.
+3. Include 'philo.urls' somewhere in your urls.py file.
4. Optionally add a root node to your current Site.
+5. (philo.contrib.gilbert) Add 'django.core.context_processors.request' to settings.TEMPLATE_CONTEXT_PROCESSORS.
-Philo should be ready to go!
-
-If you are using philo.contrib.gilbert, you will additionally need to complete the following steps:
-
-1. add 'django.core.context_processors.request' to settings.TEMPLATE_CONTEXT_PROCESSORS
+Philo should be ready to go! All that's left is to [learn more](http://docs.philocms.org/) and [contribute](http://docs.philocms.org/en/latest/contribute.html).
diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py
index 7710786..0d433de 100644
--- a/docs/_ext/djangodocs.py
+++ b/docs/_ext/djangodocs.py
@@ -32,16 +32,16 @@ def setup(app):
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 = "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",
diff --git a/docs/_ext/philodocs.py b/docs/_ext/philodocs.py
new file mode 100644
index 0000000..6c1ecf7
--- /dev/null
+++ b/docs/_ext/philodocs.py
@@ -0,0 +1,56 @@
+import inspect
+
+from sphinx.addnodes import desc_addname
+from sphinx.domains.python import PyModulelevel, PyXRefRole
+from sphinx.ext import autodoc
+
+
+DOMAIN = 'py'
+
+
+class TemplateTag(PyModulelevel):
+ indextemplate = "pair: %s; template tag"
+
+ def get_signature_prefix(self, sig):
+ return self.objtype + ' '
+
+ def handle_signature(self, sig, signode):
+ fullname, name_prefix = PyModulelevel.handle_signature(self, sig, signode)
+
+ for i, node in enumerate(signode):
+ if isinstance(node, desc_addname):
+ lib = '.'.join(node[0].split('.')[-2:])
+ new_node = desc_addname(lib, lib)
+ signode[i] = new_node
+
+ return fullname, name_prefix
+
+
+class TemplateTagDocumenter(autodoc.FunctionDocumenter):
+ objtype = 'templatetag'
+ domain = DOMAIN
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ # Only document explicitly.
+ return False
+
+ def format_args(self):
+ return None
+
+class TemplateFilterDocumenter(autodoc.FunctionDocumenter):
+ objtype = 'templatefilter'
+ domain = DOMAIN
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ # Only document explicitly.
+ return False
+
+def setup(app):
+ app.add_directive_to_domain(DOMAIN, 'templatetag', TemplateTag)
+ app.add_role_to_domain(DOMAIN, 'ttag', PyXRefRole())
+ app.add_directive_to_domain(DOMAIN, 'templatefilter', TemplateTag)
+ app.add_role_to_domain(DOMAIN, 'tfilter', PyXRefRole())
+ app.add_autodocumenter(TemplateTagDocumenter)
+ app.add_autodocumenter(TemplateFilterDocumenter)
\ No newline at end of file
diff --git a/docs/cla/ithinksw-ccla.txt b/docs/cla/ithinksw-ccla.txt
new file mode 100644
index 0000000..0e6b2ae
--- /dev/null
+++ b/docs/cla/ithinksw-ccla.txt
@@ -0,0 +1,140 @@
+ iThink Software
+ Corporate Contributor License Agreement ("Agreement") v1.0
+
+Thank you for your interest in iThink Software. In order to clarify
+the intellectual property license granted with Contributions from
+any person or entity, iThink Software must have a Contributor
+License Agreement ("CLA") on file that has been signed by each
+Contributor, indicating agreement to the license terms below. This
+license is for your protection as a Contributor as well as the
+protection of iThink Software and its users; it does not change
+your rights to use your own Contributions for any other purpose.
+
+This version of the Agreement allows an entity (the "Corporation")
+to submit Contributions to iThink Software, to authorize Contributions
+submitted by its designated employees to iThink Software, and to grant
+copyright and patent licenses thereto.
+
+If you have not already done so, please complete and sign, then scan
+and email a pdf file of this Agreement to contact@ithinksw.com.
+Alternatively, you may send an original signed Agreement to
+iThink Software, 261 West Lorain Street, Oberlin, OH 44074, U.S.A.
+Please read this document carefully before signing and
+keep a copy for your records.
+
+ Corporation name: ______________________________________________
+
+ Corporation address: ______________________________________________
+
+ ______________________________________________
+
+ ______________________________________________
+
+ Point of Contact: ______________________________________________
+
+ E-Mail: ______________________________________________
+
+ Telephone: ____________________ Fax: ____________________
+
+
+You accept and agree to the following terms and conditions for Your
+present and future Contributions submitted to iThink Software. Except
+for the license granted herein to iThink Software and recipients of
+software distributed by iThink Software, You reserve all right,
+title, and interest in and to Your Contributions.
+
+1. Definitions.
+
+ "You" (or "Your") shall mean the copyright owner or legal entity
+ authorized by the copyright owner that is making this Agreement
+ with iThink Software. For legal entities, the entity making a
+ Contribution and all other entities that control, are controlled
+ by, or are under common control with that entity are considered to
+ be a single Contributor. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "Contribution" shall mean any original work of authorship,
+ including any modifications or additions to an existing work, that
+ is intentionally submitted by You to iThink Software for inclusion
+ in, or documentation of, any of the products owned or managed by
+ iThink Software (the "Work"). For the purposes of this definition,
+ "submitted" means any form of electronic, verbal, or written
+ communication sent to iThink Software or its representatives,
+ including but not limited to communication on electronic mailing
+ lists, source code control systems, and issue tracking systems that
+ are managed by, or on behalf of, iThink Software for the purpose of
+ discussing and improving the Work, but excluding communication that
+ is conspicuously marked or otherwise designated in writing by You
+ as "Not a Contribution."
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this Agreement, You hereby grant to iThink Software and to
+ recipients of software distributed by iThink Software a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare derivative works of,
+ publicly display, publicly perform, sublicense, and distribute Your
+ Contributions and such derivative works.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this Agreement, You hereby grant to iThink Software and to
+ recipients of software distributed by iThink Software a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have
+ made, use, offer to sell, sell, import, and otherwise transfer the
+ Work, where such license applies only to those patent claims
+ licensable by You that are necessarily infringed by Your
+ Contribution(s) alone or by combination of Your Contribution(s)
+ with the Work to which such Contribution(s) were submitted. If any
+ entity institutes patent litigation against You or any other entity
+ (including a cross-claim or counterclaim in a lawsuit) alleging
+ that your Contribution, or the Work to which you have contributed,
+ constitutes direct or contributory patent infringement, then any
+ patent licenses granted to that entity under this Agreement for
+ that Contribution or Work shall terminate as of the date such
+ litigation is filed.
+
+4. You represent that You are legally entitled to grant the above
+ license. You represent further that each employee of the
+ Corporation designated on Schedule A below (or in a subsequent
+ written modification to that Schedule) is authorized to submit
+ Contributions on behalf of the Corporation.
+
+5. You represent that each of Your Contributions is Your original
+ creation (see section 7 for submissions on behalf of others).
+
+6. You are not expected to provide support for Your Contributions,
+ except to the extent You desire to provide support. You may provide
+ support for free, for a fee, or not at all. Unless required by
+ applicable law or agreed to in writing, You provide Your
+ Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ OF ANY KIND, either express or implied, including, without
+ limitation, any warranties or conditions of TITLE, NON-
+ INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
+
+7. Should You wish to submit work that is not Your original creation,
+ You may submit it to iThink Software separately from any
+ Contribution, identifying the complete details of its source and of
+ any license or other restriction (including, but not limited to,
+ related patents, trademarks, and license agreements) of which you
+ are personally aware, and conspicuously marking the work as
+ "Submitted on behalf of a third-party: [named here]".
+
+8. It is your responsibility to notify iThink Software when any change
+ is required to the list of designated employees authorized to submit
+ Contributions on behalf of the Corporation, or to the Corporation's
+ Point of Contact with iThink Software.
+
+
+Please sign: __________________________________ Date: _______________
+
+Title: __________________________________
+
+Corporation: __________________________________
+
+
+Schedule A
+
+ [Initial list of designated employees.]
diff --git a/docs/cla/ithinksw-icla.txt b/docs/cla/ithinksw-icla.txt
new file mode 100644
index 0000000..929452e
--- /dev/null
+++ b/docs/cla/ithinksw-icla.txt
@@ -0,0 +1,130 @@
+ iThink Software
+ Individual Contributor License Agreement ("Agreement") v1.0.1
+
+Thank you for your interest in iThink Software. In order to clarify
+the intellectual property license granted with Contributions from
+any person or entity, iThink Software must have a Contributor
+License Agreement ("CLA") on file that has been signed by each
+Contributor, indicating agreement to the license terms below. This
+license is for your protection as a Contributor as well as the
+protection of iThink Software and its users; it does not change
+your rights to use your own Contributions for any other purpose.
+If you have not already done so, please complete and sign, then scan
+and email a pdf file of this Agreement to contact@ithinksw.com.
+Alternatively, you may send an original signed Agreement to
+iThink Software, 261 West Lorain Street, Oberlin, OH 44074, U.S.A.
+Please read this document carefully before signing and
+keep a copy for your records.
+
+ Full name: ______________________________________________________
+
+ Mailing Address: ________________________________________________
+
+ _________________________________________________________________
+
+ Country: ______________________________________________________
+
+ Telephone: ______________________________________________________
+
+ Facsimile: ______________________________________________________
+
+ E-Mail: ______________________________________________________
+
+You accept and agree to the following terms and conditions for Your
+present and future Contributions submitted to iThink Software. Except
+for the license granted herein to iThink Software and recipients of
+software distributed by iThink Software, You reserve all right,
+title, and interest in and to Your Contributions.
+
+1. Definitions.
+
+ "You" (or "Your") shall mean the copyright owner or legal entity
+ authorized by the copyright owner that is making this Agreement
+ with iThink Software. For legal entities, the entity making a
+ Contribution and all other entities that control, are controlled
+ by, or are under common control with that entity are considered to
+ be a single Contributor. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "Contribution" shall mean any original work of authorship,
+ including any modifications or additions to an existing work, that
+ is intentionally submitted by You to iThink Software for inclusion
+ in, or documentation of, any of the products owned or managed by
+ iThink Software (the "Work"). For the purposes of this definition,
+ "submitted" means any form of electronic, verbal, or written
+ communication sent to iThink Software or its representatives,
+ including but not limited to communication on electronic mailing
+ lists, source code control systems, and issue tracking systems that
+ are managed by, or on behalf of, iThink Software for the purpose of
+ discussing and improving the Work, but excluding communication that
+ is conspicuously marked or otherwise designated in writing by You
+ as "Not a Contribution."
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this Agreement, You hereby grant to iThink Software and to
+ recipients of software distributed by iThink Software a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare derivative works of,
+ publicly display, publicly perform, sublicense, and distribute Your
+ Contributions and such derivative works.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this Agreement, You hereby grant to iThink Software and to
+ recipients of software distributed by iThink Software a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have
+ made, use, offer to sell, sell, import, and otherwise transfer the
+ Work, where such license applies only to those patent claims
+ licensable by You that are necessarily infringed by Your
+ Contribution(s) alone or by combination of Your Contribution(s)
+ with the Work to which such Contribution(s) were submitted. If any
+ entity institutes patent litigation against You or any other entity
+ (including a cross-claim or counterclaim in a lawsuit) alleging
+ that your Contribution, or the Work to which you have contributed,
+ constitutes direct or contributory patent infringement, then any
+ patent licenses granted to that entity under this Agreement for
+ that Contribution or Work shall terminate as of the date such
+ litigation is filed.
+
+4. You represent that you are legally entitled to grant the above
+ license. If your employer(s) has rights to intellectual property
+ that you create that includes your Contributions, you represent
+ that you have received permission to make Contributions on behalf
+ of that employer, that your employer has waived such rights for
+ your Contributions to iThink Software, or that your employer has
+ executed a separate Corporate CLA with iThink Software.
+
+5. You represent that each of Your Contributions is Your original
+ creation (see section 7 for submissions on behalf of others). You
+ represent that Your Contribution submissions include complete
+ details of any third-party license or other restriction (including,
+ but not limited to, related patents and trademarks) of which you
+ are personally aware and which are associated with any part of Your
+ Contributions.
+
+6. You are not expected to provide support for Your Contributions,
+ except to the extent You desire to provide support. You may provide
+ support for free, for a fee, or not at all. Unless required by
+ applicable law or agreed to in writing, You provide Your
+ Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ OF ANY KIND, either express or implied, including, without
+ limitation, any warranties or conditions of TITLE, NON-
+ INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
+
+7. Should You wish to submit work that is not Your original creation,
+ You may submit it to iThink Software separately from any
+ Contribution, identifying the complete details of its source and of
+ any license or other restriction (including, but not limited to,
+ related patents, trademarks, and license agreements) of which you
+ are personally aware, and conspicuously marking the work as
+ "Submitted on behalf of a third-party: [named here]".
+
+8. You agree to notify iThink Software of any facts or circumstances of
+ which you become aware that would make these representations
+ inaccurate in any respect.
+
+
+Please sign: __________________________________ Date: ________________
diff --git a/docs/conf.py b/docs/conf.py
index 043219d..2e703d0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -21,6 +21,16 @@ sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
os.environ['DJANGO_SETTINGS_MODULE'] = 'dummy-settings'
+# Import loader so that loader_tags will be correctly added to builtins. Weird import situations... this is necessary for doc build to work.
+from django.template import loader
+
+# HACK to override descriptors that would cause AttributeErrors to be raised otherwise (which would keep them from being documented.)
+from philo.contrib.sobol.models import SearchView
+SearchView.searches = 5
+from philo.models.nodes import TargetURLModel, File
+TargetURLModel.reversing_parameters = 5
+File.file = 5
+
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@@ -28,7 +38,7 @@ 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']
+extensions = ['djangodocs', 'sphinx.ext.autodoc', 'philodocs']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -44,7 +54,7 @@ master_doc = 'index'
# General information about the project.
project = u'Philo'
-copyright = u'2011, Joseph Spiros'
+copyright = u'2009-2011, iThink Software'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -52,7 +62,7 @@ copyright = u'2011, Joseph Spiros'
#
# The short X.Y version.
from philo import VERSION
-version = '%s.%s' % (VERSION[0], VERSION[1])
+version = '.'.join([str(v) for v in VERSION])
# The full version, including alpha/beta/rc tags.
release = version
@@ -91,6 +101,10 @@ pygments_style = 'sphinx'
#modindex_common_prefix = []
+# Autodoc config
+autodoc_member_order = "bysource"
+
+
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
@@ -183,7 +197,7 @@ htmlhelp_basename = 'Philodoc'
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Philo.tex', u'Philo Documentation',
- u'Stephen Burrows', 'manual'),
+ u'iThink Software', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -216,5 +230,14 @@ latex_documents = [
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'philo', u'Philo Documentation',
- [u'Stephen Burrows'], 1)
+ [u'iThink Software'], 1)
]
+
+def skip_attribute_attrs(app, what, name, obj, skip, options):
+ if name in ("attribute_set", "get_attribute_mapper", "nodes"):
+ return True
+ return skip
+
+def setup(app):
+ app.connect('autodoc-skip-member', skip_attribute_attrs)
+ #app.connect('autodoc-process-signature', )
diff --git a/docs/contrib/intro.rst b/docs/contrib/intro.rst
new file mode 100644
index 0000000..e833317
--- /dev/null
+++ b/docs/contrib/intro.rst
@@ -0,0 +1,14 @@
+Contrib apps
+============
+
+.. toctree::
+ :maxdepth: 2
+ :hidden:
+
+ penfield
+ shipherd
+ sobol
+ waldo
+ winer
+
+.. automodule:: philo.contrib
diff --git a/docs/contrib/penfield.rst b/docs/contrib/penfield.rst
new file mode 100644
index 0000000..87073b9
--- /dev/null
+++ b/docs/contrib/penfield.rst
@@ -0,0 +1,37 @@
+Penfield
+========
+
+.. automodule:: philo.contrib.penfield
+
+.. automodule:: philo.contrib.penfield.models
+
+Blogs
++++++
+.. autoclass:: philo.contrib.penfield.models.Blog
+ :members:
+
+.. autoclass:: philo.contrib.penfield.models.BlogEntry
+ :members:
+
+.. autoclass:: philo.contrib.penfield.models.BlogView
+ :members:
+
+Newsletters
++++++++++++
+.. autoclass:: philo.contrib.penfield.models.Newsletter
+ :members:
+
+.. autoclass:: philo.contrib.penfield.models.NewsletterArticle
+ :members:
+
+.. autoclass:: philo.contrib.penfield.models.NewsletterView
+ :members:
+
+Template filters
+++++++++++++++++
+
+.. automodule:: philo.contrib.penfield.templatetags.penfield
+
+.. autotemplatefilter:: monthname
+
+.. autotemplatefilter:: apmonthname
diff --git a/docs/contrib/shipherd.rst b/docs/contrib/shipherd.rst
new file mode 100644
index 0000000..9e03f67
--- /dev/null
+++ b/docs/contrib/shipherd.rst
@@ -0,0 +1,46 @@
+Shipherd
+========
+
+.. automodule:: philo.contrib.shipherd
+ :members:
+
+ :class:`.Node`\ s are useful for structuring a website; however, they are inherently unsuitable for creating site navigation.
+
+ The most glaring problem is that a navigation tree based on :class:`.Node`\ s would have one :class:`.Node` as the root, whereas navigation usually has multiple objects at the top level.
+
+ Additionally, navigation needs to have display text that is relevant to the current context; however, :class:`.Node`\ s do not have a field for that, and :class:`.View` subclasses with a name or title field will generally need to use it for database-searchable names.
+
+ Finally, :class:`.Node` structures are inherently unordered, while navigation is inherently ordered.
+
+ :mod:`~philo.contrib.shipherd` exists to resolve these issues by separating navigation structures from :class:`.Node` structures. It is instead structured around the way that site navigation works in the wild:
+
+ * A site may have one or more independent navigation bars (Main navigation, side navigation, etc.)
+ * A navigation bar may be shared by sections of the website, or even by the entire site.
+ * A navigation bar has a certain depth that it displays to.
+
+ The :class:`.Navigation` model supplies these features by attaching itself to a :class:`.Node` via :class:`ForeignKey` and adding a :attr:`navigation` property to :class:`.Node` which provides access to a :class:`.Node` instance's inherited :class:`.Navigation`\ s.
+
+ Each entry in the navigation bar is then represented by a :class:`.NavigationItem`, which stores information such as the :attr:`~.NavigationItem.order` and :attr:`~.NavigationItem.text` for the entry. Given an :class:`HttpRequest`, a :class:`.NavigationItem` can also tell whether it :meth:`~.NavigationItem.is_active` or :meth:`~.NavigationItem.has_active_descendants`.
+
+ Since the common pattern is to recurse through a navigation tree and render each part similarly, :mod:`~philo.contrib.shipherd` also ships with the :ttag:`~philo.contrib.shipherd.templatetags.shipherd.recursenavigation` template tag.
+
+Models
+++++++
+
+.. automodule:: philo.contrib.shipherd.models
+ :members: Navigation, NavigationItem, NavigationMapper
+ :show-inheritance:
+
+.. autoclass:: NavigationManager
+ :members:
+
+Template tags
++++++++++++++
+
+.. automodule:: philo.contrib.shipherd.templatetags.shipherd
+
+.. autotemplatetag:: recursenavigation
+
+.. autotemplatefilter:: has_navigation
+
+.. autotemplatefilter:: navigation_host
diff --git a/docs/contrib/sobol.rst b/docs/contrib/sobol.rst
new file mode 100644
index 0000000..353b547
--- /dev/null
+++ b/docs/contrib/sobol.rst
@@ -0,0 +1,17 @@
+Sobol
+=====
+
+.. automodule:: philo.contrib.sobol
+ :members:
+
+Models
+++++++
+
+.. automodule:: philo.contrib.sobol.models
+ :members:
+
+Search API
+++++++++++
+
+.. automodule:: philo.contrib.sobol.search
+ :members:
diff --git a/docs/contrib/waldo.rst b/docs/contrib/waldo.rst
new file mode 100644
index 0000000..89045d0
--- /dev/null
+++ b/docs/contrib/waldo.rst
@@ -0,0 +1,27 @@
+Waldo
+=====
+
+.. automodule:: philo.contrib.waldo
+ :members:
+
+Models
+++++++
+
+.. automodule:: philo.contrib.waldo.models
+ :members:
+
+Forms
++++++
+
+.. automodule:: philo.contrib.waldo.forms
+ :members:
+
+Token generators
+++++++++++++++++
+
+.. automodule:: philo.contrib.waldo.tokens
+
+
+.. autodata:: registration_token_generator
+
+.. autodata:: email_token_generator
diff --git a/docs/contrib/winer.rst b/docs/contrib/winer.rst
new file mode 100644
index 0000000..4b8a670
--- /dev/null
+++ b/docs/contrib/winer.rst
@@ -0,0 +1,15 @@
+Winer
+=====
+
+.. automodule:: philo.contrib.winer
+
+.. automodule:: philo.contrib.winer.models
+
+ .. autoclass:: FeedView
+ :members:
+
+.. automodule:: philo.contrib.winer.exceptions
+ :members:
+
+.. automodule:: philo.contrib.winer.middleware
+ :members:
\ No newline at end of file
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 100644
index 0000000..4c9fb7d
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1,34 @@
+Contributing to Philo
+=====================
+
+So you want to contribute to Philo? That's great! Here's some ways you can get started:
+
+* **Report bugs and request features** using the issue tracker at the `project site `_.
+* **Contribute code** using `git `_. You can fork philo's repository either on `GitHub `_ or `Gitorious `_. If you are contributing to Philo, you will need to submit a :ref:`Contributor License Agreement `.
+* **Join the discussion** on IRC at `irc://irc.oftc.net/#philo `_ if you have any questions or suggestions or just want to chat about the project. You can also keep in touch using the project mailing lists: `philo@ithinksw.org `_ and `philo-devel@ithinksw.org `_.
+
+
+Branches and Code Style
++++++++++++++++++++++++
+
+We use `A successful Git branching model`__ with the blessed repository. To make things easier, you probably should too. This means that you should work on and against the develop branch in most cases, and leave it to the release manager to create the commits on the master branch if and when necessary. When pulling changes into the blessed repository at your request, the release manager will usually merge them into the develop branch unless you explicitly note they be treated otherwise.
+
+__ http://nvie.com/posts/a-successful-git-branching-model/
+
+Philo adheres to PEP8 for its code style, with two exceptions: tabs are used rather than spaces, and lines are not truncated at 79 characters.
+
+.. _cla:
+
+Licensing and Legal
++++++++++++++++++++
+
+In order for the release manager to merge your changes into the blessed repository, you will need to have already submitted a signed CLA. Our CLAs are based on the Apache Software Foundation's CLAs, which is the same source as the `Django Project's CLAs`_. You might, therefore, find the `Django Project's CLA FAQ`_. helpful.
+
+.. _`Django Project's CLAs`: https://www.djangoproject.com/foundation/cla/
+.. _`Django Project's CLA FAQ`: https://www.djangoproject.com/foundation/cla/faq/
+
+If you are an individual not doing work for an employer, then you can simply submit the :download:`Individual CLA `.
+
+If you are doing work for an employer, they will need to submit the :download:`Corporate CLA ` and you will need to submit the Individual CLA :download:`Individual CLA ` as well.
+
+Both documents include information on how to submit them.
diff --git a/docs/dummy-settings.py b/docs/dummy-settings.py
index e69de29..7e424ab 100644
--- a/docs/dummy-settings.py
+++ b/docs/dummy-settings.py
@@ -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
index 0000000..679ac77
--- /dev/null
+++ b/docs/exceptions.rst
@@ -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/forms.rst b/docs/forms.rst
new file mode 100644
index 0000000..b2dfbb4
--- /dev/null
+++ b/docs/forms.rst
@@ -0,0 +1,12 @@
+Forms
+=====
+
+.. automodule:: philo.forms.entities
+ :members:
+
+
+Fields
+++++++
+
+.. automodule:: philo.forms.fields
+ :members:
diff --git a/docs/handling_requests.rst b/docs/handling_requests.rst
new file mode 100644
index 0000000..940d541
--- /dev/null
+++ b/docs/handling_requests.rst
@@ -0,0 +1,10 @@
+Handling Requests
+=================
+
+.. automodule:: philo.middleware
+ :members:
+
+.. automodule:: philo.views
+
+
+.. autofunction:: node_view(request[, path=None, **kwargs])
diff --git a/docs/index.rst b/docs/index.rst
index cfc7136..05422dd 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,36 +3,46 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
+.. module:: philo
+
Welcome to Philo's documentation!
=================================
-Contents:
+Philo is a foundation for developing web content management systems. Please, read the :doc:`notes for our latest release `.
+
+Prerequisites:
+
+* `Python 2.5.4+ `_
+* `Django 1.3+ `_
+* `django-mptt e734079+ `_
+* (optional) `django-grappelli 2.0+ `_
+* (optional) `south 0.7.2+ `_
+* (:mod:`philo.contrib.penfield`) `django-taggit 0.9.3+ `_
+* (:mod:`philo.contrib.waldo`, optional) `recaptcha-django r6+ `_
+
+Contents
+++++++++
.. toctree::
- :maxdepth: 2
-
- intro
- models/intro
+ :maxdepth: 1
+
+ what
+ tutorials/intro
+ models/intro
+ exceptions
+ handling_requests
+ signals
+ validators
+ utilities
+ templatetags
+ forms
+ loaders
+ contrib/intro
+ contributing
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+ `_
-* `Django 1.2+ `_
-* `django-mptt e734079+ `_
-* (Optional) `django-grappelli 2.0+ `_
-* (Optional) `south 0.7.2+ `_
-* (Optional) `recaptcha-django r6 `_
-
-To contribute, please visit the `project website `_ or make a fork of the `git repository `_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo `_.
diff --git a/docs/intro.rst b/docs/intro.rst
deleted file mode 100644
index 33d1a98..0000000
--- a/docs/intro.rst
+++ /dev/null
@@ -1,35 +0,0 @@
-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 ` 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/loaders.rst b/docs/loaders.rst
new file mode 100644
index 0000000..41c4cd9
--- /dev/null
+++ b/docs/loaders.rst
@@ -0,0 +1,5 @@
+Database Template Loader
+========================
+
+.. automodule:: philo.loaders.database
+ :members:
diff --git a/docs/models/collections.rst b/docs/models/collections.rst
new file mode 100644
index 0000000..0519494
--- /dev/null
+++ b/docs/models/collections.rst
@@ -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
index a834b13..b39a253 100644
--- a/docs/models/entities.rst
+++ b/docs/models/entities.rst
@@ -10,25 +10,27 @@ Attributes
----------
.. autoclass:: Attribute
- :members:
+ :members:
.. autoclass:: AttributeValue
- :members:
+ :members:
.. automodule:: philo.models.base
- :members: attribute_value_limiter
+ :noindex:
+ :members: attribute_value_limiter
.. autoclass:: JSONValue
- :show-inheritance:
+ :show-inheritance:
.. autoclass:: ForeignKeyValue
- :show-inheritance:
+ :show-inheritance:
.. autoclass:: ManyToManyValue
- :show-inheritance:
+ :show-inheritance:
.. automodule:: philo.models.base
- :members: value_content_type_limiter
+ :noindex:
+ :members: value_content_type_limiter
.. autofunction:: register_value_model(model)
.. autofunction:: unregister_value_model(model)
@@ -37,18 +39,17 @@ Entities
--------
.. autoclass:: Entity
- :members:
- :exclude-members: attribute_set
+ :members:
-.. autoclass:: TreeManager
- :members:
+.. autoclass:: TreeEntityManager
+ :members:
.. autoclass:: TreeEntity
- :members:
- :exclude-members: attribute_set
+ :show-inheritance:
+ :members:
- .. attribute:: objects
+ .. attribute:: objects
- An instance of :class:`TreeManager`.
-
- .. automethod:: get_path
\ No newline at end of file
+ An instance of :class:`TreeEntityManager`.
+
+ .. 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
index 0000000..3092fa4
--- /dev/null
+++ b/docs/models/fields.rst
@@ -0,0 +1,21 @@
+Custom Fields
+=============
+
+.. automodule:: philo.models.fields
+ :members:
+ :exclude-members: JSONField, SlugMultipleChoiceField
+
+ .. autoclass:: JSONField()
+ :members:
+
+ .. autoclass:: SlugMultipleChoiceField()
+ :members:
+
+AttributeProxyFields
+--------------------
+
+.. automodule:: philo.models.fields.entities
+ :members:
+
+ .. autoclass:: AttributeProxyField(attribute_key=None, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs)
+ :members:
\ No newline at end of file
diff --git a/docs/models/intro.rst b/docs/models/intro.rst
index 49b2ac1..4f65585 100644
--- a/docs/models/intro.rst
+++ b/docs/models/intro.rst
@@ -8,6 +8,9 @@ Contents:
entities
nodes-and-views
+ collections
+ miscellaneous
+ fields
-.. :module: philo.models
+.. automodule:: philo.models
diff --git a/docs/models/miscellaneous.rst b/docs/models/miscellaneous.rst
new file mode 100644
index 0000000..005e112
--- /dev/null
+++ b/docs/models/miscellaneous.rst
@@ -0,0 +1,5 @@
+Miscellaneous Models
+=============================
+.. autoclass:: philo.models.nodes.TargetURLModel
+ :members:
+ :exclude-members: get_target_url
\ No newline at end of file
diff --git a/docs/models/nodes-and-views.rst b/docs/models/nodes-and-views.rst
index bd31ceb..442509d 100644
--- a/docs/models/nodes-and-views.rst
+++ b/docs/models/nodes-and-views.rst
@@ -1,270 +1,56 @@
Nodes and Views: Building Website structure
===========================================
-.. currentmodule:: philo.models
+.. automodule:: philo.models.nodes
Nodes
-----
-: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.
-
-.. class:: Node
-
- :class:`!Node` subclasses :class:`TreeEntity`. It defines the following additional methods and attributes:
-
- .. attribute:: view
-
- :class:`GenericForeignKey` to a non-abstract subclass of :class:`View`
-
- .. attribute:: accepts_subpath
-
- A property shortcut for :attr:`self.view.accepts_subpath `
-
- .. method:: render_to_response(request[, extra_context=None])
-
- This is a shortcut method for :meth:`View.render_to_response`
-
- .. method:: get_absolute_url([request=None, with_domain=False, secure=False])
-
- This is essentially a shortcut for calling :meth:`construct_url` without a subpath - which will return the URL of the Node.
-
- .. method:: construct_url([subpath="/", request=None, with_domain=False, secure=False])
-
- 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 ` if with_domain is True but no :class:`Site` or :class:`RequestSite` can be built.
- - :class:`AncestorDoesNotExist` if the root node of the site isn't an ancestor of the node constructing the URL.
+.. autoclass:: Node
+ :show-inheritance:
+ :members:
Views
-----
Abstract View Models
++++++++++++++++++++
-.. class:: View
-
- :class:`!View` is an abstract model that represents an item which can be "rendered", either in response to an :class:`HttpRequest` or as a standalone. It subclasses :class:`Entity`, and defines the following additional methods and attributes:
-
- .. attribute:: accepts_subpath
-
- Defines whether this :class:`View` can handle subpaths. Default: ``False``
-
- .. method:: handles_subpath(subpath)
-
- Returns True if the the :class:`View` handles the given subpath, and False otherwise.
-
- .. attribute:: nodes
-
- A generic relation back to nodes.
-
- .. method:: reverse([view_name=None, args=None, kwargs=None, node=None, obj=None])
-
- 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.
-
- This method will raise the following exceptions:
-
- - :class:`ViewDoesNotProvideSubpaths` if :attr:`accepts_subpath` is False.
- - :class:`ViewCanNotProvideSubpath` if a reversal is not possible.
-
- .. method:: get_reverse_params(obj)
-
- 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:`ViewCanNotProvideSubpath`.
- .. method:: attributes_with_node(node)
+.. autoclass:: View
+ :show-inheritance:
+ :members:
- Returns a :class:`QuerySetMapper` using the :class:`node `'s attributes as a passthrough.
-
- .. method:: render_to_response(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 :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 :obj:`view_about_to_render ` signal, then call :meth:`actually_render_to_response`, and finally send the :obj:`view_finished_rendering ` signal before returning the ``response``.
-
- .. method:: actually_render_to_response(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`.
-
-.. class:: MultiView
-
- :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:
-
- .. attribute:: accepts_subpath
-
- Same as :attr:`View.accepts_subpath`. Default: ``True``
-
- .. attribute:: urlpatterns
-
- Returns urlpatterns that point to views (generally methods on the class). :class:`!MultiView`\ s can be thought of as "managing" these subpaths.
-
- .. method:: actually_render_to_response(request[, extra_context=None])
-
- Resolves the remaining subpath left after finding this :class:`View`'s node using :attr:`self.urlpatterns ` and renders the view function (or method) found with the appropriate args and kwargs.
-
- .. method:: get_context()
-
- Hook for providing instance-specific context - such as the value of a Field - to all views.
-
- .. method:: basic_view(field_name)
-
- Given the name of a field on ``self``, accesses the value of that field and treats it as a :class:`View` instance. Creates a basic context based on :meth:`get_context` and any extra_context that was passed in, then calls the :class:`View` instance's :meth:`~View.render_to_response` method. This method is meant to be called to return a view function appropriate for :attr:`urlpatterns`.
+.. autoclass:: MultiView
+ :show-inheritance:
+ :members:
Concrete View Subclasses
++++++++++++++++++++++++
-.. class:: Redirect
-
- A :class:`View` subclass. Defines a 301 or 302 redirect to a different url on an absolute or relative path.
-
- .. attribute:: STATUS_CODES
-
- A choices tuple of redirect status codes (temporary or permanent).
-
- .. attribute:: status_code
-
- An :class:`IntegerField` which uses :attr:`STATUS_CODES` as its choices. Determines whether the redirect is considered temporary or permanent.
-
- .. attribute:: target_node
-
- An optional :class:`ForeignKey` to a :class:`Node`. If provided, that node will be used as the basis for the redirect.
-
- .. attribute:: url_or_subpath
-
- A :class:`CharField` which may contain an absolute or relative URL. This will be validated with :class:`philo.validators.RedirectValidator`.
-
- .. attribute:: reversing_parameters
-
- 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.
+.. autoclass:: Redirect
+ :show-inheritance:
+ :members:
- .. attribute:: target_url
-
- Calculates and returns the target url based on the :attr:`target_node`, :attr:`url_or_subpath`, and :attr:`reversing_parameters`.
-
- .. method:: actually_render_to_response(request[, extra_context=None])
-
- Returns an :class:`HttpResponseRedirect` to :attr:`self.target`.
-
-.. class:: File
-
- A :class:`View` subclass. Stores an arbitrary file.
-
- .. attribute:: mimetype
-
- Defines the mimetype of the uploaded file. This will not be validated.
-
- .. attribute:: file
-
- Contains the uploaded file. Files are uploaded to ``philo/files/%Y/%m/%d``.
-
- .. method:: __unicode__()
-
- Returns the name of :attr:`self.file `.
+.. autoclass:: File
+ :show-inheritance:
+ :members:
Pages
*****
-: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` tags, which define related :class:`Contentlet`\ s and :class:`ContentReference`\ s for any page using that :class:`Template`.
-
-.. class:: Page
-
- A :class:`View` subclass. 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.
-
- .. attribute:: template
-
- A :class:`ForeignKey` to the :class:`Template` used to render this :class:`Page`.
-
- .. attribute:: title
-
- 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.
-
- .. attribute:: containers
-
- Returns :attr:`self.template.containers ` - a tuple containing the specs of all :ttag:`container`\ s defined in the :class:`Template`. The value will be cached on the instance so that multiple accesses will be less expensive.
-
- .. method:: render_to_string([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-related content with the same :ttag:`container`-based functionality as is used for HTML.
-
- .. method:: actually_render_to_response(request[, extra_context=None])
-
- Returns an :class:`HttpResponse` with the content of the :meth:`render_to_string` method and the mimetype set to :attr:`self.template.mimetype `.
-
- .. 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.
-
- .. method:: __unicode__()
-
- Returns :meth:`self.title `
-
-.. class:: Template
-
- Subclasses :class:`TreeModel`. Represents a database-driven django template. Defines the following additional methods and attributes:
-
- .. attribute:: name
-
- The name of the template. Used for organization and debugging.
-
- .. attribute:: documentation
-
- Can be used to let users know what the template is meant to be used for.
-
- .. attribute:: mimetype
-
- Defines the mimetype of the template. This is not validated. Default: ``text/html``.
-
- .. attribute:: code
-
- An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template.
-
- .. attribute:: containers
-
- 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.
-
- .. method:: __unicode__()
-
- Returns the results of the :meth:`~TreeModel.get_path` method, using the "name" field and a chevron joiner.
-
-.. class:: Contentlet
-
- Defines a piece of content on a page. This content is treated as a secure :class:`~philo.models.fields.TemplateField`.
-
- .. attribute:: page
-
- The page which this :class:`Contentlet` is related to.
-
- .. attribute:: name
-
- This represents the name of the container as defined by a :ttag:`container` tag.
-
- .. attribute:: content
-
- 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` template tag.
-
- .. method:: __unicode__()
-
- Returns :attr:`self.name `
-
-.. class:: ContentReference
-
- Defines a model instance related to a page.
-
- .. attribute:: page
-
- The page which this :class:`ContentReference` is related to.
-
- .. attribute:: name
-
- This represents the name of the container as defined by a :ttag:`container` tag.
+.. automodule:: philo.models.pages
- .. attribute:: content
+.. autoclass:: Page
+ :members:
+ :show-inheritance:
- A :class:`GenericForeignKey` to a model instance. The content type of this instance is defined by the :ttag:`container` tag which defines this :class:`ContentReference`.
+.. autoclass:: Template
+ :members:
+ :show-inheritance:
+
+ .. seealso:: :mod:`philo.loaders.database`
- .. method:: __unicode__()
+.. autoclass:: Contentlet
+ :members:
- Returns :attr:`self.name `
\ No newline at end of file
+.. autoclass:: ContentReference
+ :members:
\ No newline at end of file
diff --git a/docs/releases/0.9.1.rst b/docs/releases/0.9.1.rst
new file mode 100644
index 0000000..2003350
--- /dev/null
+++ b/docs/releases/0.9.1.rst
@@ -0,0 +1,13 @@
+Philo version 0.9.1 release notes
+=================================
+
+The primary focus of the 0.9.1 release has been streamlining and optimization. Requests in 0.9.1 are served two to three times faster than in 0.9. A number of bugs in code, documentation, and migrations have also been corrected.
+
+New Features and backwards-incompatible changes
++++++++++++++++++++++++++++++++++++++++++++++++
+
+* :class:`.FeedView` and related syndication code has been migrated to :mod:`philo.contrib.winer` so it can be used independently of :mod:`philo.contrib.penfield`.
+* :class:`.FeedView` has been refactored; the result of :meth:`.FeedView.get_object` is now passed into :meth:`.FeedView.get_items` to allow for more flexibility and for :class:`.FeedView`\ s which do not have a :class:`ForeignKey` relationship to the items that the feed is for.
+* :class:`.BlogView` has been refactored to take advantage of the more flexible :meth:`~.BlogView.get_object` method. Many of its former entry-fetching methods have been removed.
+* :class:`.EmbedWidget` is now used for text fields on, for example, :class:`BlogEntry`. The widget allows javascript-based generation of embed tags for model instances, using the same popup interface as raw id fields.
+* :class:`philo.models.Tag` has been removed in favor of an optional requirement for ``django-taggit``. This will allow :mod:`philo` to remain more focused. Migrations are provided for :mod:`philo.contrib.penfield` which losslessly convert :mod:`philo` :class:`~philo.models.Tag`\ s to ``django-taggit`` :class:`Tags`.
diff --git a/docs/signals.rst b/docs/signals.rst
new file mode 100644
index 0000000..8b3da3c
--- /dev/null
+++ b/docs/signals.rst
@@ -0,0 +1,5 @@
+Signals
+=======
+
+.. automodule:: philo.signals
+ :members:
diff --git a/docs/templatetags.rst b/docs/templatetags.rst
new file mode 100644
index 0000000..41d30d5
--- /dev/null
+++ b/docs/templatetags.rst
@@ -0,0 +1,42 @@
+Template Tags
+=============
+
+.. automodule:: philo.templatetags
+
+Collections
++++++++++++
+
+.. automodule:: philo.templatetags.collections
+
+.. autotemplatetag:: membersof
+
+Containers
+++++++++++
+
+.. automodule:: philo.templatetags.containers
+
+
+.. autotemplatetag:: container
+
+
+Embedding
++++++++++
+
+.. automodule:: philo.templatetags.embed
+
+.. autotemplatetag:: embed
+
+
+Nodes
++++++
+
+.. automodule:: philo.templatetags.nodes
+
+.. autotemplatetag:: node_url
+
+String inclusion
+++++++++++++++++
+
+.. automodule:: philo.templatetags.include_string
+
+.. autotemplatetag:: include_string
diff --git a/docs/tutorials/getting-started.rst b/docs/tutorials/getting-started.rst
new file mode 100644
index 0000000..11eb927
--- /dev/null
+++ b/docs/tutorials/getting-started.rst
@@ -0,0 +1,89 @@
+Getting started with philo
+==========================
+
+.. note:: This guide assumes that you have worked with Django's built-in administrative interface.
+
+Once you've installed `philo`_ and `mptt`_ to your python path, there are only a few things that you need to do to get :mod:`philo` working.
+
+1. Add :mod:`philo` and :mod:`mptt` to :setting:`settings.INSTALLED_APPS`::
+
+ INSTALLED_APPS = (
+ ...
+ 'philo',
+ 'mptt',
+ ...
+ )
+
+2. Syncdb or run migrations to set up your database.
+
+3. Add :class:`philo.middleware.RequestNodeMiddleware` to :setting:`settings.MIDDLEWARE_CLASSES`::
+
+ MIDDLEWARE_CLASSES = (
+ ...
+ 'philo.middleware.RequestNodeMiddleware',
+ ...
+ )
+
+4. 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')),
+ )
+
+Philo should be ready to go! (Almost.)
+
+.. _philo: http://philocms.org/
+.. _mptt: http://github.com/django-mptt/django-mptt
+
+Hello world
++++++++++++
+
+Now that you've got everything configured, it's time to set up your first page! Easy peasy. Open up the admin and add a new :class:`.Template`. Call it "Hello World Template". The code can be something like this::
+
+
+
+ Hello world!
+
+
+
Hello world!
+
The time is {% now %}.
+
+
+
+Next, add a philo :class:`.Page` - let's call it "Hello World Page" and use the template you just made.
+
+Now make a philo :class:`.Node`. Give it the slug ``hello-world``. Set the ``view_content_type`` to "Page" and the ``view_object_id`` to the id of the page that you just made - probably 1. If you navigate to ``/hello-world``, you will see the results of rendering the page!
+
+Setting the root node
++++++++++++++++++++++
+
+So what's at ``/``? If you try to load it, you'll get a 404 error. This is because there's no :class:`.Node` located there - and since :attr:`.Node.slug` is a required field, getting a node there is not as simple as leaving the :attr:`.~Node.slug` blank.
+
+In :mod:`philo`, the node that is displayed at ``/`` is called the "root node" of the current :class:`Site`. To represent this idea cleanly in the database, :mod:`philo` adds a :class:`ForeignKey` to :class:`.Node` to the :class:`django.contrib.sites.models.Site` model.
+
+Since there's only one :class:`.Node` in your :class:`Site`, we probably want ``hello-world`` to be the root node. All you have to do is edit the current :class:`Site` and set its root node to ``hello-world``. Now you can see the page rendered at ``/``!
+
+Editing page contents
++++++++++++++++++++++
+
+Great! We've got a page that says "Hello World". But what if we want it to say something else? Should we really have to edit the :class:`.Template` to change the content of the :class:`.Page`? And what if we want to share the :class:`.Template` but have different content? Adjust the :class:`.Template` to look like this::
+
+
+
+ {% container page_title %}
+
+
+ {% container page_body as content %}
+ {% if content %}
+
{{ content }}
+ {% endif %}
+
The time is {% now "jS F Y H:i" %}.
+
+
+
+Now go edit your :class:`.Page`. Two new fields called "Page title" and "Page body" have shown up! You can put anything you like in here and have it show up in the appropriate places when the page is rendered.
+
+.. seealso:: :ttag:`philo.templatetags.containers.container`
+
+Congrats! You've done it!
diff --git a/docs/tutorials/intro.rst b/docs/tutorials/intro.rst
new file mode 100644
index 0000000..c7d3e99
--- /dev/null
+++ b/docs/tutorials/intro.rst
@@ -0,0 +1,8 @@
+Tutorials
+=========
+
+.. toctree::
+ :maxdepth: 1
+
+ getting-started
+ shipherd
diff --git a/docs/tutorials/shipherd.rst b/docs/tutorials/shipherd.rst
new file mode 100644
index 0000000..914a6bb
--- /dev/null
+++ b/docs/tutorials/shipherd.rst
@@ -0,0 +1,63 @@
+Using Shipherd in the Admin
+===========================
+
+The navigation mechanism is fairly complex; unfortunately, there's no real way around that - without a lot of equally complex code that you are quite welcome to write and contribute! ;-)
+
+For this guide, we'll assume that you have the setup described in :doc:`getting-started`. We'll be adding a main :class:`.Navigation` to the root :class:`.Node` and making it display as part of the :class:`.Template`.
+
+Before getting started, make sure that you've added :mod:`philo.contrib.shipherd` to your :setting:`INSTALLED_APPS`. :mod:`~philo.contrib.shipherd` template tags also require the request context processor, so make sure to set :setting:`TEMPLATE_CONTEXT_PROCESSORS` appropriately::
+
+ TEMPLATE_CONTEXT_PROCESSORS = (
+ # Defaults
+ "django.contrib.auth.context_processors.auth",
+ "django.core.context_processors.debug",
+ "django.core.context_processors.i18n",
+ "django.core.context_processors.media",
+ "django.core.context_processors.static",
+ "django.contrib.messages.context_processors.messages"
+ ...
+ "django.core.context_processors.request"
+ )
+
+Creating the Navigation
++++++++++++++++++++++++
+
+Start off by adding a new :class:`.Navigation` instance with :attr:`~.Navigation.node` set to the good ole' ``root`` node and :attr:`~.Navigation.key` set to ``main``. The default :attr:`~.Navigation.depth` of 3 is fine.
+
+Now open up that first inline :class:`.NavigationItem`. Make the text ``Hello World`` and set the target :class:`.Node` to, again, ``root``. (Of course, this is a special case. If we had another node that we wanted to point to, we would choose that.)
+
+Press save and you've created your first navigation.
+
+Displaying the Navigation
++++++++++++++++++++++++++
+
+All you need to do now is show the navigation in the template! This is quite easy, using the :ttag:`~philo.contrib.shipherd.templatetags.shipherd.recursenavigation` templatetag. For now we'll keep it simple. Adjust the "Hello World Template" to look like this::
+
+ {% load shipherd %}
+
+ {% container page_title %}
+
+
+
+ {% container page_body as content %}
+ {% if content %}
+
{{ content }}
+ {% endif %}
+
The time is {% now %}.
+
+
+
+Now have a look at the page - your navigation is there!
+
+Linking to google
++++++++++++++++++
+
+Edit the ``main`` :class:`.Navigation` again to add another :class:`.NavigationItem`. This time give it the :attr:`~.NavigationItem.text` ``Google`` and set the :attr:`~.TargetURLModel.url_or_subpath` field to ``http://google.com``. A navigation item will show up on the Hello World page that points to ``google.com``! Granted, your navigation probably shouldn't do that, because confusing navigation is confusing; the point is that it is possible to provide navigation to arbitrary URLs.
+
+:attr:`~.TargetURLModel.url_or_subpath` can also be used in conjuction with a :class:`.Node` to link to a subpath beyond that :class:`.Node`'s url.
diff --git a/docs/utilities.rst b/docs/utilities.rst
new file mode 100644
index 0000000..d1386b1
--- /dev/null
+++ b/docs/utilities.rst
@@ -0,0 +1,39 @@
+Utilities
+=========
+
+.. automodule:: philo.utils
+ :members:
+
+AttributeMappers
+++++++++++++++++
+
+.. module:: philo.utils.entities
+
+.. autoclass:: AttributeMapper
+ :members:
+
+.. autoclass:: TreeAttributeMapper
+ :members:
+ :show-inheritance:
+
+.. autoclass:: PassthroughAttributeMapper
+ :members:
+ :show-inheritance:
+
+LazyAttributeMappers
+--------------------
+
+.. autoclass:: LazyAttributeMapperMixin
+ :members:
+
+.. autoclass:: LazyAttributeMapper
+ :members:
+ :show-inheritance:
+
+.. autoclass:: LazyTreeAttributeMapper
+ :members:
+ :show-inheritance:
+
+.. autoclass:: LazyPassthroughAttributeMapper
+ :members:
+ :show-inheritance:
diff --git a/docs/validators.rst b/docs/validators.rst
new file mode 100644
index 0000000..f91818b
--- /dev/null
+++ b/docs/validators.rst
@@ -0,0 +1,5 @@
+Validators
+==========
+
+.. automodule:: philo.validators
+ :members:
diff --git a/docs/what.rst b/docs/what.rst
new file mode 100644
index 0000000..efa8537
--- /dev/null
+++ b/docs/what.rst
@@ -0,0 +1,25 @@
+What is Philo, anyway?
+======================
+
+Philo allows the creation of site structures using Django's built-in admin interface. Like Django, Philo separates URL structure from backend code from display:
+
+* :class:`.Node`\ s represent the URL hierarchy of the website.
+* :class:`.View`\ s contain the logic for each :class:`.Node`, as simple as a :class:`.Redirect` or as complex as a :class:`.Blog`.
+* :class:`.Page`\ s (the most commonly used :class:`.View`) render whatever context they are passed using database-driven :class:`.Template`\ s written with Django's template language.
+* :class:`.Attribute`\ s are arbitrary key/value pairs which can be attached to most of the models that Philo provides. Attributes of a :class:`.Node` will be inherited by all of the :class:`.Node`'s descendants and will be available in the template's context.
+
+The :ttag:`~philo.templatetags.containers.container` template tag that Philo provides makes it easy to mark areas in a template which need to be editable page-by-page; every :class:`.Page` will have an additional field in the admin for each :ttag:`~philo.templatetags.containers.container` in the template it uses.
+
+How's that different than other CMSes?
+++++++++++++++++++++++++++++++++++++++
+
+Philo developed according to principles that grew out of the observation of the limitations and practices of other content management systems. For example, Philo believes that:
+
+* Designers are in charge of how content is displayed, not end users. For example, users should be able to embed images in blog entries -- but the display of the image, even the presence or absence of a wrapping ``
";
+ };
+ $(sobol.search);
+}(jQuery));
\ No newline at end of file
diff --git a/philo/contrib/sobol/templates/admin/sobol/search/change_form.html b/philo/contrib/sobol/templates/admin/sobol/search/change_form.html
new file mode 100644
index 0000000..8dfba08
--- /dev/null
+++ b/philo/contrib/sobol/templates/admin/sobol/search/change_form.html
@@ -0,0 +1,43 @@
+{% extends 'admin/change_form.html' %}
+{% load i18n %}
+
+{% block javascripts %}{% endblock %}
+{% block object-tools %}{% endblock %}
+{% block title %}Results for "{{ original.string }}" | {% trans 'Django site admin' %}{% endblock %}
+{% block content_title %}
-
\ No newline at end of file
diff --git a/philo/contrib/sobol/templates/sobol/search/_list.html b/philo/contrib/sobol/templates/sobol/search/_list.html
new file mode 100644
index 0000000..99db761
--- /dev/null
+++ b/philo/contrib/sobol/templates/sobol/search/_list.html
@@ -0,0 +1,56 @@
+{% with node.view.enable_ajax_api as ajax %}
+{% if ajax %}
+ {% if not suppress_scripts %}{% endif %}
+
+{% endif %}
+{% if favored_results %}
+
+
+
Favored results
+
+ {% if not ajax %}
+
+ {% for search in searches %}
+ {% for result in search.results %}
+ {% if result.get_actual_url in favored_results %}
+ {{ result }}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ {% if search.get_actual_more_results_url in favored_results %}
+
{% endif %}
\ No newline at end of file
diff --git a/philo/contrib/sobol/utils.py b/philo/contrib/sobol/utils.py
index 3c5e537..6fd5a49 100644
--- a/philo/contrib/sobol/utils.py
+++ b/philo/contrib/sobol/utils.py
@@ -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'
@@ -11,22 +12,40 @@ HASH_REDIRECT_GET_KEY = 's'
def make_redirect_hash(search_arg, url):
+ """Hashes a redirect for a ``search_arg`` and ``url`` to avoid providing a simple URL spoofing service."""
return sha1(smart_str(search_arg + url + settings.SECRET_KEY)).hexdigest()[::2]
def check_redirect_hash(hash, search_arg, url):
+ """Checks whether a hash is valid for a given ``search_arg`` and ``url``."""
return hash == make_redirect_hash(search_arg, url)
def make_tracking_querydict(search_arg, url):
- """
- Returns a QueryDict instance containing the information necessary
- for tracking clicks of this url.
-
- NOTE: will this kind of initialization handle quoting correctly?
- """
+ """Returns a :class:`QueryDict` instance containing the information necessary for tracking :class:`.Click`\ s on the ``url``."""
return QueryDict("%s=%s&%s=%s&%s=%s" % (
SEARCH_ARG_GET_KEY, urlquote_plus(search_arg),
URL_REDIRECT_GET_KEY, urlquote(url),
HASH_REDIRECT_GET_KEY, make_redirect_hash(search_arg, url))
- )
\ No newline at end of file
+ )
+
+
+class RegistryIterator(object):
+ def __init__(self, registry, iterattr='__iter__', transform=lambda x:x):
+ if not hasattr(registry, iterattr):
+ raise AttributeError("Registry has no attribute %s" % iterattr)
+ self.registry = registry
+ self.iterattr = iterattr
+ self.transform = transform
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ if not hasattr(self, '_iter'):
+ self._iter = getattr(self.registry, self.iterattr)()
+
+ return self.transform(self._iter.next())
+
+ def copy(self):
+ return self.__class__(self.registry, self.iterattr, self.transform)
\ No newline at end of file
diff --git a/philo/contrib/waldo/forms.py b/philo/contrib/waldo/forms.py
index 2ee64d0..8e14ba5 100644
--- a/philo/contrib/waldo/forms.py
+++ b/philo/contrib/waldo/forms.py
@@ -1,4 +1,5 @@
from datetime import date
+
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
@@ -6,14 +7,23 @@ 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
class EmailInput(forms.TextInput):
+ """Displays an HTML5 email input on browsers which support it and a normal text input on other browsers."""
input_type = 'email'
class RegistrationForm(UserCreationForm):
+ """
+ Handles user registration. If :mod:`recaptcha_django` is installed on the system and :class:`recaptcha_django.middleware.ReCaptchaMiddleware` is in :setting:`settings.MIDDLEWARE_CLASSES`, then a recaptcha field will automatically be added to the registration form.
+
+ .. seealso:: `recaptcha-django `_
+
+ """
+ #: An :class:`EmailField` using the :class:`EmailInput` widget.
email = forms.EmailField(widget=EmailInput)
try:
from recaptcha_django import ReCaptchaField
@@ -55,6 +65,7 @@ class RegistrationForm(UserCreationForm):
class UserAccountForm(forms.ModelForm):
+ """Handles a user's account - by default, :attr:`auth.User.first_name`, :attr:`auth.User.last_name`, :attr:`auth.User.email`."""
first_name = User._meta.get_field('first_name').formfield(required=True)
last_name = User._meta.get_field('last_name').formfield(required=True)
email = User._meta.get_field('email').formfield(required=True, widget=EmailInput)
@@ -63,12 +74,37 @@ class UserAccountForm(forms.ModelForm):
kwargs['instance'] = user
super(UserAccountForm, self).__init__(*args, **kwargs)
+ def email_changed(self):
+ """Returns ``True`` if the email field changed value and ``False`` if it did not, or if there is no email field on the form. This method must be supplied by account forms used with :mod:`~philo.contrib.waldo`."""
+ return 'email' in self.changed_data
+
+ def reset_email(self):
+ """
+ ModelForms modify their instances in-place during :meth:`_post_clean`; this method resets the email value to its initial state and returns the altered value. This is a method on the form to allow unusual behavior such as storing email on a :class:`UserProfile`.
+
+ """
+ email = self.instance.email
+ self.instance.email = self.initial['email']
+ self.cleaned_data.pop('email')
+ return email
+
+ @classmethod
+ def set_email(cls, user, email):
+ """
+ Given a valid instance and an email address, correctly set the email address for that instance and save the changes. This is a class method in order to allow unusual behavior such as storing email on a :class:`UserProfile`.
+
+ """
+ user.email = email
+ user.save()
+
+
class Meta:
model = User
fields = ('first_name', 'last_name', 'email')
class WaldoAuthenticationForm(AuthenticationForm):
+ """Handles user authentication. Checks that the user has not mistakenly entered their email address (like :class:`django.contrib.admin.forms.AdminAuthenticationForm`) but does not require that the user be staff."""
ERROR_MESSAGE = _("Please enter a correct username and password. Note that both fields are case-sensitive.")
def clean(self):
@@ -92,11 +128,4 @@ class WaldoAuthenticationForm(AuthenticationForm):
elif not self.user_cache.is_active:
raise ValidationError(message)
self.check_for_test_cookie()
- return self.cleaned_data
-
- def check_for_test_cookie(self):
- # This method duplicates the Django 1.3 AuthenticationForm method.
- if self.request and not self.request.session.test_cookie_worked():
- raise forms.ValidationError(
- _("Your Web browser doesn't appear to have cookies enabled. "
- "Cookies are required for logging in."))
\ No newline at end of file
+ return self.cleaned_data
\ No newline at end of file
diff --git a/philo/contrib/waldo/models.py b/philo/contrib/waldo/models.py
index f63cdb1..cdadead 100644
--- a/philo/contrib/waldo/models.py
+++ b/philo/contrib/waldo/models.py
@@ -1,3 +1,15 @@
+"""
+Waldo provides abstract :class:`.MultiView`\ s to handle several levels of common authentication:
+
+* :class:`LoginMultiView` handles the case where users only need to be able to log in and out.
+* :class:`PasswordMultiView` handles the case where users will also need to change their password.
+* :class:`RegistrationMultiView` builds on top of :class:`PasswordMultiView` to handle user registration, as well.
+* :class:`AccountMultiView` adds account-handling functionality to the :class:`RegistrationMultiView`.
+
+"""
+
+import urlparse
+
from django import forms
from django.conf.urls.defaults import url, patterns, include
from django.contrib import messages
@@ -15,17 +27,17 @@ 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):
- """
- Handles exclusively methods and views related to logging users in and out.
- """
+ """Handles exclusively methods and views related to logging users in and out."""
+ #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the login form.
login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
+ #: A django form class which will be used for the authentication process. Default: :class:`.WaldoAuthenticationForm`.
login_form = WaldoAuthenticationForm
@property
@@ -36,7 +48,7 @@ class LoginMultiView(MultiView):
)
def set_requirement_redirect(self, request, redirect=None):
- "Figure out where someone should end up after landing on a `requirement` page like the login page."
+ """Figures out and stores where a user should end up after landing on a page (like the login page) because they have not fulfilled some kind of requirement."""
if redirect is not None:
pass
elif 'requirement_redirect' in request.session:
@@ -61,6 +73,7 @@ class LoginMultiView(MultiView):
request.session['requirement_redirect'] = redirect
def get_requirement_redirect(self, request, default=None):
+ """Returns the location which a user should be redirected to after fulfilling a requirement (like logging in)."""
redirect = request.session.pop('requirement_redirect', None)
# Security checks a la django.contrib.auth.views.login
if not redirect or ' ' in redirect:
@@ -75,9 +88,7 @@ class LoginMultiView(MultiView):
@never_cache
def login(self, request, extra_context=None):
- """
- Displays the login form for the given HttpRequest.
- """
+ """Renders the :attr:`login_page` with an instance of the :attr:`login_form` for the given :class:`HttpRequest`."""
self.set_requirement_redirect(request)
# Redirect already-authenticated users to the index page.
@@ -96,7 +107,7 @@ class LoginMultiView(MultiView):
return HttpResponseRedirect(redirect)
else:
- form = self.login_form()
+ form = self.login_form(request)
request.session.set_test_cookie()
@@ -109,9 +120,11 @@ class LoginMultiView(MultiView):
@never_cache
def logout(self, request, extra_context=None):
+ """Logs the given :class:`HttpRequest` out, redirecting the user to the page they just left or to the :meth:`~.Node.get_absolute_url` for the ``request.node``."""
return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
def login_required(self, view):
+ """Wraps a view function to require that the user be logged in."""
def inner(request, *args, **kwargs):
if not request.user.is_authenticated():
self.set_requirement_redirect(request, redirect=request.path)
@@ -127,33 +140,55 @@ class LoginMultiView(MultiView):
class PasswordMultiView(LoginMultiView):
- "Adds on views for password-related functions."
+ """
+ Adds support for password setting, resetting, and changing to the :class:`LoginMultiView`. Password reset support includes handling of a confirmation email.
+
+ """
+ #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the password reset request form.
password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
+ #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the password reset confirmation email.
password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
+ #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the password setting form (i.e. the page that users will see after confirming a password reset).
password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
+ #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the password change form.
password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
+ #: The password change form class. Default: :class:`django.contrib.auth.forms.PasswordChangeForm`.
password_change_form = PasswordChangeForm
+ #: The password set form class. Default: :class:`django.contrib.auth.forms.SetPasswordForm`.
password_set_form = SetPasswordForm
+ #: The password reset request form class. Default: :class:`django.contrib.auth.forms.PasswordResetForm`.
password_reset_form = PasswordResetForm
@property
def urlpatterns(self):
urlpatterns = super(PasswordMultiView, self).urlpatterns
- if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
+ if self.password_reset_page_id and self.password_reset_confirmation_email_id and self.password_set_page_id:
urlpatterns += patterns('',
url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
url(r'^password/reset/(?P\w+)/(?P[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
)
- if self.password_change_page:
+ if self.password_change_page_id:
urlpatterns += patterns('',
url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
)
return urlpatterns
def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None, secure=False):
+ """
+ Generates a confirmation link for an arbitrary action, such as a password reset.
+
+ :param confirmation_view: The view function which needs to be linked to.
+ :param token_generator: Generates a confirmable token for the action.
+ :param user: The user who is trying to take the action.
+ :param node: The node which is providing the basis for the confirmation URL.
+ :param token_args: A list of additional arguments (i.e. besides the user) to be used for token creation.
+ :param reverse_kwargs: A dictionary of any additional keyword arguments necessary for correctly reversing the view.
+ :param secure: Whether the link should use the https:// or http://.
+
+ """
token = token_generator.make_token(user, *(token_args or []))
kwargs = {
'uidb36': int_to_base36(user.id),
@@ -163,6 +198,15 @@ class PasswordMultiView(LoginMultiView):
return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True, secure=secure)
def send_confirmation_email(self, subject, email, page, extra_context):
+ """
+ Sends a confirmation email for an arbitrary action, such as a password reset. If the ``page``'s :class:`.Template` has a mimetype of ``text/html``, then the email will be sent with an HTML alternative version.
+
+ :param subject: The subject line of the email.
+ :param email: The recipient's address.
+ :param page: The page which will be used to render the email body.
+ :param extra_context: The context for rendering the ``page``.
+
+ """
text_content = page.render_to_string(extra_context=extra_context)
from_email = 'noreply@%s' % Site.objects.get_current().domain
@@ -174,6 +218,21 @@ class PasswordMultiView(LoginMultiView):
send_mail(subject, text_content, from_email, [email])
def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
+ """
+ Handles the process by which users request a password reset, and generates the context for the confirmation email. That context will contain:
+
+ link
+ The confirmation link for the password reset.
+ user
+ The user requesting the reset.
+ site
+ The current :class:`Site`.
+ request
+ The current :class:`HttpRequest` instance.
+
+ :param token_generator: The token generator to use for the confirmation link.
+
+ """
if request.user.is_authenticated():
return HttpResponseRedirect(request.node.get_absolute_url())
@@ -186,10 +245,7 @@ class PasswordMultiView(LoginMultiView):
'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node, secure=request.is_secure()),
'user': user,
'site': current_site,
- 'request': request,
-
- # Deprecated... leave in for backwards-compatibility
- 'username': user.username
+ 'request': request
}
self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
messages.add_message(request, messages.SUCCESS, "An email has been sent to the address you provided with details on resetting your password.", fail_silently=True)
@@ -206,8 +262,10 @@ class PasswordMultiView(LoginMultiView):
def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
"""
- Checks that a given hash in a password reset link is valid. If so,
- displays the password set form.
+ Checks that ``token``` is valid, and if so, renders an instance of :attr:`password_set_form` with :attr:`password_set_page`.
+
+ :param token_generator: The token generator used to check the ``token``.
+
"""
assert uidb36 is not None and token is not None
try:
@@ -238,6 +296,7 @@ class PasswordMultiView(LoginMultiView):
raise Http404
def password_change(self, request, extra_context=None):
+ """Renders an instance of :attr:`password_change_form` with :attr:`password_change_page`."""
if request.method == 'POST':
form = self.password_change_form(request.user, request.POST)
if form.is_valid():
@@ -259,15 +318,18 @@ class PasswordMultiView(LoginMultiView):
class RegistrationMultiView(PasswordMultiView):
- """Adds on the pages necessary for letting new users register."""
+ """Adds support for user registration to the :class:`PasswordMultiView`."""
+ #: A :class:`ForeignKey` to the :class:`.Page` which will be used to display the registration form.
register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
+ #: A :class:`ForeignKey` to the :class:`.Page` which will be used to render the registration confirmation email.
register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
+ #: The registration form class. Default: :class:`.RegistrationForm`.
registration_form = RegistrationForm
@property
def urlpatterns(self):
urlpatterns = super(RegistrationMultiView, self).urlpatterns
- if self.register_page and self.register_confirmation_email:
+ if self.register_page_id and self.register_confirmation_email_id:
urlpatterns += patterns('',
url(r'^register$', csrf_protect(self.register), name='register'),
url(r'^register/(?P\w+)/(?P[^/]+)$', self.register_confirm, name='register_confirm')
@@ -275,6 +337,12 @@ class RegistrationMultiView(PasswordMultiView):
return urlpatterns
def register(self, request, extra_context=None, token_generator=registration_token_generator):
+ """
+ Renders the :attr:`register_page` with an instance of :attr:`registration_form` in the context as ``form``. If the form has been submitted, sends a confirmation email using :attr:`register_confirmation_email` and the same context as :meth:`PasswordMultiView.password_reset`.
+
+ :param token_generator: The token generator to use for the confirmation link.
+
+ """
if request.user.is_authenticated():
return HttpResponseRedirect(request.node.get_absolute_url())
@@ -304,9 +372,9 @@ class RegistrationMultiView(PasswordMultiView):
def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
"""
- Checks that a given hash in a registration link is valid and activates
- the given account. If so, log them in and redirect to
- self.post_register_confirm_redirect.
+ Checks that ``token`` is valid, and if so, logs the user in and redirects them to :meth:`post_register_confirm_redirect`.
+
+ :param token_generator: The token generator used to check the ``token``.
"""
assert uidb36 is not None and token is not None
try:
@@ -333,6 +401,7 @@ class RegistrationMultiView(PasswordMultiView):
raise Http404
def post_register_confirm_redirect(self, request):
+ """Returns an :class:`HttpResponseRedirect` for post-registration-confirmation. Default: :meth:`Node.get_absolute_url` for ``request.node``."""
return HttpResponseRedirect(request.node.get_absolute_url())
class Meta:
@@ -340,45 +409,43 @@ class RegistrationMultiView(PasswordMultiView):
class AccountMultiView(RegistrationMultiView):
- """
- By default, the `account` consists of the first_name, last_name, and email fields
- of the User model. Using a different account model is as simple as writing a form that
- accepts a User instance as the first argument.
- """
+ """Adds support for user accounts on top of the :class:`RegistrationMultiView`. By default, the account consists of the first_name, last_name, and email fields of the User model. Using a different account model is as simple as replacing :attr:`account_form` with any form class that takes an :class:`auth.User` instance as the first argument."""
+ #: A :class:`ForeignKey` to the :class:`Page` which will be used to render the account management form.
manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
+ #: A :class:`ForeignKey` to a :class:`Page` which will be used to render an email change confirmation email. This is optional; if it is left blank, then email changes will be performed without confirmation.
email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related', blank=True, null=True, help_text="If this is left blank, email changes will be performed without confirmation.")
+ #: A django form class which will be used to manage the user's account. Default: :class:`.UserAccountForm`
account_form = UserAccountForm
@property
def urlpatterns(self):
urlpatterns = super(AccountMultiView, self).urlpatterns
- if self.manage_account_page:
+ if self.manage_account_page_id:
urlpatterns += patterns('',
url(r'^account$', self.login_required(self.account_view), name='account'),
)
- if self.email_change_confirmation_email:
+ if self.email_change_confirmation_email_id:
urlpatterns += patterns('',
url(r'^account/email/(?P\w+)/(?P[\w.]+[+][\w.]+)/(?P[^/]+)$', self.email_change_confirm, name='email_change_confirm')
)
return urlpatterns
def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
+ """
+ Renders the :attr:`manage_account_page` with an instance of :attr:`account_form` in the context as ``form``. If the form has been posted, the user's email was changed, and :attr:`email_change_confirmation_email` is not ``None``, sends a confirmation email to the new email to make sure it exists before making the change. The email will have the same context as :meth:`PasswordMultiView.password_reset`.
+
+ :param token_generator: The token generator to use for the confirmation link.
+
+ """
if request.method == 'POST':
form = self.account_form(request.user, request.POST, request.FILES)
if form.is_valid():
message = "Account information saved."
redirect = self.get_requirement_redirect(request, default='')
- if 'email' in form.changed_data and self.email_change_confirmation_email:
- # ModelForms modify their instances in-place during
- # validation, so reset the instance's email to its
- # previous value here, then remove the new value
- # from cleaned_data. We only do this if an email
- # change confirmation email is available.
- request.user.email = form.initial['email']
-
- email = form.cleaned_data.pop('email')
+ if form.email_changed() and self.email_change_confirmation_email:
+ email = form.reset_email()
current_site = Site.objects.get_current()
@@ -390,7 +457,7 @@ class AccountMultiView(RegistrationMultiView):
}
self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
- message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
+ message = "An email has be sent to %s to confirm the email%s." % (email, " change" if bool(request.user.email) else "")
if not request.user.email:
message += " You will need to confirm the email before accessing pages that require a valid account."
redirect = ''
@@ -413,11 +480,13 @@ class AccountMultiView(RegistrationMultiView):
return self.manage_account_page.render_to_response(request, extra_context=context)
def has_valid_account(self, user):
+ """Returns ``True`` if the ``user`` has a valid account and ``False`` otherwise."""
form = self.account_form(user, {})
form.data = form.initial
return form.is_valid()
def account_required(self, view):
+ """Wraps a view function to allow access only to users with valid accounts and otherwise redirect them to the :meth:`account_view`."""
def inner(request, *args, **kwargs):
if not self.has_valid_account(request.user):
messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
@@ -425,7 +494,7 @@ class AccountMultiView(RegistrationMultiView):
self.set_requirement_redirect(request, redirect=request.path)
redirect = self.reverse('account', node=request.node)
else:
- redirect = node.get_absolute_url()
+ redirect = request.node.get_absolute_url()
return HttpResponseRedirect(redirect)
return view(request, *args, **kwargs)
@@ -433,6 +502,7 @@ class AccountMultiView(RegistrationMultiView):
return inner
def post_register_confirm_redirect(self, request):
+ """Automatically redirects users to the :meth:`account_view` after registration."""
if self.manage_account_page:
messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
return HttpResponseRedirect(self.reverse('account', node=request.node))
@@ -440,7 +510,10 @@ class AccountMultiView(RegistrationMultiView):
def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
"""
- Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
+ Checks that ``token`` is valid, and if so, changes the user's email.
+
+ :param token_generator: The token generator used to check the ``token``.
+
"""
assert uidb36 is not None and token is not None and email is not None
@@ -458,8 +531,7 @@ class AccountMultiView(RegistrationMultiView):
raise Http404
if token_generator.check_token(user, email, token):
- user.email = email
- user.save()
+ self.account_form.set_email(user, email)
messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
if self.manage_account_page:
redirect = self.reverse('account', node=request.node)
diff --git a/philo/contrib/waldo/tokens.py b/philo/contrib/waldo/tokens.py
index 80f0b11..1a7c3a9 100644
--- a/philo/contrib/waldo/tokens.py
+++ b/philo/contrib/waldo/tokens.py
@@ -1,13 +1,20 @@
"""
-Based on django.contrib.auth.tokens
-"""
+Based on :mod:`django.contrib.auth.tokens`. Supports the following settings:
+
+:setting:`WALDO_REGISTRATION_TIMEOUT_DAYS`
+ The number of days a registration link will be valid before expiring. Default: 1.
+:setting:`WALDO_EMAIL_TIMEOUT_DAYS`
+ The number of days an email change link will be valid before expiring. Default: 1.
+"""
+
+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)
@@ -15,13 +22,10 @@ EMAIL_TIMEOUT_DAYS = getattr(settings, 'WALDO_EMAIL_TIMEOUT_DAYS', 1)
class RegistrationTokenGenerator(PasswordResetTokenGenerator):
- """
- Strategy object used to generate and check tokens for the user registration mechanism.
- """
+ """Strategy object used to generate and check tokens for the user registration mechanism."""
+
def check_token(self, user, token):
- """
- Check that a registration token is correct for a given user.
- """
+ """Check that a registration token is correct for a given user."""
# If the user is active, the hash can't be valid.
if user.is_active:
return False
@@ -61,13 +65,10 @@ registration_token_generator = RegistrationTokenGenerator()
class EmailTokenGenerator(PasswordResetTokenGenerator):
- """
- Strategy object used to generate and check tokens for a user email change mechanism.
- """
+ """Strategy object used to generate and check tokens for a user email change mechanism."""
+
def make_token(self, user, email):
- """
- Returns a token that can be used once to do an email change for the given user and email.
- """
+ """Returns a token that can be used once to do an email change for the given user and email."""
return self._make_token_with_timestamp(user, email, self._num_days(self._today()))
def check_token(self, user, email, token):
diff --git a/philo/contrib/winer/__init__.py b/philo/contrib/winer/__init__.py
new file mode 100644
index 0000000..83fb303
--- /dev/null
+++ b/philo/contrib/winer/__init__.py
@@ -0,0 +1,4 @@
+"""
+Winer provides the same API as `django's syndication Feed class `_, adapted to a Philo-style :class:`~philo.models.nodes.MultiView` for easy database management. Apps that need syndication can simply subclass :class:`~philo.contrib.winer.models.FeedView`, override a few methods, and start serving RSS and Atom feeds. See :class:`~philo.contrib.penfield.models.BlogView` for a concrete implementation example.
+
+"""
\ No newline at end of file
diff --git a/philo/contrib/winer/exceptions.py b/philo/contrib/winer/exceptions.py
new file mode 100644
index 0000000..e2045f9
--- /dev/null
+++ b/philo/contrib/winer/exceptions.py
@@ -0,0 +1,3 @@
+class HttpNotAcceptable(Exception):
+ """This will be raised in :meth:`.FeedView.get_feed_type` if an Http-Accept header will not accept any of the feed content types that are available."""
+ pass
\ No newline at end of file
diff --git a/philo/contrib/winer/feeds.py b/philo/contrib/winer/feeds.py
new file mode 100644
index 0000000..0554591
--- /dev/null
+++ b/philo/contrib/winer/feeds.py
@@ -0,0 +1,13 @@
+from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
+
+from philo.utils.registry import Registry
+
+
+DEFAULT_FEED = Atom1Feed
+
+
+registry = Registry()
+
+
+registry.register(Atom1Feed, verbose_name='Atom')
+registry.register(Rss201rev2Feed, verbose_name='RSS')
\ No newline at end of file
diff --git a/philo/contrib/penfield/middleware.py b/philo/contrib/winer/middleware.py
similarity index 59%
rename from philo/contrib/penfield/middleware.py
rename to philo/contrib/winer/middleware.py
index b25a28b..89a5bd2 100644
--- a/philo/contrib/penfield/middleware.py
+++ b/philo/contrib/winer/middleware.py
@@ -1,11 +1,11 @@
from django.http import HttpResponse
from django.utils.decorators import decorator_from_middleware
-from philo.contrib.penfield.exceptions import HttpNotAcceptable
+
+from philo.contrib.winer.exceptions import HttpNotAcceptable
class HttpNotAcceptableMiddleware(object):
- """Middleware to catch HttpNotAcceptable errors and return an Http406 response.
- See RFC 2616."""
+ """Middleware to catch :exc:`~philo.contrib.winer.exceptions.HttpNotAcceptable` and return an :class:`HttpResponse` with a 406 response code. See :rfc:`2616`."""
def process_exception(self, request, exception):
if isinstance(exception, HttpNotAcceptable):
return HttpResponse(status=406)
diff --git a/philo/contrib/winer/models.py b/philo/contrib/winer/models.py
new file mode 100644
index 0000000..4acf5d1
--- /dev/null
+++ b/philo/contrib/winer/models.py
@@ -0,0 +1,347 @@
+from django.conf import settings
+from django.conf.urls.defaults import url, patterns, include
+from django.contrib.sites.models import Site, RequestSite
+from django.contrib.syndication.views import add_domain
+from django.db import models
+from django.http import HttpResponse
+from django.template import RequestContext, Template as DjangoTemplate
+from django.utils import feedgenerator, tzinfo
+from django.utils.encoding import smart_unicode, force_unicode
+from django.utils.html import escape
+
+from philo.contrib.winer.exceptions import HttpNotAcceptable
+from philo.contrib.winer.feeds import registry, DEFAULT_FEED
+from philo.contrib.winer.middleware import http_not_acceptable
+from philo.models import Page, Template, MultiView
+
+try:
+ import mimeparse
+except:
+ mimeparse = None
+
+
+class FeedView(MultiView):
+ """
+ :class:`FeedView` is an abstract model which handles a number of pages and related feeds for a single object such as a blog or newsletter. In addition to all other methods and attributes, :class:`FeedView` supports the same generic API as `django.contrib.syndication.views.Feed `_.
+
+ """
+ #: The type of feed which should be served by the :class:`FeedView`.
+ feed_type = models.CharField(max_length=50, choices=registry.choices, default=registry.get_slug(DEFAULT_FEED))
+ #: The suffix which will be appended to a page URL for a :attr:`feed_type` feed of its items. Default: "feed". Note that RSS and Atom feeds will always be available at ``/rss`` and ``/atom`` regardless of the value of this setting.
+ #:
+ #: .. seealso:: :meth:`get_feed_type`, :meth:`feed_patterns`
+ feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
+ #: A :class:`BooleanField` - whether or not feeds are enabled.
+ feeds_enabled = models.BooleanField(default=True)
+ #: A :class:`PositiveIntegerField` - the maximum number of items to return for this feed. All items will be returned if this field is blank. Default: 15.
+ feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.")
+
+ #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided.
+ item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
+ #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided.
+ item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
+
+ #: An attribute holding the name of the context variable to be populated with the items managed by the :class:`FeedView`. Default: "items"
+ item_context_var = 'items'
+ #: An attribute holding the name of the attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`~philo.contrib.penfield.models.Blog`.) Default: "object"
+ #:
+ #: Example::
+ #:
+ #: class BlogView(FeedView):
+ #: blog = models.ForeignKey(Blog)
+ #:
+ #: object_attr = 'blog'
+ #: item_context_var = 'entries'
+ object_attr = 'object'
+
+ #: An attribute holding a description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API.
+ description = ""
+
+ def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
+ """
+ Given the name to be used to reverse this view and the names of the attributes for the function that fetches the objects, returns patterns suitable for inclusion in urlpatterns. In addition to ``base`` (which will serve the page at ``page_attr``) and ``base`` + :attr:`feed_suffix` (which will serve a :attr:`feed_type` feed), patterns will be provided for each registered feed type as ``base`` + ``slug``.
+
+ :param base: The base of the returned patterns - that is, the subpath pattern which will reference the page for the items. The :attr:`feed_suffix` will be appended to this subpath.
+ :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` which will return an (``items``, ``extra_context``) tuple. This will be passed directly to :meth:`feed_view` and :meth:`page_view`.
+ :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be passed directly to :meth:`page_view` and will be rendered with the items from ``get_items_attr``.
+ :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters.
+ :returns: Patterns suitable for use in urlpatterns.
+
+ Example::
+
+ class BlogView(FeedView):
+ blog = models.ForeignKey(Blog)
+ entry_archive_page = models.ForeignKey(Page)
+
+ @property
+ def urlpatterns(self):
+ urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index')
+ urlpatterns += self.feed_patterns(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
+ return urlpatterns
+
+ def get_entries_by_ymd(request, year, month, day, extra_context=None):
+ entries = Blog.entries.all()
+ # filter entries based on the year, month, and day.
+ return entries, extra_context
+
+ .. seealso:: :meth:`get_feed_type`
+
+ """
+ feed_patterns = ()
+ if self.feeds_enabled:
+ suffixes = [(self.feed_suffix, None)] + [(slug, slug) for slug in registry]
+ for suffix, feed_type in suffixes:
+ feed_view = http_not_acceptable(self.feed_view(get_items_attr, reverse_name, feed_type))
+ feed_pattern = r'%s%s%s$' % (base, "/" if base and base[-1] != "^" else "", suffix)
+ feed_patterns += (url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),)
+ feed_patterns += (url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name),)
+ return patterns('', *feed_patterns)
+
+ def get_object(self, request, **kwargs):
+ """By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API."""
+ return getattr(self, self.object_attr)
+
+ def feed_view(self, get_items_attr, reverse_name, feed_type=None):
+ """
+ Returns a view function that renders a list of items as a feed.
+
+ :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with the object for the feed and view arguments.
+ :param reverse_name: The name which can be used reverse the page for this feed using the :class:`FeedView` as the urlconf.
+ :param feed_type: The slug used to render the feed class which will be used by the returned view function.
+
+ :returns: A view function that renders a list of items as a feed.
+
+ """
+ get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
+
+ def inner(request, extra_context=None, *args, **kwargs):
+ obj = self.get_object(request, *args, **kwargs)
+ feed = self.get_feed(obj, request, reverse_name, feed_type, *args, **kwargs)
+ items, xxx = get_items(obj, request, extra_context=extra_context, *args, **kwargs)
+ self.populate_feed(feed, items, request)
+
+ response = HttpResponse(mimetype=feed.mime_type)
+ feed.write(response, 'utf-8')
+ return response
+
+ return inner
+
+ def page_view(self, get_items_attr, page_attr):
+ """
+ :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments.
+ :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be rendered with the items from ``get_items_attr``.
+
+ :returns: A view function that renders a list of items as an :class:`HttpResponse`.
+
+ """
+ get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr)
+
+ def inner(request, extra_context=None, *args, **kwargs):
+ obj = self.get_object(request, *args, **kwargs)
+ items, extra_context = get_items(obj, request, extra_context=extra_context, *args, **kwargs)
+ items, item_context = self.process_page_items(request, items)
+
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update(item_context or {})
+
+ page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr)
+ return page.render_to_response(request, extra_context=context)
+ return inner
+
+ def process_page_items(self, request, items):
+ """
+ Hook for handling any extra processing of ``items`` based on an :class:`HttpRequest`, such as pagination or searching. This method is expected to return a list of items and a dictionary to be added to the page context.
+
+ """
+ item_context = {
+ self.item_context_var: items
+ }
+ return items, item_context
+
+ def get_feed_type(self, request, feed_type=None):
+ """
+ If ``feed_type`` is not ``None``, returns the corresponding class from the registry or raises :exc:`.HttpNotAcceptable`.
+
+ Otherwise, intelligently chooses a feed type for a given request. Tries to return :attr:`feed_type`, but if the Accept header does not include that mimetype, tries to return the best match from the feed types that are offered by the :class:`FeedView`. If none of the offered feed types are accepted by the :class:`HttpRequest`, raises :exc:`.HttpNotAcceptable`.
+
+ If `mimeparse `_ is installed, it will be used to select the best matching accepted format; otherwise, the first available format that is accepted will be selected.
+
+ """
+ if feed_type is not None:
+ feed_type = registry[feed_type]
+ loose = False
+ else:
+ feed_type = registry.get(self.feed_type, DEFAULT_FEED)
+ loose = True
+ mt = feed_type.mime_type
+ accept = request.META.get('HTTP_ACCEPT')
+ if accept and mt not in accept and "*/*" not in accept and "%s/*" % mt.split("/")[0] not in accept:
+ # Wups! They aren't accepting the chosen format.
+ feed_type = None
+ if loose:
+ # Is there another format we can use?
+ accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()])
+ if mimeparse:
+ mt = mimeparse.best_match(accepted_mts.keys(), accept)
+ if mt:
+ feed_type = accepted_mts[mt]
+ else:
+ for mt in accepted_mts:
+ if mt in accept or "%s/*" % mt.split("/")[0] in accept:
+ feed_type = accepted_mts[mt]
+ break
+ if not feed_type:
+ raise HttpNotAcceptable
+ return feed_type
+
+ def get_feed(self, obj, request, reverse_name, feed_type=None, *args, **kwargs):
+ """
+ Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object.
+
+ :param obj: The object for which the feed should be generated.
+ :param request: The current request.
+ :param reverse_name: The name which can be used to reverse the URL of the page corresponding to this feed.
+ :param feed_type: The slug used to register the feed class that will be instantiated and returned.
+
+ :returns: An instance of the feed class registered as ``feed_type``, falling back to :attr:`feed_type` if ``feed_type`` is ``None``.
+
+ """
+ try:
+ current_site = Site.objects.get_current()
+ except Site.DoesNotExist:
+ current_site = RequestSite(request)
+
+ feed_type = self.get_feed_type(request, feed_type)
+ node = request.node
+ link = node.construct_url(self.reverse(reverse_name, args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure())
+
+ feed = feed_type(
+ title = self.__get_dynamic_attr('title', obj),
+ subtitle = self.__get_dynamic_attr('subtitle', obj),
+ link = link,
+ description = self.__get_dynamic_attr('description', obj),
+ language = settings.LANGUAGE_CODE.decode(),
+ feed_url = add_domain(
+ current_site.domain,
+ self.__get_dynamic_attr('feed_url', obj) or node.construct_url(self.reverse("%s_%s" % (reverse_name, registry.get_slug(feed_type)), args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure()),
+ request.is_secure()
+ ),
+ author_name = self.__get_dynamic_attr('author_name', obj),
+ author_link = self.__get_dynamic_attr('author_link', obj),
+ author_email = self.__get_dynamic_attr('author_email', obj),
+ categories = self.__get_dynamic_attr('categories', obj),
+ feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
+ feed_guid = self.__get_dynamic_attr('feed_guid', obj),
+ ttl = self.__get_dynamic_attr('ttl', obj),
+ **self.feed_extra_kwargs(obj)
+ )
+ return feed
+
+ def populate_feed(self, feed, items, request):
+ """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``."""
+ if self.item_title_template:
+ title_template = DjangoTemplate(self.item_title_template.code)
+ else:
+ title_template = None
+ if self.item_description_template:
+ description_template = DjangoTemplate(self.item_description_template.code)
+ else:
+ description_template = None
+
+ node = request.node
+ try:
+ current_site = Site.objects.get_current()
+ except Site.DoesNotExist:
+ current_site = RequestSite(request)
+
+ if self.feed_length is not None:
+ items = items[:self.feed_length]
+
+ for item in items:
+ if title_template is not None:
+ title = title_template.render(RequestContext(request, {'obj': item}))
+ else:
+ title = self.__get_dynamic_attr('item_title', item)
+ if description_template is not None:
+ description = description_template.render(RequestContext(request, {'obj': item}))
+ else:
+ description = self.__get_dynamic_attr('item_description', item)
+
+ link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure())
+
+ enc = None
+ enc_url = self.__get_dynamic_attr('item_enclosure_url', item)
+ if enc_url:
+ enc = feedgenerator.Enclosure(
+ url = smart_unicode(add_domain(
+ current_site.domain,
+ enc_url,
+ request.is_secure()
+ )),
+ length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)),
+ mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item))
+ )
+ author_name = self.__get_dynamic_attr('item_author_name', item)
+ if author_name is not None:
+ author_email = self.__get_dynamic_attr('item_author_email', item)
+ author_link = self.__get_dynamic_attr('item_author_link', item)
+ else:
+ author_email = author_link = None
+
+ pubdate = self.__get_dynamic_attr('item_pubdate', item)
+ if pubdate and not pubdate.tzinfo:
+ ltz = tzinfo.LocalTimezone(pubdate)
+ pubdate = pubdate.replace(tzinfo=ltz)
+
+ feed.add_item(
+ title = title,
+ link = link,
+ description = description,
+ unique_id = self.__get_dynamic_attr('item_guid', item, link),
+ enclosure = enc,
+ pubdate = pubdate,
+ author_name = author_name,
+ author_email = author_email,
+ author_link = author_link,
+ categories = self.__get_dynamic_attr('item_categories', item),
+ item_copyright = self.__get_dynamic_attr('item_copyright', item),
+ **self.item_extra_kwargs(item)
+ )
+
+ def __get_dynamic_attr(self, attname, obj, default=None):
+ try:
+ attr = getattr(self, attname)
+ except AttributeError:
+ return default
+ if callable(attr):
+ # Check func_code.co_argcount rather than try/excepting the
+ # function and catching the TypeError, because something inside
+ # the function may raise the TypeError. This technique is more
+ # accurate.
+ if hasattr(attr, 'func_code'):
+ argcount = attr.func_code.co_argcount
+ else:
+ argcount = attr.__call__.func_code.co_argcount
+ if argcount == 2: # one argument is 'self'
+ return attr(obj)
+ else:
+ return attr()
+ return attr
+
+ def feed_extra_kwargs(self, obj):
+ """Returns an extra keyword arguments dictionary that is used when initializing the feed generator."""
+ return {}
+
+ def item_extra_kwargs(self, item):
+ """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator."""
+ return {}
+
+ def item_title(self, item):
+ return escape(force_unicode(item))
+
+ def item_description(self, item):
+ return force_unicode(item)
+
+ class Meta:
+ abstract=True
\ No newline at end of file
diff --git a/philo/exceptions.py b/philo/exceptions.py
index f53083d..9f908c0 100644
--- a/philo/exceptions.py
+++ b/philo/exceptions.py
@@ -1,19 +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 View.reverse when the View does not provide subpaths (the default). """
+ """Raised by :meth:`.View.reverse` when the :class:`.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. """
+ """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 get_path if the root model is not an ancestor of the current model """
+ """Raised by :meth:`.TreeEntity.get_path` if the root instance is not an ancestor of the current instance."""
pass
\ No newline at end of file
diff --git a/philo/fixtures/test_fixtures.json b/philo/fixtures/test_fixtures.json
index 4c55372..2bda0d1 100644
--- a/philo/fixtures/test_fixtures.json
+++ b/philo/fixtures/test_fixtures.json
@@ -91,8 +91,8 @@
"rght": 143,
"view_object_id": 1,
"view_content_type": [
- "penfield",
- "blogview"
+ "philo",
+ "page"
],
"parent": 1,
"level": 1,
@@ -1236,7 +1236,7 @@
"model": "philo.redirect",
"fields": {
"status_code": 302,
- "target": "second"
+ "url_or_subpath": "second"
}
},
{
@@ -1382,47 +1382,5 @@
"template": 6,
"title": "Tag Archive Page"
}
- },
- {
- "pk": 1,
- "model": "penfield.blog",
- "fields": {
- "slug": "free-lovin",
- "title": "Free lovin'"
- }
- },
- {
- "pk": 1,
- "model": "penfield.blogentry",
- "fields": {
- "content": "Lorem ipsum.\r\n\r\nDolor sit amet.",
- "author": 1,
- "title": "First Entry",
- "excerpt": "",
- "blog": 1,
- "date": "2010-10-20 10:38:58",
- "slug": "first-entry",
- "tags": [
- 1
- ]
- }
- },
- {
- "pk": 1,
- "model": "penfield.blogview",
- "fields": {
- "entry_archive_page": 5,
- "tag_page": 4,
- "feed_suffix": "feed",
- "entry_permalink_style": "D",
- "tag_permalink_base": "tags",
- "feeds_enabled": true,
- "entries_per_page": null,
- "tag_archive_page": 6,
- "blog": 1,
- "entry_permalink_base": "entries",
- "index_page": 2,
- "entry_page": 3
- }
}
]
diff --git a/philo/forms/entities.py b/philo/forms/entities.py
index e781128..ba72d7d 100644
--- a/philo/forms/entities.py
+++ b/philo/forms/entities.py
@@ -1,5 +1,6 @@
from django.forms.models import ModelFormMetaclass, ModelForm, ModelFormOptions
from django.utils.datastructures import SortedDict
+
from philo.utils import fattr
@@ -93,6 +94,10 @@ class EntityFormMetaclass(ModelFormMetaclass):
class EntityForm(ModelForm):
+ """
+ :class:`EntityForm` knows how to handle :class:`.Entity` instances - specifically, how to set initial values for :class:`.AttributeProxyField`\ s and save cleaned values to an instance on save.
+
+ """
__metaclass__ = EntityFormMetaclass
def __init__(self, *args, **kwargs):
diff --git a/philo/forms/fields.py b/philo/forms/fields.py
index b148947..66b96ad 100644
--- a/philo/forms/fields.py
+++ b/philo/forms/fields.py
@@ -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
@@ -8,6 +9,7 @@ __all__ = ('JSONFormField',)
class JSONFormField(forms.Field):
+ """A form field which is validated by :func:`philo.validators.json_validator`."""
default_validators = [json_validator]
def clean(self, value):
diff --git a/philo/loaders/database.py b/philo/loaders/database.py
index 141aedd..4c9c379 100644
--- a/philo/loaders/database.py
+++ b/philo/loaders/database.py
@@ -1,10 +1,15 @@
from django.template import TemplateDoesNotExist
from django.template.loader import BaseLoader
from django.utils.encoding import smart_unicode
+
from philo.models import Template
class Loader(BaseLoader):
+ """
+ :class:`philo.loaders.database.Loader` enables loading of template code from :class:`.Template`\ s. This would let :class:`.Template`\ s be used with ``{% include %}`` and ``{% extends %}`` tags, as well as any other features that use template loading.
+
+ """
is_usable=True
def load_template_source(self, template_name, template_dirs=None):
diff --git a/philo/middleware.py b/philo/middleware.py
index 5ec3e77..f4f7e9d 100644
--- a/philo/middleware.py
+++ b/philo/middleware.py
@@ -1,57 +1,54 @@
from django.conf import settings
from django.contrib.sites.models import Site
from django.http import Http404
+
from philo.models import Node, View
+from philo.utils.lazycompat import SimpleLazyObject
-class LazyNode(object):
- def __get__(self, request, obj_type=None):
- if not hasattr(request, '_cached_node_path'):
- return None
-
- if not hasattr(request, '_found_node'):
- try:
- current_site = Site.objects.get_current()
- except Site.DoesNotExist:
- current_site = None
-
- path = request._cached_node_path
- trailing_slash = False
- if path[-1] == '/':
- trailing_slash = True
-
- try:
- node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False)
- except Node.DoesNotExist:
- node = None
- else:
- if subpath is None:
- subpath = ""
- subpath = "/" + subpath
-
- if not node.handles_subpath(subpath):
- node = None
- else:
- if trailing_slash and subpath[-1] != "/":
- subpath += "/"
-
- node.subpath = subpath
-
- request._found_node = node
-
- return request._found_node
+def get_node(path):
+ """Returns a :class:`Node` instance at ``path`` (relative to the current site) or ``None``."""
+ try:
+ current_site = Site.objects.get_current()
+ except Site.DoesNotExist:
+ current_site = None
+
+ trailing_slash = False
+ if path[-1] == '/':
+ trailing_slash = True
+
+ try:
+ node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False)
+ except Node.DoesNotExist:
+ return None
+
+ if subpath is None:
+ subpath = ""
+ subpath = "/" + subpath
+
+ if trailing_slash and subpath[-1] != "/":
+ subpath += "/"
+
+ node._path = path
+ node._subpath = subpath
+
+ return node
class RequestNodeMiddleware(object):
- """Middleware to process the request's path and attach the closest ancestor node."""
- def process_request(self, request):
- request.__class__.node = LazyNode()
+ """
+ Adds a ``node`` attribute, representing the currently-viewed :class:`.Node`, to every incoming :class:`HttpRequest` object. This is required by :func:`philo.views.node_view`.
+ :class:`RequestNodeMiddleware` also catches all exceptions raised while handling requests that have attached :class:`.Node`\ s if :setting:`settings.DEBUG` is ``True``. If a :exc:`django.http.Http404` error was caught, :class:`RequestNodeMiddleware` will look for an "Http404" :class:`.Attribute` on the request's :class:`.Node`; otherwise it will look for an "Http500" :class:`.Attribute`. If an appropriate :class:`.Attribute` is found, and the value of the attribute is a :class:`.View` instance, then the :class:`.View` will be rendered with the exception in the ``extra_context``, bypassing any later handling of exceptions.
+
+ """
def process_view(self, request, view_func, view_args, view_kwargs):
try:
- request._cached_node_path = view_kwargs['path']
+ path = view_kwargs['path']
except KeyError:
- pass
+ request.node = None
+ else:
+ request.node = SimpleLazyObject(lambda: get_node(path))
def process_exception(self, request, exception):
if settings.DEBUG or not hasattr(request, 'node') or not request.node:
@@ -59,12 +56,16 @@ class RequestNodeMiddleware(object):
if isinstance(exception, Http404):
error_view = request.node.attributes.get('Http404', None)
+ status_code = 404
else:
error_view = request.node.attributes.get('Http500', None)
+ status_code = 500
if error_view is None or not isinstance(error_view, View):
# Should this be duck-typing? Perhaps even no testing?
return
extra_context = {'exception': exception}
- return error_view.render_to_response(request, extra_context)
\ No newline at end of file
+ response = error_view.render_to_response(request, extra_context)
+ response.status_code = status_code
+ return response
\ No newline at end of file
diff --git a/philo/migrations/0015_auto__add_unique_node_slug_parent__add_unique_template_slug_parent.py b/philo/migrations/0015_auto__add_unique_node_slug_parent__add_unique_template_slug_parent.py
new file mode 100644
index 0000000..7a79fec
--- /dev/null
+++ b/philo/migrations/0015_auto__add_unique_node_slug_parent__add_unique_template_slug_parent.py
@@ -0,0 +1,144 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding unique constraint on 'Node', fields ['slug', 'parent']
+ db.create_unique('philo_node', ['slug', 'parent_id'])
+
+ # Adding unique constraint on 'Template', fields ['slug', 'parent']
+ db.create_unique('philo_template', ['slug', 'parent_id'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'Template', fields ['slug', 'parent']
+ db.delete_unique('philo_template', ['slug', 'parent_id'])
+
+ # Removing unique constraint on 'Node', fields ['slug', 'parent']
+ db.delete_unique('philo_node', ['slug', 'parent_id'])
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.tag': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ }
+ }
+
+ complete_apps = ['philo']
diff --git a/philo/migrations/0016_auto__add_field_file_name.py b/philo/migrations/0016_auto__add_field_file_name.py
new file mode 100644
index 0000000..0d8e654
--- /dev/null
+++ b/philo/migrations/0016_auto__add_field_file_name.py
@@ -0,0 +1,139 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'File.name'
+ db.add_column('philo_file', 'name', self.gf('django.db.models.fields.CharField')(default='', max_length=255), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'File.name'
+ db.delete_column('philo_file', 'name')
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.tag': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ }
+ }
+
+ complete_apps = ['philo']
diff --git a/philo/migrations/0017_generate_filenames.py b/philo/migrations/0017_generate_filenames.py
new file mode 100644
index 0000000..613ac7a
--- /dev/null
+++ b/philo/migrations/0017_generate_filenames.py
@@ -0,0 +1,139 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ for f in orm.File.objects.filter(name=""):
+ f.name = f.file.name
+ f.save()
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ pass
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.tag': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ }
+ }
+
+ complete_apps = ['philo']
diff --git a/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py b/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py
new file mode 100644
index 0000000..75a3dee
--- /dev/null
+++ b/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py
@@ -0,0 +1,145 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Changing field 'Node.view_object_id'
+ db.alter_column('philo_node', 'view_object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True))
+
+ # Changing field 'Node.view_content_type'
+ db.alter_column('philo_node', 'view_content_type_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['contenttypes.ContentType']))
+
+
+ def backwards(self, orm):
+
+ # User chose to not deal with backwards NULL issues for 'Node.view_object_id'
+ raise RuntimeError("Cannot reverse this migration. 'Node.view_object_id' and its values cannot be restored.")
+
+ # User chose to not deal with backwards NULL issues for 'Node.view_content_type'
+ raise RuntimeError("Cannot reverse this migration. 'Node.view_content_type' and its values cannot be restored.")
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.tag': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ }
+ }
+
+ complete_apps = ['philo']
diff --git a/philo/migrations/0019_to_taggit.py b/philo/migrations/0019_to_taggit.py
new file mode 100644
index 0000000..fb5e8f0
--- /dev/null
+++ b/philo/migrations/0019_to_taggit.py
@@ -0,0 +1,155 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ # If any tags are longer than 100, this will result in some data loss.
+ PhiloTag = orm['philo.Tag']
+ Tag = orm['taggit.Tag']
+
+ for tag in PhiloTag.objects.all():
+ Tag.objects.get_or_create(name=tag.name, slug=tag.slug)
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ pass
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.tag': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ },
+ 'taggit.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'})
+ },
+ 'taggit.taggeditem': {
+ 'Meta': {'object_name': 'TaggedItem'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
+ }
+ }
+
+ complete_apps = ['taggit', 'philo']
diff --git a/philo/migrations/0020_from_taggit.py b/philo/migrations/0020_from_taggit.py
new file mode 100644
index 0000000..9a43df9
--- /dev/null
+++ b/philo/migrations/0020_from_taggit.py
@@ -0,0 +1,154 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ pass
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ PhiloTag = orm['philo.Tag']
+ Tag = orm['taggit.Tag']
+
+ for tag in Tag.objects.all():
+ PhiloTag.objects.get_or_create(name=tag.name, slug=tag.slug)
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.tag': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ },
+ 'taggit.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'})
+ },
+ 'taggit.taggeditem': {
+ 'Meta': {'object_name': 'TaggedItem'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
+ }
+ }
+
+ complete_apps = ['taggit', 'philo']
diff --git a/philo/migrations/0021_auto__del_tag.py b/philo/migrations/0021_auto__del_tag.py
new file mode 100644
index 0000000..f63b906
--- /dev/null
+++ b/philo/migrations/0021_auto__del_tag.py
@@ -0,0 +1,138 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting model 'Tag'
+ db.delete_table('philo_tag')
+
+
+ def backwards(self, orm):
+
+ # Adding model 'Tag'
+ db.create_table('philo_tag', (
+ ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, unique=True, db_index=True)),
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ))
+ db.send_create_signal('philo', ['Tag'])
+
+
+ models = {
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'philo.attribute': {
+ 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'},
+ 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.collection': {
+ 'Meta': {'object_name': 'Collection'},
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.collectionmember': {
+ 'Meta': {'object_name': 'CollectionMember'},
+ 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'philo.contentlet': {
+ 'Meta': {'object_name': 'Contentlet'},
+ 'content': ('philo.models.fields.TemplateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.contentreference': {
+ 'Meta': {'object_name': 'ContentReference'},
+ 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"})
+ },
+ 'philo.file': {
+ 'Meta': {'object_name': 'File'},
+ 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.foreignkeyvalue': {
+ 'Meta': {'object_name': 'ForeignKeyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.jsonvalue': {
+ 'Meta': {'object_name': 'JSONValue'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'})
+ },
+ 'philo.manytomanyvalue': {
+ 'Meta': {'object_name': 'ManyToManyValue'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'})
+ },
+ 'philo.node': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'philo.page': {
+ 'Meta': {'object_name': 'Page'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'philo.redirect': {
+ 'Meta': {'object_name': 'Redirect'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'philo.template': {
+ 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'},
+ 'code': ('philo.models.fields.TemplateField', [], {}),
+ 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'})
+ }
+ }
+
+ complete_apps = ['philo']
diff --git a/philo/models/__init__.py b/philo/models/__init__.py
index 523f789..3942b84 100644
--- a/philo/models/__init__.py
+++ b/philo/models/__init__.py
@@ -1,12 +1,20 @@
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from django.contrib.sites.models import Site
+
from philo.models.base import *
from philo.models.collections import *
from philo.models.nodes import *
from philo.models.pages import *
-from django.contrib.auth.models import User, Group
-from django.contrib.sites.models import Site
register_value_model(User)
register_value_model(Group)
register_value_model(Site)
-register_templatetags('philo.templatetags.embed')
\ No newline at end of file
+
+if 'philo' in settings.INSTALLED_APPS:
+ from django.template import add_to_builtins
+ add_to_builtins('philo.templatetags.embed')
+ add_to_builtins('philo.templatetags.containers')
+ add_to_builtins('philo.templatetags.collections')
+ add_to_builtins('philo.templatetags.nodes')
\ No newline at end of file
diff --git a/philo/models/base.py b/philo/models/base.py
index cf420c7..e7918f5 100644
--- a/philo/models/base.py
+++ b/philo/models/base.py
@@ -1,44 +1,25 @@
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.exceptions import ValidationError
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.utils.entities import AttributeMapper, TreeAttributeMapper
from philo.validators import json_validator
-from UserDict import DictMixin
-from mptt.models import MPTTModel, MPTTModelBase, MPTTOptions
-class Tag(models.Model):
- name = models.CharField(max_length=255)
- slug = models.SlugField(max_length=255, unique=True)
-
- def __unicode__(self):
- return self.name
-
- class Meta:
- app_label = 'philo'
- ordering = ('name',)
+__all__ = ('value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity', 'SlugTreeEntity')
-class Titled(models.Model):
- title = models.CharField(max_length=255)
- slug = models.SlugField(max_length=255)
-
- def __unicode__(self):
- return self.title
-
- class Meta:
- abstract = True
-
-
-#: An instance of :class:`ContentTypeRegistryLimiter` which is used to track the content types which can be related to by ForeignKeyValues and ManyToManyValues.
+#: 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()
@@ -47,9 +28,6 @@ def register_value_model(model):
value_content_type_limiter.register_class(model)
-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)
@@ -91,7 +69,7 @@ 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`.
+#: 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)
@@ -237,7 +215,12 @@ 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`."""
+ """
+ :class:`Attribute`\ s exist primarily to let arbitrary data be attached to arbitrary model instances without altering the database schema and without guaranteeing that the data will be available on every instance of that model.
+
+ Generally, :class:`Attribute`\ s will not be accessed as models; instead, they will be accessed through the :attr:`Entity.attributes` property, which allows direct dictionary getting and setting of the value of an :class:`Attribute` with its key.
+
+ """
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)
@@ -256,35 +239,26 @@ class Attribute(models.Model):
def __unicode__(self):
return u'"%s": %s' % (self.key, self.value)
+ def set_value(self, value, value_class=JSONValue):
+ """Given a value and a value class, sets up self.value appropriately."""
+ if isinstance(self.value, value_class):
+ val = self.value
+ else:
+ if isinstance(self.value, models.Model):
+ self.value.delete()
+ val = value_class()
+
+ val.set_value(value)
+ val.save()
+
+ self.value = val
+ self.save()
+
class Meta:
app_label = 'philo'
unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
-class QuerySetMapper(object, DictMixin):
- def __init__(self, queryset, passthrough=None):
- self.queryset = queryset
- self.passthrough = passthrough
-
- def __getitem__(self, key):
- try:
- value = self.queryset.get(key__exact=key).value
- except ObjectDoesNotExist:
- if self.passthrough is not None:
- return self.passthrough.__getitem__(key)
- raise KeyError
- else:
- if value is not None:
- return value.value
- return value
-
- def keys(self):
- keys = set(self.queryset.values_list('key', flat=True).distinct())
- if self.passthrough is not None:
- keys |= set(self.passthrough.keys())
- return list(keys)
-
-
class EntityOptions(object):
def __init__(self, options):
if options is not None:
@@ -312,10 +286,9 @@ class Entity(models.Model):
attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
- @property
- def attributes(self):
+ def get_attribute_mapper(self, mapper=AttributeMapper):
"""
- Property that returns a dictionary-like object which can be used to retrieve related :class:`Attribute`\ s' values directly.
+ Returns an :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly.
Example::
@@ -326,19 +299,32 @@ class Entity(models.Model):
u'eggs'
"""
-
- return QuerySetMapper(self.attribute_set.all())
+ return mapper(self)
+
+ @property
+ def attributes(self):
+ if not hasattr(self, '_attributes'):
+ self._attributes = self.get_attribute_mapper()
+ return self._attributes
class Meta:
abstract = True
-class TreeManager(models.Manager):
+class TreeEntityBase(MPTTModelBase, EntityBase):
+ def __new__(meta, name, bases, attrs):
+ attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
+ cls = EntityBase.__new__(meta, name, bases, attrs)
+
+ return meta.register(cls)
+
+
+class TreeEntityManager(models.Manager):
use_for_related_fields = True
- def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
+ def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='pk'):
"""
- If ``absolute_result`` is ``True``, returns the object at ``path`` (starting at ``root``) or raises a :exception:`DoesNotExist` 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).
+ 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.
@@ -349,7 +335,8 @@ class TreeManager(models.Manager):
: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 (instance, remaining_path) otherwise.
+ :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.
"""
@@ -445,16 +432,19 @@ class TreeManager(models.Manager):
return find_obj(segments, len(segments)/2 or len(segments))
-class TreeModel(MPTTModel):
- objects = TreeManager()
+class TreeEntity(Entity, MPTTModel):
+ """An abstract subclass of Entity which represents a tree relationship."""
+
+ __metaclass__ = TreeEntityBase
+ objects = TreeEntityManager()
parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
- slug = models.SlugField(max_length=255)
- def get_path(self, root=None, pathsep='/', field='slug'):
+ def get_path(self, root=None, pathsep='/', field='pk', memoize=True):
"""
: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.
+ :param memoize: Whether to use memoized results. Since, in most cases, the ancestors of a TreeEntity will not change over the course of an instance's lifetime, this defaults to ``True``.
:returns: A string representation of an object's path.
"""
@@ -462,42 +452,38 @@ class TreeModel(MPTTModel):
if root == self:
return ''
+ parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr)
+ if getattr(root, 'pk', None) == parent_id:
+ return getattr(self, field, '?')
+
if root is not None and not self.is_descendant_of(root):
raise AncestorDoesNotExist(root)
+ if memoize:
+ memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?'))
+ try:
+ return self._path_memo[memo_args]
+ except AttributeError:
+ self._path_memo = {}
+ except KeyError:
+ pass
+
qs = self.get_ancestors(include_self=True)
if root is not None:
qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
- return pathsep.join([getattr(parent, field, '?') for parent in qs])
- path = property(get_path)
-
- def __unicode__(self):
- return self.path
-
- class Meta:
- unique_together = (('parent', 'slug'),)
- abstract = True
-
-
-class TreeEntityBase(MPTTModelBase, EntityBase):
- def __new__(meta, name, bases, attrs):
- attrs['_mptt_meta'] = MPTTOptions(attrs.pop('MPTTMeta', None))
- cls = EntityBase.__new__(meta, name, bases, attrs)
+ path = pathsep.join([getattr(parent, field, '?') for parent in qs])
- return meta.register(cls)
-
-
-class TreeEntity(Entity, TreeModel):
- """An abstract subclass of Entity which represents a tree relationship."""
-
- __metaclass__ = TreeEntityBase
+ if memoize:
+ self._path_memo[memo_args] = path
+
+ return path
+ path = property(get_path)
- @property
- def attributes(self):
+ def get_attribute_mapper(self, mapper=None):
"""
- 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.
+ Returns a :class:`.TreeAttributeMapper` or :class:`.AttributeMapper` which can be used to retrieve related :class:`Attribute`\ s' values directly. If an :class:`Attribute` with a given key is not related to the :class:`Entity`, then the mapper will check the parent's attributes.
Example::
@@ -510,10 +496,42 @@ class TreeEntity(Entity, TreeModel):
u'eggs'
"""
-
- if self.parent:
- return QuerySetMapper(self.attribute_set.all(), passthrough=self.parent.attributes)
- return super(TreeEntity, self).attributes
+ if mapper is None:
+ if getattr(self, "%s_id" % self._mptt_meta.parent_attr):
+ mapper = TreeAttributeMapper
+ else:
+ mapper = AttributeMapper
+ return super(TreeEntity, self).get_attribute_mapper(mapper)
+
+ def __unicode__(self):
+ return self.path
+
+ class Meta:
+ abstract = True
+
+
+class SlugTreeEntityManager(TreeEntityManager):
+ def get_with_path(self, path, root=None, absolute_result=True, pathsep='/', field='slug'):
+ return super(SlugTreeEntityManager, self).get_with_path(path, root, absolute_result, pathsep, field)
+
+
+class SlugTreeEntity(TreeEntity):
+ objects = SlugTreeEntityManager()
+ slug = models.SlugField(max_length=255)
+
+ def get_path(self, root=None, pathsep='/', field='slug', memoize=True):
+ return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize)
+ path = property(get_path)
+
+ def clean(self):
+ if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None:
+ try:
+ self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True)
+ except self.DoesNotExist:
+ pass
+ else:
+ raise ValidationError(self.unique_error_message(self.__class__, ('parent', 'slug')))
class Meta:
+ unique_together = ('parent', 'slug')
abstract = True
\ No newline at end of file
diff --git a/philo/models/collections.py b/philo/models/collections.py
index 539ecdb..be7b706 100644
--- a/philo/models/collections.py
+++ b/philo/models/collections.py
@@ -1,17 +1,27 @@
-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 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
+
+
+__all__ = ('Collection', 'CollectionMember')
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 +35,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()
+ [, , ]
+ >>> collection.members.with_model(User)
+ [, ]
+
+ """
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):
@@ -43,5 +75,4 @@ class CollectionMember(models.Model):
app_label = 'philo'
-register_templatetags('philo.templatetags.collections')
register_value_model(Collection)
\ No newline at end of file
diff --git a/philo/models/fields/__init__.py b/philo/models/fields/__init__.py
index 1f9603e..7ab4326 100644
--- a/philo/models/fields/__init__.py
+++ b/philo/models/fields/__init__.py
@@ -5,12 +5,15 @@ 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.utils.registry import RegistryIterator
from philo.validators import TemplateValidator, json_validator
#from philo.models.fields.entities import *
class TemplateField(models.TextField):
+ """A :class:`TextField` which is validated with a :class:`.TemplateValidator`. ``allow``, ``disallow``, and ``secure`` will be passed into the validator's construction."""
def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
super(TemplateField, self).__init__(*args, **kwargs)
self.validators.append(TemplateValidator(allow, disallow, secure))
@@ -40,6 +43,7 @@ class JSONDescriptor(object):
class JSONField(models.TextField):
+ """A :class:`TextField` which stores its value on the model instance as a python object and stores its value in the database as JSON. Validated with :func:`.json_validator`."""
default_validators = [json_validator]
def get_attname(self):
@@ -68,6 +72,7 @@ class JSONField(models.TextField):
class SlugMultipleChoiceField(models.Field):
+ """Stores a selection of multiple items with unique slugs in the form of a comma-separated list. Also knows how to correctly handle :class:`RegistryIterator`\ s passed in as choices."""
__metaclass__ = models.SubfieldBase
description = _("Comma-separated slug field")
@@ -123,6 +128,16 @@ class SlugMultipleChoiceField(models.Field):
if invalid_values:
# should really make a custom message.
raise ValidationError(self.error_messages['invalid_choice'] % invalid_values)
+
+ def _get_choices(self):
+ if isinstance(self._choices, RegistryIterator):
+ return self._choices.copy()
+ elif hasattr(self._choices, 'next'):
+ choices, self._choices = itertools.tee(self._choices)
+ return choices
+ else:
+ return self._choices
+ choices = property(_get_choices)
try:
diff --git a/philo/models/fields/entities.py b/philo/models/fields/entities.py
index 6c407d0..0558d3e 100644
--- a/philo/models/fields/entities.py
+++ b/philo/models/fields/entities.py
@@ -1,29 +1,14 @@
-"""
-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.
-
-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')
@@ -32,8 +17,23 @@ __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
ATTRIBUTE_REGISTRY = '_attribute_registry'
-class EntityProxyField(object):
- def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs):
+class AttributeProxyField(object):
+ """
+ :class:`AttributeProxyField`\ s can be assigned as fields on a subclass of :class:`philo.models.base.Entity`. They act like any other model fields, but instead of saving their data to the model's table, they save it to :class:`.Attribute`\ s related to a model instance. Additionally, a new :class:`.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:`.PassthroughAttributeMapper`\ s and :class:`.TreeAttributeMapper`\ s, where even an :class:`.Attribute` with a value of ``None`` will prevent a passthrough.
+
+ Example::
+
+ class Thing(Entity):
+ numbers = models.PositiveIntegerField()
+ improvised = JSONAttribute(models.BooleanField)
+
+ :param attribute_key: The key of the attribute that will be used to store this field's value, if it is different than the field's name.
+
+ The remaining parameters have the same meaning as for ordinary model fields.
+
+ """
+ def __init__(self, attribute_key=None, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs):
+ self.attribute_key = attribute_key
self.verbose_name = verbose_name
self.help_text = help_text
self.default = default
@@ -42,8 +42,15 @@ class EntityProxyField(object):
def actually_contribute_to_class(self, sender, **kwargs):
sender._entity_meta.add_proxy_field(self)
+ setattr(sender, self.name, AttributeFieldDescriptor(self))
+ opts = sender._entity_meta
+ if not hasattr(opts, '_has_attribute_fields'):
+ opts._has_attribute_fields = True
+ models.signals.post_save.connect(process_attribute_fields, sender=sender)
def contribute_to_class(self, cls, name):
+ if self.attribute_key is None:
+ self.attribute_key = name
if issubclass(cls, Entity):
self.name = self.attname = name
self.model = cls
@@ -54,6 +61,10 @@ class EntityProxyField(object):
raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__)
def formfield(self, form_class=forms.CharField, **kwargs):
+ """
+ Returns a form field capable of accepting values for the :class:`AttributeProxyField`.
+
+ """
defaults = {
'required': False,
'label': capfirst(self.verbose_name),
@@ -65,25 +76,34 @@ class EntityProxyField(object):
return form_class(**defaults)
def value_from_object(self, obj):
- """The return value of this method will be used by the EntityForm as
- this field's initial value."""
+ """Returns the value of this field in the given model instance."""
return getattr(obj, self.name)
def get_storage_value(self, value):
- """Final conversion of `value` before it gets stored on an Entity instance.
- This step is performed by the ProxyFieldForm."""
+ """Final conversion of ``value`` before it gets stored on an :class:`.Entity` instance. This will be called during :meth:`.EntityForm.save`."""
return value
+ def validate_value(self, value):
+ "Raise an appropriate exception if ``value`` is not valid for this :class:`AttributeProxyField`."
+ pass
+
def has_default(self):
+ """Returns ``True`` if a default value was provided and ``False`` otherwise."""
return self.default is not NOT_PROVIDED
def _get_choices(self):
+ """Returns the choices passed into the constructor."""
if hasattr(self._choices, 'next'):
choices, self._choices = tee(self._choices)
return choices
else:
return self._choices
choices = property(_get_choices)
+
+ @property
+ def value_class(self):
+ """Each :class:`AttributeProxyField` subclass can define a value_class to use for creation of new :class:`.AttributeValue`\ s"""
+ raise AttributeError("value_class must be defined on %s subclasses." % self.__class__.__name__)
class AttributeFieldDescriptor(object):
@@ -124,62 +144,32 @@ 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 :class:`AttributeProxyField`\ s 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()
for field in registry['added']:
+ # TODO: Should this perhaps just use instance.attributes[field.attribute_key] = getattr(instance, field.name, None)?
+ # (Would eliminate the need for field.value_class.)
try:
attribute = instance.attribute_set.get(key=field.attribute_key)
except Attribute.DoesNotExist:
attribute = Attribute()
attribute.entity = instance
attribute.key = field.attribute_key
-
- value_class = field.value_class
- if isinstance(attribute.value, value_class):
- value = attribute.value
- else:
- if isinstance(attribute.value, models.Model):
- attribute.value.delete()
- value = value_class()
-
- value.set_value(getattr(instance, field.name, None))
- value.save()
-
- attribute.value = value
- attribute.save()
+ attribute.set_value(value=getattr(instance, field.name, None), value_class=field.value_class)
del instance.__dict__[ATTRIBUTE_REGISTRY]
-class AttributeField(EntityProxyField):
- def __init__(self, attribute_key=None, **kwargs):
- self.attribute_key = attribute_key
- super(AttributeField, self).__init__(**kwargs)
+class JSONAttribute(AttributeProxyField):
+ """
+ Handles an :class:`.Attribute` with a :class:`.JSONValue`.
- def actually_contribute_to_class(self, sender, **kwargs):
- super(AttributeField, self).actually_contribute_to_class(sender, **kwargs)
- setattr(sender, self.name, AttributeFieldDescriptor(self))
- opts = sender._entity_meta
- if not hasattr(opts, '_has_attribute_fields'):
- opts._has_attribute_fields = True
- models.signals.post_save.connect(process_attribute_fields, sender=sender)
+ :param field_template: A django form field instance that will be used to guide rendering and interpret values. For example, using :class:`django.forms.BooleanField` will make this field render as a checkbox.
- def contribute_to_class(self, cls, name):
- if self.attribute_key is None:
- self.attribute_key = name
- super(AttributeField, self).contribute_to_class(cls, name)
-
- def validate_value(self, value):
- "Confirm that the value is valid or raise an appropriate error."
- pass
+ """
- @property
- def value_class(self):
- raise AttributeError("value_class must be defined on AttributeField subclasses.")
-
-
-class JSONAttribute(AttributeField):
value_class = JSONValue
def __init__(self, field_template=None, **kwargs):
@@ -200,12 +190,14 @@ class JSONAttribute(AttributeField):
return self.field_template.formfield(**defaults)
def value_from_object(self, obj):
+ """If the field template is a :class:`DateField` or a :class:`DateTimeField`, this will convert the default return value to a datetime instance."""
value = super(JSONAttribute, self).value_from_object(obj)
if isinstance(self.field_template, (models.DateField, models.DateTimeField)):
value = self.field_template.to_python(value)
return value
def get_storage_value(self, value):
+ """If ``value`` is a :class:`datetime.datetime` instance, this will convert it to a format which can be stored as correct JSON."""
if isinstance(value, datetime.datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
if isinstance(value, datetime.date):
@@ -213,11 +205,18 @@ class JSONAttribute(AttributeField):
return value
-class ForeignKeyAttribute(AttributeField):
+class ForeignKeyAttribute(AttributeProxyField):
+ """
+ Handles an :class:`.Attribute` with a :class:`.ForeignKeyValue`.
+
+ :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter ` to restrict the queryset for the :class:`ForeignKeyAttribute`.
+
+ """
value_class = ForeignKeyValue
def __init__(self, model, limit_choices_to=None, **kwargs):
super(ForeignKeyAttribute, self).__init__(**kwargs)
+ # Spoof being a rel from a ForeignKey for admin widgets.
self.to = model
if limit_choices_to is None:
limit_choices_to = {}
@@ -235,15 +234,22 @@ class ForeignKeyAttribute(AttributeField):
return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults)
def value_from_object(self, obj):
+ """Converts the default value type (a model instance) to a pk."""
relobj = super(ForeignKeyAttribute, self).value_from_object(obj)
return getattr(relobj, 'pk', None)
def get_related_field(self):
- """Spoof being a rel from a ForeignKey."""
+ # Spoof being a rel from a ForeignKey for admin widgets.
return self.to._meta.pk
class ManyToManyAttribute(ForeignKeyAttribute):
+ """
+ Handles an :class:`.Attribute` with a :class:`.ManyToManyValue`.
+
+ :param limit_choices_to: A :class:`Q` object, dictionary, or :class:`ContentTypeLimiter ` to restrict the queryset for the :class:`ManyToManyAttribute`.
+
+ """
value_class = ManyToManyValue
def validate_value(self, value):
@@ -254,6 +260,7 @@ class ManyToManyAttribute(ForeignKeyAttribute):
return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs)
def value_from_object(self, obj):
+ """Converts the default value type (a queryset) to a list of pks."""
qs = super(ForeignKeyAttribute, self).value_from_object(obj)
try:
return qs.values_list('pk', flat=True)
diff --git a/philo/models/nodes.py b/philo/models/nodes.py
index 99be196..647ba81 100644
--- a/philo/models/nodes.py
+++ b/philo/models/nodes.py
@@ -1,59 +1,103 @@
-from django.db import models
-from django.contrib.contenttypes.models import ContentType
+from inspect import getargspec
+import mimetypes
+from os.path import basename
+
+from django.conf import settings
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.cache import cache
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.template import add_to_builtins as register_templatetags
+from django.db import models
+from django.http import HttpResponse, HttpResponseServerError, HttpResponseRedirect, Http404
from django.utils.encoding import smart_str
-from inspect import getargspec
-from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED
-from philo.models.base import TreeEntity, Entity, QuerySetMapper, register_value_model
+
+from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED, ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths
+from philo.models.base import SlugTreeEntity, Entity, 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.utils.entities import LazyPassthroughAttributeMapper
from philo.signals import view_about_to_render, view_finished_rendering
+__all__ = ('Node', 'View', 'MultiView', 'Redirect', 'File')
+
+
_view_content_type_limiter = ContentTypeSubclassLimiter(None)
+CACHE_PHILO_ROOT = getattr(settings, "PHILO_CACHE_PHILO_ROOT", True)
-class Node(TreeEntity):
- view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter)
- view_object_id = models.PositiveIntegerField()
+class Node(SlugTreeEntity):
+ """
+ :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, blank=True, null=True)
+ view_object_id = models.PositiveIntegerField(blank=True, null=True)
+ #: :class:`GenericForeignKey` to a non-abstract subclass of :class:`View`
view = generic.GenericForeignKey('view_content_type', 'view_object_id')
@property
def accepts_subpath(self):
- if self.view:
- return self.view.accepts_subpath
+ """A property shortcut for :attr:`self.view.accepts_subpath `"""
+ if self.view_object_id and self.view_content_type_id:
+ return ContentType.objects.get_for_id(self.view_content_type_id).model_class().accepts_subpath
return False
def handles_subpath(self, subpath):
- return self.view.handles_subpath(subpath)
+ if self.view_object_id and self.view_content_type_id:
+ return ContentType.objects.get_for_id(self.view_content_type_id).model_class().handles_subpath(subpath)
+ return False
def render_to_response(self, request, extra_context=None):
- return self.view.render_to_response(request, extra_context)
+ """This is a shortcut method for :meth:`View.render_to_response`"""
+ if self.view_object_id and self.view_content_type_id:
+ view_model = ContentType.objects.get_for_id(self.view_content_type_id).model_class()
+ self.view = view_model._default_manager.get(pk=self.view_object_id)
+ return self.view.render_to_response(request, extra_context)
+ raise Http404
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`.
+
+ Because this method will be called frequently and will always try to reverse ``philo-root``, the results of that reversal will be cached by default. This can be disabled by setting :setting:`PHILO_CACHE_PHILO_ROOT` to ``False``.
+
+ :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 ` 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')
+ if CACHE_PHILO_ROOT:
+ key = "CACHE_PHILO_ROOT__" + settings.ROOT_URLCONF
+ root_url = cache.get(key)
+ if root_url is None:
+ root_url = reverse('philo-root')
+ cache.set(key, root_url)
+ else:
+ root_url = reverse('philo-root')
try:
current_site = Site.objects.get_current()
@@ -80,7 +124,7 @@ class Node(TreeEntity):
return '%s%s%s%s' % (domain, root_url, path, subpath)
- class Meta:
+ class Meta(SlugTreeEntity.Meta):
app_label = 'philo'
@@ -89,18 +133,39 @@ 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')
+ #: An attribute on the class which defines whether this :class:`View` can handle subpaths. Default: ``False``
accepts_subpath = False
- def handles_subpath(self, subpath):
- if not self.accepts_subpath and subpath != "/":
+ @classmethod
+ def handles_subpath(cls, subpath):
+ """Returns True if the :class:`View` handles the given subpath, and False otherwise."""
+ if not cls.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 +188,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):
- return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
+ def attributes_with_node(self, node, mapper=LazyPassthroughAttributeMapper):
+ """
+ Returns a :class:`LazyPassthroughAttributeMapper` which can be used to directly retrieve the values of :class:`Attribute`\ s related to the :class:`View`, falling back on the :class:`Attribute`\ s of the passed-in :class:`Node` and its ancestors.
+
+ """
+ return mapper((self, node))
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 +218,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,24 +229,25 @@ _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):
- if not super(MultiView, self).handles_subpath(subpath):
- return False
- try:
- resolve(subpath, urlconf=self)
- except Http404:
- return False
- 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 ` and renders the view function (or method) found with the appropriate args and kwargs.
+
+ """
clear_url_caches()
- subpath = request.node.subpath
+ subpath = request.node._subpath
view, args, kwargs = resolve(subpath, urlconf=self)
view_args = getargspec(view)
if extra_context is not None and ('extra_context' in view_args[0] or view_args[2] is not None):
@@ -177,17 +257,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 +302,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):
@@ -235,7 +335,17 @@ class TargetURLModel(models.Model):
kwargs = dict([(smart_str(k, 'ascii'), v) for k, v in params.items()])
return self.url_or_subpath, args, kwargs
- def get_target_url(self):
+ def get_target_url(self, memoize=True):
+ """Calculates and returns the target url based on the :attr:`target_node`, :attr:`url_or_subpath`, and :attr:`reversing_parameters`. The results will be memoized by default; this can be prevented by passing in ``memoize=False``."""
+ if memoize:
+ memo_args = (self.target_node_id, self.url_or_subpath, self.reversing_parameters_json)
+ try:
+ return self._target_url_memo[memo_args]
+ except AttributeError:
+ self._target_url_memo = {}
+ except KeyError:
+ pass
+
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:
@@ -245,14 +355,19 @@ class TargetURLModel(models.Model):
subpath = self.url_or_subpath
if subpath[0] != '/':
subpath = '/' + subpath
- return node.construct_url(subpath)
+ target_url = node.construct_url(subpath)
elif node is not None:
- return node.get_absolute_url()
+ target_url = node.get_absolute_url()
else:
if self.reversing_parameters is not None:
view_name, args, kwargs = self.get_reverse_params()
- return reverse(view_name, args=args, kwargs=kwargs)
- return self.url_or_subpath
+ target_url = reverse(view_name, args=args, kwargs=kwargs)
+ else:
+ target_url = self.url_or_subpath
+
+ if memoize:
+ self._target_url_memo[memo_args] = target_url
+ return target_url
target_url = property(get_target_url)
class Meta:
@@ -260,13 +375,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,23 +395,33 @@ class Redirect(TargetURLModel, View):
class File(View):
- """ For storing arbitrary files """
-
- mimetype = models.CharField(max_length=255)
+ """Stores an arbitrary file."""
+ #: The name of the uploaded file. This is meant for finding the file again later, not for display.
+ name = models.CharField(max_length=255)
+ #: Defines the mimetype of the uploaded file. This will not be validated. If no mimetype is provided, it will be automatically generated based on the filename.
+ mimetype = models.CharField(max_length=255, blank=True)
+ #: Contains the uploaded file. Files are uploaded to ``philo/files/%Y/%m/%d``.
file = models.FileField(upload_to='philo/files/%Y/%m/%d')
+ def clean(self):
+ if not self.mimetype:
+ self.mimetype = mimetypes.guess_type(self.file.name, strict=False)[0]
+ if self.mimetype is None:
+ raise ValidationError("Unknown file type.")
+
def actually_render_to_response(self, request, extra_context=None):
wrapper = FileWrapper(self.file)
response = HttpResponse(wrapper, content_type=self.mimetype)
response['Content-Length'] = self.file.size
+ response['Content-Disposition'] = "inline; filename=%s" % basename(self.file.name)
return response
class Meta:
app_label = 'philo'
def __unicode__(self):
- return self.file.name
+ """Returns the value of :attr:`File.name`."""
+ return self.name
-register_templatetags('philo.templatetags.nodes')
register_value_model(Node)
\ No newline at end of file
diff --git a/philo/models/pages.py b/philo/models/pages.py
index 2221ee4..350bce5 100644
--- a/philo/models/pages.py
+++ b/philo/models/pages.py
@@ -1,155 +1,82 @@
# 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` 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
from django.core.exceptions import ValidationError
from django.db import models
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 django.template import Context, RequestContext, Template as DjangoTemplate
+
+from philo.models.base import SlugTreeEntity, register_value_model
from philo.models.fields import TemplateField
from philo.models.nodes import View
-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
+from philo.utils import templates
-class LazyContainerFinder(object):
- def __init__(self, nodes, extends=False):
- self.nodes = nodes
- self.initialized = False
- self.contentlet_specs = set()
- self.contentreference_specs = SortedDict()
- self.blocks = {}
- self.block_super = False
- self.extends = extends
-
- def process(self, nodelist):
- for node in nodelist:
- if self.extends:
- if isinstance(node, BlockNode):
- self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
- block.initialize()
- self.blocks.update(block.blocks)
- continue
-
- if isinstance(node, ContainerNode):
- if not node.references:
- self.contentlet_specs.add(node.name)
- else:
- if node.name not in self.contentreference_specs.keys():
- self.contentreference_specs[node.name] = node.references
- continue
-
- if isinstance(node, VariableNode):
- if node.filter_expression.var.lookups == (u'block', u'super'):
- self.block_super = True
-
- if hasattr(node, 'child_nodelists'):
- for nodelist_name in node.child_nodelists:
- if hasattr(node, nodelist_name):
- nodelist = getattr(node, nodelist_name)
- self.process(nodelist)
-
- # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
- # node as rendering an additional template. Philo monkeypatches the attribute onto
- # the relevant default nodes and declares it on any native nodes.
- if hasattr(node, LOADED_TEMPLATE_ATTR):
- loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
- if loaded_template:
- nodelist = loaded_template.nodelist
- self.process(nodelist)
-
- def initialize(self):
- if not self.initialized:
- self.process(self.nodes)
- self.initialized = True
+__all__ = ('Template', 'Page', 'Contentlet', 'ContentReference')
-class Template(TreeModel):
+class Template(SlugTreeEntity):
+ """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):
+ def get_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)
-
- def build_extension_tree(nodelist):
- nodelists = []
- extends = None
- for node in nodelist:
- if not isinstance(node, TextNode):
- if isinstance(node, ExtendsNode):
- extends = node
- break
-
- if extends:
- if extends.nodelist:
- nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
- loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
- nodelists.extend(build_extension_tree(loaded_template.nodelist))
- else:
- # Base case: root.
- nodelists.append(LazyContainerFinder(nodelist))
- return nodelists
-
- # Build a tree of the templates we're using, placing the root template first.
- levels = build_extension_tree(template.nodelist)[::-1]
-
- contentlet_specs = set()
- contentreference_specs = SortedDict()
- blocks = {}
-
- for level in levels:
- level.initialize()
- contentlet_specs |= level.contentlet_specs
- contentreference_specs.update(level.contentreference_specs)
- for name, block in level.blocks.items():
- if block.block_super:
- blocks.setdefault(name, []).append(block)
- else:
- blocks[name] = [block]
-
- for block_list in blocks.values():
- for block in block_list:
- block.initialize()
- contentlet_specs |= block.contentlet_specs
- contentreference_specs.update(block.contentreference_specs)
-
- return contentlet_specs, contentreference_specs
+ return templates.get_containers(template)
+ containers = property(get_containers)
def __unicode__(self):
+ """Returns the value of the :attr:`name` field."""
return self.name
- class Meta:
+ class Meta(SlugTreeEntity.Meta):
app_label = 'philo'
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:`container`\ s 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`-based functionality as is used for HTML.
+
+ The :class:`Page` will add itself to the context as ``page`` and its :attr:`~.Entity.attributes` as ``attributes``. If a request is provided, then :class:`request.node <.Node>` will also be added to the context as ``node`` and ``attributes`` will be set to the result of calling :meth:`~.View.attributes_with_node` with that :class:`.Node`.
+
+ """
context = {}
context.update(extra_context or {})
context.update({'page': self, 'attributes': self.attributes})
@@ -165,12 +92,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 +129,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` 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` template tag.
content = TemplateField()
def __unicode__(self):
+ """Returns the value of the :attr:`name` field."""
return self.name
class Meta:
@@ -208,21 +146,23 @@ 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` 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` 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:
app_label = 'philo'
-register_templatetags('philo.templatetags.containers')
-
-
register_value_model(Template)
register_value_model(Page)
\ No newline at end of file
diff --git a/philo/signals.py b/philo/signals.py
index 3653c54..13f6cd1 100644
--- a/philo/signals.py
+++ b/philo/signals.py
@@ -1,8 +1,60 @@
from django.dispatch import Signal
+#: Sent whenever an Entity subclass has been "prepared" -- that is, after the processing necessary to make :mod:`.AttributeProxyField`\ s 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
diff --git a/philo/static/admin/js/TagCreation.js b/philo/static/admin/js/TagCreation.js
deleted file mode 100644
index d08d41e..0000000
--- a/philo/static/admin/js/TagCreation.js
+++ /dev/null
@@ -1,101 +0,0 @@
-var tagCreation = window.tagCreation;
-
-(function($) {
- location_re = new RegExp("^https?:\/\/" + window.location.host + "/")
-
- $('html').ajaxSend(function(event, xhr, settings) {
- function getCookie(name) {
- var cookieValue = null;
- if (document.cookie && document.cookie != '') {
- var cookies = document.cookie.split(';');
- for (var i = 0; i < cookies.length; i++) {
- var cookie = $.trim(cookies[i]);
- // Does this cookie string begin with the name we want?
- if (cookie.substring(0, name.length + 1) == (name + '=')) {
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
- break;
- }
- }
- }
- return cookieValue;
- }
- if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url)) || location_re.test(settings.url)) {
- // Only send the token to relative URLs i.e. locally.
- xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
- }
- });
- tagCreation = {
- 'cache': {},
- 'addTagFromSlug': function(triggeringLink) {
- var id = triggeringLink.id.replace(/^ajax_add_/, '') + '_input';
- var slug = document.getElementById(id).value;
-
- var name = slug.split(' ');
- for(var i=0;iAdd this tag"
- addLink.style.marginLeft = "20px";
- addLink.style.display = "block";
- addLink.style.backgroundPosition = "10px 5px";
- addLink.style.width = "120px";
- $(input).after(addLink);
- if (window.grappelli) {
- addLink.parentNode.style.backgroundPosition = "6px 8px";
- } else {
- addLink.style.marginTop = "5px";
- }
- tagCreation.toggleButton(id);
- addEvent(input, 'keyup', function() {
- tagCreation.toggleButton(id);
- })
- addEvent(addLink, 'click', function(e) {
- e.preventDefault();
- tagCreation.addTagFromSlug(addLink);
- })
- },
- 'toggleButton': function(id) {
- var addLink = tagCreation.cache[id].addLink;
- var select = $(tagCreation.cache[id].select);
- if (select[0].options.length == 0) {
- if (addLink.style.display == 'none') {
- addLink.style.display = 'block';
- select.height(select.height() - $(addLink).outerHeight(false))
- }
- } else {
- if (addLink.style.display == 'block') {
- select[0].style.height = null;
- addLink.style.display = 'none';
- }
- }
- }
- }
-}(django.jQuery))
\ No newline at end of file
diff --git a/philo/static/philo/css/EmbedWidget.css b/philo/static/philo/css/EmbedWidget.css
new file mode 100644
index 0000000..525e5e3
--- /dev/null
+++ b/philo/static/philo/css/EmbedWidget.css
@@ -0,0 +1,51 @@
+.embed-widget{
+ float:left;
+}
+.embed-toolbar{
+ border:1px solid #CCC;
+ border-bottom:0;
+ padding:3px 5px;
+ background:#EEE -webkit-linear-gradient(#F5F5F5, #DDD);
+ background:#EEE -moz-linear-gradient(#F5F5F5, #DDD);
+ background-color:#EEE;
+}
+.embed-widget textarea{
+ margin-top:0;
+}
+.embed-widget button, .embed-widget select{
+ vertical-align:middle;
+ margin-right:3px;
+}
+.embed-toolbar button{
+ background:#FFF;
+ border:1px solid #CCC;
+ border-radius:3px;
+ -webkit-border-radius:3px;
+ -moz-border-radius:3px;
+ color:#666;
+}
+.embed-toolbar button:hover{
+ color:#444;
+}
+.embed-toolbar button:active{
+ color:#FFF;
+ background:#666;
+ border-color:#666;
+}
+
+.grappelli .embed-widget{
+ background:#DDD;
+ padding:2px;
+ border:1px solid #CCC;
+ border-radius:5px;
+ -webkit-border-radius:5px;
+ -moz-border-radius:5px;
+ display:inline-block;
+ margin:0 -3px;
+}
+.grappelli .embed-toolbar{
+ padding:0;
+ padding-bottom:3px;
+ background:none;
+ border:none;
+}
\ No newline at end of file
diff --git a/philo/static/philo/js/EmbedWidget.js b/philo/static/philo/js/EmbedWidget.js
new file mode 100644
index 0000000..7293125
--- /dev/null
+++ b/philo/static/philo/js/EmbedWidget.js
@@ -0,0 +1,152 @@
+;(function ($) {
+ var widget = window.embedWidget;
+
+ widget = {
+ options: {},
+ optgroups: {},
+ init: function () {
+ var EmbedFields = widget.EmbedFields = $('.embedding'),
+ EmbedWidgets = widget.EmbedWidgets,
+ EmbedBars = widget.EmbedBars,
+ EmbedButtons = widget.EmbedButtons,
+ EmbedSelects = widget.EmbedSelects;
+
+ EmbedFields.wrap($(''));
+ EmbedWidgets = $('.embed-widget');
+ EmbedWidgets.prepend($(''));
+ EmbedBars = $('.embed-toolbar');
+ EmbedBars.append('');
+ EmbedButtons = $('.embed-button');
+ EmbedSelects = $('.embed-select');
+
+ widget.parseContentTypes();
+ EmbedSelects.each(widget.populateSelect);
+
+ EmbedButtons.click(widget.buttonHandler);
+
+ // overload the dismissRelatedLookupPopup function
+ oldDismissRelatedLookupPopup = window.dismissRelatedLookupPopup;
+ window.dismissRelatedLookupPopup = function (win, chosenId) {
+ var name = windowname_to_id(win.name),
+ elem = $('#'+name), val;
+ // if the original element was an embed widget, run our script
+ if (elem.parent().hasClass('embed-widget')) {
+ contenttype = $('select',elem.parent()).val();
+ widget.appendEmbed(elem, contenttype, chosenId);
+ elem.focus();
+ win.close();
+ return;
+ }
+ // otherwise, do what you usually do
+ oldDismissRelatedLookupPopup.apply(this, arguments);
+ }
+
+ // overload the dismissAddAnotherPopup function
+ oldDismissAddAnotherPopup = window.dismissAddAnotherPopup;
+ window.dismissAddAnotherPopup = function (win, newId, newRepr) {
+ var name = windowname_to_id(win.name),
+ elem = $('#'+win.name), val;
+ if (elem.parent().hasClass('embed-widget')) {
+ dismissRelatedLookupPopup(win, newId);
+ }
+ // otherwise, do what you usually do
+ oldDismissAddAnotherPopup.apply(this, arguments);
+ }
+
+ // Add grappelli to the body class if the admin is grappelli. This will allow us to customize styles accordingly.
+ if (window.grappelli) {
+ $(document.body).addClass('grappelli');
+ }
+ },
+ parseContentTypes: function () {
+ var string = widget.EmbedFields.eq(0).attr('data-content-types'),
+ data = $.parseJSON(string),
+ i=0,
+ current_app_label = '',
+ optgroups = {};
+
+ // this loop relies on data being clustered by app
+ for(i=0; i < data.length; i++){
+ item = data[i]
+ // run this next loop every time we encounter a new app label
+ if (item.app_label !== current_app_label) {
+ current_app_label = item.app_label;
+ optgroups[current_app_label] = {}
+ }
+ optgroups[current_app_label][item.verbose_name] = [item.app_label,item.object_name].join('.');
+
+ widget.optgroups = optgroups;
+ }
+ },
+ populateSelect: function () {
+ var $this = $(this),
+ optgroups = widget.optgroups,
+ optgroup_els = {},
+ optgroup_el, group;
+
+ // append a title
+ $this.append('');
+
+ // for each group
+ for (name in optgroups){
+ if(optgroups.hasOwnProperty(name)){
+ // assign the group to variable group, temporarily
+ group = optgroups[name];
+ // create an element for this group and assign it to optgroup_el, temporarily
+ optgroup_el = optgroup_els[name] = $('');
+ // append this element to the select menu
+ $this.append(optgroup_el);
+ // for each item in the group
+ for (name in group) {
+ // append an option to the optgroup
+ optgroup_el.append('');
+ }
+ }
+ }
+ },
+ buttonHandler: function (e) {
+ var $this = $(this),
+ select = $this.prev('select'),
+ embed_widget = $this.closest('.embed-widget'),
+ textarea = embed_widget.children('.embedding').eq(0),
+ val, app_label, object_name,
+ href,
+ win;
+
+ // prevent the button from submitting the form
+ e.preventDefault();
+
+ // handle the case that they haven't chosen a type to embed
+ if (select.val()==='') {
+ alert('Please select a media type to embed.');
+ textarea.focus();
+ return;
+ }
+
+ // split the val into app and object
+ val = select.val();
+ app_label = val.split('.')[0];
+ object_name = val.split('.')[1];
+
+ // generate the url for the popup
+ // TODO: Find a better way to get the admin URL if possible. This will break if the URL patterns for the admin ever change.
+ href=['../../../', app_label, '/', object_name, '/?pop=1'].join('');
+
+ // open a new window
+ win = window.open(href, id_to_windowname(textarea.attr('id')), 'height=500,width=980,resizable=yes,scrollbars=yes');
+ },
+ appendEmbed: function (textarea, embed_type, embed_id) {
+ var $textarea = $(textarea),
+ textarea = $textarea[0], // make sure we're *not* working with a jQuery object
+ current_selection = [textarea.selectionStart, textarea.selectionEnd],
+ current_text = $textarea.val(),
+ embed_string = ['{% embed', embed_type, embed_id, '%}'].join(' '),
+ new_text = current_text.substring(0, current_selection[0]) + embed_string + current_text.substring(current_selection[1]),
+ new_cursor_pos = current_selection[0]+embed_string.length;
+ $textarea.val(new_text);
+ textarea.setSelectionRange(new_cursor_pos, new_cursor_pos);
+ }
+ }
+
+ $(widget.init);
+}(django.jQuery));
\ No newline at end of file
diff --git a/philo/templatetags/collections.py b/philo/templatetags/collections.py
index 38b3f91..e9db2bd 100644
--- a/philo/templatetags/collections.py
+++ b/philo/templatetags/collections.py
@@ -1,3 +1,8 @@
+"""
+The collection template tags are automatically included as builtins if :mod:`philo` is an installed app.
+
+"""
+
from django import template
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
@@ -21,9 +26,15 @@ class MembersofNode(template.Node):
return ''
-def do_membersof(parser, token):
+@register.tag
+def membersof(parser, token):
"""
- {% membersof with . as %}
+ Given a collection and a content type, sets the results of :meth:`collection.members.with_model <.CollectionMemberManager.with_model>` as a variable in the context.
+
+ Usage::
+
+ {% membersof with . as %}
+
"""
params=token.split_contents()
tag = params[0]
@@ -36,7 +47,7 @@ def do_membersof(parser, token):
try:
app_label, model = params[3].strip('"').split('.')
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
except ValueError:
raise template.TemplateSyntaxError('"%s" template tag option "with" requires an argument of the form app_label.model (see django.contrib.contenttypes)' % tag)
except ContentType.DoesNotExist:
@@ -45,7 +56,4 @@ def do_membersof(parser, token):
if params[4] != 'as':
raise template.TemplateSyntaxError('"%s" template tag requires the fifth parameter to be "as"' % tag)
- return MembersofNode(collection=params[1], model=ct.model_class(), as_var=params[5])
-
-
-register.tag('membersof', do_membersof)
\ No newline at end of file
+ return MembersofNode(collection=params[1], model=ct.model_class(), as_var=params[5])
\ No newline at end of file
diff --git a/philo/templatetags/containers.py b/philo/templatetags/containers.py
index c5fd445..fdcd82c 100644
--- a/philo/templatetags/containers.py
+++ b/philo/templatetags/containers.py
@@ -1,13 +1,38 @@
+"""
+The container template tags are automatically included as builtins if :mod:`philo` is an installed app.
+
+"""
+
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.db.models import Q
+from django.utils.safestring import SafeUnicode, mark_safe
register = template.Library()
+CONTAINER_CONTEXT_KEY = 'philo_container_context'
+
+
+class ContainerContext(object):
+ def __init__(self, page):
+ self.page = page
+
+ def get_contentlets(self):
+ if not hasattr(self, '_contentlets'):
+ self._contentlets = dict(((c.name, c) for c in self.page.contentlets.all()))
+ return self._contentlets
+
+ def get_references(self):
+ if not hasattr(self, '_references'):
+ references = self.page.contentreferences.all()
+ self._references = dict((((c.name, ContentType.objects.get_for_id(c.content_type_id)), c) for c in references))
+ return self._references
+
+
class ContainerNode(template.Node):
def __init__(self, name, references=None, as_var=None):
self.name = name
@@ -15,53 +40,54 @@ class ContainerNode(template.Node):
self.references = references
def render(self, context):
- content = settings.TEMPLATE_STRING_IF_INVALID
- if 'page' in context:
- container_content = self.get_container_content(context)
- else:
- container_content = None
+ container_content = self.get_container_content(context)
if self.as_var:
context[self.as_var] = container_content
return ''
- if not container_content:
- return ''
-
return container_content
def get_container_content(self, context):
- page = context['page']
+ try:
+ container_context = context.render_context[CONTAINER_CONTEXT_KEY]
+ except KeyError:
+ try:
+ page = context['page']
+ except KeyError:
+ return settings.TEMPLATE_STRING_IF_INVALID
+
+ container_context = ContainerContext(page)
+ context.render_context[CONTAINER_CONTEXT_KEY] = container_context
+
if self.references:
# Then it's a content reference.
try:
- contentreference = page.contentreferences.get(name__exact=self.name, content_type=self.references)
- content = contentreference.content
- except ObjectDoesNotExist:
+ contentreference = container_context.get_references()[(self.name, self.references)]
+ except KeyError:
content = ''
+ else:
+ content = contentreference.content
else:
# Otherwise it's a contentlet.
try:
- contentlet = page.contentlets.get(name__exact=self.name)
- if '{%' in contentlet.content or '{{' in contentlet.content:
- try:
- content = template.Template(contentlet.content, name=contentlet.name).render(context)
- except template.TemplateSyntaxError, error:
- if settings.DEBUG:
- content = ('[Error parsing contentlet \'%s\': %s]' % (self.name, error))
- else:
- content = settings.TEMPLATE_STRING_IF_INVALID
- else:
- content = contentlet.content
- except ObjectDoesNotExist:
- content = settings.TEMPLATE_STRING_IF_INVALID
- content = mark_safe(content)
+ contentlet = container_context.get_contentlets()[self.name]
+ except KeyError:
+ content = ''
+ else:
+ content = contentlet.content
return content
-def do_container(parser, token):
+@register.tag
+def container(parser, token):
"""
- {% container [[references ] as ] %}
+ If a template using this tag is used to render a :class:`.Page`, that :class:`.Page` will have associated content which can be set in the admin interface. If a content type is referenced, then a :class:`.ContentReference` object will be created; otherwise, a :class:`.Contentlet` object will be created.
+
+ Usage::
+
+ {% container [[references .] as ] %}
+
"""
params = token.split_contents()
if len(params) >= 2:
@@ -76,7 +102,7 @@ def do_container(parser, token):
if option_token == 'references':
try:
app_label, model = remaining_tokens.pop(0).strip('"').split('.')
- references = ContentType.objects.get(app_label=app_label, model=model)
+ references = ContentType.objects.get_by_natural_key(app_label, model)
except IndexError:
raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument specifying a content type' % tag)
except ValueError:
@@ -94,6 +120,3 @@ def do_container(parser, token):
else: # error
raise template.TemplateSyntaxError('"%s" template tag provided without arguments (at least one required)' % tag)
-
-
-register.tag('container', do_container)
diff --git a/philo/templatetags/embed.py b/philo/templatetags/embed.py
index eb4cd68..b024b1b 100644
--- a/philo/templatetags/embed.py
+++ b/philo/templatetags/embed.py
@@ -1,8 +1,13 @@
+"""
+The embed template tags are automatically included as builtins if :mod:`philo` is an installed app.
+
+"""
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
+
+from philo.utils.templates import LOADED_TEMPLATE_ATTR
register = template.Library()
@@ -280,17 +285,29 @@ def parse_content_type(bit, tagname):
except ValueError:
raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tagname)
try:
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
except ContentType.DoesNotExist:
raise template.TemplateSyntaxError('"%s" template tag requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tagname)
return ct
-def do_embed(parser, token):
+@register.tag
+def embed(parser, token):
"""
- The {% embed %} tag can be used in two ways:
- {% embed . with %} :: Sets which template will be used to render a particular model.
- {% embed (. || ) [= ...] %} :: Embeds the instance specified by the given parameters in the document with the previously-specified template. Any kwargs provided will be passed into the context of the template.
+ The {% embed %} tag can be used in two ways.
+
+ First, to set which template will be used to render a particular model. This declaration can be placed in a base template and will propagate into all templates that extend that template.
+
+ Syntax::
+
+ {% embed . with %}
+
+ Second, to embed a specific model instance in the document with a template specified earlier in the template or in a parent template using the first syntax. The instance can be specified as a content type and pk or as a context variable. Any kwargs provided will be passed into the context of the template.
+
+ Syntax::
+
+ {% embed (. || ) [= ...] %}
+
"""
bits = token.split_contents()
tag = bits.pop(0)
@@ -330,7 +347,4 @@ def do_embed(parser, token):
except ValueError:
return EmbedNode(ct, object_pk=parser.compile_filter(pk), kwargs=kwargs)
else:
- return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)
-
-
-register.tag('embed', do_embed)
\ No newline at end of file
+ return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)
\ No newline at end of file
diff --git a/philo/templatetags/include_string.py b/philo/templatetags/include_string.py
index 260dcff..cb0a8b5 100644
--- a/philo/templatetags/include_string.py
+++ b/philo/templatetags/include_string.py
@@ -6,8 +6,6 @@ register = template.Library()
class IncludeStringNode(template.Node):
- """The passed variable is expected to be a string of template code to be rendered with
- the current context."""
def __init__(self, string):
self.string = string
@@ -23,16 +21,18 @@ class IncludeStringNode(template.Node):
return settings.TEMPLATE_STRING_IF_INVALID
-def do_include_string(parser, token):
+@register.tag
+def include_string(parser, token):
"""
- Include a flat string by interpreting it as a template.
- {% include_string %}
+ Include a flat string by interpreting it as a template. The compiled template will be rendered with the current context.
+
+ Usage::
+
+ {% include_string %}
+
"""
bits = token.split_contents()
if len(bits) != 2:
raise TemplateSyntaxError("%r tag takes one argument: the template string to be included" % bits[0])
string = parser.compile_filter(bits[1])
- return IncludeStringNode(string)
-
-
-register.tag('include_string', do_include_string)
\ No newline at end of file
+ return IncludeStringNode(string)
\ No newline at end of file
diff --git a/philo/templatetags/nodes.py b/philo/templatetags/nodes.py
index 5ae507d..52da236 100644
--- a/philo/templatetags/nodes.py
+++ b/philo/templatetags/nodes.py
@@ -1,9 +1,15 @@
+"""
+The node template tags are automatically included as builtins if :mod:`philo` is an installed app.
+
+"""
+
from django import template
from django.conf import settings
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
@@ -33,7 +39,7 @@ class NodeURLNode(template.Node):
if self.with_obj is None and self.view_name is None:
url = node.get_absolute_url()
else:
- if not node.view.accepts_subpath:
+ if not node.accepts_subpath:
return settings.TEMPLATE_STRING_IF_INVALID
if self.with_obj is not None:
@@ -64,13 +70,18 @@ class NodeURLNode(template.Node):
return url
-@register.tag(name='node_url')
-def do_node_url(parser, token):
+@register.tag
+def node_url(parser, token):
"""
- {% node_url [for ] [as ] %}
- {% node_url with [for ] [as ] %}
- {% node_url [ [ ...] ] [for ] [as ] %}
- {% node_url [= [= ...] ] [for ] [as ]%}
+ The :ttag:`node_url` tag allows access to :meth:`.View.reverse` from a template for a :class:`.Node`. By default, the :class:`.Node` that is used for the call is pulled from the context variable ``node``; however, this can be overridden with the ``[for ]`` option.
+
+ Usage::
+
+ {% node_url [for ] [as ] %}
+ {% node_url with [for ] [as ] %}
+ {% node_url [ [ ...] ] [for ] [as ] %}
+ {% node_url [= [= ...] ] [for ] [as ] %}
+
"""
params = token.split_contents()
tag = params[0]
diff --git a/philo/tests.py b/philo/tests.py
index a0e0184..15ce752 100644
--- a/philo/tests.py
+++ b/philo/tests.py
@@ -3,15 +3,16 @@ import traceback
from django import template
from django.conf import settings
-from django.db import connection
+from django.contrib.contenttypes.models import ContentType
+from django.db import connection, models
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 django.test.utils import setup_test_template_loader, restore_template_loaders
+from django.utils.datastructures import SortedDict
-from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
from philo.exceptions import AncestorDoesNotExist
-from philo.models import Node, Page, Template
+from philo.models import Node, Page, Template, Tag
class TemplateTestCase(TestCase):
@@ -56,7 +57,7 @@ class TemplateTestCase(TestCase):
# Cleanup
settings.TEMPLATE_DEBUG = old_td
settings.TEMPLATE_STRING_IF_INVALID = old_invalid
- loader.template_source_loaders = old_template_loaders
+ restore_template_loaders()
self.assertEqual(failures, [], "Tests failed:\n%s\n%s" % ('-'*70, ("\n%s\n" % ('-'*70)).join(failures)))
@@ -64,43 +65,43 @@ class TemplateTestCase(TestCase):
def get_template_tests(self):
# SYNTAX --
# 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
- blog = Blog.objects.all()[0]
+ embedded = Tag.objects.get(pk=1)
return {
# EMBED INCLUSION HANDLING
- 'embed01': ('{{ embedded.title|safe }}', {'embedded': blog}, blog.title),
- 'embed02': ('{{ embedded.title|safe }}{{ var1 }}{{ var2 }}', {'embedded': blog}, blog.title),
- 'embed03': ('{{ embedded.title|safe }} is a lie!', {'embedded': blog}, '%s is a lie!' % blog.title),
+ 'embed01': ('{{ embedded.name|safe }}', {'embedded': embedded}, embedded.name),
+ 'embed02': ('{{ embedded.name|safe }}{{ var1 }}{{ var2 }}', {'embedded': embedded}, embedded.name),
+ 'embed03': ('{{ embedded.name|safe }} is a lie!', {'embedded': embedded}, '%s is a lie!' % embedded.name),
# Simple template structure with embed
- 'simple01': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog 1 %}Simple{% block one %}{% endblock %}', {'blog': blog}, '%sSimple' % blog.title),
- 'simple02': ('{% extends "simple01" %}', {}, '%sSimple' % blog.title),
- 'simple03': ('{% embed penfield.blog with "embed000" %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
- 'simple04': ('{% embed penfield.blog 1 %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
- 'simple05': ('{% embed penfield.blog with "embed01" %}{% embed blog %}', {'blog': blog}, blog.title),
+ 'simple01': ('{% embed philo.tag with "embed01" %}{% embed philo.tag 1 %}Simple{% block one %}{% endblock %}', {'embedded': embedded}, '%sSimple' % embedded.name),
+ 'simple02': ('{% extends "simple01" %}', {}, '%sSimple' % embedded.name),
+ 'simple03': ('{% embed philo.tag with "embed000" %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+ 'simple04': ('{% embed philo.tag 1 %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+ 'simple05': ('{% embed philo.tag with "embed01" %}{% embed embedded %}', {'embedded': embedded}, embedded.name),
# Kwargs
- 'kwargs01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1="hi" var2=lo %}', {'lo': 'lo'}, '%shilo' % blog.title),
+ 'kwargs01': ('{% embed philo.tag with "embed02" %}{% embed philo.tag 1 var1="hi" var2=lo %}', {'lo': 'lo'}, '%shilo' % embedded.name),
# Filters/variables
- 'filters01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1=hi|first var2=lo|slice:"3" %}', {'hi': ["These", "words"], 'lo': 'lower'}, '%sTheselow' % blog.title),
- 'filters02': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog entry %}', {'entry': 1}, blog.title),
+ 'filters01': ('{% embed philo.tag with "embed02" %}{% embed philo.tag 1 var1=hi|first var2=lo|slice:"3" %}', {'hi': ["These", "words"], 'lo': 'lower'}, '%sTheselow' % embedded.name),
+ 'filters02': ('{% embed philo.tag with "embed01" %}{% embed philo.tag entry %}', {'entry': 1}, embedded.name),
# Blocky structure
'block01': ('{% block one %}Hello{% endblock %}', {}, 'Hello'),
- 'block02': ('{% extends "simple01" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s" % (blog.title, blog.title)),
- 'block03': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s is a lie!" % (blog.title, blog.title)),
+ 'block02': ('{% extends "simple01" %}{% block one %}{% embed philo.tag 1 %}{% endblock %}', {}, "%sSimple%s" % (embedded.name, embedded.name)),
+ 'block03': ('{% extends "simple01" %}{% embed philo.tag with "embed03" %}{% block one %}{% embed philo.tag 1 %}{% endblock %}', {}, "%sSimple%s is a lie!" % (embedded.name, embedded.name)),
# Blocks and includes
- 'block-include01': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% include "simple01" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%sSimple%s is a lie!" % (blog.title, blog.title, blog.title)),
- 'block-include02': ('{% extends "simple01" %}{% block one %}{% include "simple04" %}{% embed penfield.blog with "embed03" %}{% include "simple04" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s%s is a lie!%s is a lie!" % (blog.title, blog.title, blog.title, blog.title)),
+ 'block-include01': ('{% extends "simple01" %}{% embed philo.tag with "embed03" %}{% block one %}{% include "simple01" %}{% embed philo.tag 1 %}{% endblock %}', {}, "%sSimple%sSimple%s is a lie!" % (embedded.name, embedded.name, embedded.name)),
+ 'block-include02': ('{% extends "simple01" %}{% block one %}{% include "simple04" %}{% embed philo.tag with "embed03" %}{% include "simple04" %}{% embed philo.tag 1 %}{% endblock %}', {}, "%sSimple%s%s is a lie!%s is a lie!" % (embedded.name, embedded.name, embedded.name, embedded.name)),
# Tests for more complex situations...
'complex01': ('{% block one %}{% endblock %}complex{% block two %}{% endblock %}', {}, 'complex'),
'complex02': ('{% extends "complex01" %}', {}, 'complex'),
- 'complex03': ('{% extends "complex02" %}{% embed penfield.blog with "embed01" %}', {}, 'complex'),
- 'complex04': ('{% extends "complex03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, '%scomplex' % blog.title),
- 'complex05': ('{% extends "complex03" %}{% block one %}{% include "simple04" %}{% endblock %}', {}, '%scomplex' % blog.title),
+ 'complex03': ('{% extends "complex02" %}{% embed philo.tag with "embed01" %}', {}, 'complex'),
+ 'complex04': ('{% extends "complex03" %}{% block one %}{% embed philo.tag 1 %}{% endblock %}', {}, '%scomplex' % embedded.name),
+ 'complex05': ('{% extends "complex03" %}{% block one %}{% include "simple04" %}{% endblock %}', {}, '%scomplex' % embedded.name),
}
@@ -110,35 +111,19 @@ class NodeURLTestCase(TestCase):
fixtures = ['test_fixtures.json']
def setUp(self):
- if 'south' in settings.INSTALLED_APPS:
- from south.management.commands.migrate import Command
- command = Command()
- command.handle(all_apps=True)
-
self.templates = [
- ("{% node_url %}", "/root/second/"),
- ("{% node_url for node2 %}", "/root/second2/"),
- ("{% node_url as hello %}
{{ hello|slice:'1:' }}
", "
root/second/
"),
- ("{% node_url for nodes|first %}", "/root/"),
- ("{% node_url with entry %}", settings.TEMPLATE_STRING_IF_INVALID),
- ("{% node_url with entry for node2 %}", "/root/second2/2010/10/20/first-entry"),
- ("{% node_url with tag for node2 %}", "/root/second2/tags/test-tag/"),
- ("{% node_url with date for node2 %}", "/root/second2/2010/10/20"),
- ("{% node_url entries_by_day year=date|date:'Y' month=date|date:'m' day=date|date:'d' for node2 as goodbye %}{{ goodbye|upper }}", "/ROOT/SECOND2/2010/10/20"),
- ("{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", "/root/second2/2010/10"),
- ("{% node_url entries_by_year year=date|date:'Y' for node2 %}", "/root/second2/2010/"),
+ ("{% node_url %}", "/root/second"),
+ ("{% node_url for node2 %}", "/root/second2"),
+ ("{% node_url as hello %}
{{ hello|slice:'1:' }}
", "
root/second
"),
+ ("{% node_url for nodes|first %}", "/root"),
]
nodes = Node.objects.all()
- blog = Blog.objects.all()[0]
self.context = template.Context({
'node': nodes.get(slug='second'),
'node2': nodes.get(slug='second2'),
'nodes': nodes,
- 'entry': BlogEntry.objects.all()[0],
- 'tag': blog.entry_tags.all()[0],
- 'date': blog.entry_dates['day'][0]
})
def test_nodeurl(self):
@@ -149,12 +134,6 @@ class TreePathTestCase(TestCase):
urls = 'philo.urls'
fixtures = ['test_fixtures.json']
- def setUp(self):
- if 'south' in settings.INSTALLED_APPS:
- from south.management.commands.migrate import Command
- command = Command()
- command.handle(all_apps=True)
-
def assertQueryLimit(self, max, expected_result, *args, **kwargs):
# As a rough measure of efficiency, limit the number of queries required for a given operation.
settings.DEBUG = True
@@ -193,7 +172,7 @@ class TreePathTestCase(TestCase):
# Non-absolute result (binary search)
self.assertQueryLimit(2, (second2, 'sub/path/tail'), 'root/second2/sub/path/tail', absolute_result=False)
- self.assertQueryLimit(3, (second2, 'sub/'), 'root/second2/sub/', absolute_result=False)
+ self.assertQueryLimit(3, (second2, 'sub'), 'root/second2/sub/', absolute_result=False)
self.assertQueryLimit(2, e, 'invalid/path/1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7/8/9/0', absolute_result=False)
self.assertQueryLimit(1, (root, None), 'root', absolute_result=False)
self.assertQueryLimit(2, (second2, None), 'root/second2', absolute_result=False)
@@ -203,8 +182,8 @@ class TreePathTestCase(TestCase):
self.assertQueryLimit(1, (second2, None), 'second2', root=root, absolute_result=False)
self.assertQueryLimit(2, (third, None), 'second/third', root=root, absolute_result=False)
- # Preserve trailing slash
- self.assertQueryLimit(2, (second2, 'sub/path/tail/'), 'root/second2/sub/path/tail/', absolute_result=False)
+ # Eliminate trailing slash
+ self.assertQueryLimit(2, (second2, 'sub/path/tail'), 'root/second2/sub/path/tail/', absolute_result=False)
# Speed increase for leaf nodes - should this be tested?
self.assertQueryLimit(1, (fifth, 'sub/path/tail/len/five'), 'root/second/third/fourth/fifth/sub/path/tail/len/five', absolute_result=False)
@@ -223,3 +202,17 @@ class TreePathTestCase(TestCase):
self.assertQueryLimit(1, 'second/third', root, callable=third.get_path)
self.assertQueryLimit(1, e, third, callable=second2.get_path)
self.assertQueryLimit(1, '? - ?', root, ' - ', 'title', callable=third.get_path)
+
+
+class ContainerTestCase(TestCase):
+ def test_simple_containers(self):
+ t = Template(code="{% container one %}{% container two %}{% container three %}{% container two %}")
+ contentlet_specs, contentreference_specs = t.containers
+ self.assertEqual(len(contentreference_specs.keyOrder), 0)
+ self.assertEqual(contentlet_specs, ['one', 'two', 'three'])
+
+ ct = ContentType.objects.get_for_model(Tag)
+ t = Template(code="{% container one references philo.tag as tag1 %}{% container two references philo.tag as tag2 %}{% container one references philo.tag as tag1 %}")
+ contentlet_specs, contentreference_specs = t.containers
+ self.assertEqual(len(contentlet_specs), 0)
+ self.assertEqual(contentreference_specs, SortedDict([('one', ct), ('two', ct)]))
diff --git a/philo/urls.py b/philo/urls.py
index 0363224..d4dfc7b 100644
--- a/philo/urls.py
+++ b/philo/urls.py
@@ -1,4 +1,5 @@
from django.conf.urls.defaults import patterns, url
+
from philo.views import node_view
diff --git a/philo/utils.py b/philo/utils/__init__.py
similarity index 59%
rename from philo/utils.py
rename to philo/utils/__init__.py
index 57f949e..34ad1f0 100644
--- a/philo/utils.py
+++ b/philo/utils/__init__.py
@@ -1,8 +1,31 @@
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator, EmptyPage
-from django.template import Context
-from django.template.loader_tags import ExtendsNode, ConstantIncludeNode
+
+
+def fattr(*args, **kwargs):
+ """
+ Returns a wrapper which takes a function as its only argument and sets the key/value pairs passed in with kwargs as attributes on that function. This can be used as a decorator.
+
+ Example::
+
+ >>> from philo.utils import fattr
+ >>> @fattr(short_description="Hello World!")
+ ... def x():
+ ... pass
+ ...
+ >>> x.short_description
+ 'Hello World!'
+
+ """
+ def wrapper(function):
+ for key in kwargs:
+ setattr(function, key, kwargs[key])
+ return function
+ return wrapper
+
+
+### ContentTypeLimiters
class ContentTypeLimiter(object):
@@ -14,13 +37,16 @@ class ContentTypeLimiter(object):
class ContentTypeRegistryLimiter(ContentTypeLimiter):
+ """Can be used to limit the choices for a :class:`ForeignKey` or :class:`ManyToManyField` to the :class:`ContentType`\ s which have been registered with this limiter."""
def __init__(self):
self.classes = []
def register_class(self, cls):
+ """Registers a model class with this limiter."""
self.classes.append(cls)
def unregister_class(self, cls):
+ """Unregisters a model class from this limiter."""
self.classes.remove(cls)
def q_object(self):
@@ -37,6 +63,13 @@ class ContentTypeRegistryLimiter(ContentTypeLimiter):
class ContentTypeSubclassLimiter(ContentTypeLimiter):
+ """
+ Can be used to limit the choices for a :class:`ForeignKey` or :class:`ManyToManyField` to the :class:`ContentType`\ s for all non-abstract models which subclass the class passed in on instantiation.
+
+ :param cls: The class whose non-abstract subclasses will be valid choices.
+ :param inclusive: Whether ``cls`` should also be considered a valid choice (if it is a non-abstract subclass of :class:`models.Model`)
+
+ """
def __init__(self, cls, inclusive=False):
self.cls = cls
self.inclusive = inclusive
@@ -59,17 +92,18 @@ class ContentTypeSubclassLimiter(ContentTypeLimiter):
return models.Q(pk__in=contenttype_pks)
-def fattr(*args, **kwargs):
- def wrapper(function):
- for key in kwargs:
- setattr(function, key, kwargs[key])
- return function
- return wrapper
+### Pagination
def paginate(objects, per_page=None, page_number=1):
"""
- Given a list of objects, return a (paginator, page, objects) tuple.
+ Given a list of objects, return a (``paginator``, ``page``, ``objects``) tuple.
+
+ :param objects: The list of objects to be paginated.
+ :param per_page: The number of objects per page.
+ :param page_number: The number of the current page.
+ :returns tuple: (``paginator``, ``page``, ``objects``) where ``paginator`` is a :class:`django.core.paginator.Paginator` instance, ``page`` is the result of calling :meth:`Paginator.page` with ``page_number``, and objects is ``page.objects``. Any of the return values which can't be calculated will be returned as ``None``.
+
"""
try:
per_page = int(per_page)
@@ -104,21 +138,4 @@ def paginate(objects, per_page=None, page_number=1):
else:
objects = page.object_list
- return paginator, page, objects
-
-
-LOADED_TEMPLATE_ATTR = '_philo_loaded_template'
-BLANK_CONTEXT = Context()
-
-
-def get_extended(self):
- return self.get_parent(BLANK_CONTEXT)
-
-
-def get_included(self):
- return self.template
-
-
-# We ignore the IncludeNode because it will never work in a blank context.
-setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
-setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
\ No newline at end of file
+ return paginator, page, objects
\ No newline at end of file
diff --git a/philo/utils/entities.py b/philo/utils/entities.py
new file mode 100644
index 0000000..830276e
--- /dev/null
+++ b/philo/utils/entities.py
@@ -0,0 +1,216 @@
+from functools import partial
+from UserDict import DictMixin
+
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+
+from philo.utils.lazycompat import SimpleLazyObject
+
+
+### AttributeMappers
+
+
+class AttributeMapper(object, DictMixin):
+ """
+ Given an :class:`~philo.models.base.Entity` subclass instance, this class allows dictionary-style access to the :class:`~philo.models.base.Entity`'s :class:`~philo.models.base.Attribute`\ s. In order to prevent unnecessary queries, the :class:`AttributeMapper` will cache all :class:`~philo.models.base.Attribute`\ s and the associated python values when it is first accessed.
+
+ :param entity: The :class:`~philo.models.base.Entity` subclass instance whose :class:`~philo.models.base.Attribute`\ s will be made accessible.
+
+ """
+ def __init__(self, entity):
+ self.entity = entity
+ self.clear_cache()
+
+ def __getitem__(self, key):
+ """Returns the ultimate python value of the :class:`~philo.models.base.Attribute` with the given ``key`` from the cache, populating the cache if necessary."""
+ if not self._cache_filled:
+ self._fill_cache()
+ return self._cache[key]
+
+ def __setitem__(self, key, value):
+ """Given a python value, sets the value of the :class:`~philo.models.base.Attribute` with the given ``key`` to that value."""
+ # Prevent circular import.
+ from philo.models.base import JSONValue, ForeignKeyValue, ManyToManyValue, Attribute
+ old_attr = self.get_attribute(key)
+ if old_attr and old_attr.entity_content_type == ContentType.objects.get_for_model(self.entity) and old_attr.entity_object_id == self.entity.pk:
+ attribute = old_attr
+ else:
+ attribute = Attribute(key=key)
+ attribute.entity = self.entity
+ attribute.full_clean()
+
+ if isinstance(value, models.query.QuerySet):
+ value_class = ManyToManyValue
+ elif isinstance(value, models.Model):
+ value_class = ForeignKeyValue
+ else:
+ value_class = JSONValue
+
+ attribute.set_value(value=value, value_class=value_class)
+ self._cache[key] = attribute.value.value
+ self._attributes_cache[key] = attribute
+
+ def get_attributes(self):
+ """Returns an iterable of all of the :class:`~philo.models.base.Entity`'s :class:`~philo.models.base.Attribute`\ s."""
+ return self.entity.attribute_set.all()
+
+ def get_attribute(self, key, default=None):
+ """Returns the :class:`~philo.models.base.Attribute` instance with the given ``key`` from the cache, populating the cache if necessary, or ``default`` if no such attribute is found."""
+ if not self._cache_filled:
+ self._fill_cache()
+ return self._attributes_cache.get(key, default)
+
+ def keys(self):
+ """Returns the keys from the cache, first populating the cache if necessary."""
+ if not self._cache_filled:
+ self._fill_cache()
+ return self._cache.keys()
+
+ def items(self):
+ """Returns the items from the cache, first populating the cache if necessary."""
+ if not self._cache_filled:
+ self._fill_cache()
+ return self._cache.items()
+
+ def values(self):
+ """Returns the values from the cache, first populating the cache if necessary."""
+ if not self._cache_filled:
+ self._fill_cache()
+ return self._cache.values()
+
+ def _fill_cache(self):
+ if self._cache_filled:
+ return
+
+ attributes = self.get_attributes()
+ value_lookups = {}
+
+ for a in attributes:
+ value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
+ self._attributes_cache[a.key] = a
+
+ values_bulk = dict(((ct_pk, SimpleLazyObject(partial(ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk, pks))) for ct_pk, pks in value_lookups.items()))
+
+ cache = {}
+
+ for a in attributes:
+ cache[a.key] = SimpleLazyObject(partial(self._lazy_value_from_bulk, values_bulk, a))
+ a._value_cache = cache[a.key]
+
+ self._cache.update(cache)
+ self._cache_filled = True
+
+ def _lazy_value_from_bulk(self, bulk, attribute):
+ v = bulk[attribute.value_content_type_id].get(attribute.value_object_id)
+ return getattr(v, 'value', None)
+
+ def clear_cache(self):
+ """Clears the cache."""
+ self._cache = {}
+ self._attributes_cache = {}
+ self._cache_filled = False
+
+
+class LazyAttributeMapperMixin(object):
+ """In some cases, it may be that only one attribute value needs to be fetched. In this case, it is more efficient to avoid populating the cache whenever possible. This mixin overrides the :meth:`__getitem__` and :meth:`get_attribute` methods to prevent their populating the cache. If the cache has been populated (i.e. through :meth:`keys`, :meth:`values`, etc.), then the value or attribute will simply be returned from the cache."""
+ def __getitem__(self, key):
+ if key not in self._cache and not self._cache_filled:
+ self._add_to_cache(key)
+ return self._cache[key]
+
+ def get_attribute(self, key, default=None):
+ if key not in self._attributes_cache and not self._cache_filled:
+ self._add_to_cache(key)
+ return self._attributes_cache.get(key, default)
+
+ def _raw_get_attribute(self, key):
+ return self.get_attributes().get(key=key)
+
+ def _add_to_cache(self, key):
+ from philo.models.base import Attribute
+ try:
+ attr = self._raw_get_attribute(key)
+ except Attribute.DoesNotExist:
+ raise KeyError
+ else:
+ val = getattr(attr.value, 'value', None)
+ self._cache[key] = val
+ self._attributes_cache[key] = attr
+
+
+class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
+ def get_attributes(self):
+ return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
+
+
+class TreeAttributeMapper(AttributeMapper):
+ """The :class:`~philo.models.base.TreeEntity` class allows the inheritance of :class:`~philo.models.base.Attribute`\ s down the tree. This mapper will return the most recently declared :class:`~philo.models.base.Attribute` among the :class:`~philo.models.base.TreeEntity`'s ancestors or set an attribute on the :class:`~philo.models.base.Entity` it is attached to."""
+ def get_attributes(self):
+ """Returns a list of :class:`~philo.models.base.Attribute`\ s sorted by increasing parent level. When used to populate the cache, this will cause :class:`~philo.models.base.Attribute`\ s on the root to be overwritten by those on its children, etc."""
+ from philo.models import Attribute
+ ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
+ ct = ContentType.objects.get_for_model(self.entity)
+ attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys())
+ return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
+
+
+class LazyTreeAttributeMapper(LazyAttributeMapperMixin, TreeAttributeMapper):
+ def get_attributes(self):
+ from philo.models import Attribute
+ ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
+ ct = ContentType.objects.get_for_model(self.entity)
+ attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()).exclude(key__in=self._cache.keys())
+ return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
+
+ def _raw_get_attribute(self, key):
+ from philo.models import Attribute
+ ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
+ ct = ContentType.objects.get_for_model(self.entity)
+ try:
+ attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys(), key=key)
+ sorted_attrs = sorted(attrs, key=lambda x: ancestors[x.entity_object_id], reverse=True)
+ return sorted_attrs[0]
+ except IndexError:
+ raise Attribute.DoesNotExist
+
+
+class PassthroughAttributeMapper(AttributeMapper):
+ """
+ Given an iterable of :class:`Entities `, this mapper will fetch an :class:`AttributeMapper` for each one. Lookups will return the value from the first :class:`AttributeMapper` which has an entry for a given key. Assignments will be made to the first :class:`.Entity` in the iterable.
+
+ :param entities: An iterable of :class:`.Entity` subclass instances.
+
+ """
+ def __init__(self, entities):
+ self._attributes = [e.attributes for e in entities]
+ super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
+
+ def _fill_cache(self):
+ if self._cache_filled:
+ return
+
+ for a in reversed(self._attributes):
+ a._fill_cache()
+ self._attributes_cache.update(a._attributes_cache)
+ self._cache.update(a._cache)
+
+ self._cache_filled = True
+
+ def get_attributes(self):
+ raise NotImplementedError
+
+ def clear_cache(self):
+ super(PassthroughAttributeMapper, self).clear_cache()
+ for a in self._attributes:
+ a.clear_cache()
+
+
+class LazyPassthroughAttributeMapper(LazyAttributeMapperMixin, PassthroughAttributeMapper):
+ """The :class:`LazyPassthroughAttributeMapper` is lazy in that it tries to avoid accessing the :class:`AttributeMapper`\ s that it uses for lookups. However, those :class:`AttributeMapper`\ s may or may not be lazy themselves."""
+ def _raw_get_attribute(self, key):
+ from philo.models import Attribute
+ for a in self._attributes:
+ attr = a.get_attribute(key)
+ if attr is not None:
+ return attr
+ raise Attribute.DoesNotExist
\ No newline at end of file
diff --git a/philo/utils/lazycompat.py b/philo/utils/lazycompat.py
new file mode 100644
index 0000000..3876562
--- /dev/null
+++ b/philo/utils/lazycompat.py
@@ -0,0 +1,97 @@
+try:
+ from django.utils.functional import empty, LazyObject, SimpleLazyObject
+except ImportError:
+ # Supply LazyObject and SimpleLazyObject for django < r16308
+ import operator
+
+
+ empty = object()
+ def new_method_proxy(func):
+ def inner(self, *args):
+ if self._wrapped is empty:
+ self._setup()
+ return func(self._wrapped, *args)
+ return inner
+
+ class LazyObject(object):
+ """
+ A wrapper for another class that can be used to delay instantiation of the
+ wrapped class.
+
+ By subclassing, you have the opportunity to intercept and alter the
+ instantiation. If you don't need to do that, use SimpleLazyObject.
+ """
+ def __init__(self):
+ self._wrapped = empty
+
+ __getattr__ = new_method_proxy(getattr)
+
+ def __setattr__(self, name, value):
+ if name == "_wrapped":
+ # Assign to __dict__ to avoid infinite __setattr__ loops.
+ self.__dict__["_wrapped"] = value
+ else:
+ if self._wrapped is empty:
+ self._setup()
+ setattr(self._wrapped, name, value)
+
+ def __delattr__(self, name):
+ if name == "_wrapped":
+ raise TypeError("can't delete _wrapped.")
+ if self._wrapped is empty:
+ self._setup()
+ delattr(self._wrapped, name)
+
+ def _setup(self):
+ """
+ Must be implemented by subclasses to initialise the wrapped object.
+ """
+ raise NotImplementedError
+
+ # introspection support:
+ __members__ = property(lambda self: self.__dir__())
+ __dir__ = new_method_proxy(dir)
+
+
+ class SimpleLazyObject(LazyObject):
+ """
+ A lazy object initialised from any function.
+
+ Designed for compound objects of unknown type. For builtins or objects of
+ known type, use django.utils.functional.lazy.
+ """
+ def __init__(self, func):
+ """
+ Pass in a callable that returns the object to be wrapped.
+
+ If copies are made of the resulting SimpleLazyObject, which can happen
+ in various circumstances within Django, then you must ensure that the
+ callable can be safely run more than once and will return the same
+ value.
+ """
+ self.__dict__['_setupfunc'] = func
+ super(SimpleLazyObject, self).__init__()
+
+ def _setup(self):
+ self._wrapped = self._setupfunc()
+
+ __str__ = new_method_proxy(str)
+ __unicode__ = new_method_proxy(unicode)
+
+ def __deepcopy__(self, memo):
+ if self._wrapped is empty:
+ # We have to use SimpleLazyObject, not self.__class__, because the
+ # latter is proxied.
+ result = SimpleLazyObject(self._setupfunc)
+ memo[id(self)] = result
+ return result
+ else:
+ import copy
+ return copy.deepcopy(self._wrapped, memo)
+
+ # Need to pretend to be the wrapped class, for the sake of objects that care
+ # about this (especially in equality tests)
+ __class__ = property(new_method_proxy(operator.attrgetter("__class__")))
+ __eq__ = new_method_proxy(operator.eq)
+ __hash__ = new_method_proxy(hash)
+ __nonzero__ = new_method_proxy(bool)
\ No newline at end of file
diff --git a/philo/utils/registry.py b/philo/utils/registry.py
new file mode 100644
index 0000000..1673429
--- /dev/null
+++ b/philo/utils/registry.py
@@ -0,0 +1,141 @@
+from django.core.validators import slug_re
+from django.template.defaultfilters import slugify
+from django.utils.encoding import smart_str
+
+
+class RegistryIterator(object):
+ """
+ Wraps the iterator returned by calling ``getattr(registry, iterattr)`` to provide late instantiation of the wrapped iterator and to allow copying of the iterator for even later instantiation.
+
+ :param registry: The object which provides the iterator at ``iterattr``.
+ :param iterattr: The name of the method on ``registry`` that provides the iterator.
+ :param transform: A function which will be called on each result from the wrapped iterator before it is returned.
+
+ """
+ def __init__(self, registry, iterattr='__iter__', transform=lambda x:x):
+ if not hasattr(registry, iterattr):
+ raise AttributeError("Registry has no attribute %s" % iterattr)
+ self.registry = registry
+ self.iterattr = iterattr
+ self.transform = transform
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ if not hasattr(self, '_iter'):
+ self._iter = getattr(self.registry, self.iterattr)()
+
+ return self.transform(self._iter.next())
+
+ def copy(self):
+ """Returns a fresh copy of this iterator."""
+ return self.__class__(self.registry, self.iterattr, self.transform)
+
+
+class RegistrationError(Exception):
+ """Raised if there is a problem registering a object with a :class:`Registry`"""
+ pass
+
+
+class Registry(object):
+ """Holds a registry of arbitrary objects by slug."""
+
+ def __init__(self):
+ self._registry = {}
+
+ def register(self, obj, slug=None, verbose_name=None):
+ """
+ Register an object with the registry.
+
+ :param obj: The object to register.
+ :param slug: The slug which will be used to register the object. If ``slug`` is ``None``, it will be generated from ``verbose_name`` or looked for at ``obj.slug``.
+ :param verbose_name: The verbose name for the object. If ``verbose_name`` is ``None``, it will be looked for at ``obj.verbose_name``.
+ :raises: :class:`RegistrationError` if a different object is already registered with ``slug``, or if ``slug`` is not a valid slug.
+
+ """
+ verbose_name = verbose_name if verbose_name is not None else obj.verbose_name
+
+ if slug is None:
+ slug = getattr(obj, 'slug', slugify(verbose_name))
+ slug = smart_str(slug)
+
+ if not slug_re.search(slug):
+ raise RegistrationError(u"%s is not a valid slug." % slug)
+
+
+ if slug in self._registry:
+ reg = self._registry[slug]
+ if reg['obj'] != obj:
+ raise RegistrationError(u"A different object is already registered as `%s`" % slug)
+ else:
+ self._registry[slug] = {
+ 'obj': obj,
+ 'verbose_name': verbose_name
+ }
+
+ def unregister(self, obj, slug=None):
+ """
+ Unregister an object from the registry.
+
+ :param obj: The object to unregister.
+ :param slug: If provided, the object will only be removed if it was registered with ``slug``. If not provided, the object will be unregistered no matter what slug it was registered with.
+ :raises: :class:`RegistrationError` if ``slug`` is provided and an object other than ``obj`` is registered as ``slug``.
+
+ """
+ if slug is not None:
+ if slug in self._registry:
+ if self._registry[slug]['obj'] == obj:
+ del self._registry[slug]
+ else:
+ raise RegistrationError(u"`%s` is not registered as `%s`" % (obj, slug))
+ else:
+ for slug, reg in self.items():
+ if obj == reg:
+ del self._registry[slug]
+
+ def items(self):
+ """Returns a list of (slug, obj) items in the registry."""
+ return [(slug, self[slug]) for slug in self._registry]
+
+ def values(self):
+ """Returns a list of objects in the registry."""
+ return [self[slug] for slug in self._registry]
+
+ def iteritems(self):
+ """Returns a :class:`RegistryIterator` over the (slug, obj) pairs in the registry."""
+ return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1]['obj']))
+
+ def itervalues(self):
+ """Returns a :class:`RegistryIterator` over the objects in the registry."""
+ return RegistryIterator(self._registry, 'itervalues', lambda x: x['obj'])
+
+ def iterchoices(self):
+ """Returns a :class:`RegistryIterator` over (slug, verbose_name) pairs for the registry."""
+ return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1]['verbose_name']))
+ choices = property(iterchoices)
+
+ def get(self, key, default=None):
+ """Returns the object registered with ``key`` or ``default`` if no object was registered."""
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
+ def get_slug(self, obj, default=None):
+ """Returns the slug used to register ``obj`` or ``default`` if ``obj`` was not registered."""
+ for slug, reg in self.iteritems():
+ if obj == reg:
+ return slug
+ return default
+
+ def __getitem__(self, key):
+ """Returns the obj registered with ``key``."""
+ return self._registry[key]['obj']
+
+ def __iter__(self):
+ """Returns an iterator over the keys in the registry."""
+ return self._registry.__iter__()
+
+ def __contains__(self, item):
+ return self._registry.__contains__(item)
\ No newline at end of file
diff --git a/philo/utils/templates.py b/philo/utils/templates.py
new file mode 100644
index 0000000..e0be31f
--- /dev/null
+++ b/philo/utils/templates.py
@@ -0,0 +1,123 @@
+import itertools
+
+from django.template import TextNode, VariableNode, Context
+from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext, ConstantIncludeNode
+from django.utils.datastructures import SortedDict
+
+from philo.templatetags.containers import ContainerNode
+
+
+LOADED_TEMPLATE_ATTR = '_philo_loaded_template'
+BLANK_CONTEXT = Context()
+
+
+def get_extended(self):
+ return self.get_parent(BLANK_CONTEXT)
+
+
+def get_included(self):
+ return self.template
+
+
+# We ignore the IncludeNode because it will never work in a blank context.
+setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
+setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
+
+
+def get_containers(template):
+ # Build a tree of the templates we're using, placing the root template first.
+ levels = build_extension_tree(template.nodelist)
+
+ contentlet_specs = []
+ contentreference_specs = SortedDict()
+ blocks = {}
+
+ for level in reversed(levels):
+ level.initialize()
+ contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, level.contentlet_specs))
+ contentreference_specs.update(level.contentreference_specs)
+ for name, block in level.blocks.items():
+ if block.block_super:
+ blocks.setdefault(name, []).append(block)
+ else:
+ blocks[name] = [block]
+
+ for block_list in blocks.values():
+ for block in block_list:
+ block.initialize()
+ contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, block.contentlet_specs))
+ contentreference_specs.update(block.contentreference_specs)
+
+ return contentlet_specs, contentreference_specs
+
+
+class LazyContainerFinder(object):
+ def __init__(self, nodes, extends=False):
+ self.nodes = nodes
+ self.initialized = False
+ self.contentlet_specs = []
+ self.contentreference_specs = SortedDict()
+ self.blocks = {}
+ self.block_super = False
+ self.extends = extends
+
+ def process(self, nodelist):
+ for node in nodelist:
+ if self.extends:
+ if isinstance(node, BlockNode):
+ self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
+ block.initialize()
+ self.blocks.update(block.blocks)
+ continue
+
+ if isinstance(node, ContainerNode):
+ if not node.references:
+ self.contentlet_specs.append(node.name)
+ else:
+ if node.name not in self.contentreference_specs.keys():
+ self.contentreference_specs[node.name] = node.references
+ continue
+
+ if isinstance(node, VariableNode):
+ if node.filter_expression.var.lookups == (u'block', u'super'):
+ self.block_super = True
+
+ if hasattr(node, 'child_nodelists'):
+ for nodelist_name in node.child_nodelists:
+ if hasattr(node, nodelist_name):
+ nodelist = getattr(node, nodelist_name)
+ self.process(nodelist)
+
+ # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
+ # node as rendering an additional template. Philo monkeypatches the attribute onto
+ # the relevant default nodes and declares it on any native nodes.
+ if hasattr(node, LOADED_TEMPLATE_ATTR):
+ loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
+ if loaded_template:
+ nodelist = loaded_template.nodelist
+ self.process(nodelist)
+
+ def initialize(self):
+ if not self.initialized:
+ self.process(self.nodes)
+ self.initialized = True
+
+
+def build_extension_tree(nodelist):
+ nodelists = []
+ extends = None
+ for node in nodelist:
+ if not isinstance(node, TextNode):
+ if isinstance(node, ExtendsNode):
+ extends = node
+ break
+
+ if extends:
+ if extends.nodelist:
+ nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
+ loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
+ nodelists.extend(build_extension_tree(loaded_template.nodelist))
+ else:
+ # Base case: root.
+ nodelists.append(LazyContainerFinder(nodelist))
+ return nodelists
\ No newline at end of file
diff --git a/philo/validators.py b/philo/validators.py
index c8e5dc9..4b43047 100644
--- a/philo/validators.py
+++ b/philo/validators.py
@@ -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 philo.utils import LOADED_TEMPLATE_ATTR
+from django.utils.translation import ugettext_lazy as _
+
+from philo.utils.templates 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:
@@ -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
diff --git a/philo/views.py b/philo/views.py
index 598be36..2c2a952 100644
--- a/philo/views.py
+++ b/philo/views.py
@@ -2,11 +2,23 @@ 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
@vary_on_headers('Accept')
def node_view(request, path=None, **kwargs):
+ """
+ :func:`node_view` handles incoming requests by checking to make sure that:
+
+ - the request has an attached :class:`.Node`.
+ - the attached :class:`~philo.models.nodes.Node` handles any remaining path beyond its location.
+
+ If these conditions are not met, then :func:`node_view` will either raise :exc:`Http404` or, if it seems like the address was mistyped (for example missing a trailing slash), return an :class:`HttpResponseRedirect` to the correct address.
+
+ Otherwise, :func:`node_view` will call the :class:`.Node`'s :meth:`~.Node.render_to_response` method, passing ``kwargs`` in as the ``extra_context``.
+
+ """
if "philo.middleware.RequestNodeMiddleware" not in settings.MIDDLEWARE_CLASSES:
raise MIDDLEWARE_NOT_CONFIGURED
@@ -25,10 +37,10 @@ def node_view(request, path=None, **kwargs):
raise Http404
node = request.node
- subpath = request.node.subpath
+ subpath = request.node._subpath
# Explicitly disallow trailing slashes if we are otherwise at a node's url.
- if request._cached_node_path != "/" and request._cached_node_path[-1] == "/" and subpath == "/":
+ if node._path != "/" and node._path[-1] == "/" and subpath == "/":
return HttpResponseRedirect(node.get_absolute_url())
if not node.handles_subpath(subpath):
diff --git a/setup.py b/setup.py
index 3c18b16..8a91d14 100644
--- a/setup.py
+++ b/setup.py
@@ -1,47 +1,49 @@
#!/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]])
+from setuptools import setup, find_packages
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
+ name = 'philo',
+ version = '.'.join([str(v) for v in version]),
+ url = "http://philocms.org/",
+ description = "A foundation for developing web content management systems.",
+ long_description = open(os.path.join(os.path.dirname(__file__), 'README')).read(),
+ maintainer = "iThink Software",
+ maintainer_email = "contact@ithinksw.com",
+ packages = find_packages(),
+ include_package_data=True,
+
+ classifiers = [
+ 'Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: ISC License (ISCL)',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+ 'Topic :: Software Development :: Libraries :: Application Frameworks',
+ ],
+ platforms = ['OS Independent'],
+ license = 'ISC License (ISCL)',
+
+ install_requires = [
+ 'django>=1.3',
+ 'django-mptt>0.4.2,==dev',
+ ],
+ extras_require = {
+ 'docs': ["sphinx>=1.0"],
+ 'grappelli': ['django-grappelli>=2.3'],
+ 'migrations': ['south>=0.7.2'],
+ 'waldo-recaptcha': ['recaptcha-django'],
+ 'sobol-eventlet': ['eventlet'],
+ 'sobol-scrape': ['BeautifulSoup'],
+ 'penfield': ['django-taggit>=0.9'],
+ },
+ dependency_links = [
+ 'https://github.com/django-mptt/django-mptt/tarball/master#egg=django-mptt-dev'
+ ]
+)