-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.
--- /dev/null
+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
-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/>
* 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/>
- * (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
+ * (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/>. 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 <http://docs.philocms.org/> and contribute <http://docs.philocms.org/en/latest/contribute.html>.
-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).
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",
--- /dev/null
+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
--- /dev/null
+ 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.]
--- /dev/null
+ 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: ________________
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.
# 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']
# 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
#
# 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
#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
# (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
# (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', )
--- /dev/null
+Contrib apps
+============
+
+.. toctree::
+ :maxdepth: 2
+ :hidden:
+
+ penfield
+ shipherd
+ sobol
+ waldo
+ winer
+
+.. automodule:: philo.contrib
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+Sobol
+=====
+
+.. automodule:: philo.contrib.sobol
+ :members:
+
+Models
+++++++
+
+.. automodule:: philo.contrib.sobol.models
+ :members:
+
+Search API
+++++++++++
+
+.. automodule:: philo.contrib.sobol.search
+ :members:
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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 <http://project.philocms.org/>`_.
+* **Contribute code** using `git <http://git-scm.com/>`_. You can fork philo's repository either on `GitHub <http://github.com/ithinksw/philo/>`_ or `Gitorious <http://gitorious.org/ithinksw/philo/>`_. If you are contributing to Philo, you will need to submit a :ref:`Contributor License Agreement <cla>`.
+* **Join the discussion** on IRC at `irc://irc.oftc.net/#philo <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 <mailto:philo@ithinksw.org>`_ and `philo-devel@ithinksw.org <mailto: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 <cla/ithinksw-icla.txt>`.
+
+If you are doing work for an employer, they will need to submit the :download:`Corporate CLA <cla/ithinksw-ccla.txt>` and you will need to submit the Individual CLA :download:`Individual CLA <cla/ithinksw-icla.txt>` as well.
+
+Both documents include information on how to submit them.
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'db.sl3'
+ }
+}
\ No newline at end of file
--- /dev/null
+Exceptions
+==========
+
+.. automodule:: philo.exceptions
+ :members: MIDDLEWARE_NOT_CONFIGURED, AncestorDoesNotExist, ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths
\ No newline at end of file
--- /dev/null
+Forms
+=====
+
+.. automodule:: philo.forms.entities
+ :members:
+
+
+Fields
+++++++
+
+.. automodule:: philo.forms.fields
+ :members:
--- /dev/null
+Handling Requests
+=================
+
+.. automodule:: philo.middleware
+ :members:
+
+.. automodule:: philo.views
+
+
+.. autofunction:: node_view(request[, path=None, **kwargs])
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 <releases/0.9.1>`.
+
+Prerequisites:
+
+* `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/>`_
+* (:mod:`philo.contrib.penfield`) `django-taggit 0.9.3+ <https://github.com/alex/django-taggit/>`_
+* (:mod:`philo.contrib.waldo`, optional) `recaptcha-django r6+ <http://code.google.com/p/recaptcha-django/>`_
+
+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+ <http://www.python.org>`_
-* `Django 1.2+ <http://www.djangoproject.com/>`_
-* `django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>`_
-* (Optional) `django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>`_
-* (Optional) `south 0.7.2+ <http://south.aeracode.org/>`_
-* (Optional) `recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>`_
-
-To contribute, please visit the `project website <http://philo.ithinksw.org/>`_ or make a fork of the `git repository <http://github.com/ithinksw/philo/>`_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo <irc://irc.oftc.net/#philo>`_.
+++ /dev/null
-How to get started with philo
-=============================
-
-After installing `philo`_ and `mptt`_ on your python path, make sure to complete the following steps:
-
-1. add :mod:`philo` and :mod:`mptt` to :setting:`settings.INSTALLED_APPS`::
-
- INSTALLED_APPS = (
- ...
- 'philo',
- 'mptt',
- ...
- )
-
-2. add :class:`philo.middleware.RequestNodeMiddleware` to :setting:`settings.MIDDLEWARE_CLASSES`::
-
- MIDDLEWARE_CLASSES = (
- ...
- 'philo.middleware.RequestNodeMiddleware',
- ...
- )
-
-3. include :mod:`philo.urls` somewhere in your urls.py file. For example::
-
- from django.conf.urls.defaults import patterns, include, url
- urlpatterns = patterns('',
- url(r'^', include('philo.urls')),
- )
-
-4. Optionally add a root :class:`node <philo.models.Node>` to your current :class:`Site` in the admin interface.
-
-Philo should be ready to go!
-
-.. _philo: http://github.com/ithinksw/philo
-.. _mptt: http://github.com/django-mptt/django-mptt
\ No newline at end of file
--- /dev/null
+Database Template Loader
+========================
+
+.. automodule:: philo.loaders.database
+ :members:
--- /dev/null
+Collections
+===========
+
+.. automodule:: philo.models.collections
+ :members: Collection, CollectionMember, CollectionMemberManager
+
+.. autoclass:: CollectionMemberManager
+ :members:
\ No newline at end of file
----------
.. 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)
--------
.. 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
--- /dev/null
+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
entities
nodes-and-views
+ collections
+ miscellaneous
+ fields
-.. :module: philo.models
+.. automodule:: philo.models
--- /dev/null
+Miscellaneous Models
+=============================
+.. autoclass:: philo.models.nodes.TargetURLModel
+ :members:
+ :exclude-members: get_target_url
\ No newline at end of file
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 <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 <ObjectDoesNotExist>` 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 <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 <philo.signals.view_about_to_render>` signal, then call :meth:`actually_render_to_response`, and finally send the :obj:`view_finished_rendering <philo.signals.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 <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 <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 <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 <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 <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 <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 <name>`
\ No newline at end of file
+.. autoclass:: ContentReference
+ :members:
\ No newline at end of file
--- /dev/null
+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`.
--- /dev/null
+Signals
+=======
+
+.. automodule:: philo.signals
+ :members:
--- /dev/null
+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
--- /dev/null
+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::
+
+ <html>
+ <head>
+ <title>Hello world!</title>
+ </head>
+ <body>
+ <p>Hello world!</p>
+ <p>The time is {% now %}.</p>
+ </body>
+ </html>
+
+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::
+
+ <html>
+ <head>
+ <title>{% container page_title %}</title>
+ </head>
+ <body>
+ {% container page_body as content %}
+ {% if content %}
+ <p>{{ content }}</p>
+ {% endif %}
+ <p>The time is {% now "jS F Y H:i" %}.</p>
+ </body>
+ </html>
+
+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!
--- /dev/null
+Tutorials
+=========
+
+.. toctree::
+ :maxdepth: 1
+
+ getting-started
+ shipherd
--- /dev/null
+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::
+
+ <html>{% load shipherd %}
+ <head>
+ <title>{% container page_title %}</title>
+ </head>
+ <body>
+ <ul>
+ {% recursenavigation node "main" %}
+ <li{% if navloop.active %} class="active"{% endif %}>
+ <a href="{{ item.get_target_url }}">{{ item.text }}</a>
+ </li>
+ {% endrecursenavigation %}
+ </ul>
+ {% container page_body as content %}
+ {% if content %}
+ <p>{{ content }}</p>
+ {% endif %}
+ <p>The time is {% now %}.</p>
+ </body>
+ </html>
+
+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.
--- /dev/null
+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:
--- /dev/null
+Validators
+==========
+
+.. automodule:: philo.validators
+ :members:
--- /dev/null
+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 ``<figure>`` element, should depend on the template used to render the entry, not the HTML5 knowledge of the user.
+ .. seealso:: :ttag:`~philo.templatetags.embed.embed`
+* Interpretation of content (as a django template, as markdown, as textile, etc.) is the responsibility of the template designer, not of code developers or the framework.
+ .. seealso:: :ttag:`~philo.templatetags.include_string.include_string`
+* Page content should be simple -- not reorderable. Each piece of content should only be related to one page. Any other system will cause more trouble than it's worth.
+ .. seealso:: :class:`.Contentlet`, :class:`.ContentReference`
+* Some pieces of information may be shared by an entire site, used in disparate places, and changed frequently enough that it is far too difficult to track down every use. These pieces of information should be stored separately from the content that contains them.
+ .. seealso:: :class:`.Attribute`
-VERSION = (0, 0)
+VERSION = (0, 9, 1)
from django.http import HttpResponse
from django.utils import simplejson as json
from django.utils.html import escape
-from philo.models import Tag, Attribute
+from mptt.admin import MPTTModelAdmin
+
+from philo.models import Attribute
from philo.models.fields.entities import ForeignKeyAttribute, ManyToManyAttribute
from philo.admin.forms.attributes import AttributeForm, AttributeInlineFormSet
-from philo.admin.widgets import TagFilteredSelectMultiple
from philo.forms.entities import EntityForm, proxy_fields_for_entity_model
-from mptt.admin import MPTTModelAdmin
COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
return db_field.formfield(**kwargs)
-class TreeAdmin(MPTTModelAdmin):
- pass
-
-
-class TreeEntityAdmin(EntityAdmin, TreeAdmin):
- pass
-
-
-class TagAdmin(admin.ModelAdmin):
- list_display = ('name', 'slug')
- prepopulated_fields = {"slug": ("name",)}
- search_fields = ["name"]
-
- def response_add(self, request, obj, post_url_continue='../%s/'):
- # If it's an ajax request, return a json response containing the necessary information.
- if request.is_ajax():
- return HttpResponse(json.dumps({'pk': escape(obj._get_pk_val()), 'unicode': escape(obj)}))
- return super(TagAdmin, self).response_add(request, obj, post_url_continue)
-
-
-class AddTagAdmin(admin.ModelAdmin):
- def formfield_for_manytomany(self, db_field, request=None, **kwargs):
- """
- Get a form Field for a ManyToManyField.
- """
- # If it uses an intermediary model that isn't auto created, don't show
- # a field in admin.
- if not db_field.rel.through._meta.auto_created:
- return None
-
- if db_field.rel.to == Tag and db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
- opts = Tag._meta
- if request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()):
- kwargs['widget'] = TagFilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
- return db_field.formfield(**kwargs)
-
- return super(AddTagAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
-
-
-admin.site.register(Tag, TagAdmin)
\ No newline at end of file
+class TreeEntityAdmin(EntityAdmin, MPTTModelAdmin):
+ pass
\ No newline at end of file
from django.contrib import admin
+
from philo.admin.base import COLLAPSE_CLASSES
from philo.models import CollectionMember, Collection
from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
from django.contrib.contenttypes.models import ContentType
from django.forms.models import ModelForm
+
from philo.models import Attribute
# This is necessary because model forms store changes to self.instance in their clean method.
# Mutter mutter.
value = self.instance.value
- self._cached_value_ct = self.instance.value_content_type
+ self._cached_value_ct_id = self.instance.value_content_type_id
self._cached_value = value
# If there is a value, pull in its fields.
def save(self, *args, **kwargs):
# At this point, the cleaned_data has already been stored on self.instance.
- if self.instance.value_content_type != self._cached_value_ct:
+ if self.instance.value_content_type_id != self._cached_value_ct_id:
# The value content type has changed. Clear the old value, if there was one.
if self._cached_value:
self._cached_value.delete()
# Now create a new value instance so that on next instantiation, the form will
# know what fields to add.
- if self.instance.value_content_type is not None:
- self.instance.value = self.instance.value_content_type.model_class().objects.create()
+ if self.instance.value_content_type_id is not None:
+ self.instance.value = ContentType.objects.get_for_id(self.instance.value_content_type_id).model_class().objects.create()
elif self.instance.value is not None:
# The value content type is the same, but one of the value fields has changed.
from django import forms
-from django.contrib.admin.widgets import AdminTextareaWidget
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.forms.models import ModelForm, BaseInlineFormSet, BaseModelFormSet
from django.forms.formsets import TOTAL_FORM_COUNT
from django.utils.datastructures import SortedDict
-from philo.admin.widgets import ModelLookupWidget
+
+from philo.admin.widgets import ModelLookupWidget, EmbedWidget
from philo.models import Contentlet, ContentReference
class ContentletForm(ContainerForm):
- content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
+ content = forms.CharField(required=False, widget=EmbedWidget, label='Content')
def should_delete(self):
# Delete iff: the data has changed and is now empty.
class ContentletInlineFormSet(ContainerInlineFormSet):
def get_containers(self):
try:
- containers = list(self.instance.containers[0])
+ containers = self.instance.containers[0]
except ObjectDoesNotExist:
containers = []
from django.contrib import admin
+from mptt.admin import MPTTModelAdmin
+
from philo.admin.base import EntityAdmin, TreeEntityAdmin, COLLAPSE_CLASSES
from philo.models import Node, Redirect, File
-from mptt.admin import MPTTModelAdmin
class NodeAdmin(TreeEntityAdmin):
class FileAdmin(ViewAdmin):
fieldsets = (
(None, {
- 'fields': ('file', 'mimetype')
+ 'fields': ('name', 'file', 'mimetype')
}),
)
- list_display = ('mimetype', 'file')
+ list_display = ('name', 'mimetype', 'file')
+ search_fields = ('name',)
+ list_filter = ('mimetype',)
admin.site.register(Node, NodeAdmin)
+from django import forms
from django.conf import settings
from django.contrib import admin
-from django import forms
-from philo.admin.base import COLLAPSE_CLASSES, TreeAdmin
+
+from philo.admin.base import COLLAPSE_CLASSES, TreeEntityAdmin
+from philo.admin.forms.containers import *
from philo.admin.nodes import ViewAdmin
+from philo.admin.widgets import EmbedWidget
+from philo.models.fields import TemplateField
from philo.models.pages import Page, Template, Contentlet, ContentReference
-from philo.admin.forms.containers import *
-class ContentletInline(admin.StackedInline):
- model = Contentlet
+class ContainerInline(admin.StackedInline):
extra = 0
max_num = 0
- formset = ContentletInlineFormSet
- form = ContentletForm
can_delete = False
classes = ('collapse-open', 'collapse','open')
if 'grappelli' in settings.INSTALLED_APPS:
template = 'admin/philo/edit_inline/tabular_container.html'
-class ContentReferenceInline(admin.StackedInline):
+class ContentletInline(ContainerInline):
+ model = Contentlet
+ formset = ContentletInlineFormSet
+ form = ContentletForm
+
+
+class ContentReferenceInline(ContainerInline):
model = ContentReference
- extra = 0
- max_num = 0
formset = ContentReferenceInlineFormSet
form = ContentReferenceForm
- can_delete = False
- classes = ('collapse-open', 'collapse','open')
- if 'grappelli' in settings.INSTALLED_APPS:
- template = 'admin/philo/edit_inline/grappelli_tabular_container.html'
- else:
- template = 'admin/philo/edit_inline/tabular_container.html'
class PageAdmin(ViewAdmin):
return super(PageAdmin, self).response_add(request, obj, post_url_continue)
-class TemplateAdmin(TreeAdmin):
+class TemplateAdmin(TreeEntityAdmin):
prepopulated_fields = {'slug': ('name',)}
fieldsets = (
(None, {
'fields': ('mimetype',)
}),
)
+ formfield_overrides = {
+ TemplateField: {'widget': EmbedWidget}
+ }
save_on_top = True
save_as = True
list_display = ('__unicode__', 'slug', 'get_path',)
from django import forms
from django.conf import settings
-from django.contrib.admin.widgets import FilteredSelectMultiple, url_params_from_lookup_dict
-from django.utils.translation import ugettext as _
+from django.contrib.admin.widgets import url_params_from_lookup_dict
+from django.utils import simplejson as json
+from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import truncate_words
-from django.utils.html import escape
+from django.utils.translation import ugettext as _
class ModelLookupWidget(forms.TextInput):
return mark_safe(u''.join(output))
-class TagFilteredSelectMultiple(FilteredSelectMultiple):
- """
- A SelectMultiple with a JavaScript filter interface.
-
- Note that the resulting JavaScript assumes that the jsi18n
- catalog has been loaded in the page
- """
+class EmbedWidget(forms.Textarea):
+ """A form widget with the HTML class embedding and an embedded list of content-types."""
+ def __init__(self, attrs=None):
+ from philo.models import value_content_type_limiter
+
+ content_types = value_content_type_limiter.classes
+ data = []
+
+ for content_type in content_types:
+ data.append({'app_label': content_type._meta.app_label, 'object_name': content_type._meta.object_name.lower(), 'verbose_name': unicode(content_type._meta.verbose_name)})
+
+ json_ = json.dumps(data)
+
+ default_attrs = {'class': 'embedding vLargeTextField', 'data-content-types': json_ }
+
+ if attrs:
+ default_attrs.update(attrs)
+
+ super(EmbedWidget, self).__init__(default_attrs)
+
class Media:
- js = (
- settings.ADMIN_MEDIA_PREFIX + "js/core.js",
- settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js",
- settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js",
- settings.ADMIN_MEDIA_PREFIX + "js/TagCreation.js",
- )
-
- def render(self, name, value, attrs=None, choices=()):
- if attrs is None: attrs = {}
- attrs['class'] = 'selectfilter'
- if self.is_stacked: attrs['class'] += 'stacked'
- output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
- output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
- # TODO: "id_" is hard-coded here. This should instead use the correct
- # API to determine the ID dynamically.
- output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); tagCreation.init("id_%s"); });</script>\n' % \
- (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX, name))
- return mark_safe(u''.join(output))
\ No newline at end of file
+ css = {
+ 'all': ('philo/css/EmbedWidget.css',),
+ }
+ js = ('philo/js/EmbedWidget.js',)
+#encoding: utf-8
+"""
+Following Python and Django’s “batteries included” philosophy, Philo includes a number of optional packages that simplify common website structures:
+
+* :mod:`~philo.contrib.penfield` — Basic blog and newsletter management.
+* :mod:`~philo.contrib.shipherd` — Powerful site navigation.
+* :mod:`~philo.contrib.sobol` — Custom web and database searches.
+* :mod:`~philo.contrib.waldo` — Custom authentication systems.
+* :mod:`~philo.contrib.winer` — Abstract framework for Philo-based syndication.
+
+"""
\ No newline at end of file
+"""
+This version of julian is currently in development and is not considered stable.
+
+"""
\ No newline at end of file
from django.contrib import admin
+
from philo.admin import EntityAdmin, COLLAPSE_CLASSES
from philo.contrib.julian.models import Location, Event, Calendar, CalendarView
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
},
- 'oberlin.locationcoordinates': {
- 'Meta': {'unique_together': "(('location_ct', 'location_pk'),)", 'object_name': 'LocationCoordinates'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'latitude': ('django.db.models.fields.FloatField', [], {}),
- 'location_ct': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
- 'location_pk': ('django.db.models.fields.TextField', [], {}),
- 'longitude': ('django.db.models.fields.FloatField', [], {})
- },
'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']"}),
+import calendar
+import datetime
+
from django.conf import settings
from django.conf.urls.defaults import url, patterns, include
from django.contrib.auth.models import User
from django.db.models.query import QuerySet
from django.http import HttpResponse, Http404
from django.utils.encoding import force_unicode
+from taggit.managers import TaggableManager
+
from philo.contrib.julian.feedgenerator import ICalendarFeed
-from philo.contrib.penfield.models import FeedView, FEEDS
+from philo.contrib.winer.models import FeedView
+from philo.contrib.winer.feeds import registry
from philo.exceptions import ViewCanNotProvideSubpath
-from philo.models import Tag, Entity, Page, TemplateField
+from philo.models import Tag, Entity, Page
+from philo.models.fields import TemplateField
from philo.utils import ContentTypeRegistryLimiter
-import datetime, calendar
__all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',)
-ICALENDAR = ICalendarFeed.mime_type
-FEEDS[ICALENDAR] = ICalendarFeed
+registry.register(ICalendarFeed, verbose_name="iCalendar")
try:
DEFAULT_SITE = Site.objects.get_current()
except:
# or per-calendar-view basis.
#url(r'^%s/(?P<slug>[\w-]+)' % self.location_permalink_base, ...)
- if self.tag_archive_page:
+ if self.tag_archive_page_id:
urlpatterns += patterns('',
url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive')
)
- if self.owner_archive_page:
+ if self.owner_archive_page_id:
urlpatterns += patterns('',
url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive')
)
- if self.location_archive_page:
+ if self.location_archive_page_id:
urlpatterns += patterns('',
url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive')
)
def get_events_by_location(self, request, app_label, model, pk, extra_context=None):
try:
- ct = ContentType.objects.get(app_label=app_label, model=model)
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
location = ct.model_class()._default_manager.get(pk=pk)
except ObjectDoesNotExist:
raise Http404
return u"%s for %s" % (self.__class__.__name__, self.calendar)
field = CalendarView._meta.get_field('feed_type')
-field._choices += ((ICALENDAR, 'iCalendar'),)
-field.default = ICALENDAR
\ No newline at end of file
+field.default = registry.get_slug(ICalendarFeed, field.default)
\ No newline at end of file
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, QueryDict
-from philo.admin import EntityAdmin, AddTagAdmin, COLLAPSE_CLASSES
+
+from philo.admin import EntityAdmin, COLLAPSE_CLASSES
+from philo.admin.widgets import EmbedWidget
from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView
+from philo.models.fields import TemplateField
class DelayedDateForm(forms.ModelForm):
self.fields[self.date_field].required = False
-class TitledAdmin(EntityAdmin):
+class BlogAdmin(EntityAdmin):
prepopulated_fields = {'slug': ('title',)}
list_display = ('title', 'slug')
-class BlogAdmin(TitledAdmin):
- pass
-
-
-class BlogEntryAdmin(TitledAdmin, AddTagAdmin):
+class BlogEntryAdmin(EntityAdmin):
form = DelayedDateForm
- filter_horizontal = ['tags']
list_filter = ['author', 'blog']
date_hierarchy = 'date'
search_fields = ('content',)
})
)
related_lookup_fields = {'fk': raw_id_fields}
+ prepopulated_fields = {'slug': ('title',)}
+ formfield_overrides = {
+ TemplateField: {'widget': EmbedWidget}
+ }
class BlogViewAdmin(EntityAdmin):
related_lookup_fields = {'fk': raw_id_fields}
-class NewsletterAdmin(TitledAdmin):
- pass
+class NewsletterAdmin(EntityAdmin):
+ prepopulated_fields = {'slug': ('title',)}
+ list_display = ('title', 'slug')
-class NewsletterArticleAdmin(TitledAdmin, AddTagAdmin):
+class NewsletterArticleAdmin(EntityAdmin):
form = DelayedDateForm
- filter_horizontal = ('tags', 'authors')
+ filter_horizontal = ('authors',)
list_filter = ('newsletter',)
date_hierarchy = 'date'
search_fields = ('title', 'authors__name',)
})
)
actions = ['make_issue']
+ prepopulated_fields = {'slug': ('title',)}
+ formfield_overrides = {
+ TemplateField: {'widget': EmbedWidget}
+ }
def author_names(self, obj):
return ', '.join([author.get_full_name() for author in obj.authors.all()])
make_issue.short_description = u"Create issue from selected %(verbose_name_plural)s"
-class NewsletterIssueAdmin(TitledAdmin):
- filter_horizontal = TitledAdmin.filter_horizontal + ('articles',)
+class NewsletterIssueAdmin(EntityAdmin):
+ filter_horizontal = ('articles',)
+ prepopulated_fields = {'slug': ('title',)}
+ list_display = ('title', 'slug')
class NewsletterViewAdmin(EntityAdmin):
+++ /dev/null
-class HttpNotAcceptable(Exception):
- """This will be raised if an Http-Accept header will not accept the feed content types that are available."""
- pass
\ No newline at end of file
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
+from philo.migrations import person_model, frozen_person
class Migration(SchemaMigration):
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
- 'oberlin.person': {
- 'Meta': {'object_name': 'Person'},
- 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '70', 'blank': 'True'}),
- 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'})
- },
+ person_model: frozen_person,
'penfield.blog': {
'Meta': {'object_name': 'Blog'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
},
'penfield.blogentry': {
'Meta': {'object_name': 'BlogEntry'},
- 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['oberlin.Person']"}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['%s']" % person_model}),
'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}),
'content': ('django.db.models.fields.TextField', [], {}),
'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
},
'penfield.newsletterarticle': {
'Meta': {'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'},
- 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['oberlin.Person']"}),
+ 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['%s']" % person_model}),
'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
+from philo.migrations import person_model, frozen_person
class Migration(SchemaMigration):
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
- 'oberlin.person': {
- 'Meta': {'object_name': 'Person'},
- 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '70', 'blank': 'True'}),
- 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'})
- },
+ person_model: frozen_person,
'penfield.blog': {
'Meta': {'object_name': 'Blog'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
},
'penfield.blogentry': {
'Meta': {'object_name': 'BlogEntry'},
- 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['oberlin.Person']"}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['%s']" % person_model}),
'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}),
'content': ('django.db.models.fields.TextField', [], {}),
'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
},
'penfield.newsletterarticle': {
'Meta': {'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'},
- 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['oberlin.Person']"}),
+ 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['%s']" % person_model}),
'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
--- /dev/null
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+from philo.migrations import person_model, frozen_person
+
+class Migration(DataMigration):
+
+ depends_on = (
+ ("philo", "0019_to_taggit"),
+ )
+
+ needed_by = (
+ ("philo", "0020_from_taggit"),
+ )
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ BlogEntry = orm['penfield.BlogEntry']
+ NewsletterArticle = orm['penfield.NewsletterArticle']
+ TaggitTag = orm['taggit.Tag']
+ TaggedItem = orm['taggit.TaggedItem']
+ ContentType = orm['contenttypes.contenttype']
+
+ entry_ct = ContentType.objects.get(app_label="penfield", model="blogentry")
+ article_ct = ContentType.objects.get(app_label="penfield", model="newsletterarticle")
+
+ for entry in BlogEntry.objects.all():
+ for tag in entry.tags.all():
+ ttag = TaggitTag.objects.get(slug=tag.slug)
+ TaggedItem.objects.get_or_create(tag=ttag, content_type=entry_ct, object_id=entry.pk)
+
+ for article in NewsletterArticle.objects.all():
+ for tag in article.tags.all():
+ ttag = TaggitTag.objects.get(slug=tag.slug)
+ TaggedItem.objects.get_or_create(tag=ttag, content_type=article_ct, object_id=article.pk)
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ BlogEntry = orm['penfield.BlogEntry']
+ NewsletterArticle = orm['penfield.NewsletterArticle']
+ Tag = orm['philo.Tag']
+ TaggitTag = orm['taggit.Tag']
+ TaggedItem = orm['taggit.TaggedItem']
+ ContentType = orm['contenttypes.contenttype']
+
+ entry_ct = ContentType.objects.get(app_label="penfield", model="blogentry")
+ article_ct = ContentType.objects.get(app_label="penfield", model="newsletterarticle")
+
+ for entry in BlogEntry.objects.all():
+ tag_slugs = list(TaggitTag.objects.filter(taggit_taggeditem_items__content_type=entry_ct, taggit_taggeditem_items__object_id=entry.pk).values_list('slug', flat=True).distinct())
+ entry.tags = Tag.objects.filter(slug__in=tag_slugs)
+ entry.save()
+
+ for article in NewsletterArticle.objects.all():
+ tag_slugs = list(TaggitTag.objects.filter(taggit_taggeditem_items__content_type=article_ct, taggit_taggeditem_items__object_id=article.pk).values_list('slug', flat=True).distinct())
+ article.tags = Tag.objects.filter(slug__in=tag_slugs)
+ article.save()
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ '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': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ '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'})
+ },
+ person_model: frozen_person,
+ 'penfield.blog': {
+ 'Meta': {'object_name': 'Blog'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.blogentry': {
+ 'Meta': {'ordering': "['-date']", 'object_name': 'BlogEntry'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['%s']" % person_model}),
+ 'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'excerpt': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'blogentries'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['philo.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.blogview': {
+ 'Meta': {'object_name': 'BlogView'},
+ 'blog': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogviews'", 'to': "orm['penfield.Blog']"}),
+ 'entries_per_page': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'entry_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_entry_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'entry_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_entry_related'", 'to': "orm['philo.Page']"}),
+ 'entry_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'entries'", 'max_length': '255'}),
+ 'entry_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+ 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}),
+ 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", 'max_length': '50'}),
+ 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_index_related'", 'to': "orm['philo.Page']"}),
+ 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'tag_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_tag_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'tag_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_tag_related'", 'to': "orm['philo.Page']"}),
+ 'tag_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'tags'", 'max_length': '255'})
+ },
+ 'penfield.newsletter': {
+ 'Meta': {'object_name': 'Newsletter'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterarticle': {
+ 'Meta': {'ordering': "['-date']", 'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'},
+ 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['%s']" % person_model}),
+ 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lede': ('philo.models.fields.TemplateField', [], {'null': 'True', 'blank': 'True'}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'articles'", 'to': "orm['penfield.Newsletter']"}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'newsletterarticles'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['philo.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterissue': {
+ 'Meta': {'ordering': "['-numbering']", 'unique_together': "(('newsletter', 'numbering'),)", 'object_name': 'NewsletterIssue'},
+ 'articles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'issues'", 'symmetrical': 'False', 'to': "orm['penfield.NewsletterArticle']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issues'", 'to': "orm['penfield.Newsletter']"}),
+ 'numbering': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterview': {
+ 'Meta': {'object_name': 'NewsletterView'},
+ 'article_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_article_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'article_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_article_related'", 'to': "orm['philo.Page']"}),
+ 'article_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'articles'", 'max_length': '255'}),
+ 'article_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+ 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}),
+ 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", 'max_length': '50'}),
+ 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_index_related'", 'to': "orm['philo.Page']"}),
+ 'issue_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_issue_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'issue_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_issue_related'", 'to': "orm['philo.Page']"}),
+ 'issue_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'issues'", 'max_length': '255'}),
+ 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletterviews'", 'to': "orm['penfield.Newsletter']"})
+ },
+ '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.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.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 = ['penfield', 'taggit']
+ symmetrical = True
--- /dev/null
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+from philo.migrations import person_model, frozen_person
+
+class Migration(SchemaMigration):
+
+ needed_by = (
+ ('philo', '0021_auto__del_tag'),
+ )
+
+ def forwards(self, orm):
+
+ # Removing M2M table for field tags on 'BlogEntry'
+ db.delete_table('penfield_blogentry_tags')
+
+ # Removing M2M table for field tags on 'NewsletterArticle'
+ db.delete_table('penfield_newsletterarticle_tags')
+
+
+ def backwards(self, orm):
+
+ # Adding M2M table for field tags on 'BlogEntry'
+ db.create_table('penfield_blogentry_tags', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('blogentry', models.ForeignKey(orm['penfield.blogentry'], null=False)),
+ ('tag', models.ForeignKey(orm['philo.tag'], null=False))
+ ))
+ db.create_unique('penfield_blogentry_tags', ['blogentry_id', 'tag_id'])
+
+ # Adding M2M table for field tags on 'NewsletterArticle'
+ db.create_table('penfield_newsletterarticle_tags', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('newsletterarticle', models.ForeignKey(orm['penfield.newsletterarticle'], null=False)),
+ ('tag', models.ForeignKey(orm['philo.tag'], null=False))
+ ))
+ db.create_unique('penfield_newsletterarticle_tags', ['newsletterarticle_id', 'tag_id'])
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ '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': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ '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'})
+ },
+ person_model: frozen_person,
+ 'penfield.blog': {
+ 'Meta': {'object_name': 'Blog'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.blogentry': {
+ 'Meta': {'object_name': 'BlogEntry'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['%s']" % person_model}),
+ 'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'excerpt': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.blogview': {
+ 'Meta': {'object_name': 'BlogView'},
+ 'blog': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogviews'", 'to': "orm['penfield.Blog']"}),
+ 'entries_per_page': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'entry_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_entry_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'entry_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_entry_related'", 'to': "orm['philo.Page']"}),
+ 'entry_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'entries'", 'max_length': '255'}),
+ 'entry_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+ 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}),
+ 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", 'max_length': '50'}),
+ 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_index_related'", 'to': "orm['philo.Page']"}),
+ 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'tag_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_tag_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'tag_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_tag_related'", 'to': "orm['philo.Page']"}),
+ 'tag_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'tags'", 'max_length': '255'})
+ },
+ 'penfield.newsletter': {
+ 'Meta': {'object_name': 'Newsletter'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterarticle': {
+ 'Meta': {'ordering': "['-date']", 'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'},
+ 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['%s']" % person_model}),
+ 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lede': ('philo.models.fields.TemplateField', [], {'null': 'True', 'blank': 'True'}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'articles'", 'to': "orm['penfield.Newsletter']"}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterissue': {
+ 'Meta': {'ordering': "['-numbering']", 'unique_together': "(('newsletter', 'numbering'),)", 'object_name': 'NewsletterIssue'},
+ 'articles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'issues'", 'symmetrical': 'False', 'to': "orm['penfield.NewsletterArticle']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issues'", 'to': "orm['penfield.Newsletter']"}),
+ 'numbering': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ 'penfield.newsletterview': {
+ 'Meta': {'object_name': 'NewsletterView'},
+ 'article_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_article_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'article_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_article_related'", 'to': "orm['philo.Page']"}),
+ 'article_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'articles'", 'max_length': '255'}),
+ 'article_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
+ 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}),
+ 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", 'max_length': '50'}),
+ 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_index_related'", 'to': "orm['philo.Page']"}),
+ 'issue_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_issue_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}),
+ 'issue_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_issue_related'", 'to': "orm['philo.Page']"}),
+ 'issue_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'issues'", 'max_length': '255'}),
+ 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}),
+ 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletterviews'", 'to': "orm['penfield.Newsletter']"})
+ },
+ '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.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.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 = ['penfield', 'taggit']
+# encoding: utf-8
+from datetime import date, datetime
+
from django.conf import settings
from django.conf.urls.defaults import url, patterns, include
-from django.contrib.sites.models import Site, RequestSite
-from django.contrib.syndication.views import add_domain
from django.db import models
from django.http import Http404, HttpResponse
-from django.template import RequestContext, Template as DjangoTemplate
-from django.utils import feedgenerator, tzinfo
-from django.utils.datastructures import SortedDict
-from django.utils.encoding import smart_unicode, force_unicode
-from django.utils.html import escape
-from datetime import date, datetime
-from philo.contrib.penfield.exceptions import HttpNotAcceptable
-from philo.contrib.penfield.middleware import http_not_acceptable
-from philo.contrib.penfield.validators import validate_pagination_count
+from taggit.managers import TaggableManager
+from taggit.models import Tag, TaggedItem
+
+from philo.contrib.winer.models import FeedView
from philo.exceptions import ViewCanNotProvideSubpath
-from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField, Template
+from philo.models import Entity, Page, register_value_model
+from philo.models.fields import TemplateField
from philo.utils import paginate
-try:
- import mimeparse
-except:
- mimeparse = None
-ATOM = feedgenerator.Atom1Feed.mime_type
-RSS = feedgenerator.Rss201rev2Feed.mime_type
-FEEDS = SortedDict([
- (ATOM, feedgenerator.Atom1Feed),
- (RSS, feedgenerator.Rss201rev2Feed),
-])
-FEED_CHOICES = (
- (ATOM, "Atom"),
- (RSS, "RSS"),
-)
-
-
-class FeedView(MultiView):
- """
- The FeedView expects to handle a number of different feeds for the
- same object - i.e. patterns for a blog to handle all entries or
- just entries for a certain year/month/day.
-
- This class would subclass django.contrib.syndication.views.Feed, but
- that would make it callable, which causes problems.
- """
- feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM)
- feed_suffix = models.CharField(max_length=255, blank=False, default="feed")
- feeds_enabled = models.BooleanField(default=True)
- 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.")
-
- item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related")
- item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related")
-
- item_context_var = 'items'
- object_attr = 'object'
-
- 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.
- """
- urlpatterns = patterns('')
- if self.feeds_enabled:
- feed_reverse_name = "%s_feed" % reverse_name
- feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name))
- feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
- urlpatterns += patterns('',
- url(feed_pattern, feed_view, name=feed_reverse_name),
- )
- urlpatterns += patterns('',
- url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
- )
- return urlpatterns
-
- def get_object(self, request, **kwargs):
- return getattr(self, self.object_attr)
+class Blog(Entity):
+ """Represents a blog which can be posted to."""
+ #: The name of the :class:`Blog`, currently called 'title' for historical reasons.
+ title = models.CharField(max_length=255)
- def feed_view(self, get_items_attr, reverse_name):
- """
- Returns a view function that renders a list of items as a feed.
- """
- get_items = callable(get_items_attr) and get_items_attr or 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)
- items, xxx = get_items(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):
- """
- Returns a view function that renders a list of items as a page.
- """
- get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr)
- page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr)
-
- def inner(request, extra_context=None, *args, **kwargs):
- items, extra_context = get_items(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 {})
-
- 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 a
- request, 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
+ #: A slug used to identify the :class:`Blog`.
+ slug = models.SlugField(max_length=255)
- def get_feed_type(self, request):
- feed_type = self.feed_type
- if feed_type not in FEEDS:
- feed_type = FEEDS.keys()[0]
- accept = request.META.get('HTTP_ACCEPT')
- if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept:
- # Wups! They aren't accepting the chosen format. Is there another format we can use?
- if mimeparse:
- feed_type = mimeparse.best_match(FEEDS.keys(), accept)
- else:
- for feed_type in FEEDS.keys():
- if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept:
- break
- else:
- feed_type = None
- if not feed_type:
- raise HttpNotAcceptable
- return FEEDS[feed_type]
-
- def get_feed(self, obj, request, reverse_name):
- """
- Returns an unpopulated feedgenerator.DefaultFeed object for this object.
- """
- try:
- current_site = Site.objects.get_current()
- except Site.DoesNotExist:
- current_site = RequestSite(request)
-
- feed_type = self.get_feed_type(request)
- node = request.node
- link = node.get_absolute_url(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(node.subpath, 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):
- 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)
+ def __unicode__(self):
+ return self.title
- class Meta:
- abstract=True
-
-
-class Blog(Entity, Titled):
@property
def entry_tags(self):
- """ Returns a QuerySet of Tags that are used on any entries in this blog. """
- return Tag.objects.filter(blogentries__blog=self).distinct()
+ """Returns a :class:`QuerySet` of :class:`.Tag`\ s that are used on any entries in this blog."""
+ entry_pks = list(self.entries.values_list('pk', flat=True))
+ kwargs = {
+ '%s__object_id__in' % TaggedItem.tag_relname(): entry_pks
+ }
+ return TaggedItem.tags_for(BlogEntry).filter(**kwargs)
@property
def entry_dates(self):
+ """Returns a dictionary of date :class:`QuerySet`\ s for years, months, and days for which there are entries."""
dates = {'year': self.entries.dates('date', 'year', order='DESC'), 'month': self.entries.dates('date', 'month', order='DESC'), 'day': self.entries.dates('date', 'day', order='DESC')}
return dates
register_value_model(Blog)
-class BlogEntry(Entity, Titled):
+class BlogEntry(Entity):
+ """Represents an entry in a :class:`Blog`."""
+ #: The title of the :class:`BlogEntry`.
+ title = models.CharField(max_length=255)
+
+ #: A slug which identifies the :class:`BlogEntry`.
+ slug = models.SlugField(max_length=255)
+
+ #: The :class:`Blog` which this entry has been posted to. Can be left blank to represent a "draft" status.
blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True)
+
+ #: A :class:`ForeignKey` to the author. The model is either :setting:`PHILO_PERSON_MODULE` or :class:`auth.User`.
author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
+
+ #: The date and time which the :class:`BlogEntry` is considered posted at.
date = models.DateTimeField(default=None)
- content = models.TextField()
- excerpt = models.TextField(blank=True, null=True)
- tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True)
+
+ #: The content of the :class:`BlogEntry`.
+ content = TemplateField()
+
+ #: An optional brief excerpt from the :class:`BlogEntry`.
+ excerpt = TemplateField(blank=True, null=True)
+
+ #: A ``django-taggit`` :class:`TaggableManager`.
+ tags = TaggableManager()
def save(self, *args, **kwargs):
if self.date is None:
self.date = datetime.now()
super(BlogEntry, self).save(*args, **kwargs)
+ def __unicode__(self):
+ return self.title
+
class Meta:
ordering = ['-date']
verbose_name_plural = "blog entries"
class BlogView(FeedView):
+ """
+ A subclass of :class:`.FeedView` which handles patterns and feeds for a :class:`Blog` and its related :class:`entries <BlogEntry>`.
+
+ """
ENTRY_PERMALINK_STYLE_CHOICES = (
('D', 'Year, month, and day'),
('M', 'Year and month'),
('N', 'No base')
)
+ #: The :class:`Blog` whose entries should be managed by this :class:`BlogView`
blog = models.ForeignKey(Blog, related_name='blogviews')
+ #: The main page of the :class:`Blog` will be rendered with this :class:`.Page`.
index_page = models.ForeignKey(Page, related_name='blog_index_related')
+ #: The detail view of a :class:`BlogEntry` will be rendered with this :class:`Page`.
entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
# TODO: entry_archive is misleading. Rename to ymd_page or timespan_page.
+ #: Views of :class:`BlogEntry` archives will be rendered with this :class:`Page` (optional).
entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
+ #: Views of :class:`BlogEntry` archives according to their :class:`.Tag`\ s will be rendered with this :class:`Page`.
tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
+ #: The archive of all available tags will be rendered with this :class:`Page` (optional).
tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
- entries_per_page = models.IntegerField(blank=True, validators=[validate_pagination_count], null=True)
-
+ #: This number will be passed directly into pagination for :class:`BlogEntry` list pages. Pagination will be disabled if this is left blank.
+ entries_per_page = models.IntegerField(blank=True, null=True)
+
+ #: Depending on the needs of the site, different permalink styles may be appropriate. Example subpaths are provided for a :class:`BlogEntry` posted on May 2nd, 2011 with a slug of "hello". The choices are:
+ #:
+ #: * Year, month, and day - ``2011/05/02/hello``
+ #: * Year and month - ``2011/05/hello``
+ #: * Year - ``2011/hello``
+ #: * Custom base - :attr:`entry_permalink_base`\ ``/hello``
+ #: * No base - ``hello``
entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
+ #: If the :attr:`entry_permalink_style` is set to "Custom base" then the value of this field will be used as the base subpath for year/month/day entry archive pages and entry detail pages. Default: "entries"
entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
+ #: This will be used as the base for the views of :attr:`tag_page` and :attr:`tag_archive_page`. Default: "tags"
tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
item_context_var = 'entries'
- object_attr = 'blog'
def __unicode__(self):
return u'BlogView for %s' % self.blog.title
def get_reverse_params(self, obj):
if isinstance(obj, BlogEntry):
- if obj.blog == self.blog:
+ if obj.blog_id == self.blog_id:
kwargs = {'slug': obj.slug}
if self.entry_permalink_style in 'DMY':
kwargs.update({'year': str(obj.date.year).zfill(4)})
elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
if isinstance(obj, Tag):
obj = [obj]
- slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
+ slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset(self.blog)]
if slugs:
return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)}
elif isinstance(obj, (date, datetime)):
@property
def urlpatterns(self):
- urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
- self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
+ urlpatterns = self.feed_patterns(r'^', 'get_entries', 'index_page', 'index') +\
+ self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries', 'tag_page', 'entries_by_tag')
- if self.tag_archive_page:
+ if self.tag_archive_page_id:
urlpatterns += patterns('',
url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
)
- if self.entry_archive_page:
+ if self.entry_archive_page_id:
if self.entry_permalink_style in 'DMY':
- urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries', 'entry_archive_page', 'entries_by_year')
if self.entry_permalink_style in 'DM':
- urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_month')
if self.entry_permalink_style == 'D':
- urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
+ urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_day')
if self.entry_permalink_style == 'D':
urlpatterns += patterns('',
url((r'^%s/(?P<slug>[-\w]+)$' % self.entry_permalink_base), self.entry_view)
)
else:
- urlpatterns = patterns('',
+ urlpatterns += patterns('',
url(r'^(?P<slug>[-\w]+)$', self.entry_view)
)
return urlpatterns
- def get_context(self):
- return {'blog': self.blog}
-
- def get_entry_queryset(self):
- return self.blog.entries.all()
+ def get_entry_queryset(self, obj):
+ """Returns the default :class:`QuerySet` of :class:`BlogEntry` instances for the :class:`BlogView` - all entries that are considered posted in the past. This allows for scheduled posting of entries."""
+ return obj.entries.filter(date__lte=datetime.now())
- def get_tag_queryset(self):
- return self.blog.entry_tags
+ def get_tag_queryset(self, obj):
+ """Returns the default :class:`QuerySet` of :class:`.Tag`\ s for the :class:`BlogView`'s :meth:`get_entries_by_tag` and :meth:`tag_archive_view`."""
+ return obj.entry_tags
- def get_all_entries(self, request, extra_context=None):
- return self.get_entry_queryset(), extra_context
-
- def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
- if not self.entry_archive_page:
- raise Http404
- entries = self.get_entry_queryset()
- if year:
- entries = entries.filter(date__year=year)
- if month:
- entries = entries.filter(date__month=month)
- if day:
- entries = entries.filter(date__day=day)
+ def get_object(self, request, year=None, month=None, day=None, tag_slugs=None):
+ """Returns a dictionary representing the parameters for a feed which will be exposed."""
+ if tag_slugs is None:
+ tags = None
+ else:
+ tag_slugs = tag_slugs.replace('+', '/').split('/')
+ tags = self.get_tag_queryset(self.blog).filter(slug__in=tag_slugs)
+ if not tags:
+ raise Http404
+
+ # Raise a 404 on an incorrect slug.
+ found_slugs = set([tag.slug for tag in tags])
+ for slug in tag_slugs:
+ if slug and slug not in found_slugs:
+ raise Http404
- context = extra_context or {}
- context.update({'year': year, 'month': month, 'day': day})
- return entries, context
+ try:
+ if year and month and day:
+ context_date = date(int(year), int(month), int(day))
+ elif year and month:
+ context_date = date(int(year), int(month), 1)
+ elif year:
+ context_date = date(int(year), 1, 1)
+ else:
+ context_date = None
+ except TypeError, ValueError:
+ context_date = None
+
+ return {
+ 'blog': self.blog,
+ 'tags': tags,
+ 'year': year,
+ 'month': month,
+ 'day': day,
+ 'date': context_date
+ }
- def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
- tag_slugs = tag_slugs.replace('+', '/').split('/')
- tags = self.get_tag_queryset().filter(slug__in=tag_slugs)
+ def get_entries(self, obj, request, year=None, month=None, day=None, tag_slugs=None, extra_context=None):
+ """Returns the :class:`BlogEntry` objects which will be exposed for the given object, as returned from :meth:`get_object`."""
+ entries = self.get_entry_queryset(obj['blog'])
- if not tags:
- raise Http404
+ if obj['tags'] is not None:
+ tags = obj['tags']
+ for tag in tags:
+ entries = entries.filter(tags=tag)
- # Raise a 404 on an incorrect slug.
- found_slugs = [tag.slug for tag in tags]
- for slug in tag_slugs:
- if slug and slug not in found_slugs:
- raise Http404
-
- entries = self.get_entry_queryset()
- for tag in tags:
- entries = entries.filter(tags=tag)
+ if obj['date'] is not None:
+ if year:
+ entries = entries.filter(date__year=year)
+ if month:
+ entries = entries.filter(date__month=month)
+ if day:
+ entries = entries.filter(date__day=day)
context = extra_context or {}
- context.update({'tags': tags})
+ context.update(obj)
return entries, context
def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
- entries = self.get_entry_queryset()
+ """Renders :attr:`entry_page` with the entry specified by the given parameters."""
+ entries = self.get_entry_queryset(self.blog)
if year:
entries = entries.filter(date__year=year)
if month:
return self.entry_page.render_to_response(request, extra_context=context)
def tag_archive_view(self, request, extra_context=None):
+ """Renders :attr:`tag_archive_page` with the result of :meth:`get_tag_queryset` added to the context."""
if not self.tag_archive_page:
raise Http404
context = self.get_context()
context.update(extra_context or {})
context.update({
- 'tags': self.get_tag_queryset()
+ 'tags': self.get_tag_queryset(self.blog)
})
return self.tag_archive_page.render_to_response(request, extra_context=context)
- def feed_view(self, get_items_attr, reverse_name):
- get_items = callable(get_items_attr) and get_items_attr or 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)
- items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
- self.populate_feed(feed, items, request)
-
- if 'tags' in extra_context:
- tags = extra_context['tags']
- feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
- else:
- tags = obj.entry_tags
-
- feed.feed['categories'] = [tag.name for tag in tags]
-
- response = HttpResponse(mimetype=feed.mime_type)
- feed.write(response, 'utf-8')
- return response
-
- return inner
-
def process_page_items(self, request, items):
+ """Overrides :meth:`.FeedView.process_page_items` to add pagination."""
if self.entries_per_page:
page_num = request.GET.get('page', 1)
paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num)
return items, item_context
def title(self, obj):
- return obj.title
+ title = obj['blog'].title
+ if obj['tags']:
+ title += u" – %s" % u", ".join((tag.name for tag in obj['tags']))
+ date = obj['date']
+ if date:
+ if obj['day']:
+ datestr = date.strftime("%F %j, %Y")
+ elif obj['month']:
+ datestr = date.strftime("%F, %Y")
+ elif obj['year']:
+ datestr = date.strftime("%Y")
+ title += u" – %s" % datestr
+ return title
+
+ def categories(self, obj):
+ tags = obj['tags']
+ if tags:
+ return (tag.name for tag in tags)
+ return None
def item_title(self, item):
return item.title
return [tag.name for tag in item.tags.all()]
-class Newsletter(Entity, Titled):
- pass
+class Newsletter(Entity):
+ """Represents a newsletter which will contain :class:`articles <NewsletterArticle>` organized into :class:`issues <NewsletterIssue>`."""
+ #: The name of the :class:`Newsletter`, currently callse 'title' for historical reasons.
+ title = models.CharField(max_length=255)
+ #: A slug used to identify the :class:`Newsletter`.
+ slug = models.SlugField(max_length=255)
+
+ def __unicode__(self):
+ return self.title
register_value_model(Newsletter)
-class NewsletterArticle(Entity, Titled):
+class NewsletterArticle(Entity):
+ """Represents an article in a :class:`Newsletter`"""
+ #: The title of the :class:`NewsletterArticle`.
+ title = models.CharField(max_length=255)
+ #: A slug which identifies the :class:`NewsletterArticle`.
+ slug = models.SlugField(max_length=255)
+ #: A :class:`ForeignKey` to :class:`Newsletter` representing the newsletter which this article was written for.
newsletter = models.ForeignKey(Newsletter, related_name='articles')
+ #: A :class:`ManyToManyField` to the author(s) of the :class:`NewsletterArticle`. The model is either :setting:`PHILO_PERSON_MODULE` or :class:`auth.User`.
authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
+ #: The date and time which the :class:`NewsletterArticle` is considered published at.
date = models.DateTimeField(default=None)
+ #: A :class:`.TemplateField` containing an optional short summary of the article, meant to grab a reader's attention and draw them in.
lede = TemplateField(null=True, blank=True, verbose_name='Summary')
+ #: A :class:`.TemplateField` containing the full text of the article.
full_text = TemplateField(db_index=True)
- tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
+ #: A ``django-taggit`` :class:`TaggableManager`.
+ tags = TaggableManager()
def save(self, *args, **kwargs):
if self.date is None:
self.date = datetime.now()
super(NewsletterArticle, self).save(*args, **kwargs)
+ def __unicode__(self):
+ return self.title
+
class Meta:
get_latest_by = 'date'
ordering = ['-date']
register_value_model(NewsletterArticle)
-class NewsletterIssue(Entity, Titled):
+class NewsletterIssue(Entity):
+ """Represents an issue of the newsletter."""
+ #: The title of the :class:`NewsletterIssue`.
+ title = models.CharField(max_length=255)
+ #: A slug which identifies the :class:`NewsletterIssue`.
+ slug = models.SlugField(max_length=255)
+ #: A :class:`ForeignKey` to the :class:`Newsletter` which this issue belongs to.
newsletter = models.ForeignKey(Newsletter, related_name='issues')
+ #: The numbering of the issue - for example, 04.02 for volume 4, issue 2. This is an instance of :class:`CharField` to allow any arbitrary numbering system.
numbering = models.CharField(max_length=50, help_text='For example, 04.02 for volume 4, issue 2.')
+ #: A :class:`ManyToManyField` to articles belonging to this issue.
articles = models.ManyToManyField(NewsletterArticle, related_name='issues')
+ def __unicode__(self):
+ return self.title
+
class Meta:
ordering = ['-numbering']
unique_together = (('newsletter', 'numbering'),)
class NewsletterView(FeedView):
+ """A subclass of :class:`.FeedView` which handles patterns and feeds for a :class:`Newsletter` and its related :class:`articles <NewsletterArticle>`."""
ARTICLE_PERMALINK_STYLE_CHOICES = (
('D', 'Year, month, and day'),
('M', 'Year and month'),
('S', 'Slug only')
)
+ #: A :class:`ForeignKey` to the :class:`Newsletter` managed by this :class:`NewsletterView`.
newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
+ #: A :class:`ForeignKey` to the :class:`Page` used to render the main page of this :class:`NewsletterView`.
index_page = models.ForeignKey(Page, related_name='newsletter_index_related')
+ #: A :class:`ForeignKey` to the :class:`Page` used to render the detail view of a :class:`NewsletterArticle` for this :class:`NewsletterView`.
article_page = models.ForeignKey(Page, related_name='newsletter_article_related')
+ #: A :class:`ForeignKey` to the :class:`Page` used to render the :class:`NewsletterArticle` archive pages for this :class:`NewsletterView`.
article_archive_page = models.ForeignKey(Page, related_name='newsletter_article_archive_related', null=True, blank=True)
+ #: A :class:`ForeignKey` to the :class:`Page` used to render the detail view of a :class:`NewsletterIssue` for this :class:`NewsletterView`.
issue_page = models.ForeignKey(Page, related_name='newsletter_issue_related')
+ #: A :class:`ForeignKey` to the :class:`Page` used to render the :class:`NewsletterIssue` archive pages for this :class:`NewsletterView`.
issue_archive_page = models.ForeignKey(Page, related_name='newsletter_issue_archive_related', null=True, blank=True)
+ #: Depending on the needs of the site, different permalink styles may be appropriate. Example subpaths are provided for a :class:`NewsletterArticle` posted on May 2nd, 2011 with a slug of "hello". The choices are:
+ #:
+ #: * Year, month, and day - :attr:`article_permalink_base`\ ``/2011/05/02/hello``
+ #: * Year and month - :attr:`article_permalink_base`\ ``/2011/05/hello``
+ #: * Year - :attr:`article_permalink_base`\ ``/2011/hello``
+ #: * Slug only - :attr:`article_permalink_base`\ ``/hello``
article_permalink_style = models.CharField(max_length=1, choices=ARTICLE_PERMALINK_STYLE_CHOICES)
+ #: This will be used as the base subpath for year/month/day article archive pages and article detail pages. Default: "articles"
article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
+ #: This will be used as the base subpath for issue detail pages and the issue archive page.
issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
item_context_var = 'articles'
def get_reverse_params(self, obj):
if isinstance(obj, NewsletterArticle):
- if obj.newsletter == self.newsletter:
+ if obj.newsletter_id == self.newsletter_id:
kwargs = {'slug': obj.slug}
if self.article_permalink_style in 'DMY':
kwargs.update({'year': str(obj.date.year).zfill(4)})
kwargs.update({'day': str(obj.date.day).zfill(2)})
return self.article_view, [], kwargs
elif isinstance(obj, NewsletterIssue):
- if obj.newsletter == self.newsletter:
+ if obj.newsletter_id == self.newsletter_id:
return 'issue', [], {'numbering': obj.numbering}
elif isinstance(obj, (date, datetime)):
kwargs = {
urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
)
- if self.issue_archive_page:
+ if self.issue_archive_page_id:
urlpatterns += patterns('',
url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive')
)
- if self.article_archive_page:
- urlpatterns += patterns('',
- url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
- )
+ if self.article_archive_page_id:
+ urlpatterns += self.feed_patterns(r'^%s' % self.article_permalink_base, 'get_all_articles', 'article_archive_page', 'articles')
if self.article_permalink_style in 'DMY':
urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
if self.article_permalink_style in 'DM':
def get_context(self):
return {'newsletter': self.newsletter}
- def get_article_queryset(self):
- return self.newsletter.articles.all()
+ def get_article_queryset(self, obj):
+ """Returns the default :class:`QuerySet` of :class:`NewsletterArticle` instances for the :class:`NewsletterView` - all articles that are considered posted in the past. This allows for scheduled posting of articles."""
+ return obj.articles.filter(date__lte=datetime.now())
- def get_issue_queryset(self):
- return self.newsletter.issues.all()
+ def get_issue_queryset(self, obj):
+ """Returns the default :class:`QuerySet` of :class:`NewsletterIssue` instances for the :class:`NewsletterView`."""
+ return obj.issues.all()
- def get_all_articles(self, request, extra_context=None):
- return self.get_article_queryset(), extra_context
+ def get_all_articles(self, obj, request, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for all entries."""
+ return self.get_article_queryset(obj), extra_context
- def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
- articles = self.get_article_queryset().filter(date__year=year)
+ def get_articles_by_ymd(self, obj, request, year, month=None, day=None, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for a specific year, month, and day."""
+ articles = self.get_article_queryset(obj).filter(date__year=year)
if month:
articles = articles.filter(date__month=month)
if day:
articles = articles.filter(date__day=day)
return articles, extra_context
- def get_articles_by_issue(self, request, numbering, extra_context=None):
+ def get_articles_by_issue(self, obj, request, numbering, extra_context=None):
+ """Used to generate :meth:`~.FeedView.feed_patterns` for articles from a certain issue."""
try:
- issue = self.get_issue_queryset().get(numbering=numbering)
+ issue = self.get_issue_queryset(obj).get(numbering=numbering)
except NewsletterIssue.DoesNotExist:
raise Http404
context = extra_context or {}
context.update({'issue': issue})
- return self.get_article_queryset().filter(issues=issue), context
+ return self.get_article_queryset(obj).filter(issues=issue), context
def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
- articles = self.get_article_queryset()
+ """Renders :attr:`article_page` with the article specified by the given parameters."""
+ articles = self.get_article_queryset(self.newsletter)
if year:
articles = articles.filter(date__year=year)
if month:
return self.article_page.render_to_response(request, extra_context=context)
def issue_archive_view(self, request, extra_context):
+ """Renders :attr:`issue_archive_page` with the result of :meth:`get_issue_queryset` added to the context."""
if not self.issue_archive_page:
raise Http404
context = self.get_context()
context.update(extra_context or {})
context.update({
- 'issues': self.get_issue_queryset()
+ 'issues': self.get_issue_queryset(self.newsletter)
})
return self.issue_archive_page.render_to_response(request, extra_context=context)
+"""
+Penfield supplies two template filters to handle common use cases for blogs and newsletters.
+
+"""
from django import template
from django.utils.dates import MONTHS, MONTHS_AP
+
register = template.Library()
+
+@register.filter
def monthname(value):
- monthnum = int(value)
- if 1 <= monthnum <= 12:
- return MONTHS[monthnum]
- else:
+ """Returns the name of a month with the supplied numeric value."""
+ try:
+ value = int(value)
+ except:
+ pass
+
+ try:
+ return MONTHS[value]
+ except KeyError:
return value
-register.filter('monthname', monthname)
+@register.filter
def apmonthname(value):
- monthnum = int(value)
- if 1 <= monthnum <= 12:
- return MONTHS_AP[monthnum]
- else:
- return value
-
-register.filter('apmonthname', apmonthname)
+ """Returns the Associated Press abbreviated month name for the supplied numeric value."""
+ try:
+ value = int(value)
+ except:
+ pass
+
+ try:
+ return MONTHS_AP[value]
+ except KeyError:
+ return value
\ No newline at end of file
+++ /dev/null
-from django.core.exceptions import ValidationError
-
-
-def validate_pagination_count(x):
- if x not in range(1, 10000):
- raise ValidationError('Please enter an integer between 1 and 9999.')
\ No newline at end of file
from django.contrib import admin
+
from philo.admin import TreeEntityAdmin, COLLAPSE_CLASSES, NodeAdmin, EntityAdmin
from philo.models import Node
from philo.contrib.shipherd.models import NavigationItem, Navigation
class NavigationItemInline(admin.StackedInline):
raw_id_fields = NAVIGATION_RAW_ID_FIELDS
model = NavigationItem
- extra = 1
+ extra = 0
sortable_field_name = 'order'
+ ordering = ('order',)
related_lookup_fields = {'fk': raw_id_fields}
class NodeNavigationInline(admin.TabularInline):
model = Navigation
- extra = 1
+ extra = 0
NodeAdmin.inlines = [NodeNavigationInline, NodeNavigationItemInline] + NodeAdmin.inlines
--- /dev/null
+# 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 field 'NavigationItem.slug'
+ db.delete_column('shipherd_navigationitem', 'slug')
+
+
+ def backwards(self, orm):
+
+ # User chose to not deal with backwards NULL issues for 'NavigationItem.slug'
+ raise RuntimeError("Cannot reverse this migration. 'NavigationItem.slug' 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.node': {
+ 'Meta': {'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'})
+ },
+ 'shipherd.navigation': {
+ 'Meta': {'unique_together': "(('node', 'key'),)", 'object_name': 'Navigation'},
+ 'depth': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '3'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'navigation_set'", 'to': "orm['philo.Node']"})
+ },
+ 'shipherd.navigationitem': {
+ 'Meta': {'object_name': 'NavigationItem'},
+ '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'}),
+ 'navigation': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'roots'", 'null': 'True', 'to': "orm['shipherd.Navigation']"}),
+ 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['shipherd.NavigationItem']"}),
+ 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}),
+ 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shipherd_navigationitem_related'", 'null': 'True', 'to': "orm['philo.Node']"}),
+ 'text': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+ 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['shipherd']
#encoding: utf-8
+from UserDict import DictMixin
+from hashlib import sha1
+
+from django.contrib.sites.models import Site
+from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.urlresolvers import NoReverseMatch
from django.core.validators import RegexValidator, MinValueValidator
from django.db import models
from django.forms.models import model_to_dict
-from philo.models import TreeEntity, Node, TreeManager, Entity, TargetURLModel
-from philo.validators import RedirectValidator
-from UserDict import DictMixin
+
+from philo.models.base import TreeEntity, TreeEntityManager, Entity
+from philo.models.nodes import Node, TargetURLModel
DEFAULT_NAVIGATION_DEPTH = 3
-class NavigationQuerySetMapper(object, DictMixin):
- """This class exists to prevent setting of items in the navigation cache through node.navigation."""
+class NavigationMapper(object, DictMixin):
+ """
+ The :class:`NavigationMapper` is a dictionary-like object which allows easy fetching of the root items of a navigation for a node according to a key. A :class:`NavigationMapper` instance will be available on each node instance as :attr:`Node.navigation` if :mod:`~philo.contrib.shipherd` is in the :setting:`INSTALLED_APPS`
+
+ """
def __init__(self, node):
self.node = node
+ self._cache = {}
def __getitem__(self, key):
- return Navigation.objects.get_cache_for(self.node)[key]['root_items']
-
- def keys(self):
- return Navigation.objects.get_cache_for(self.node).keys()
+ if key not in self._cache:
+ try:
+ self._cache[key] = Navigation.objects.get_for_node(self.node, key)
+ except Navigation.DoesNotExist:
+ self._cache[key] = None
+ return self._cache[key]
def navigation(self):
if not hasattr(self, '_navigation'):
- self._navigation = NavigationQuerySetMapper(self)
+ self._navigation = NavigationMapper(self)
return self._navigation
Node.navigation = property(navigation)
-class NavigationCacheQuerySet(models.query.QuerySet):
- """
- This subclass will trigger general cache clearing for Navigation.objects when a mass
- update or deletion is performed. As there is no convenient way to iterate over the
- changed or deleted instances, there's no way to be more precise about what gets cleared.
- """
- def update(self, *args, **kwargs):
- super(NavigationCacheQuerySet, self).update(*args, **kwargs)
- Navigation.objects.clear_cache()
-
- def delete(self, *args, **kwargs):
- super(NavigationCacheQuerySet, self).delete(*args, **kwargs)
- Navigation.objects.clear_cache()
-
-
class NavigationManager(models.Manager):
- # Since navigation is going to be hit frequently and changed
- # relatively infrequently, cache it. Analogous to contenttypes.
use_for_related = True
- _cache = {}
- def get_queryset(self):
- return NavigationCacheQuerySet(self.model, using=self._db)
-
- def get_cache_for(self, node, update_targets=True):
- created = False
- if not self.has_cache_for(node):
- self.create_cache_for(node)
- created = True
-
- if update_targets and not created:
- self.update_targets_for(node)
-
- return self.__class__._cache[self.db][node]
-
- def has_cache_for(self, node):
- return self.db in self.__class__._cache and node in self.__class__._cache[self.db]
-
- def create_cache_for(self, node):
- "This method loops through the nodes ancestors and caches all unique navigation keys."
- ancestors = node.get_ancestors(ascending=True, include_self=True)
-
- nodes_to_cache = []
-
- for node in ancestors:
- if self.has_cache_for(node):
- cache = self.get_cache_for(node).copy()
- break
- else:
- nodes_to_cache.insert(0, node)
- else:
- cache = {}
-
- for node in nodes_to_cache:
- cache = cache.copy()
- cache.update(self._build_cache_for(node))
- self.__class__._cache.setdefault(self.db, {})[node] = cache
-
- def _build_cache_for(self, node):
- cache = {}
- tree_id_attr = NavigationItem._mptt_meta.tree_id_attr
- level_attr = NavigationItem._mptt_meta.level_attr
-
- for navigation in node.navigation_set.all():
- tree_ids = navigation.roots.values_list(tree_id_attr)
- items = list(NavigationItem.objects.filter(**{'%s__in' % tree_id_attr: tree_ids, '%s__lt' % level_attr: navigation.depth}).order_by('order', 'lft'))
+ def get_for_node(self, node, key):
+ cache_key = self._get_cache_key(node, key)
+ cached = cache.get(cache_key)
+
+ if cached is None:
+ opts = Node._mptt_meta
+ left = getattr(node, opts.left_attr)
+ right = getattr(node, opts.right_attr)
+ tree_id = getattr(node, opts.tree_id_attr)
+ kwargs = {
+ "node__%s__lte" % opts.left_attr: left,
+ "node__%s__gte" % opts.right_attr: right,
+ "node__%s" % opts.tree_id_attr: tree_id
+ }
+ navs = self.filter(key=key, **kwargs).select_related('node').order_by('-node__%s' % opts.level_attr)
+ nav = navs[0]
+ roots = nav.roots.all().select_related('target_node').order_by('order')
+ item_opts = NavigationItem._mptt_meta
+ by_pk = {}
+ tree_ids = []
- root_items = []
+ site_root_node = Site.objects.get_current().root_node
- for item in items:
- item._is_cached = True
-
- if not hasattr(item, '_cached_children'):
- item._cached_children = []
-
- if item.parent:
- # alternatively, if I don't want to force it to a list, I could keep track of
- # instances where the parent hasn't yet been met and do this step later for them.
- # delayed action.
- item.parent = items[items.index(item.parent)]
- if not hasattr(item.parent, '_cached_children'):
- item.parent._cached_children = []
- item.parent._cached_children.append(item)
- else:
- root_items.append(item)
+ for root in roots:
+ by_pk[root.pk] = root
+ tree_ids.append(getattr(root, item_opts.tree_id_attr))
+ root._cached_children = []
+ if root.target_node:
+ root.target_node.get_path(root=site_root_node)
+ root.navigation = nav
- cache[navigation.key] = {
- 'navigation': navigation,
- 'root_items': root_items,
- 'items': items
+ kwargs = {
+ '%s__in' % item_opts.tree_id_attr: tree_ids,
+ '%s__lt' % item_opts.level_attr: nav.depth,
+ '%s__gt' % item_opts.level_attr: 0
}
+ items = NavigationItem.objects.filter(**kwargs).select_related('target_node').order_by('level', 'order')
+ for item in items:
+ by_pk[item.pk] = item
+ item._cached_children = []
+ parent_pk = getattr(item, '%s_id' % item_opts.parent_attr)
+ item.parent = by_pk[parent_pk]
+ item.parent._cached_children.append(item)
+ if item.target_node:
+ item.target_node.get_path(root=site_root_node)
+
+ cached = roots
+ cache.set(cache_key, cached)
- return cache
-
- def clear_cache_for(self, node):
- # Clear the cache for this node and all its descendants. The
- # navigation for this node has probably changed, and for now,
- # it isn't worth it to only clear the descendants actually
- # affected by this.
- if not self.has_cache_for(node):
- # Already cleared.
- return
-
- descendants = node.get_descendants(include_self=True)
- cache = self.__class__._cache[self.db]
- for node in descendants:
- cache.pop(node, None)
+ return cached
- def update_targets_for(self, node):
- # Manually update a cache's target nodes in case something's changed there.
- # This should be a less complex operation than reloading the models each
- # time. Not as good as selective updates... but not much to be done
- # about that. TODO: Benchmark it.
- caches = self.__class__._cache[self.db][node].values()
-
- target_pks = set()
+ def _get_cache_key(self, node, key):
+ opts = Node._mptt_meta
+ left = getattr(node, opts.left_attr)
+ right = getattr(node, opts.right_attr)
+ tree_id = getattr(node, opts.tree_id_attr)
+ parent_id = getattr(node, "%s_id" % opts.parent_attr)
- for cache in caches:
- target_pks |= set([item.target_node_id for item in cache['items']])
-
- # A distinct query is not strictly necessary. TODO: benchmark the efficiency
- # with/without distinct.
- targets = list(Node.objects.filter(pk__in=target_pks).distinct())
-
- for cache in caches:
- for item in cache['items']:
- if item.target_node_id:
- item.target_node = targets[targets.index(item.target_node)]
-
- def clear_cache(self):
- self.__class__._cache.pop(self.db, None)
+ return sha1(unicode(left) + unicode(right) + unicode(tree_id) + unicode(parent_id) + unicode(node.pk) + unicode(key)).hexdigest()
class Navigation(Entity):
+ """
+ :class:`Navigation` represents a group of :class:`NavigationItem`\ s that have an intrinsic relationship in terms of navigating a website. For example, a ``main`` navigation versus a ``side`` navigation, or a ``authenticated`` navigation versus an ``anonymous`` navigation.
+
+ A :class:`Navigation`'s :class:`NavigationItem`\ s will be accessible from its related :class:`.Node` and that :class:`.Node`'s descendants through a :class:`NavigationMapper` instance at :attr:`Node.navigation`. Example::
+
+ >>> node.navigation_set.all()
+ []
+ >>> parent = node.parent
+ >>> items = parent.navigation_set.get(key='main').roots.all()
+ >>> parent.navigation["main"] == node.navigation["main"] == list(items)
+ True
+
+ """
+ #: A :class:`NavigationManager` instance.
objects = NavigationManager()
+ #: The :class:`.Node` which the :class:`Navigation` is attached to. The :class:`Navigation` will also be available to all the :class:`.Node`'s descendants and will override any :class:`Navigation` with the same key on any of the :class:`.Node`'s ancestors.
node = models.ForeignKey(Node, related_name='navigation_set', help_text="Be available as navigation for this node.")
+ #: Each :class:`Navigation` has a ``key`` which consists of one or more word characters so that it can easily be accessed in a template as ``{{ node.navigation.this_key }}``.
key = models.CharField(max_length=255, validators=[RegexValidator("\w+")], help_text="Must contain one or more alphanumeric characters or underscores.", db_index=True)
+ #: There is no limit to the depth of a tree of :class:`NavigationItem`\ s, but ``depth`` will limit how much of the tree will be displayed.
depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.")
- def __init__(self, *args, **kwargs):
- super(Navigation, self).__init__(*args, **kwargs)
- self._initial_data = model_to_dict(self)
-
def __unicode__(self):
return "%s[%s]" % (self.node, self.key)
- def _has_changed(self):
- return self._initial_data != model_to_dict(self)
-
- def save(self, *args, **kwargs):
- super(Navigation, self).save(*args, **kwargs)
-
- if self._has_changed():
- Navigation.objects.clear_cache_for(self.node)
- self._initial_data = model_to_dict(self)
-
- def delete(self, *args, **kwargs):
- super(Navigation, self).delete(*args, **kwargs)
- Navigation.objects.clear_cache_for(self.node)
-
class Meta:
unique_together = ('node', 'key')
-class NavigationItemManager(TreeManager):
- use_for_related = True
-
- def get_queryset(self):
- return NavigationCacheQuerySet(self.model, using=self._db)
-
-
class NavigationItem(TreeEntity, TargetURLModel):
- objects = NavigationItemManager()
-
+ #: A :class:`ForeignKey` to a :class:`Navigation` instance. If this is not null, then the :class:`NavigationItem` will be a root node of the :class:`Navigation` instance.
navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.")
+ #: The text which will be displayed in the navigation. This is a :class:`CharField` instance with max length 50.
text = models.CharField(max_length=50)
+ #: The order in which the :class:`NavigationItem` will be displayed.
order = models.PositiveSmallIntegerField(default=0)
- def __init__(self, *args, **kwargs):
- super(NavigationItem, self).__init__(*args, **kwargs)
- self._initial_data = model_to_dict(self)
- self._is_cached = False
-
- def __unicode__(self):
- return self.get_path(field='text', pathsep=u' › ')
+ def get_path(self, root=None, pathsep=u' › ', field='text'):
+ return super(NavigationItem, self).get_path(root, pathsep, field)
+ path = property(get_path)
def clean(self):
super(NavigationItem, self).clean()
raise ValidationError("Exactly one of `parent` and `navigation` must be defined.")
def is_active(self, request):
+ """Returns ``True`` if the :class:`NavigationItem` is considered active for a given request and ``False`` otherwise."""
if self.target_url == request.path:
# Handle the `default` case where the target_url and requested path
# are identical.
# the same as the request path, check whether the target node is an ancestor
# of the requested node. If so, this is active unless the target node
# is the same as the ``host node`` for this navigation structure.
- try:
- host_node = self.get_root().navigation.node
- except AttributeError:
- pass
- else:
- if self.target_node != host_node and self.target_node.is_ancestor_of(request.node):
- return True
+ root = self
+
+ # The common case will be cached items, whose parents are cached with them.
+ while root.parent is not None:
+ root = root.parent
+
+ host_node_id = root.navigation.node_id
+ if self.target_node.pk != host_node_id and self.target_node.is_ancestor_of(request.node):
+ return True
return False
def has_active_descendants(self, request):
+ """Returns ``True`` if the :class:`NavigationItem` has active descendants and ``False`` otherwise."""
for child in self.get_children():
if child.is_active(request) or child.has_active_descendants(request):
return True
- return False
-
- def _has_changed(self):
- if model_to_dict(self) == self._initial_data:
- return False
- return True
-
- def _clear_cache(self):
- try:
- root = self.get_root()
- if self.get_level() < root.navigation.depth:
- Navigation.objects.clear_cache_for(self.get_root().navigation.node)
- except AttributeError:
- pass
-
- def save(self, *args, **kwargs):
- super(NavigationItem, self).save(*args, **kwargs)
-
- if self._has_changed():
- self._clear_cache()
-
- def delete(self, *args, **kwargs):
- super(NavigationItem, self).delete(*args, **kwargs)
- self._clear_cache()
\ No newline at end of file
+ return False
\ No newline at end of file
@register.tag
def recursenavigation(parser, token):
"""
- The recursenavigation templatetag takes two arguments:
- - the node for which the navigation should be found
- - the navigation's key.
+ The :ttag:`recursenavigation` templatetag takes two arguments:
- It will then recursively loop over each item in the navigation and render the template
- chunk within the block. recursenavigation sets the following variables in the context:
+ * the :class:`.Node` for which the :class:`.Navigation` should be found
+ * the :class:`.Navigation`'s :attr:`~.Navigation.key`.
+
+ It will then recursively loop over each :class:`.NavigationItem` in the :class:`.Navigation` and render the template
+ chunk within the block. :ttag:`recursenavigation` sets the following variables in the context:
============================== ================================================
Variable Description
``navloop.first`` True if this is the first time through the current level
``navloop.last`` True if this is the last time through the current level
``navloop.parentloop`` This is the loop one level "above" the current one
- ============================== ================================================
- ``item`` The current item in the loop (a NavigationItem instance)
+
+ ``item`` The current item in the loop (a :class:`.NavigationItem` instance)
``children`` If accessed, performs the next level of recursion.
``navloop.active`` True if the item is active for this request
``navloop.active_descendants`` True if the item has active descendants for this request
============================== ================================================
- Example:
+ Example::
+
<ul>
- {% recursenavigation node main %}
- <li{% if navloop.active %} class='active'{% endif %}>
- {{ navloop.item.text }}
- {% if item.get_children %}
- <ul>
- {{ children }}
- </ul>
- {% endif %}
- </li>
- {% endrecursenavigation %}
+ {% recursenavigation node "main" %}
+ <li{% if navloop.active %} class='active'{% endif %}>
+ <a href="{{ item.get_target_url }}">{{ item.text }}</a>
+ {% if item.get_children %}
+ <ul>
+ {{ children }}
+ </ul>
+ {% endif %}
+ </li>
+ {% endrecursenavigation %}
</ul>
+
+ .. note:: {% recursenavigation %} requires that the current :class:`HttpRequest` be present in the context as ``request``. The simplest way to do this is with the `request context processor`_. Simply make sure that ``django.core.context_processors.request`` is included in your :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting.
+
+ .. _request context processor: https://docs.djangoproject.com/en/dev/ref/templates/api/#django-core-context-processors-request
+
"""
bits = token.contents.split()
if len(bits) != 3:
@register.filter
def has_navigation(node, key=None):
+ """Returns ``True`` if the node has a :class:`.Navigation` with the given key and ``False`` otherwise. If ``key`` is ``None``, returns whether the node has any :class:`.Navigation`\ s at all."""
try:
- nav = node.navigation
- if key is not None:
- if key in nav and bool(node.navigation[key]):
- return True
- elif key not in node.navigation:
- return False
- return bool(node.navigation)
+ return bool(node.navigation[key])
except:
return False
@register.filter
def navigation_host(node, key):
+ """Returns the :class:`.Node` which hosts the :class:`.Navigation` which ``node`` has inherited for ``key``. Returns ``node`` if any exceptions are encountered."""
try:
- return Navigation.objects.filter(node__in=node.get_ancestors(include_self=True), key=key).order_by('-node__level')[0].node
+ return node.navigation[key].node
except:
return node
\ No newline at end of file
+"""
+Sobol implements a generic search interface, which can be used to search databases or websites. No assumptions are made about the search method. If SOBOL_USE_CACHE is ``True`` (default), the results will be cached using django's cache framework. Be aware that this may use a large number of cache entries, as a unique entry will be made for each search string for each type of search.
+
+Settings
+--------
+
+:setting:`SOBOL_USE_CACHE`
+ Whether sobol will use django's cache framework. Defaults to ``True``; this may cause a lot of entries in the cache.
+
+:setting:`SOBOL_USE_EVENTLET`
+ If :mod:`eventlet` is installed and this setting is ``True``, sobol web searches will use :mod:`eventlet.green.urllib2` instead of the built-in :mod:`urllib2` module. Default: ``False``.
+
+Templates
+---------
+
+For convenience, :mod:`.sobol` provides a template at ``sobol/search/_list.html`` which can be used with an ``{% include %}`` tag inside a full search page template to list the search results. The ``_list.html`` template also uses a basic jQuery script (``static/sobol/ajax_search.js``) to handle AJAX search result loading if the AJAX API of the current :class:`.SearchView` is enabled. If you want to use ``_list.html``, but want to provide your own version of jQuery or your own AJAX loading script, or if you want to include the basic script somewhere else (like inside the ``<head>``) simply do the following::
+
+ {% include "sobol/search/_list.html" with suppress_scripts=1 %}
+
+"""
+
from philo.contrib.sobol.search import *
\ No newline at end of file
+from functools import update_wrapper
+
from django.conf import settings
from django.conf.urls.defaults import patterns, url
from django.contrib import admin
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.translation import ugettext_lazy as _
+
from philo.admin import EntityAdmin
from philo.contrib.sobol.models import Search, ResultURL, SearchView
-from functools import update_wrapper
class ResultURLInline(admin.TabularInline):
search_fields = ['string', 'result_urls__url']
actions = ['results_action']
if 'grappelli' in settings.INSTALLED_APPS:
- results_template = 'admin/sobol/search/grappelli_results.html'
- else:
- results_template = 'admin/sobol/search/results.html'
-
- def get_urls(self):
- urlpatterns = super(SearchAdmin, self).get_urls()
-
- def wrap(view):
- def wrapper(*args, **kwargs):
- return self.admin_site.admin_view(view)(*args, **kwargs)
- return update_wrapper(wrapper, view)
-
- info = self.model._meta.app_label, self.model._meta.module_name
-
- urlpatterns = patterns('',
- url(r'^results/$', wrap(self.results_view), name="%s_%s_selected_results" % info),
- url(r'^(.+)/results/$', wrap(self.results_view), name="%s_%s_results" % info)
- ) + urlpatterns
- return urlpatterns
+ change_form_template = 'admin/sobol/search/grappelli_change_form.html'
def unique_urls(self, obj):
return obj.unique_urls
def queryset(self, request):
qs = super(SearchAdmin, self).queryset(request)
return qs.annotate(total_clicks=Count('result_urls__clicks', distinct=True), unique_urls=Count('result_urls', distinct=True))
-
- def results_action(self, request, queryset):
- info = self.model._meta.app_label, self.model._meta.module_name
- if len(queryset) == 1:
- return HttpResponseRedirect(reverse("admin:%s_%s_results" % info, args=(queryset[0].pk,)))
- else:
- url = reverse("admin:%s_%s_selected_results" % info)
- return HttpResponseRedirect("%s?ids=%s" % (url, ','.join([str(item.pk) for item in queryset])))
- results_action.short_description = "View results for selected %(verbose_name_plural)s"
-
- def results_view(self, request, object_id=None, extra_context=None):
- if object_id is not None:
- object_ids = [object_id]
- else:
- object_ids = request.GET.get('ids').split(',')
-
- if object_ids is None:
- raise Http404
-
- qs = self.queryset(request).filter(pk__in=object_ids)
- opts = self.model._meta
-
- if len(object_ids) == 1:
- title = _(u"Search results for %s" % qs[0])
- else:
- title = _(u"Search results for multiple objects")
-
- context = {
- 'title': title,
- 'queryset': qs,
- 'opts': opts,
- 'root_path': self.admin_site.root_path,
- 'app_label': opts.app_label
- }
- return render_to_response(self.results_template, context, context_instance=RequestContext(request))
class SearchViewAdmin(EntityAdmin):
from django import forms
+
from philo.contrib.sobol.utils import SEARCH_ARG_GET_KEY
--- /dev/null
+# 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 model 'Search'
+ db.create_table('sobol_search', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('string', self.gf('django.db.models.fields.TextField')()),
+ ))
+ db.send_create_signal('sobol', ['Search'])
+
+ # Adding model 'ResultURL'
+ db.create_table('sobol_resulturl', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('search', self.gf('django.db.models.fields.related.ForeignKey')(related_name='result_urls', to=orm['sobol.Search'])),
+ ('url', self.gf('django.db.models.fields.TextField')()),
+ ))
+ db.send_create_signal('sobol', ['ResultURL'])
+
+ # Adding model 'Click'
+ db.create_table('sobol_click', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('result', self.gf('django.db.models.fields.related.ForeignKey')(related_name='clicks', to=orm['sobol.ResultURL'])),
+ ('datetime', self.gf('django.db.models.fields.DateTimeField')()),
+ ))
+ db.send_create_signal('sobol', ['Click'])
+
+ # Adding model 'SearchView'
+ db.create_table('sobol_searchview', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('results_page', self.gf('django.db.models.fields.related.ForeignKey')(related_name='search_results_related', to=orm['philo.Page'])),
+ ('searches', self.gf('philo.models.fields.SlugMultipleChoiceField')()),
+ ('enable_ajax_api', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('placeholder_text', self.gf('django.db.models.fields.CharField')(default='Search', max_length=75)),
+ ))
+ db.send_create_signal('sobol', ['SearchView'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'Search'
+ db.delete_table('sobol_search')
+
+ # Deleting model 'ResultURL'
+ db.delete_table('sobol_resulturl')
+
+ # Deleting model 'Click'
+ db.delete_table('sobol_click')
+
+ # Deleting model 'SearchView'
+ db.delete_table('sobol_searchview')
+
+
+ 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.node': {
+ 'Meta': {'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', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}),
+ 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ '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.template': {
+ 'Meta': {'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'})
+ },
+ 'sobol.click': {
+ 'Meta': {'ordering': "['datetime']", 'object_name': 'Click'},
+ 'datetime': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'result': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'clicks'", 'to': "orm['sobol.ResultURL']"})
+ },
+ 'sobol.resulturl': {
+ 'Meta': {'ordering': "['url']", 'object_name': 'ResultURL'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'search': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'result_urls'", 'to': "orm['sobol.Search']"}),
+ 'url': ('django.db.models.fields.TextField', [], {})
+ },
+ 'sobol.search': {
+ 'Meta': {'ordering': "['string']", 'object_name': 'Search'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'string': ('django.db.models.fields.TextField', [], {})
+ },
+ 'sobol.searchview': {
+ 'Meta': {'object_name': 'SearchView'},
+ 'enable_ajax_api': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'placeholder_text': ('django.db.models.fields.CharField', [], {'default': "'Search'", 'max_length': '75'}),
+ 'results_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'search_results_related'", 'to': "orm['philo.Page']"}),
+ 'searches': ('philo.models.fields.SlugMultipleChoiceField', [], {})
+ }
+ }
+
+ complete_apps = ['sobol']
+import datetime
+import itertools
+
+from django.conf import settings
from django.conf.urls.defaults import patterns, url
from django.contrib import messages
from django.core.exceptions import ValidationError
+from django.core.validators import URLValidator
from django.db import models
from django.http import HttpResponseRedirect, Http404, HttpResponse
from django.utils import simplejson as json
from django.utils.datastructures import SortedDict
-from philo.contrib.sobol import registry
+
+from philo.contrib.sobol import registry, get_search_instance
from philo.contrib.sobol.forms import SearchForm
-from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KEY, SEARCH_ARG_GET_KEY, check_redirect_hash
+from philo.contrib.sobol.utils import HASH_REDIRECT_GET_KEY, URL_REDIRECT_GET_KEY, SEARCH_ARG_GET_KEY, check_redirect_hash, RegistryIterator
from philo.exceptions import ViewCanNotProvideSubpath
from philo.models import MultiView, Page
from philo.models.fields import SlugMultipleChoiceField
-from philo.validators import RedirectValidator
-import datetime
-try:
- import eventlet
-except:
- eventlet = False
+
+eventlet = None
+if getattr(settings, 'SOBOL_USE_EVENTLET', False):
+ try:
+ import eventlet
+ except:
+ pass
class Search(models.Model):
+ """Represents all attempts to search for a unique string."""
+ #: The string which was searched for.
string = models.TextField()
def __unicode__(self):
return self.string
def get_weighted_results(self, threshhold=None):
- "Returns this search's results ordered by decreasing weight."
+ """
+ Returns a list of :class:`ResultURL` instances related to the search and ordered by decreasing weight. This will be cached on the instance.
+
+ :param threshhold: The earliest datetime that a :class:`Click` can have been made on a related :class:`ResultURL` in order to be included in the weighted results (or ``None`` to include all :class:`Click`\ s and :class:`ResultURL`\ s).
+
+ """
if not hasattr(self, '_weighted_results'):
result_qs = self.result_urls.all()
def get_favored_results(self, error=5, threshhold=None):
"""
- Calculate the set of most-favored results. A higher error
- will cause this method to be more reticent about adding new
- items.
-
- The thought is to see whether there are any results which
- vastly outstrip the other options. As such, evenly-weighted
- results should be grouped together and either added or
- excluded as a group.
+ Calculates the set of most-favored results based on their weight. Evenly-weighted results will be grouped together and either added or excluded as a group.
+
+ :param error: An arbitrary number; higher values will cause this method to be more reticent about adding new items to the favored results.
+ :param threshhold: Will be passed directly into :meth:`get_weighted_results`
+
"""
if not hasattr(self, '_favored_results'):
results = self.get_weighted_results(threshhold)
self._favored_results += subresults
else:
break
+ if len(self._favored_results) == len(results):
+ self._favored_results = []
return self._favored_results
class Meta:
class ResultURL(models.Model):
+ """Represents a URL which has been selected one or more times for a :class:`Search`."""
+ #: A :class:`ForeignKey` to the :class:`Search` which the :class:`ResultURL` is related to.
search = models.ForeignKey(Search, related_name='result_urls')
- url = models.TextField(validators=[RedirectValidator()])
+ #: The URL which was selected.
+ url = models.TextField(validators=[URLValidator()])
def __unicode__(self):
return self.url
def get_weight(self, threshhold=None):
+ """
+ Calculates, caches, and returns the weight of the :class:`ResultURL`.
+
+ :param threshhold: The datetime limit before which :class:`Click`\ s will not contribute to the weight of the :class:`ResultURL`.
+
+ """
if not hasattr(self, '_weight'):
clicks = self.clicks.all()
class Click(models.Model):
+ """Represents a click on a :class:`ResultURL`."""
+ #: A :class:`ForeignKey` to the :class:`ResultURL` which the :class:`Click` is related to.
result = models.ForeignKey(ResultURL, related_name='clicks')
+ #: The datetime when the click was registered in the system.
datetime = models.DateTimeField()
def __unicode__(self):
return self.datetime.strftime('%B %d, %Y %H:%M:%S')
def get_weight(self, default=1, weighted=lambda value, days: value/days**2):
+ """Calculates and returns the weight of the :class:`Click`."""
if not hasattr(self, '_weight'):
days = (datetime.datetime.now() - self.datetime).days
if days < 0:
get_latest_by = 'datetime'
+try:
+ from south.modelsinspector import add_introspection_rules
+except ImportError:
+ pass
+else:
+ add_introspection_rules([], ["^philo\.contrib\.sobol\.models\.RegistryChoiceField"])
+
+
class SearchView(MultiView):
+ """Handles a view for the results of a search, anonymously tracks the selections made by end users, and provides an AJAX API for asynchronous search result loading. This can be particularly useful if some searches are slow."""
+ #: :class:`ForeignKey` to a :class:`.Page` which will be used to render the search results.
results_page = models.ForeignKey(Page, related_name='search_results_related')
+ #: A :class:`.SlugMultipleChoiceField` whose choices are the contents of :obj:`.sobol.search.registry`
searches = SlugMultipleChoiceField(choices=registry.iterchoices())
- enable_ajax_api = models.BooleanField("Enable AJAX API", default=True, help_text="Search results will be available <i>only</i> by AJAX, not as template variables.")
+ #: A :class:`BooleanField` which controls whether or not the AJAX API is enabled.
+ #:
+ #: .. note:: If the AJAX API is enabled, a ``ajax_api_url`` attribute will be added to each search instance containing the url and get parameters for an AJAX request to retrieve results for that search.
+ #:
+ #: .. note:: Be careful not to access :attr:`search_instance.results <.BaseSearch.results>` if the AJAX API is enabled - otherwise the search will be run immediately rather than on the AJAX request.
+ enable_ajax_api = models.BooleanField("Enable AJAX API", default=True)
+ #: A :class:`CharField` containing the placeholder text which is intended to be used for the search box for the :class:`SearchView`. It is the template author's responsibility to make use of this information.
placeholder_text = models.CharField(max_length=75, default="Search")
+ #: The form which will be used to validate the input to the search box for this :class:`SearchView`.
search_form = SearchForm
def __unicode__(self):
)
return urlpatterns
- def get_search_instance(self, slug, search_string):
- return registry[slug](search_string.lower())
-
def results_view(self, request, extra_context=None):
+ """
+ Renders :attr:`results_page` with a context containing an instance of :attr:`search_form`. If the form was submitted and was valid, then one of two things has happened:
+
+ * A search has been initiated. In this case, a list of search instances will be added to the context as ``searches``. If :attr:`enable_ajax_api` is enabled, each instance will have an ``ajax_api_url`` attribute containing the url needed to make an AJAX request for the search results.
+ * A link has been chosen. In this case, corresponding :class:`Search`, :class:`ResultURL`, and :class:`Click` instances will be created and the user will be redirected to the link's actual url.
+
+ """
results = None
context = self.get_context()
messages.add_message(request, messages.INFO, "The link you followed had been tampered with. Here are all the results for your search term instead!")
# TODO: Should search_string be escaped here?
return HttpResponseRedirect("%s?%s=%s" % (request.path, SEARCH_ARG_GET_KEY, search_string))
- if not self.enable_ajax_api:
- search_instances = []
- if eventlet:
- pool = eventlet.GreenPool()
- for slug in self.searches:
- search_instance = self.get_search_instance(slug, search_string)
+
+ search_instances = []
+ for slug in self.searches:
+ if slug in registry:
+ search_instance = get_search_instance(slug, search_string)
search_instances.append(search_instance)
- if eventlet:
- pool.spawn_n(self.make_result_cache, search_instance)
- else:
- self.make_result_cache(search_instance)
- if eventlet:
- pool.waitall()
- context.update({
- 'searches': search_instances
- })
+
+ if self.enable_ajax_api:
+ search_instance.ajax_api_url = "%s?%s=%s" % (self.reverse('ajax_api_view', kwargs={'slug': slug}, node=request.node), SEARCH_ARG_GET_KEY, search_string)
+
+ if eventlet and not self.enable_ajax_api:
+ pool = eventlet.GreenPool()
+ for instance in search_instances:
+ pool.spawn_n(lambda x: x.results, search_instance)
+ pool.waitall()
+
+ context.update({
+ 'searches': search_instances,
+ 'favored_results': []
+ })
+
+ try:
+ search = Search.objects.get(string=search_string)
+ except Search.DoesNotExist:
+ pass
else:
- context.update({
- 'searches': [{'verbose_name': verbose_name, 'slug': slug, 'url': self.reverse('ajax_api_view', kwargs={'slug': slug}, node=request.node), 'result_template': registry[slug].result_template} for slug, verbose_name in registry.iterchoices() if slug in self.searches]
- })
+ context['favored_results'] = [r.url for r in search.get_favored_results()]
else:
form = SearchForm()
})
return self.results_page.render_to_response(request, extra_context=context)
- def make_result_cache(self, search_instance):
- search_instance.results
-
def ajax_api_view(self, request, slug, extra_context=None):
+ """
+ Returns a JSON object containing the following variables:
+
+ search
+ Contains the slug for the search.
+ results
+ Contains the results of :meth:`.Result.get_context` for each result.
+ rendered
+ Contains the results of :meth:`.Result.render` for each result.
+ hasMoreResults
+ ``True`` or ``False`` whether the search has more results according to :meth:`BaseSearch.has_more_results`
+ moreResultsURL
+ Contains ``None`` or a querystring which, once accessed, will note the :class:`Click` and redirect the user to a page containing more results.
+
+ """
search_string = request.GET.get(SEARCH_ARG_GET_KEY)
- if not request.is_ajax() or not self.enable_ajax_api or slug not in self.searches or search_string is None:
+ if not request.is_ajax() or not self.enable_ajax_api or slug not in registry or slug not in self.searches or search_string is None:
raise Http404
- search_instance = self.get_search_instance(slug, search_string)
- response = HttpResponse(json.dumps({
+ search_instance = get_search_instance(slug, search_string)
+
+ return HttpResponse(json.dumps({
+ 'search': search_instance.slug,
'results': [result.get_context() for result in search_instance.results],
- }))
- return response
\ No newline at end of file
+ 'hasMoreResults': search_instance.has_more_results,
+ 'moreResultsURL': search_instance.more_results_url,
+ }), mimetype="application/json")
\ No newline at end of file
#encoding: utf-8
+import datetime
+from hashlib import sha1
from django.conf import settings
from django.contrib.sites.models import Site
from django.utils.http import urlquote_plus
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
-from django.template import loader, Context, Template
-import datetime
+from django.template import loader, Context, Template, TemplateDoesNotExist
+
from philo.contrib.sobol.utils import make_tracking_querydict
+from philo.utils.registry import Registry
-try:
- from eventlet.green import urllib2
-except:
+
+if getattr(settings, 'SOBOL_USE_EVENTLET', False):
+ try:
+ from eventlet.green import urllib2
+ except:
+ import urllib2
+else:
import urllib2
__all__ = (
- 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'registry'
+ 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'registry', 'get_search_instance'
)
-SEARCH_CACHE_KEY = 'philo_sobol_search_results'
-DEFAULT_RESULT_TEMPLATE_STRING = "{% if url %}<a href='{{ url }}'>{% endif %}{{ title }}{% if url %}</a>{% endif %}"
-DEFAULT_RESULT_TEMPLATE = Template(DEFAULT_RESULT_TEMPLATE_STRING)
+SEARCH_CACHE_SEED = 'philo_sobol_search_results'
+USE_CACHE = getattr(settings, 'SOBOL_USE_CACHE', True)
-# Determines the timeout on the entire result cache.
-MAX_CACHE_TIMEOUT = 60*24*7
+#: A registry for :class:`BaseSearch` subclasses that should be available in the admin.
+registry = Registry()
-class RegistrationError(Exception):
- pass
-
-class SearchRegistry(object):
- # Holds a registry of search types by slug.
- def __init__(self):
- self._registry = {}
-
- def register(self, search, slug=None):
- slug = slug or search.slug
- if slug in self._registry:
- registered = self._registry[slug]
- if registered.__module__ != search.__module__:
- raise RegistrationError("A different search is already registered as `%s`" % slug)
- else:
- self._registry[slug] = search
-
- def unregister(self, search, slug=None):
- if slug is not None:
- if slug in self._registry and self._registry[slug] == search:
- del self._registry[slug]
- raise RegistrationError("`%s` is not registered as `%s`" % (search, slug))
- else:
- for slug, search in self._registry.items():
- if search == search:
- del self._registry[slug]
-
- def items(self):
- return self._registry.items()
-
- def iteritems(self):
- return self._registry.iteritems()
-
- def iterchoices(self):
- for slug, search in self.iteritems():
- yield slug, search.verbose_name
-
- def __getitem__(self, key):
- return self._registry[key]
-
- def __iter__(self):
- return self._registry.__iter__()
+def _make_cache_key(search, search_arg):
+ return sha1(SEARCH_CACHE_SEED + search.slug + search_arg).hexdigest()
-registry = SearchRegistry()
+def get_search_instance(slug, search_arg):
+ """Returns a search instance for the given slug, either from the cache or newly-instantiated."""
+ search = registry[slug]
+ search_arg = search_arg.lower()
+ if USE_CACHE:
+ key = _make_cache_key(search, search_arg)
+ cached = cache.get(key)
+ if cached:
+ return cached
+ instance = search(search_arg)
+ instance.slug = slug
+ return instance
class Result(object):
"""
- A result is instantiated with a configuration dictionary, a search,
- and a template name. The configuration dictionary is expected to
- define a `title` and optionally a `url`. Any other variables may be
- defined; they will be made available through the result object in
- the template, if one is defined.
+ :class:`Result` is a helper class that, given a search and a result of that search, is able to correctly render itself with a template defined by the search. Every :class:`Result` will pass a ``title``, a ``url`` (if applicable), and the raw ``result`` returned by the search into the template context when rendering.
+
+ :param search: An instance of a :class:`BaseSearch` subclass or an object that implements the same API.
+ :param result: An arbitrary result from the ``search``.
+
"""
def __init__(self, search, result):
self.search = search
self.result = result
def get_title(self):
+ """Returns the title of the result by calling :meth:`BaseSearch.get_result_title` on the raw result."""
return self.search.get_result_title(self.result)
def get_url(self):
- qd = self.search.get_result_querydict(self.result)
- if qd is None:
- return ""
- return "?%s" % qd.urlencode()
+ """Returns the url of the result or ``None`` by calling :meth:`BaseSearch.get_result_url` on the raw result. This url will contain a querystring which, if used, will track a :class:`.Click` for the actual url."""
+ return self.search.get_result_url(self.result)
+
+ def get_actual_url(self):
+ """Returns the actual url of the result by calling :meth:`BaseSearch.get_actual_result_url` on the raw result."""
+ return self.search.get_actual_result_url(self.result)
+
+ def get_content(self):
+ """Returns the content of the result by calling :meth:`BaseSearch.get_result_content` on the raw result."""
+ return self.search.get_result_content(self.result)
def get_template(self):
+ """Returns the template which will be used to render the :class:`Result` by calling :meth:`BaseSearch.get_result_template` on the raw result."""
return self.search.get_result_template(self.result)
- def get_extra_context(self):
- return self.search.get_result_extra_context(self.result)
-
def get_context(self):
- context = self.get_extra_context()
- context.update({
- 'title': self.get_title(),
- 'url': self.get_url()
- })
- return context
+ """
+ Returns the context dictionary for the result. This is used both in rendering the result and in the AJAX return value for :meth:`.SearchView.ajax_api_view`. The context will contain the following keys:
+
+ title
+ The result of calling :meth:`get_title`
+ url
+ The result of calling :meth:`get_url`
+ content
+ The result of calling :meth:`get_content`
+
+ """
+ if not hasattr(self, '_context'):
+ self._context = {
+ 'title': self.get_title(),
+ 'url': self.get_url(),
+ 'actual_url': self.get_actual_url(),
+ 'content': self.get_content()
+ }
+ return self._context
def render(self):
+ """Returns the template from :meth:`get_template` rendered with the context from :meth:`get_context`."""
t = self.get_template()
c = Context(self.get_context())
return t.render(c)
def __unicode__(self):
+ """Returns :meth:`render`"""
return self.render()
class BaseSearchMetaclass(type):
def __new__(cls, name, bases, attrs):
if 'verbose_name' not in attrs:
- attrs['verbose_name'] = capfirst(convert_camelcase(name))
+ attrs['verbose_name'] = capfirst(' '.join(convert_camelcase(name).rsplit(' ', 1)[:-1]))
if 'slug' not in attrs:
- attrs['slug'] = name.lower()
+ attrs['slug'] = name[:-6].lower() if name.endswith("Search") else name.lower()
return super(BaseSearchMetaclass, cls).__new__(cls, name, bases, attrs)
class BaseSearch(object):
"""
- Defines a generic search interface. Accessing self.results will
- attempt to retrieve cached results and, if that fails, will
- initiate a new search and store the results in the cache.
+ Defines a generic search api. Accessing :attr:`results` will attempt to retrieve cached results and, if that fails, will initiate a new search and store the results in the cache. Each search has a ``verbose_name`` and a ``slug``. If these are not provided as attributes, they will be automatically generated based on the name of the class.
+
+ :param search_arg: The string which is being searched for.
+
"""
__metaclass__ = BaseSearchMetaclass
- result_limit = 10
+ #: The number of results to return from the complete list. Default: 5
+ result_limit = 5
+ #: How long the items for the search should be cached (in minutes). Default: 48 hours.
_cache_timeout = 60*48
+ #: The path to the template which will be used to render the :class:`Result`\ s for this search. If this is ``None``, then the framework will try ``sobol/search/<slug>/result.html`` and ``sobol/search/result.html``.
+ result_template = None
+ #: The path to the template which will be used to generate the title of the :class:`Result`\ s for this search. If this is ``None``, then the framework will try ``sobol/search/<slug>/title.html`` and ``sobol/search/title.html``.
+ title_template = None
+ #: The path to the template which will be used to generate the content of the :class:`Result`\ s for this search. If this is ``None``, then the framework will try ``sobol/search/<slug>/content.html`` and ``sobol/search/content.html``.
+ content_template = None
def __init__(self, search_arg):
self.search_arg = search_arg
- def _get_cached_results(self):
- """Return the cached results if the results haven't timed out. Otherwise return None."""
- result_cache = cache.get(SEARCH_CACHE_KEY)
- if result_cache and self.__class__ in result_cache and self.search_arg.lower() in result_cache[self.__class__]:
- cached = result_cache[self.__class__][self.search_arg.lower()]
- if cached['timeout'] >= datetime.datetime.now():
- return cached['results']
- return None
-
- def _set_cached_results(self, results, timeout):
- """Sets the results to the cache for <timeout> minutes."""
- result_cache = cache.get(SEARCH_CACHE_KEY) or {}
- cached = result_cache.setdefault(self.__class__, {}).setdefault(self.search_arg.lower(), {})
- cached.update({
- 'results': results,
- 'timeout': datetime.datetime.now() + datetime.timedelta(minutes=timeout)
- })
- cache.set(SEARCH_CACHE_KEY, result_cache, MAX_CACHE_TIMEOUT)
-
@property
def results(self):
+ """Retrieves cached results or initiates a new search via :meth:`get_results` and caches the results."""
if not hasattr(self, '_results'):
- results = self._get_cached_results()
- if results is None:
- try:
- # Cache one extra result so we can see if there are
- # more results to be had.
- limit = self.result_limit
- if limit is not None:
- limit += 1
- results = self.get_results(limit)
- except:
- if settings.DEBUG:
- raise
- # On exceptions, don't set any cache; just return.
- return []
+ try:
+ # Cache one extra result so we can see if there are
+ # more results to be had.
+ limit = self.result_limit
+ if limit is not None:
+ limit += 1
+ results = self.get_results(limit)
+ except:
+ if settings.DEBUG:
+ raise
+ # On exceptions, don't set any cache; just return.
+ return []
- self._set_cached_results(results, self._cache_timeout)
self._results = results
+
+ if USE_CACHE:
+ for result in results:
+ result.get_context()
+ key = _make_cache_key(self, self.search_arg)
+ cache.set(key, self, self._cache_timeout)
return self._results
def get_results(self, limit=None, result_class=Result):
"""
- Calls self.search() and parses the return value into Result objects.
+ Calls :meth:`search` and parses the return value into :class:`Result` instances.
+
+ :param limit: Passed directly to :meth:`search`.
+ :param result_class: The class used to represent the results. This will be instantiated with the :class:`BaseSearch` instance and the raw result from the search.
+
"""
results = self.search(limit)
return [result_class(self, result) for result in results]
def search(self, limit=None):
- """
- Returns an iterable of up to <limit> results. The
- get_result_title, get_result_url, get_result_template, and
- get_result_extra_context methods will be used to interpret the
- individual items that this function returns, so the result can
- be an object with attributes as easily as a dictionary
- with keys. The only restriction is that the objects be
- pickleable so that they can be used with django's cache system.
- """
+ """Returns an iterable of up to ``limit`` results. The :meth:`get_result_title`, :meth:`get_result_url`, :meth:`get_result_template`, and :meth:`get_result_extra_context` methods will be used to interpret the individual items that this function returns, so the result can be an object with attributes as easily as a dictionary with keys. However, keep in mind that the raw results will be stored with django's caching mechanisms and will be converted to JSON."""
raise NotImplementedError
- def get_result_title(self, result):
- raise NotImplementedError
-
- def get_result_url(self, result):
- "Subclasses override this to provide the actual URL for the result."
+ def get_actual_result_url(self, result):
+ """Returns the actual URL for the ``result`` or ``None`` if there is no URL. Must be implemented by subclasses."""
raise NotImplementedError
def get_result_querydict(self, result):
- url = self.get_result_url(result)
+ """Returns a querydict for tracking selection of the result, or ``None`` if there is no URL for the result."""
+ url = self.get_actual_result_url(result)
if url is None:
return None
return make_tracking_querydict(self.search_arg, url)
+ def get_result_url(self, result):
+ """Returns ``None`` or a url which, when accessed, will register a :class:`.Click` for that url."""
+ qd = self.get_result_querydict(result)
+ if qd is None:
+ return None
+ return "?%s" % qd.urlencode()
+
+ def get_result_title(self, result):
+ """Returns the title of the ``result``. By default, renders ``sobol/search/<slug>/title.html`` or ``sobol/search/title.html`` with the result in the context. This can be overridden by setting :attr:`title_template` or simply overriding :meth:`get_result_title`. If no template can be found, this will raise :exc:`TemplateDoesNotExist`."""
+ return loader.render_to_string(self.title_template or [
+ 'sobol/search/%s/title.html' % self.slug,
+ 'sobol/search/title.html'
+ ], {'result': result})
+
+ def get_result_content(self, result):
+ """Returns the content for the ``result``. By default, renders ``sobol/search/<slug>/content.html`` or ``sobol/search/content.html`` with the result in the context. This can be overridden by setting :attr:`content_template` or simply overriding :meth:`get_result_content`. If no template is found, this will return an empty string."""
+ try:
+ return loader.render_to_string(self.content_template or [
+ 'sobol/search/%s/content.html' % self.slug,
+ 'sobol/search/content.html'
+ ], {'result': result})
+ except TemplateDoesNotExist:
+ return ""
+
def get_result_template(self, result):
- if hasattr(self, 'result_template'):
+ """Returns the template to be used for rendering the ``result``. For a search with slug ``google``, this would first try ``sobol/search/google/result.html``, then fall back on ``sobol/search/result.html``. Subclasses can override this by setting :attr:`result_template` to the path of another template."""
+ if self.result_template:
return loader.get_template(self.result_template)
- if not hasattr(self, '_result_template'):
- self._result_template = DEFAULT_RESULT_TEMPLATE
- return self._result_template
-
- def get_result_extra_context(self, result):
- return {}
+ return loader.select_template([
+ 'sobol/search/%s/result.html' % self.slug,
+ 'sobol/search/result.html'
+ ])
+ @property
def has_more_results(self):
- """Useful to determine whether to display a `view more results` link."""
+ """Returns ``True`` if there are more results than :attr:`result_limit` and ``False`` otherwise."""
return len(self.results) > self.result_limit
- @property
- def more_results_url(self):
- """
- Returns the actual url for more results. This will be encoded
- into a querystring for tracking purposes.
- """
- raise NotImplementedError
+ def get_actual_more_results_url(self):
+ """Returns the actual url for more results. By default, simply returns ``None``."""
+ return None
+
+ def get_more_results_querydict(self):
+ """Returns a :class:`QueryDict` for tracking whether people click on a 'more results' link."""
+ url = self.get_actual_more_results_url()
+ if url:
+ return make_tracking_querydict(self.search_arg, url)
+ return None
@property
- def more_results_querydict(self):
- return make_tracking_querydict(self.search_arg, self.more_results_url)
+ def more_results_url(self):
+ """Returns a URL which consists of a querystring which, when accessed, will log a :class:`.Click` for the actual URL."""
+ qd = self.get_more_results_querydict()
+ if qd is None:
+ return None
+ return "?%s" % qd.urlencode()
def __unicode__(self):
- return ' '.join(self.__class__.verbose_name.rsplit(' ', 1)[:-1]) + ' results'
+ return self.verbose_name
class DatabaseSearch(BaseSearch):
+ """Implements :meth:`~BaseSearch.search` and :meth:`get_queryset` methods to handle database queries."""
+ #: The model which should be searched by the :class:`DatabaseSearch`.
model = None
def search(self, limit=None):
return self._qs
def get_queryset(self):
+ """Returns a :class:`QuerySet` of all instances of :attr:`model`. This method should be overridden by subclasses to specify how the search should actually be implemented for the model."""
return self.model._default_manager.all()
class URLSearch(BaseSearch):
- """
- Defines a generic interface for searches that require accessing a
- certain url to get search results.
- """
+ """Defines a generic interface for searches that require accessing a certain url to get search results."""
+ #: The base URL which will be accessed to get the search results.
search_url = ''
+ #: The url-encoded query string to be used for fetching search results from :attr:`search_url`. Must have one ``%s`` to contain the search argument.
query_format_str = "%s"
@property
def url(self):
- "The URL where the search gets its results."
+ """The URL where the search gets its results. Composed from :attr:`search_url` and :attr:`query_format_str`."""
return self.search_url + self.query_format_str % urlquote_plus(self.search_arg)
-
- @property
- def more_results_url(self):
- "The URL where the users would go to get more results."
+
+ def get_actual_more_results_url(self):
return self.url
def parse_response(self, response, limit=None):
+ """Handles the ``response`` from accessing :attr:`url` (with :func:`urllib2.urlopen`) and returns a list of up to ``limit`` results."""
raise NotImplementedError
def search(self, limit=None):
class JSONSearch(URLSearch):
- """
- Makes a GET request and parses the results as JSON. The default
- behavior assumes that the return value is a list of results.
- """
+ """Makes a GET request and parses the results as JSON. The default behavior assumes that the response contains a list of results."""
def parse_response(self, response, limit=None):
return json.loads(response.read())[:limit]
class GoogleSearch(JSONSearch):
+ """An example implementation of a :class:`JSONSearch`."""
search_url = "http://ajax.googleapis.com/ajax/services/search/web"
- # TODO: Change this template to reflect the app's actual name.
- result_template = 'search/googlesearch.html'
_cache_timeout = 60
verbose_name = "Google search (current site)"
+ _more_results_url = None
@property
def query_format_str(self):
@property
def default_args(self):
+ """Unquoted default arguments for the :class:`GoogleSearch`."""
return "site:%s" % Site.objects.get_current().domain
def parse_response(self, response, limit=None):
return True
return False
- @property
- def more_results_url(self):
+ def get_actual_more_results_url(self):
return self._more_results_url
- def get_result_title(self, result):
- return result['titleNoFormatting']
-
- def get_result_url(self, result):
+ def get_actual_result_url(self, result):
return result['unescapedUrl']
- def get_result_extra_context(self, result):
- return result
+ def get_result_title(self, result):
+ return mark_safe(result['titleNoFormatting'])
+
+ def get_result_content(self, result):
+ return mark_safe(result['content'])
registry.register(GoogleSearch)
else:
__all__ += ('ScrapeSearch', 'XMLSearch',)
class ScrapeSearch(URLSearch):
- _strainer_args = []
- _strainer_kwargs = {}
+ """A base class for scrape-style searching, available if :mod:`BeautifulSoup` is installed."""
+ #: Arguments to be passed into a :class:`SoupStrainer`.
+ strainer_args = []
+ #: Keyword arguments to be passed into a :class:`SoupStrainer`.
+ strainer_kwargs = {}
@property
def strainer(self):
+ """
+ Caches and returns a :class:`SoupStrainer` initialized with :attr:`strainer_args` and :attr:`strainer_kwargs`. This strainer will be used to parse only certain parts of the document.
+
+ .. seealso:: `BeautifulSoup: Improving Performance by Parsing Only Part of the Document <http://www.crummy.com/software/BeautifulSoup/documentation.html#Improving%20Performance%20by%20Parsing%20Only%20Part%20of%20the%20Document>`_
+
+ """
if not hasattr(self, '_strainer'):
- self._strainer = SoupStrainer(*self._strainer_args, **self._strainer_kwargs)
+ self._strainer = SoupStrainer(*self.strainer_args, **self.strainer_kwargs)
return self._strainer
def parse_response(self, response, limit=None):
def parse_results(self, results):
"""
- Provides a hook for parsing the results of straining. This
- has no default behavior because the results absolutely
- must be parsed to properly extract the information.
- For more information, see http://www.crummy.com/software/BeautifulSoup/documentation.html#Improving%20Memory%20Usage%20with%20extract
+ Provides a hook for parsing the results of straining. This has no default behavior and must be implemented by subclasses because the results absolutely must be parsed to properly extract the information.
+
+ .. seealso:: `BeautifulSoup: Improving Memory Usage with extract <http://www.crummy.com/software/BeautifulSoup/documentation.html#Improving%20Memory%20Usage%20with%20extract>`_
"""
raise NotImplementedError
class XMLSearch(ScrapeSearch):
- _self_closing_tags = []
+ """A base class for searching XML results."""
+ #: Self-closing tag names to be used when interpreting the XML document
+ #:
+ #: .. seealso:: `BeautifulSoup: Parsing XML <http://www.crummy.com/software/BeautifulSoup/documentation.html#Parsing%20XML>`_
+ self_closing_tags = []
def parse_response(self, response, limit=None):
strainer = self.strainer
- soup = BeautifulStoneSoup(response, selfClosingTags=self._self_closing_tags, parseOnlyThese=strainer)
+ soup = BeautifulStoneSoup(response, selfClosingTags=self.self_closing_tags, parseOnlyThese=strainer)
return self.parse_results(soup.findAll(recursive=False, limit=limit))
\ No newline at end of file
--- /dev/null
+(function($){
+ var sobol = window.sobol = {};
+ sobol.favoredResults = []
+ sobol.favoredResultSearch = null;
+ sobol.search = function(){
+ var searches = sobol.searches = $('article.search');
+ if(sobol.favoredResults.length) sobol.favoredResultSearch = searches.eq(0);
+ for (var i=sobol.favoredResults.length ? 1 : 0;i<searches.length;i++) {
+ (function(){
+ var s = searches[i];
+ $.ajax({
+ url: s.getAttribute('data-url'),
+ dataType: 'json',
+ success: function(data){
+ sobol.onSuccess($(s), data);
+ },
+ error: function(data, textStatus, errorThrown){
+ sobol.onError($(s), textStatus, errorThrown);
+ }
+ });
+ }());
+ };
+ }
+ sobol.renderResult = function(result){
+ // Returns the result rendered as a string. Override this to provide custom rendering.
+ var url = result['url'],
+ title = result['title'],
+ content = result['content'],
+ rendered = '';
+
+ if(url){
+ rendered += "<dt><a href='" + url + "'>" + title + "</a></dt>";
+ } else {
+ rendered += "<dt>" + title + "</dt>";
+ }
+ if(content && content != ''){
+ rendered += "<dd>" + content + "</dd>"
+ }
+ return rendered
+ }
+ sobol.addFavoredResult = function(result) {
+ var dl = sobol.favoredResultSearch.find('dl');
+ if(!dl.length){
+ dl = $('<dl>');
+ dl.appendTo(sobol.favoredResultSearch);
+ sobol.favoredResultSearch.removeClass('loading');
+ }
+ dl[0].innerHTML += sobol.renderResult(result)
+ }
+ sobol.onSuccess = function(ele, data){
+ // hook for success!
+ ele.removeClass('loading');
+ if (data['results'].length) {
+ ele[0].innerHTML += "<dl>";
+ $.each(data['results'], function(i, v){
+ ele[0].innerHTML += sobol.renderResult(v);
+ })
+ ele[0].innerHTML += "</dl>";
+ if(data['hasMoreResults'] && data['moreResultsURL']) ele[0].innerHTML += "<footer><p><a href='" + data['moreResultsURL'] + "'>See more results</a></p></footer>";
+ } else {
+ ele.addClass('empty');
+ ele[0].innerHTML += "<p>No results found.</p>";
+ ele.slideUp();
+ }
+ if (sobol.favoredResultSearch){
+ for (var i=0;i<data['results'].length;i++){
+ var r = data['results'][i];
+ if ($.inArray(r['actual_url'], sobol.favoredResults) != -1){
+ sobol.addFavoredResult(r);
+ }
+ }
+ }
+ };
+ sobol.onError = function(ele, textStatus, errorThrown){
+ // Hook for error...
+ ele.removeClass('loading');
+ text = errorThrown ? errorThrown : textStatus ? textStatus : "Error occurred.";
+ ele[0].innerHTML += "<p>" + text + "</p>";
+ };
+ $(sobol.search);
+}(jQuery));
\ No newline at end of file
--- /dev/null
+{% 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 %}<h1>Results for "{{ original.string }}"</h1>{% endblock %}
+{% block extrastyle %}
+ <style type="text/css">
+ .favored td{
+ font-weight:bold;
+ }
+ #changelist table{
+ width:100%;
+ }
+ </style>
+{% endblock %}
+
+{% block content %}
+ <div class="module" id="changelist">
+ <table>
+ <thead>
+ <tr>
+ <th>Weight</th>
+ <th>URL</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for result in original.get_weighted_results %}
+ <tr class="{% cycle 'row1' 'row2' %}{% if result in original.get_favored_results %} favored{% endif %}">
+ <td>{{ result.weight }}</td>
+ <td>{{ result.url }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ {% block submit_row %}
+ <div class="submit-row">
+ {% if not is_popup and has_delete_permission %}{% if change or show_delete %}<p class="deletelink-box"><a href="delete/" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}{% endif %}
+ </div>
+ {% endblock %}
+{% endblock %}
\ No newline at end of file
--- /dev/null
+{% extends 'admin/change_list.html' %}
+
+{% block object-tools %}{% endblock %}
\ No newline at end of file
--- /dev/null
+{% extends 'admin/sobol/search/change_form.html' %}
+{% load i18n %}
+
+{% block extrastyle %}
+ <style type="text/css">
+ .favored td{
+ font-weight:bold;
+ }
+ #changelist{
+ border:none;
+ background:none;
+ }
+ thead th{color:#444;font-weight:bold;}
+ </style>
+{% endblock %}
+
+{% block submit_row %}
+ <div class="module footer">
+ <ul class="submit-row">
+ {% if not is_popup and has_delete_permission %}{% if change or show_delete %}<li class="left delete-link-container"><a href="delete/" class="delete-link">{% trans "Delete" %}</a></li>{% endif %}{% endif %}
+ </ul>
+ </div>
+{% endblock %}
\ No newline at end of file
+++ /dev/null
-{% extends "admin/base_site.html" %}
-
-<!-- LOADING -->
-{% load i18n %}
-
-<!-- EXTRASTYLES -->
-{% block extrastyle %}<style type="text/css">.favored{font-weight:bold;}</style>{% endblock %}
-
-<!-- BREADCRUMBS -->
-{% block breadcrumbs %}
- <div id="breadcrumbs">
- {% if queryset|length > 1 %}
- <a href="../../">{% trans "Home" %}</a> ›
- <a href="../">{{ app_label|capfirst }}</a> ›
- <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
- {% trans 'Search results for multiple objects' %}
- {% else %}
- <a href="../../../../">{% trans "Home" %}</a> ›
- <a href="../../../">{{ app_label|capfirst }}</a> ›
- <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> ›
- <a href="../">{{ queryset|first|truncatewords:"18" }}</a> ›
- {% trans 'Results' %}
- {% endif %}
- </div>
-{% endblock %}
-
-<!-- CONTENT -->
-{% block content %}
- <div class="container-grid delete-confirmation">
- {% for search in queryset %}
- <div class="group tabular">
- <h2>{{ search_string }}</h2>
- <div class="module table">
- <div class="module thead">
- <div class="tr">
- <div class="th">Weight</div>
- <div class="th">URL</div>
- </div>
- </div>
- <div class="module tbody">
- {% for result in search.get_weighted_results %}
- <div class="tr{% if result in search.get_favored_results %} favored{% endif %}">
- <div class="td">{{ result.weight }}</div>
- <div class="td">{{ result.url }}</div>
- </div>
- {% endfor %}
- </div>
- </div>
- </div>
- {% endfor %}
- </div>
-{% endblock %}
\ No newline at end of file
+++ /dev/null
-{% extends "admin/base_site.html" %}
-{% load i18n %}
-
-{% block extrastyle %}<style type="text/css">.favored{font-weight:bold;}</style>{% endblock %}
-
-{% block breadcrumbs %}
-<div class="breadcrumbs">
- {% if queryset|length > 1 %}
- <a href="../../">{% trans "Home" %}</a> ›
- <a href="../">{{ app_label|capfirst }}</a> ›
- <a href="./">{{ opts.verbose_name_plural|capfirst }}</a> ›
- {% trans 'Search results for multiple objects' %}
- {% else %}
- <a href="../../../../">{% trans "Home" %}</a> ›
- <a href="../../../">{{ app_label|capfirst }}</a> ›
- <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> ›
- <a href="../">{{ queryset|first|truncatewords:"18" }}</a> ›
- {% trans 'Results' %}
- {% endif %}
-</div>
-{% endblock %}
-
-
-{% block content %}
- {% for search in queryset %}
- <fieldset class="module">
- <h2>{{ search.string }}</h2>
- <table>
- <thead>
- <tr>
- <th>Weight</th>
- <th>URL</th>
- </tr>
- </thead>
- <tbody>
- {% for result in search.get_weighted_results %}
- <tr{% if result in search.favored_results %} class="favored"{% endif %}>
- <td>{{ result.weight }}</td>
- <td>{{ result.url }}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </fieldset>
- {% endfor %}
-{% endblock %}
\ No newline at end of file
+++ /dev/null
-<article>
- <h1><a href="{{ url }}">{{ title|safe }}</a></h1>
- <p>{{ content|safe }}</p>
-</article>
\ No newline at end of file
--- /dev/null
+{% with node.view.enable_ajax_api as ajax %}
+{% if ajax %}
+ {% if not suppress_scripts %}<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script><script type="text/javascript" src="{{ STATIC_URL }}sobol/ajax_search.js"></script>{% endif %}
+ <script type="text/javascript">
+ (function($){
+ var sobol = window.sobol;
+ sobol.favoredResults = [{% for r in favored_results %}"{{ r }}"{% if not forloop.last %}, {% endif %}{% endfor %}];
+ }(jQuery));
+ </script>
+{% endif %}
+{% if favored_results %}
+ <article class="search favored{% if ajax %} loading{% endif %}">
+ <header>
+ <h1>Favored results</h1>
+ </header>
+ {% if not ajax %}
+ <dl>
+ {% 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 %}
+ <dt><a href="{{ search.more_results_url }}">More results for {{ search }}</a></dt>
+ {% endif %}
+ </dl>
+ {% endif %}
+ </article>
+{% endif %}
+{% for search in searches %}
+<article {% if ajax %}class="search loading {{ search.slug }}" data-url="{{ search.ajax_api_url }}"{% else %}class="search {{ search.slug }}{% if not search.results %} empty{% endif %}"{% endif %}>
+ <header>
+ <a name='{{ search.slug }}'></a>
+ <h1>{{ search }}</h1>
+ </header>
+ {% if not ajax %}
+ {% if search.results %}
+ <dl>
+ {% for result in search.results %}
+ {{ result }}
+ {% endfor %}
+ </dl>
+ {% if search.has_more_results and search.more_results_url %}
+ <footer>
+ <p><a href="{{ search.more_results_url }}">See more results</a></p>
+ </footer>
+ {% endif %}
+ {% else %}
+ <p>No results found.</p>
+ {% endif %}
+ {% endif %}
+</article>
+{% endfor %}
+{% endwith %}
\ No newline at end of file
--- /dev/null
+{{ result.content|truncatewords_html:20 }}
\ No newline at end of file
--- /dev/null
+<dt>{% if url %}<a href="{{ url }}">{% endif %}{{ title }}{% if url %}</a>{% endif %}</dt>
+{% if content %}<dd>{{ content }}</dd>{% endif %}
\ No newline at end of file
+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'
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
from datetime import date
+
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
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 <http://code.google.com/p/recaptcha-django/>`_
+
+ """
+ #: An :class:`EmailField` using the :class:`EmailInput` widget.
email = forms.EmailField(widget=EmailInput)
try:
from recaptcha_django import ReCaptchaField
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)
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):
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
+"""
+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
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
)
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:
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:
@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.
return HttpResponseRedirect(redirect)
else:
- form = self.login_form()
+ form = self.login_form(request)
request.session.set_test_cookie()
@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)
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<uidb36>\w+)/(?P<token>[^/]+)$', 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),
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
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())
'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)
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:
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():
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<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
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())
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:
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:
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<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', 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()
}
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 = ''
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)
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)
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))
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
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)
"""
-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)
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
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):
--- /dev/null
+"""
+Winer provides the same API as `django's syndication Feed class <http://docs.djangoproject.com/en/dev/ref/contrib/syndication/#django.contrib.syndication.django.contrib.syndication.views.Feed>`_, 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
--- /dev/null
+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
--- /dev/null
+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
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)
--- /dev/null
+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 <http://docs.djangoproject.com/en/dev/ref/contrib/syndication/#django.contrib.syndication.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 ``<page_url>/rss`` and ``<page_url>/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<year>\d{4})/(?P<month>\d{2})/(?P<day>\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 <http://code.google.com/p/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
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
"rght": 143,
"view_object_id": 1,
"view_content_type": [
- "penfield",
- "blogview"
+ "philo",
+ "page"
],
"parent": 1,
"level": 1,
"model": "philo.redirect",
"fields": {
"status_code": 302,
- "target": "second"
+ "url_or_subpath": "second"
}
},
{
"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
- }
}
]
from django.forms.models import ModelFormMetaclass, ModelForm, ModelFormOptions
from django.utils.datastructures import SortedDict
+
from philo.utils import fattr
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):
from django import forms
from django.core.exceptions import ValidationError
from django.utils import simplejson as json
+
from philo.validators import json_validator
class JSONFormField(forms.Field):
+ """A form field which is validated by :func:`philo.validators.json_validator`."""
default_validators = [json_validator]
def clean(self, value):
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):
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:
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
--- /dev/null
+# 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']
--- /dev/null
+# 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='<Generated name>', 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']
--- /dev/null
+# 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="<Generated 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']
--- /dev/null
+# 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']
--- /dev/null
+# 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']
--- /dev/null
+# 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']
--- /dev/null
+# 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']
+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
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()
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)
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)
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)
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:
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::
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.
: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.
"""
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.
"""
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::
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
-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):
use_for_related_fields = True
def with_model(self, model):
+ """
+ Given a model class or instance, returns a queryset of all instances of that model which have collection members in this manager's scope.
+
+ Example::
+
+ >>> from philo.models import Collection
+ >>> from django.contrib.auth.models import User
+ >>> collection = Collection.objects.get(name="Foo")
+ >>> collection.members.all()
+ [<CollectionMember: Foo - user1>, <CollectionMember: Foo - user2>, <CollectionMember: Foo - Spam & Eggs>]
+ >>> collection.members.with_model(User)
+ [<User: user1>, <User: user2>]
+
+ """
return model._default_manager.filter(pk__in=self.filter(member_content_type=ContentType.objects.get_for_model(model)).values_list('member_object_id', flat=True))
class CollectionMember(models.Model):
+ """
+ The collection member model represents a generic link from a :class:`Collection` to an arbitrary model instance with an attached order.
+
+ """
+ #: A :class:`CollectionMemberManager` instance
objects = CollectionMemberManager()
+ #: :class:`ForeignKey` to a :class:`Collection` instance.
collection = models.ForeignKey(Collection, related_name='members')
+ #: The numerical index of the item within the collection (optional).
index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
member_content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Member type')
member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
+ #: :class:`GenericForeignKey` to an arbitrary model instance.
member = generic.GenericForeignKey('member_content_type', 'member_object_id')
def __unicode__(self):
app_label = 'philo'
-register_templatetags('philo.templatetags.collections')
register_value_model(Collection)
\ No newline at end of file
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))
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):
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")
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:
-"""
-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')
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
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
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),
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):
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):
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):
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 <philo.utils>` 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 = {}
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 <philo.utils>` to restrict the queryset for the :class:`ManyToManyAttribute`.
+
+ """
value_class = ManyToManyValue
def validate_value(self, value):
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)
-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 <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 <ObjectDoesNotExist>` if with_domain is True but no :class:`Site` or :class:`RequestSite` can be built.
+ - :class:`~philo.exceptions.AncestorDoesNotExist` if the root node of the site isn't an ancestor of the node constructing the URL.
+
+ :param string subpath: The subpath to be constructed beyond beyond the node's URL.
+ :param request: :class:`HttpRequest` instance. Will be used to construct a :class:`RequestSite` if :meth:`Site.objects.get_current` fails.
+ :param with_domain: Whether the constructed URL should include a domain name and protocol.
+ :param secure: Whether the protocol, if included, should be http:// or https://.
+ :returns: A constructed url for accessing the given subpath of the current node instance.
+
"""
# Try reversing philo-root first, since we can't do anything if that fails.
- root_url = reverse('philo-root')
+ 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()
return '%s%s%s%s' % (domain, root_url, path, subpath)
- class Meta:
+ class Meta(SlugTreeEntity.Meta):
app_label = 'philo'
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
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
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:
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 <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):
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)
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):
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:
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:
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
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
# 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})
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 = []
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:
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
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
+++ /dev/null
-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;i<name.length;i++) {
- name[i] = name[i].substr(0,1).toUpperCase() + name[i].substr(1);
- }
- name = name.join(' ');
- slug = name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]/g, '');
-
- var href = triggeringLink.href;
- var data = {
- 'name': name,
- 'slug': slug
- };
- $.post(href, data, function(data){
- newId = html_unescape(data.pk);
- newRepr = html_unescape(data.unicode);
- var toId = id.replace(/_input$/, '_to');
- elem = document.getElementById(toId);
- var o = new Option(newRepr, newId);
- SelectBox.add_to_cache(toId, o);
- SelectBox.redisplay(toId);
- }, "json")
- },
- 'init': function(id) {
- tagCreation.cache[id] = {}
- var input = tagCreation.cache[id].input = document.getElementById(id + '_input');
- var select = tagCreation.cache[id].select = document.getElementById(id + '_from');
- var addLinkTemplate = document.getElementById('add_' + input.id.replace(/_input$/, '')).cloneNode(true);
- var addLink = tagCreation.cache[id].addLink = document.createElement('A');
- addLink.id = 'ajax_add_' + id;
- addLink.className = addLinkTemplate.className;
- addLink.href = addLinkTemplate.href;
- addLink.appendChild($(addLinkTemplate).children()[0].cloneNode(false));
- addLink.innerHTML += " <span style='vertical-align:text-top;'>Add this tag</span>"
- 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
--- /dev/null
+.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
--- /dev/null
+;(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($('<div class="embed-widget" />'));
+ EmbedWidgets = $('.embed-widget');
+ EmbedWidgets.prepend($('<div class="embed-toolbar" />'));
+ EmbedBars = $('.embed-toolbar');
+ EmbedBars.append('<select class="embed-select"></select><button class="embed-button">Embed</button>');
+ 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('<option value="">Media Types</option>');
+
+ // 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] = $('<optgroup label="'+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('<option value='+group[name]+'>'+name+'</option>');
+ }
+ }
+ }
+ },
+ 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
+"""
+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
return ''
-def do_membersof(parser, token):
+@register.tag
+def membersof(parser, token):
"""
- {% membersof <collection> with <app_label>.<model_name> as <var> %}
+ 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 <collection> with <app_label>.<model_name> as <var> %}
+
"""
params=token.split_contents()
tag = params[0]
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:
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
+"""
+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
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 <name> [[references <type>] as <variable>] %}
+ 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 <name> [[references <app_label>.<model_name>] as <variable>] %}
+
"""
params = token.split_contents()
if len(params) >= 2:
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:
else: # error
raise template.TemplateSyntaxError('"%s" template tag provided without arguments (at least one required)' % tag)
-
-
-register.tag('container', do_container)
+"""
+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()
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 <app_label>.<model_name> with <template> %} :: Sets which template will be used to render a particular model.
- {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %} :: 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 <app_label>.<model_name> with <template> %}
+
+ 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 (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %}
+
"""
bits = token.split_contents()
tag = bits.pop(0)
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
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
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 <template_code> %}
+ Include a flat string by interpreting it as a template. The compiled template will be rendered with the current context.
+
+ Usage::
+
+ {% include_string <template_code> %}
+
"""
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
+"""
+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
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:
return url
-@register.tag(name='node_url')
-def do_node_url(parser, token):
+@register.tag
+def node_url(parser, token):
"""
- {% node_url [for <node>] [as <var>] %}
- {% node_url with <obj> [for <node>] [as <var>] %}
- {% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
- {% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>]%}
+ 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 <node>]`` option.
+
+ Usage::
+
+ {% node_url [for <node>] [as <var>] %}
+ {% node_url with <obj> [for <node>] [as <var>] %}
+ {% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
+ {% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>] %}
+
"""
params = token.split_contents()
tag = params[0]
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):
# 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)))
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),
}
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 %}<p>{{ hello|slice:'1:' }}</p>", "<p>root/second/</p>"),
- ("{% 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 %}<em>{{ goodbye|upper }}</em>", "<em>/ROOT/SECOND2/2010/10/20</em>"),
- ("{% 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 %}<p>{{ hello|slice:'1:' }}</p>", "<p>root/second</p>"),
+ ("{% 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):
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
# 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)
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)
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)]))
from django.conf.urls.defaults import patterns, url
+
from philo.views import node_view
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):
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):
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
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)
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
--- /dev/null
+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 <philo.models.base.Entity>`, 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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
-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',
)
-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:
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
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
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):
#!/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'
+ ]
+)