├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README ├── README.markdown ├── docs ├── Makefile ├── _ext │ ├── djangodocs.py │ └── philodocs.py ├── cla │ ├── ithinksw-ccla.txt │ └── ithinksw-icla.txt ├── conf.py ├── contrib │ ├── intro.rst │ ├── penfield.rst │ ├── shipherd.rst │ ├── sobol.rst │ ├── waldo.rst │ └── winer.rst ├── contributing.rst ├── dummy-settings.py ├── exceptions.rst ├── forms.rst ├── handling_requests.rst ├── index.rst ├── loaders.rst ├── make.bat ├── models │ ├── collections.rst │ ├── entities.rst │ ├── fields.rst │ ├── intro.rst │ ├── miscellaneous.rst │ └── nodes-and-views.rst ├── releases │ ├── 0.9.1.rst │ └── 0.9.2.rst ├── signals.rst ├── templatetags.rst ├── tutorials │ ├── getting-started.rst │ ├── intro.rst │ └── shipherd.rst ├── utilities.rst ├── validators.rst └── what.rst ├── philo ├── __init__.py ├── admin │ ├── __init__.py │ ├── base.py │ ├── collections.py │ ├── forms │ │ ├── __init__.py │ │ ├── attributes.py │ │ └── containers.py │ ├── nodes.py │ ├── pages.py │ └── widgets.py ├── contrib │ ├── __init__.py │ ├── julian │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── feedgenerator.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ └── models.py │ ├── penfield │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto.py │ │ │ ├── 0003_auto__add_field_newsletterview_feed_type__add_field_newsletterview_ite.py │ │ │ ├── 0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py │ │ │ ├── 0005_to_taggit.py │ │ │ ├── 0006_delete_tag_rels.py │ │ │ └── __init__.py │ │ ├── models.py │ │ └── templatetags │ │ │ ├── __init__.py │ │ │ └── penfield.py │ ├── shipherd │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto.py │ │ │ ├── 0003_auto__del_field_navigationitem_slug.py │ │ │ └── __init__.py │ │ ├── models.py │ │ └── templatetags │ │ │ ├── __init__.py │ │ │ └── shipherd.py │ ├── sobol │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── forms.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── search.py │ │ ├── static │ │ │ └── sobol │ │ │ │ └── ajax_search.js │ │ ├── templates │ │ │ ├── admin │ │ │ │ └── sobol │ │ │ │ │ └── search │ │ │ │ │ ├── change_form.html │ │ │ │ │ ├── change_list.html │ │ │ │ │ └── grappelli_change_form.html │ │ │ └── sobol │ │ │ │ └── search │ │ │ │ ├── _list.html │ │ │ │ ├── content.html │ │ │ │ └── result.html │ │ └── utils.py │ ├── waldo │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── models.py │ │ └── tokens.py │ └── winer │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── feeds.py │ │ ├── middleware.py │ │ └── models.py ├── exceptions.py ├── fixtures │ └── test_fixtures.json ├── forms │ ├── __init__.py │ ├── entities.py │ └── fields.py ├── loaders │ ├── __init__.py │ └── database.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_field_attribute_value.py │ ├── 0003_move_json.py │ ├── 0004_auto__del_field_attribute_json_value.py │ ├── 0005_add_attribute_values.py │ ├── 0006_move_attribute_and_relationship_values.py │ ├── 0007_auto__del_relationship__del_field_attribute_value.py │ ├── 0008_auto__del_field_manytomanyvalue_object_ids.py │ ├── 0009_auto__add_field_node_lft__add_field_node_rght__add_field_node_tree_id_.py │ ├── 0010_auto__add_field_redirect_target_node__add_field_redirect_url_or_subpat.py │ ├── 0011_move_target_url.py │ ├── 0012_auto__del_field_redirect_target.py │ ├── 0013_auto.py │ ├── 0014_auto.py │ ├── 0015_auto__add_unique_node_slug_parent__add_unique_template_slug_parent.py │ ├── 0016_auto__add_field_file_name.py │ ├── 0017_generate_filenames.py │ ├── 0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py │ ├── 0019_to_taggit.py │ ├── 0020_from_taggit.py │ ├── 0021_auto__del_tag.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── base.py │ ├── collections.py │ ├── fields │ │ ├── __init__.py │ │ └── entities.py │ ├── nodes.py │ └── pages.py ├── signals.py ├── static │ └── philo │ │ ├── css │ │ └── EmbedWidget.css │ │ └── js │ │ └── EmbedWidget.js ├── templates │ └── admin │ │ └── philo │ │ ├── edit_inline │ │ ├── grappelli_tabular_attribute.html │ │ ├── grappelli_tabular_container.html │ │ ├── tabular_attribute.html │ │ └── tabular_container.html │ │ └── page │ │ └── add_form.html ├── templatetags │ ├── __init__.py │ ├── collections.py │ ├── containers.py │ ├── embed.py │ ├── include_string.py │ └── nodes.py ├── tests.py ├── urls.py ├── utils │ ├── __init__.py │ ├── entities.py │ ├── lazycompat.py │ ├── registry.py │ └── templates.py ├── validators.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | docs/_build/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2012, iThink Software. 2 | 3 | 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. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README 2 | include README.markdown 3 | include LICENSE 4 | include MANIFEST.in 5 | recursive-include philo/templates *.html 6 | recursive-include philo/contrib/sobol/templates *.html 7 | recursive-include philo/fixtures *.json 8 | recursive-include philo/static *.css *.js 9 | recursive-include philo/contrib/sobol/static *.css *.js 10 | recursive-include docs *.py *.rst *.bat *.txt Makefile 11 | global-exclude *~ 12 | prune docs/_build -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Philo is a foundation for developing web content management systems. 2 | 3 | Prerequisites: 4 | * Python 2.5.4+ 5 | * Django 1.3+ 6 | * django-mptt e734079+ 7 | * (optional) django-grappelli 2.0+ 8 | * (optional) south 0.7.2+ 9 | * (philo.contrib.penfield) django-taggit 0.9.3+ 10 | * (philo.contrib.waldo, optional) recaptcha-django r6+ 11 | 12 | After installing philo and mptt on your PYTHONPATH, make sure to complete the following steps: 13 | 14 | 1. Add 'philo.middleware.RequestNodeMiddleware' to settings.MIDDLEWARE_CLASSES. 15 | 2. Add 'philo' and 'mptt' to settings.INSTALLED_APPS. 16 | 3. Include 'philo.urls' somewhere in your urls.py file. 17 | 4. Optionally add a root node to your current Site. 18 | 19 | Philo should be ready to go! All that's left is to learn more and contribute . 20 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | [Philo](http://philocms.org/) is a foundation for developing web content management systems. 2 | 3 | Prerequisites: 4 | 5 | * [Python 2.5.4+](http://www.python.org/) 6 | * [Django 1.3+](http://www.djangoproject.com/) 7 | * [django-mptt e734079+](https://github.com/django-mptt/django-mptt/) 8 | * (optional) [django-grappelli 2.0+](http://code.google.com/p/django-grappelli/) 9 | * (optional) [south 0.7.2+](http://south.aeracode.org/) 10 | * (philo.contrib.penfield) [django-taggit 0.9.3+](https://github.com/alex/django-taggit/) 11 | * (philo.contrib.waldo, optional) [recaptcha-django r6+](http://code.google.com/p/recaptcha-django/) 12 | 13 | After installing philo and mptt on your PYTHONPATH, make sure to complete the following steps: 14 | 15 | 1. Add 'philo.middleware.RequestNodeMiddleware' to settings.MIDDLEWARE_CLASSES. 16 | 2. Add 'philo' and 'mptt' to settings.INSTALLED_APPS. 17 | 3. Include 'philo.urls' somewhere in your urls.py file. 18 | 4. Optionally add a root node to your current Site. 19 | 20 | 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). 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Philo.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Philo.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Philo" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Philo" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_ext/philodocs.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from sphinx.addnodes import desc_addname 4 | from sphinx.domains.python import PyModulelevel, PyXRefRole 5 | from sphinx.ext import autodoc 6 | 7 | 8 | DOMAIN = 'py' 9 | 10 | 11 | class TemplateTag(PyModulelevel): 12 | indextemplate = "pair: %s; template tag" 13 | 14 | def get_signature_prefix(self, sig): 15 | return self.objtype + ' ' 16 | 17 | def handle_signature(self, sig, signode): 18 | fullname, name_prefix = PyModulelevel.handle_signature(self, sig, signode) 19 | 20 | for i, node in enumerate(signode): 21 | if isinstance(node, desc_addname): 22 | lib = '.'.join(node[0].split('.')[-2:]) 23 | new_node = desc_addname(lib, lib) 24 | signode[i] = new_node 25 | 26 | return fullname, name_prefix 27 | 28 | 29 | class TemplateTagDocumenter(autodoc.FunctionDocumenter): 30 | objtype = 'templatetag' 31 | domain = DOMAIN 32 | 33 | @classmethod 34 | def can_document_member(cls, member, membername, isattr, parent): 35 | # Only document explicitly. 36 | return False 37 | 38 | def format_args(self): 39 | return None 40 | 41 | class TemplateFilterDocumenter(autodoc.FunctionDocumenter): 42 | objtype = 'templatefilter' 43 | domain = DOMAIN 44 | 45 | @classmethod 46 | def can_document_member(cls, member, membername, isattr, parent): 47 | # Only document explicitly. 48 | return False 49 | 50 | def setup(app): 51 | app.add_directive_to_domain(DOMAIN, 'templatetag', TemplateTag) 52 | app.add_role_to_domain(DOMAIN, 'ttag', PyXRefRole()) 53 | app.add_directive_to_domain(DOMAIN, 'templatefilter', TemplateTag) 54 | app.add_role_to_domain(DOMAIN, 'tfilter', PyXRefRole()) 55 | app.add_autodocumenter(TemplateTagDocumenter) 56 | app.add_autodocumenter(TemplateFilterDocumenter) -------------------------------------------------------------------------------- /docs/cla/ithinksw-icla.txt: -------------------------------------------------------------------------------- 1 | iThink Software 2 | Individual Contributor License Agreement ("Agreement") v1.0.1 3 | 4 | Thank you for your interest in iThink Software. In order to clarify 5 | the intellectual property license granted with Contributions from 6 | any person or entity, iThink Software must have a Contributor 7 | License Agreement ("CLA") on file that has been signed by each 8 | Contributor, indicating agreement to the license terms below. This 9 | license is for your protection as a Contributor as well as the 10 | protection of iThink Software and its users; it does not change 11 | your rights to use your own Contributions for any other purpose. 12 | If you have not already done so, please complete and sign, then scan 13 | and email a pdf file of this Agreement to contact@ithinksw.com. 14 | Alternatively, you may send an original signed Agreement to 15 | iThink Software, 261 West Lorain Street, Oberlin, OH 44074, U.S.A. 16 | Please read this document carefully before signing and 17 | keep a copy for your records. 18 | 19 | Full name: ______________________________________________________ 20 | 21 | Mailing Address: ________________________________________________ 22 | 23 | _________________________________________________________________ 24 | 25 | Country: ______________________________________________________ 26 | 27 | Telephone: ______________________________________________________ 28 | 29 | Facsimile: ______________________________________________________ 30 | 31 | E-Mail: ______________________________________________________ 32 | 33 | You accept and agree to the following terms and conditions for Your 34 | present and future Contributions submitted to iThink Software. Except 35 | for the license granted herein to iThink Software and recipients of 36 | software distributed by iThink Software, You reserve all right, 37 | title, and interest in and to Your Contributions. 38 | 39 | 1. Definitions. 40 | 41 | "You" (or "Your") shall mean the copyright owner or legal entity 42 | authorized by the copyright owner that is making this Agreement 43 | with iThink Software. For legal entities, the entity making a 44 | Contribution and all other entities that control, are controlled 45 | by, or are under common control with that entity are considered to 46 | be a single Contributor. For the purposes of this definition, 47 | "control" means (i) the power, direct or indirect, to cause the 48 | direction or management of such entity, whether by contract or 49 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 50 | outstanding shares, or (iii) beneficial ownership of such entity. 51 | 52 | "Contribution" shall mean any original work of authorship, 53 | including any modifications or additions to an existing work, that 54 | is intentionally submitted by You to iThink Software for inclusion 55 | in, or documentation of, any of the products owned or managed by 56 | iThink Software (the "Work"). For the purposes of this definition, 57 | "submitted" means any form of electronic, verbal, or written 58 | communication sent to iThink Software or its representatives, 59 | including but not limited to communication on electronic mailing 60 | lists, source code control systems, and issue tracking systems that 61 | are managed by, or on behalf of, iThink Software for the purpose of 62 | discussing and improving the Work, but excluding communication that 63 | is conspicuously marked or otherwise designated in writing by You 64 | as "Not a Contribution." 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this Agreement, You hereby grant to iThink Software and to 68 | recipients of software distributed by iThink Software a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare derivative works of, 71 | publicly display, publicly perform, sublicense, and distribute Your 72 | Contributions and such derivative works. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this Agreement, You hereby grant to iThink Software and to 76 | recipients of software distributed by iThink Software a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have 79 | made, use, offer to sell, sell, import, and otherwise transfer the 80 | Work, where such license applies only to those patent claims 81 | licensable by You that are necessarily infringed by Your 82 | Contribution(s) alone or by combination of Your Contribution(s) 83 | with the Work to which such Contribution(s) were submitted. If any 84 | entity institutes patent litigation against You or any other entity 85 | (including a cross-claim or counterclaim in a lawsuit) alleging 86 | that your Contribution, or the Work to which you have contributed, 87 | constitutes direct or contributory patent infringement, then any 88 | patent licenses granted to that entity under this Agreement for 89 | that Contribution or Work shall terminate as of the date such 90 | litigation is filed. 91 | 92 | 4. You represent that you are legally entitled to grant the above 93 | license. If your employer(s) has rights to intellectual property 94 | that you create that includes your Contributions, you represent 95 | that you have received permission to make Contributions on behalf 96 | of that employer, that your employer has waived such rights for 97 | your Contributions to iThink Software, or that your employer has 98 | executed a separate Corporate CLA with iThink Software. 99 | 100 | 5. You represent that each of Your Contributions is Your original 101 | creation (see section 7 for submissions on behalf of others). You 102 | represent that Your Contribution submissions include complete 103 | details of any third-party license or other restriction (including, 104 | but not limited to, related patents and trademarks) of which you 105 | are personally aware and which are associated with any part of Your 106 | Contributions. 107 | 108 | 6. You are not expected to provide support for Your Contributions, 109 | except to the extent You desire to provide support. You may provide 110 | support for free, for a fee, or not at all. Unless required by 111 | applicable law or agreed to in writing, You provide Your 112 | Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 113 | OF ANY KIND, either express or implied, including, without 114 | limitation, any warranties or conditions of TITLE, NON- 115 | INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. 116 | 117 | 7. Should You wish to submit work that is not Your original creation, 118 | You may submit it to iThink Software separately from any 119 | Contribution, identifying the complete details of its source and of 120 | any license or other restriction (including, but not limited to, 121 | related patents, trademarks, and license agreements) of which you 122 | are personally aware, and conspicuously marking the work as 123 | "Submitted on behalf of a third-party: [named here]". 124 | 125 | 8. You agree to notify iThink Software of any facts or circumstances of 126 | which you become aware that would make these representations 127 | inaccurate in any respect. 128 | 129 | 130 | Please sign: __________________________________ Date: ________________ 131 | -------------------------------------------------------------------------------- /docs/contrib/intro.rst: -------------------------------------------------------------------------------- 1 | Contrib apps 2 | ============ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :hidden: 7 | 8 | penfield 9 | shipherd 10 | sobol 11 | waldo 12 | winer 13 | 14 | .. automodule:: philo.contrib 15 | -------------------------------------------------------------------------------- /docs/contrib/penfield.rst: -------------------------------------------------------------------------------- 1 | Penfield 2 | ======== 3 | 4 | .. automodule:: philo.contrib.penfield 5 | 6 | .. automodule:: philo.contrib.penfield.models 7 | 8 | Blogs 9 | +++++ 10 | .. autoclass:: philo.contrib.penfield.models.Blog 11 | :members: 12 | 13 | .. autoclass:: philo.contrib.penfield.models.BlogEntry 14 | :members: 15 | 16 | .. autoclass:: philo.contrib.penfield.models.BlogView 17 | :members: 18 | 19 | Newsletters 20 | +++++++++++ 21 | .. autoclass:: philo.contrib.penfield.models.Newsletter 22 | :members: 23 | 24 | .. autoclass:: philo.contrib.penfield.models.NewsletterArticle 25 | :members: 26 | 27 | .. autoclass:: philo.contrib.penfield.models.NewsletterView 28 | :members: 29 | 30 | Template filters 31 | ++++++++++++++++ 32 | 33 | .. automodule:: philo.contrib.penfield.templatetags.penfield 34 | 35 | .. autotemplatefilter:: monthname 36 | 37 | .. autotemplatefilter:: apmonthname 38 | -------------------------------------------------------------------------------- /docs/contrib/shipherd.rst: -------------------------------------------------------------------------------- 1 | Shipherd 2 | ======== 3 | 4 | .. automodule:: philo.contrib.shipherd 5 | :members: 6 | 7 | :class:`.Node`\ s are useful for structuring a website; however, they are inherently unsuitable for creating site navigation. 8 | 9 | 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. 10 | 11 | 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. 12 | 13 | Finally, :class:`.Node` structures are inherently unordered, while navigation is inherently ordered. 14 | 15 | :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: 16 | 17 | * A site may have one or more independent navigation bars (Main navigation, side navigation, etc.) 18 | * A navigation bar may be shared by sections of the website, or even by the entire site. 19 | * A navigation bar has a certain depth that it displays to. 20 | 21 | 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. 22 | 23 | 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`. 24 | 25 | 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. 26 | 27 | Models 28 | ++++++ 29 | 30 | .. automodule:: philo.contrib.shipherd.models 31 | :members: Navigation, NavigationItem, NavigationMapper 32 | :show-inheritance: 33 | 34 | .. autoclass:: NavigationManager 35 | :members: 36 | 37 | Template tags 38 | +++++++++++++ 39 | 40 | .. automodule:: philo.contrib.shipherd.templatetags.shipherd 41 | 42 | .. autotemplatetag:: recursenavigation 43 | 44 | .. autotemplatefilter:: has_navigation 45 | 46 | .. autotemplatefilter:: navigation_host 47 | -------------------------------------------------------------------------------- /docs/contrib/sobol.rst: -------------------------------------------------------------------------------- 1 | Sobol 2 | ===== 3 | 4 | .. automodule:: philo.contrib.sobol 5 | :members: 6 | 7 | Models 8 | ++++++ 9 | 10 | .. automodule:: philo.contrib.sobol.models 11 | :members: 12 | 13 | Search API 14 | ++++++++++ 15 | 16 | .. automodule:: philo.contrib.sobol.search 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/contrib/waldo.rst: -------------------------------------------------------------------------------- 1 | Waldo 2 | ===== 3 | 4 | .. automodule:: philo.contrib.waldo 5 | :members: 6 | 7 | Models 8 | ++++++ 9 | 10 | .. automodule:: philo.contrib.waldo.models 11 | :members: 12 | 13 | Forms 14 | +++++ 15 | 16 | .. automodule:: philo.contrib.waldo.forms 17 | :members: 18 | 19 | Token generators 20 | ++++++++++++++++ 21 | 22 | .. automodule:: philo.contrib.waldo.tokens 23 | 24 | 25 | .. autodata:: registration_token_generator 26 | 27 | .. autodata:: email_token_generator 28 | -------------------------------------------------------------------------------- /docs/contrib/winer.rst: -------------------------------------------------------------------------------- 1 | Winer 2 | ===== 3 | 4 | .. automodule:: philo.contrib.winer 5 | 6 | .. automodule:: philo.contrib.winer.models 7 | 8 | .. autoclass:: FeedView 9 | :members: 10 | 11 | .. automodule:: philo.contrib.winer.exceptions 12 | :members: 13 | 14 | .. automodule:: philo.contrib.winer.middleware 15 | :members: -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing to Philo 2 | ===================== 3 | 4 | So you want to contribute to Philo? That's great! Here's some ways you can get started: 5 | 6 | * **Report bugs and request features** using the issue tracker at the `project site `_. 7 | * **Contribute code** using `git `_. You can fork philo's repository either on `GitHub `_ or `Gitorious `_. If you are contributing to Philo, you will need to submit a :ref:`Contributor License Agreement `. 8 | * **Join the discussion** on IRC at `irc://irc.oftc.net/#philo `_ if you have any questions or suggestions or just want to chat about the project. You can also keep in touch using the project mailing lists: `philo@ithinksw.org `_ and `philo-devel@ithinksw.org `_. 9 | 10 | 11 | Branches and Code Style 12 | +++++++++++++++++++++++ 13 | 14 | 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. 15 | 16 | __ http://nvie.com/posts/a-successful-git-branching-model/ 17 | 18 | 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. 19 | 20 | .. _cla: 21 | 22 | Licensing and Legal 23 | +++++++++++++++++++ 24 | 25 | 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. 26 | 27 | .. _`Django Project's CLAs`: https://www.djangoproject.com/foundation/cla/ 28 | .. _`Django Project's CLA FAQ`: https://www.djangoproject.com/foundation/cla/faq/ 29 | 30 | If you are an individual not doing work for an employer, then you can simply submit the :download:`Individual CLA `. 31 | 32 | If you are doing work for an employer, they will need to submit the :download:`Corporate CLA ` and you will need to submit the Individual CLA :download:`Individual CLA ` as well. 33 | 34 | Both documents include information on how to submit them. 35 | -------------------------------------------------------------------------------- /docs/dummy-settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django.db.backends.sqlite3', 4 | 'NAME': 'db.sl3' 5 | } 6 | } -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | .. automodule:: philo.exceptions 5 | :members: MIDDLEWARE_NOT_CONFIGURED, AncestorDoesNotExist, ViewCanNotProvideSubpath, ViewDoesNotProvideSubpaths -------------------------------------------------------------------------------- /docs/forms.rst: -------------------------------------------------------------------------------- 1 | Forms 2 | ===== 3 | 4 | .. automodule:: philo.forms.entities 5 | :members: 6 | 7 | 8 | Fields 9 | ++++++ 10 | 11 | .. automodule:: philo.forms.fields 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/handling_requests.rst: -------------------------------------------------------------------------------- 1 | Handling Requests 2 | ================= 3 | 4 | .. automodule:: philo.middleware 5 | :members: 6 | 7 | .. automodule:: philo.views 8 | 9 | 10 | .. autofunction:: node_view(request[, path=None, **kwargs]) 11 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Philo documentation master file, created by 2 | sphinx-quickstart on Fri Jan 28 14:04:16 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. module:: philo 7 | 8 | Welcome to Philo's documentation! 9 | ================================= 10 | 11 | Philo is a foundation for developing web content management systems. Please, read the :doc:`notes for our latest release `. 12 | 13 | Prerequisites: 14 | 15 | * `Python 2.5.4+ `_ 16 | * `Django 1.3+ `_ 17 | * `django-mptt e734079+ `_ 18 | * (optional) `django-grappelli 2.0+ `_ 19 | * (optional) `south 0.7.2+ `_ 20 | * (:mod:`philo.contrib.penfield`) `django-taggit 0.9.3+ `_ 21 | * (:mod:`philo.contrib.waldo`, optional) `recaptcha-django r6+ `_ 22 | 23 | Contents 24 | ++++++++ 25 | 26 | .. toctree:: 27 | :maxdepth: 1 28 | 29 | what 30 | tutorials/intro 31 | models/intro 32 | exceptions 33 | handling_requests 34 | signals 35 | validators 36 | utilities 37 | templatetags 38 | forms 39 | loaders 40 | contrib/intro 41 | contributing 42 | 43 | Indices and tables 44 | ++++++++++++++++++ 45 | 46 | * :ref:`genindex` 47 | * :ref:`modindex` 48 | * :ref:`search` 49 | -------------------------------------------------------------------------------- /docs/loaders.rst: -------------------------------------------------------------------------------- 1 | Database Template Loader 2 | ======================== 3 | 4 | .. automodule:: philo.loaders.database 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Philo.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Philo.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /docs/models/collections.rst: -------------------------------------------------------------------------------- 1 | Collections 2 | =========== 3 | 4 | .. automodule:: philo.models.collections 5 | :members: Collection, CollectionMember, CollectionMemberManager 6 | 7 | .. autoclass:: CollectionMemberManager 8 | :members: -------------------------------------------------------------------------------- /docs/models/entities.rst: -------------------------------------------------------------------------------- 1 | Entities and Attributes 2 | ======================= 3 | 4 | .. module:: philo.models.base 5 | 6 | One of the core concepts in Philo is the relationship between the :class:`Entity` and :class:`Attribute` classes. :class:`Attribute`\ s represent an arbitrary key/value pair by having one :class:`GenericForeignKey` to an :class:`Entity` and another to an :class:`AttributeValue`. 7 | 8 | 9 | Attributes 10 | ---------- 11 | 12 | .. autoclass:: Attribute 13 | :members: 14 | 15 | .. autoclass:: AttributeValue 16 | :members: 17 | 18 | .. automodule:: philo.models.base 19 | :noindex: 20 | :members: attribute_value_limiter 21 | 22 | .. autoclass:: JSONValue 23 | :show-inheritance: 24 | 25 | .. autoclass:: ForeignKeyValue 26 | :show-inheritance: 27 | 28 | .. autoclass:: ManyToManyValue 29 | :show-inheritance: 30 | 31 | .. automodule:: philo.models.base 32 | :noindex: 33 | :members: value_content_type_limiter 34 | 35 | .. autofunction:: register_value_model(model) 36 | .. autofunction:: unregister_value_model(model) 37 | 38 | Entities 39 | -------- 40 | 41 | .. autoclass:: Entity 42 | :members: 43 | 44 | .. autoclass:: TreeEntityManager 45 | :members: 46 | 47 | .. autoclass:: TreeEntity 48 | :show-inheritance: 49 | :members: 50 | 51 | .. attribute:: objects 52 | 53 | An instance of :class:`TreeEntityManager`. 54 | 55 | .. automethod:: get_path -------------------------------------------------------------------------------- /docs/models/fields.rst: -------------------------------------------------------------------------------- 1 | Custom Fields 2 | ============= 3 | 4 | .. automodule:: philo.models.fields 5 | :members: 6 | :exclude-members: JSONField, SlugMultipleChoiceField 7 | 8 | .. autoclass:: JSONField() 9 | :members: 10 | 11 | .. autoclass:: SlugMultipleChoiceField() 12 | :members: 13 | 14 | AttributeProxyFields 15 | -------------------- 16 | 17 | .. automodule:: philo.models.fields.entities 18 | :members: 19 | 20 | .. autoclass:: AttributeProxyField(attribute_key=None, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs) 21 | :members: -------------------------------------------------------------------------------- /docs/models/intro.rst: -------------------------------------------------------------------------------- 1 | Philo's models 2 | ============== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | entities 10 | nodes-and-views 11 | collections 12 | miscellaneous 13 | fields 14 | 15 | 16 | .. automodule:: philo.models 17 | -------------------------------------------------------------------------------- /docs/models/miscellaneous.rst: -------------------------------------------------------------------------------- 1 | Miscellaneous Models 2 | ============================= 3 | .. autoclass:: philo.models.nodes.TargetURLModel 4 | :members: 5 | :exclude-members: get_target_url -------------------------------------------------------------------------------- /docs/models/nodes-and-views.rst: -------------------------------------------------------------------------------- 1 | Nodes and Views: Building Website structure 2 | =========================================== 3 | .. automodule:: philo.models.nodes 4 | 5 | Nodes 6 | ----- 7 | 8 | .. autoclass:: Node 9 | :show-inheritance: 10 | :members: 11 | 12 | Views 13 | ----- 14 | 15 | Abstract View Models 16 | ++++++++++++++++++++ 17 | 18 | .. autoclass:: View 19 | :show-inheritance: 20 | :members: 21 | 22 | .. autoclass:: MultiView 23 | :show-inheritance: 24 | :members: 25 | 26 | Concrete View Subclasses 27 | ++++++++++++++++++++++++ 28 | 29 | .. autoclass:: Redirect 30 | :show-inheritance: 31 | :members: 32 | 33 | .. autoclass:: File 34 | :show-inheritance: 35 | :members: 36 | 37 | Pages 38 | ***** 39 | 40 | .. automodule:: philo.models.pages 41 | 42 | .. autoclass:: Page 43 | :members: 44 | :show-inheritance: 45 | 46 | .. autoclass:: Template 47 | :members: 48 | :show-inheritance: 49 | 50 | .. seealso:: :mod:`philo.loaders.database` 51 | 52 | .. autoclass:: Contentlet 53 | :members: 54 | 55 | .. autoclass:: ContentReference 56 | :members: -------------------------------------------------------------------------------- /docs/releases/0.9.1.rst: -------------------------------------------------------------------------------- 1 | Philo version 0.9.1 release notes 2 | ================================= 3 | 4 | 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. 5 | 6 | New Features and backwards-incompatible changes 7 | +++++++++++++++++++++++++++++++++++++++++++++++ 8 | 9 | * :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`. 10 | * :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. 11 | * :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. 12 | * :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. 13 | * :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`. 14 | -------------------------------------------------------------------------------- /docs/releases/0.9.2.rst: -------------------------------------------------------------------------------- 1 | Philo version 0.9.2 release notes 2 | ================================= 3 | 4 | The primary focus of the 0.9.2 release was repairing the setuptools configuration so that Philo can be installed and updated reliably. In addition, a bug involving the use of :class:`DateTimeField` or :class:`DateField` as the field template for a :class:`JSONAttribute` has been fixed. -------------------------------------------------------------------------------- /docs/signals.rst: -------------------------------------------------------------------------------- 1 | Signals 2 | ======= 3 | 4 | .. automodule:: philo.signals 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/templatetags.rst: -------------------------------------------------------------------------------- 1 | Template Tags 2 | ============= 3 | 4 | .. automodule:: philo.templatetags 5 | 6 | Collections 7 | +++++++++++ 8 | 9 | .. automodule:: philo.templatetags.collections 10 | 11 | .. autotemplatetag:: membersof 12 | 13 | Containers 14 | ++++++++++ 15 | 16 | .. automodule:: philo.templatetags.containers 17 | 18 | 19 | .. autotemplatetag:: container 20 | 21 | 22 | Embedding 23 | +++++++++ 24 | 25 | .. automodule:: philo.templatetags.embed 26 | 27 | .. autotemplatetag:: embed 28 | 29 | 30 | Nodes 31 | +++++ 32 | 33 | .. automodule:: philo.templatetags.nodes 34 | 35 | .. autotemplatetag:: node_url 36 | 37 | String inclusion 38 | ++++++++++++++++ 39 | 40 | .. automodule:: philo.templatetags.include_string 41 | 42 | .. autotemplatetag:: include_string 43 | -------------------------------------------------------------------------------- /docs/tutorials/getting-started.rst: -------------------------------------------------------------------------------- 1 | Getting started with philo 2 | ========================== 3 | 4 | .. note:: This guide assumes that you have worked with Django's built-in administrative interface. 5 | 6 | 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. 7 | 8 | 1. Add :mod:`philo` and :mod:`mptt` to :setting:`settings.INSTALLED_APPS`:: 9 | 10 | INSTALLED_APPS = ( 11 | ... 12 | 'philo', 13 | 'mptt', 14 | ... 15 | ) 16 | 17 | 2. Syncdb or run migrations to set up your database. 18 | 19 | 3. Add :class:`philo.middleware.RequestNodeMiddleware` to :setting:`settings.MIDDLEWARE_CLASSES`:: 20 | 21 | MIDDLEWARE_CLASSES = ( 22 | ... 23 | 'philo.middleware.RequestNodeMiddleware', 24 | ... 25 | ) 26 | 27 | 4. Include :mod:`philo.urls` somewhere in your urls.py file. For example:: 28 | 29 | from django.conf.urls.defaults import patterns, include, url 30 | urlpatterns = patterns('', 31 | url(r'^', include('philo.urls')), 32 | ) 33 | 34 | Philo should be ready to go! (Almost.) 35 | 36 | .. _philo: http://philocms.org/ 37 | .. _mptt: http://github.com/django-mptt/django-mptt 38 | 39 | Hello world 40 | +++++++++++ 41 | 42 | 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:: 43 | 44 | 45 | 46 | Hello world! 47 | 48 | 49 |

Hello world!

50 |

The time is {% now %}.

51 | 52 | 53 | 54 | Next, add a philo :class:`.Page` - let's call it "Hello World Page" and use the template you just made. 55 | 56 | 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! 57 | 58 | Setting the root node 59 | +++++++++++++++++++++ 60 | 61 | 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. 62 | 63 | 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. 64 | 65 | 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 ``/``! 66 | 67 | Editing page contents 68 | +++++++++++++++++++++ 69 | 70 | 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:: 71 | 72 | 73 | 74 | {% container page_title %} 75 | 76 | 77 | {% container page_body as content %} 78 | {% if content %} 79 |

{{ content }}

80 | {% endif %} 81 |

The time is {% now "jS F Y H:i" %}.

82 | 83 | 84 | 85 | 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. 86 | 87 | .. seealso:: :ttag:`philo.templatetags.containers.container` 88 | 89 | Congrats! You've done it! 90 | -------------------------------------------------------------------------------- /docs/tutorials/intro.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | getting-started 8 | shipherd 9 | -------------------------------------------------------------------------------- /docs/tutorials/shipherd.rst: -------------------------------------------------------------------------------- 1 | Using Shipherd in the Admin 2 | =========================== 3 | 4 | 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! ;-) 5 | 6 | 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`. 7 | 8 | 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:: 9 | 10 | TEMPLATE_CONTEXT_PROCESSORS = ( 11 | # Defaults 12 | "django.contrib.auth.context_processors.auth", 13 | "django.core.context_processors.debug", 14 | "django.core.context_processors.i18n", 15 | "django.core.context_processors.media", 16 | "django.core.context_processors.static", 17 | "django.contrib.messages.context_processors.messages" 18 | ... 19 | "django.core.context_processors.request" 20 | ) 21 | 22 | Creating the Navigation 23 | +++++++++++++++++++++++ 24 | 25 | 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. 26 | 27 | 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.) 28 | 29 | Press save and you've created your first navigation. 30 | 31 | Displaying the Navigation 32 | +++++++++++++++++++++++++ 33 | 34 | 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:: 35 | 36 | {% load shipherd %} 37 | 38 | {% container page_title %} 39 | 40 | 41 |
    42 | {% recursenavigation node "main" %} 43 | 44 | {{ item.text }} 45 | 46 | {% endrecursenavigation %} 47 |
48 | {% container page_body as content %} 49 | {% if content %} 50 |

{{ content }}

51 | {% endif %} 52 |

The time is {% now %}.

53 | 54 | 55 | 56 | Now have a look at the page - your navigation is there! 57 | 58 | Linking to google 59 | +++++++++++++++++ 60 | 61 | 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. 62 | 63 | :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. 64 | -------------------------------------------------------------------------------- /docs/utilities.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========= 3 | 4 | .. automodule:: philo.utils 5 | :members: 6 | 7 | AttributeMappers 8 | ++++++++++++++++ 9 | 10 | .. module:: philo.utils.entities 11 | 12 | .. autoclass:: AttributeMapper 13 | :members: 14 | 15 | .. autoclass:: TreeAttributeMapper 16 | :members: 17 | :show-inheritance: 18 | 19 | .. autoclass:: PassthroughAttributeMapper 20 | :members: 21 | :show-inheritance: 22 | 23 | LazyAttributeMappers 24 | -------------------- 25 | 26 | .. autoclass:: LazyAttributeMapperMixin 27 | :members: 28 | 29 | .. autoclass:: LazyAttributeMapper 30 | :members: 31 | :show-inheritance: 32 | 33 | .. autoclass:: LazyTreeAttributeMapper 34 | :members: 35 | :show-inheritance: 36 | 37 | .. autoclass:: LazyPassthroughAttributeMapper 38 | :members: 39 | :show-inheritance: 40 | -------------------------------------------------------------------------------- /docs/validators.rst: -------------------------------------------------------------------------------- 1 | Validators 2 | ========== 3 | 4 | .. automodule:: philo.validators 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/what.rst: -------------------------------------------------------------------------------- 1 | What is Philo, anyway? 2 | ====================== 3 | 4 | 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: 5 | 6 | * :class:`.Node`\ s represent the URL hierarchy of the website. 7 | * :class:`.View`\ s contain the logic for each :class:`.Node`, as simple as a :class:`.Redirect` or as complex as a :class:`.Blog`. 8 | * :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. 9 | * :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. 10 | 11 | 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. 12 | 13 | How's that different than other CMSes? 14 | ++++++++++++++++++++++++++++++++++++++ 15 | 16 | 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: 17 | 18 | * 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 ``
`` element, should depend on the template used to render the entry, not the HTML5 knowledge of the user. 19 | .. seealso:: :ttag:`~philo.templatetags.embed.embed` 20 | * 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. 21 | .. seealso:: :ttag:`~philo.templatetags.include_string.include_string` 22 | * 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. 23 | .. seealso:: :class:`.Contentlet`, :class:`.ContentReference` 24 | * 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. 25 | .. seealso:: :class:`.Attribute` 26 | -------------------------------------------------------------------------------- /philo/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 9, 2) 2 | -------------------------------------------------------------------------------- /philo/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from philo.admin.base import * 2 | from philo.admin.collections import * 3 | from philo.admin.nodes import * 4 | from philo.admin.pages import * -------------------------------------------------------------------------------- /philo/admin/base.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | from django.contrib.contenttypes import generic 4 | from django.http import HttpResponse 5 | from django.utils import simplejson as json 6 | from django.utils.html import escape 7 | from mptt.admin import MPTTModelAdmin 8 | 9 | from philo.models import Attribute 10 | from philo.models.fields.entities import ForeignKeyAttribute, ManyToManyAttribute 11 | from philo.admin.forms.attributes import AttributeForm, AttributeInlineFormSet 12 | from philo.forms.entities import EntityForm, proxy_fields_for_entity_model 13 | 14 | 15 | COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',) 16 | 17 | 18 | class AttributeInline(generic.GenericTabularInline): 19 | ct_field = 'entity_content_type' 20 | ct_fk_field = 'entity_object_id' 21 | model = Attribute 22 | extra = 1 23 | allow_add = True 24 | classes = COLLAPSE_CLASSES 25 | form = AttributeForm 26 | formset = AttributeInlineFormSet 27 | fields = ['key', 'value_content_type'] 28 | if 'grappelli' in settings.INSTALLED_APPS: 29 | template = 'admin/philo/edit_inline/grappelli_tabular_attribute.html' 30 | else: 31 | template = 'admin/philo/edit_inline/tabular_attribute.html' 32 | 33 | 34 | # HACK to bypass model validation for proxy fields 35 | class SpoofedHiddenFields(object): 36 | def __init__(self, proxy_fields, value): 37 | self.value = value 38 | self.spoofed = list(set(value) - set(proxy_fields)) 39 | 40 | def __get__(self, instance, owner): 41 | if instance is None: 42 | return self.spoofed 43 | return self.value 44 | 45 | 46 | class SpoofedAddedFields(SpoofedHiddenFields): 47 | def __init__(self, proxy_fields, value): 48 | self.value = value 49 | self.spoofed = list(set(value) | set(proxy_fields)) 50 | 51 | 52 | def hide_proxy_fields(cls, attname): 53 | val = getattr(cls, attname, []) 54 | proxy_fields = getattr(cls, 'proxy_fields') 55 | if val: 56 | setattr(cls, attname, SpoofedHiddenFields(proxy_fields, val)) 57 | 58 | def add_proxy_fields(cls, attname): 59 | val = getattr(cls, attname, []) 60 | proxy_fields = getattr(cls, 'proxy_fields') 61 | setattr(cls, attname, SpoofedAddedFields(proxy_fields, val)) 62 | 63 | 64 | class EntityAdminMetaclass(admin.ModelAdmin.__metaclass__): 65 | def __new__(cls, name, bases, attrs): 66 | new_class = super(EntityAdminMetaclass, cls).__new__(cls, name, bases, attrs) 67 | hide_proxy_fields(new_class, 'raw_id_fields') 68 | add_proxy_fields(new_class, 'readonly_fields') 69 | return new_class 70 | # END HACK 71 | 72 | class EntityAdmin(admin.ModelAdmin): 73 | __metaclass__ = EntityAdminMetaclass 74 | form = EntityForm 75 | inlines = [AttributeInline] 76 | save_on_top = True 77 | proxy_fields = [] 78 | 79 | def formfield_for_dbfield(self, db_field, **kwargs): 80 | """ 81 | Override the default behavior to provide special formfields for EntityEntitys. 82 | Essentially clones the ForeignKey/ManyToManyField special behavior for the Attribute versions. 83 | """ 84 | if not db_field.choices and isinstance(db_field, (ForeignKeyAttribute, ManyToManyAttribute)): 85 | request = kwargs.pop("request", None) 86 | # Combine the field kwargs with any options for formfield_overrides. 87 | # Make sure the passed in **kwargs override anything in 88 | # formfield_overrides because **kwargs is more specific, and should 89 | # always win. 90 | if db_field.__class__ in self.formfield_overrides: 91 | kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs) 92 | 93 | # Get the correct formfield. 94 | if isinstance(db_field, ManyToManyAttribute): 95 | formfield = self.formfield_for_manytomanyattribute(db_field, request, **kwargs) 96 | elif isinstance(db_field, ForeignKeyAttribute): 97 | formfield = self.formfield_for_foreignkeyattribute(db_field, request, **kwargs) 98 | 99 | # For non-raw_id fields, wrap the widget with a wrapper that adds 100 | # extra HTML -- the "add other" interface -- to the end of the 101 | # rendered output. formfield can be None if it came from a 102 | # OneToOneField with parent_link=True or a M2M intermediary. 103 | # TODO: Implement this. 104 | #if formfield and db_field.name not in self.raw_id_fields: 105 | # formfield.widget = admin.widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field, self.admin_site) 106 | 107 | return formfield 108 | return super(EntityAdmin, self).formfield_for_dbfield(db_field, **kwargs) 109 | 110 | def formfield_for_foreignkeyattribute(self, db_field, request=None, **kwargs): 111 | """Get a form field for a ForeignKeyAttribute field.""" 112 | db = kwargs.get('using') 113 | if db_field.name in self.raw_id_fields: 114 | kwargs['widget'] = admin.widgets.ForeignKeyRawIdWidget(db_field, db) 115 | #TODO: Add support for radio fields 116 | #elif db_field.name in self.radio_fields: 117 | # kwargs['widget'] = widgets.AdminRadioSelect(attrs={ 118 | # 'class': get_ul_class(self.radio_fields[db_field.name]), 119 | # }) 120 | # kwargs['empty_label'] = db_field.blank and _('None') or None 121 | 122 | return db_field.formfield(**kwargs) 123 | 124 | def formfield_for_manytomanyattribute(self, db_field, request=None, **kwargs): 125 | """Get a form field for a ManyToManyAttribute field.""" 126 | db = kwargs.get('using') 127 | 128 | if db_field.name in self.raw_id_fields: 129 | kwargs['widget'] = admin.widgets.ManyToManyRawIdWidget(db_field, using=db) 130 | kwargs['help_text'] = '' 131 | #TODO: Add support for filtered fields. 132 | #elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): 133 | # kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) 134 | 135 | return db_field.formfield(**kwargs) 136 | 137 | 138 | class TreeEntityAdmin(EntityAdmin, MPTTModelAdmin): 139 | pass -------------------------------------------------------------------------------- /philo/admin/collections.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from philo.admin.base import COLLAPSE_CLASSES 4 | from philo.models import CollectionMember, Collection 5 | 6 | 7 | class CollectionMemberInline(admin.TabularInline): 8 | fk_name = 'collection' 9 | model = CollectionMember 10 | extra = 1 11 | classes = COLLAPSE_CLASSES 12 | allow_add = True 13 | fields = ('member_content_type', 'member_object_id', 'index') 14 | sortable_field_name = 'index' 15 | 16 | 17 | class CollectionAdmin(admin.ModelAdmin): 18 | inlines = [CollectionMemberInline] 19 | list_display = ('name', 'description', 'get_count') 20 | 21 | 22 | admin.site.register(Collection, CollectionAdmin) -------------------------------------------------------------------------------- /philo/admin/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from philo.admin.forms.attributes import * 2 | from philo.admin.forms.containers import * -------------------------------------------------------------------------------- /philo/admin/forms/attributes.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.generic import BaseGenericInlineFormSet 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.forms.models import ModelForm 4 | 5 | from philo.models import Attribute 6 | 7 | 8 | __all__ = ('AttributeForm', 'AttributeInlineFormSet') 9 | 10 | 11 | class AttributeForm(ModelForm): 12 | """ 13 | This class handles an attribute's fields as well as the fields for its value (if there is one.) 14 | The fields defined will vary depending on the value type, but the fields for defining the value 15 | (i.e. value_content_type and value_object_id) will always be defined. Except that value_object_id 16 | will never be defined. BLARGH! 17 | """ 18 | def __init__(self, *args, **kwargs): 19 | super(AttributeForm, self).__init__(*args, **kwargs) 20 | 21 | # This is necessary because model forms store changes to self.instance in their clean method. 22 | # Mutter mutter. 23 | value = self.instance.value 24 | self._cached_value_ct_id = self.instance.value_content_type_id 25 | self._cached_value = value 26 | 27 | # If there is a value, pull in its fields. 28 | if value is not None: 29 | self.value_fields = value.value_formfields() 30 | self.fields.update(self.value_fields) 31 | 32 | def save(self, *args, **kwargs): 33 | # At this point, the cleaned_data has already been stored on self.instance. 34 | 35 | if self.instance.value_content_type_id != self._cached_value_ct_id: 36 | # The value content type has changed. Clear the old value, if there was one. 37 | if self._cached_value: 38 | self._cached_value.delete() 39 | 40 | # Clear the submitted value, if any. 41 | self.cleaned_data.pop('value', None) 42 | 43 | # Now create a new value instance so that on next instantiation, the form will 44 | # know what fields to add. 45 | if self.instance.value_content_type_id is not None: 46 | self.instance.value = ContentType.objects.get_for_id(self.instance.value_content_type_id).model_class().objects.create() 47 | elif self.instance.value is not None: 48 | # The value content type is the same, but one of the value fields has changed. 49 | 50 | # Use construct_instance to apply the changes from the cleaned_data to the value instance. 51 | fields = self.value_fields.keys() 52 | if set(fields) & set(self.changed_data): 53 | self.instance.value.construct_instance(**dict([(key, self.cleaned_data[key]) for key in fields])) 54 | self.instance.value.save() 55 | 56 | return super(AttributeForm, self).save(*args, **kwargs) 57 | 58 | class Meta: 59 | model = Attribute 60 | 61 | 62 | class AttributeInlineFormSet(BaseGenericInlineFormSet): 63 | "Necessary to force the GenericInlineFormset to use the form's save method for new objects." 64 | def save_new(self, form, commit): 65 | setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk) 66 | setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk) 67 | return form.save() -------------------------------------------------------------------------------- /philo/admin/forms/containers.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.db.models import Q 4 | from django.forms.models import ModelForm, BaseInlineFormSet, BaseModelFormSet 5 | from django.forms.formsets import TOTAL_FORM_COUNT 6 | from django.utils.datastructures import SortedDict 7 | 8 | from philo.admin.widgets import ModelLookupWidget, EmbedWidget 9 | from philo.models import Contentlet, ContentReference 10 | 11 | 12 | __all__ = ( 13 | 'ContentletForm', 14 | 'ContentletInlineFormSet', 15 | 'ContentReferenceForm', 16 | 'ContentReferenceInlineFormSet' 17 | ) 18 | 19 | 20 | class ContainerForm(ModelForm): 21 | def __init__(self, *args, **kwargs): 22 | super(ContainerForm, self).__init__(*args, **kwargs) 23 | self.verbose_name = self.instance.name.replace('_', ' ') 24 | self.prefix = self.instance.name 25 | 26 | 27 | class ContentletForm(ContainerForm): 28 | content = forms.CharField(required=False, widget=EmbedWidget, label='Content') 29 | 30 | def should_delete(self): 31 | # Delete iff: the data has changed and is now empty. 32 | return self.has_changed() and not bool(self.cleaned_data['content']) 33 | 34 | class Meta: 35 | model = Contentlet 36 | fields = ['content'] 37 | 38 | 39 | class ContentReferenceForm(ContainerForm): 40 | def __init__(self, *args, **kwargs): 41 | super(ContentReferenceForm, self).__init__(*args, **kwargs) 42 | try: 43 | self.fields['content_id'].widget = ModelLookupWidget(self.instance.content_type) 44 | except ObjectDoesNotExist: 45 | # This will happen when an empty form (which we will never use) gets instantiated. 46 | pass 47 | 48 | def should_delete(self): 49 | return self.has_changed() and (self.cleaned_data['content_id'] is None) 50 | 51 | class Meta: 52 | model = ContentReference 53 | fields = ['content_id'] 54 | 55 | 56 | class ContainerInlineFormSet(BaseInlineFormSet): 57 | @property 58 | def containers(self): 59 | if not hasattr(self, '_containers'): 60 | self._containers = self.get_containers() 61 | return self._containers 62 | 63 | def total_form_count(self): 64 | # This ignores the posted management form data... but that doesn't 65 | # seem to have any ill side effects. 66 | return len(self.containers.keys()) 67 | 68 | def _get_initial_forms(self): 69 | return [form for form in self.forms if form.instance.pk is not None] 70 | initial_forms = property(_get_initial_forms) 71 | 72 | def _get_extra_forms(self): 73 | return [form for form in self.forms if form.instance.pk is None] 74 | extra_forms = property(_get_extra_forms) 75 | 76 | def _construct_form(self, i, **kwargs): 77 | if 'instance' not in kwargs: 78 | kwargs['instance'] = self.containers.values()[i] 79 | 80 | # Skip over the BaseModelFormSet. We have our own way of doing things! 81 | form = super(BaseModelFormSet, self)._construct_form(i, **kwargs) 82 | 83 | # Since we skipped over BaseModelFormSet, we need to duplicate what BaseInlineFormSet would do. 84 | if self.save_as_new: 85 | # Remove the primary key from the form's data, we are only 86 | # creating new instances 87 | form.data[form.add_prefix(self._pk_field.name)] = None 88 | 89 | # Remove the foreign key from the form's data 90 | form.data[form.add_prefix(self.fk.name)] = None 91 | 92 | # Set the fk value here so that the form can do it's validation. 93 | setattr(form.instance, self.fk.get_attname(), self.instance.pk) 94 | return form 95 | 96 | def add_fields(self, form, index): 97 | """Override the pk field's initial value with a real one.""" 98 | super(ContainerInlineFormSet, self).add_fields(form, index) 99 | if index is not None: 100 | pk_value = self.containers.values()[index].pk 101 | else: 102 | pk_value = None 103 | form.fields[self._pk_field.name].initial = pk_value 104 | 105 | def save_existing_objects(self, commit=True): 106 | self.changed_objects = [] 107 | self.deleted_objects = [] 108 | if not self.get_queryset(): 109 | return [] 110 | 111 | saved_instances = [] 112 | for form in self.initial_forms: 113 | pk_name = self._pk_field.name 114 | raw_pk_value = form._raw_value(pk_name) 115 | 116 | # clean() for different types of PK fields can sometimes return 117 | # the model instance, and sometimes the PK. Handle either. 118 | pk_value = form.fields[pk_name].clean(raw_pk_value) 119 | pk_value = getattr(pk_value, 'pk', pk_value) 120 | 121 | # if the pk_value is None, they have just switched to a 122 | # template which didn't contain data about this container. 123 | # Skip! 124 | if pk_value is not None: 125 | obj = self._existing_object(pk_value) 126 | if form.should_delete(): 127 | self.deleted_objects.append(obj) 128 | obj.delete() 129 | continue 130 | if form.has_changed(): 131 | self.changed_objects.append((obj, form.changed_data)) 132 | saved_instances.append(self.save_existing(form, obj, commit=commit)) 133 | if not commit: 134 | self.saved_forms.append(form) 135 | return saved_instances 136 | 137 | def save_new_objects(self, commit=True): 138 | self.new_objects = [] 139 | for form in self.extra_forms: 140 | if not form.has_changed(): 141 | continue 142 | # If someone has marked an add form for deletion, don't save the 143 | # object. 144 | if form.should_delete(): 145 | continue 146 | self.new_objects.append(self.save_new(form, commit=commit)) 147 | if not commit: 148 | self.saved_forms.append(form) 149 | return self.new_objects 150 | 151 | 152 | class ContentletInlineFormSet(ContainerInlineFormSet): 153 | def get_containers(self): 154 | try: 155 | containers = self.instance.containers[0] 156 | except ObjectDoesNotExist: 157 | containers = [] 158 | 159 | qs = self.get_queryset().filter(name__in=containers) 160 | container_dict = SortedDict([(container.name, container) for container in qs]) 161 | for name in containers: 162 | if name not in container_dict: 163 | container_dict[name] = self.model(name=name) 164 | 165 | container_dict.keyOrder = containers 166 | return container_dict 167 | 168 | 169 | class ContentReferenceInlineFormSet(ContainerInlineFormSet): 170 | def get_containers(self): 171 | try: 172 | containers = self.instance.containers[1] 173 | except ObjectDoesNotExist: 174 | containers = {} 175 | 176 | filter = Q() 177 | for name, ct in containers.items(): 178 | filter |= Q(name=name, content_type=ct) 179 | qs = self.get_queryset().filter(filter) 180 | 181 | container_dict = SortedDict([(container.name, container) for container in qs]) 182 | 183 | keyOrder = [] 184 | for name, ct in containers.items(): 185 | keyOrder.append(name) 186 | if name not in container_dict: 187 | container_dict[name] = self.model(name=name, content_type=ct) 188 | 189 | container_dict.keyOrder = keyOrder 190 | return container_dict -------------------------------------------------------------------------------- /philo/admin/nodes.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from mptt.admin import MPTTModelAdmin 3 | 4 | from philo.admin.base import EntityAdmin, TreeEntityAdmin, COLLAPSE_CLASSES 5 | from philo.models import Node, Redirect, File 6 | 7 | 8 | class NodeAdmin(TreeEntityAdmin): 9 | list_display = ('slug', 'view', 'accepts_subpath') 10 | raw_id_fields = ('parent',) 11 | related_lookup_fields = { 12 | 'fk': raw_id_fields, 13 | 'm2m': [], 14 | 'generic': [['view_content_type', 'view_object_id']] 15 | } 16 | 17 | def accepts_subpath(self, obj): 18 | return obj.accepts_subpath 19 | accepts_subpath.boolean = True 20 | 21 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 22 | return super(MPTTModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) 23 | 24 | 25 | class ViewAdmin(EntityAdmin): 26 | pass 27 | 28 | 29 | class RedirectAdmin(ViewAdmin): 30 | fieldsets = ( 31 | (None, { 32 | 'fields': ('target_node', 'url_or_subpath', 'status_code') 33 | }), 34 | ('Advanced', { 35 | 'fields': ('reversing_parameters',), 36 | 'classes': COLLAPSE_CLASSES 37 | }) 38 | ) 39 | list_display = ('target_url', 'status_code', 'target_node', 'url_or_subpath') 40 | list_filter = ('status_code',) 41 | raw_id_fields = ['target_node'] 42 | related_lookup_fields = { 43 | 'fk': raw_id_fields 44 | } 45 | 46 | 47 | class FileAdmin(ViewAdmin): 48 | fieldsets = ( 49 | (None, { 50 | 'fields': ('name', 'file', 'mimetype') 51 | }), 52 | ) 53 | list_display = ('name', 'mimetype', 'file') 54 | search_fields = ('name',) 55 | list_filter = ('mimetype',) 56 | 57 | 58 | admin.site.register(Node, NodeAdmin) 59 | admin.site.register(Redirect, RedirectAdmin) 60 | admin.site.register(File, FileAdmin) -------------------------------------------------------------------------------- /philo/admin/pages.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf import settings 3 | from django.contrib import admin 4 | 5 | from philo.admin.base import COLLAPSE_CLASSES, TreeEntityAdmin 6 | from philo.admin.forms.containers import * 7 | from philo.admin.nodes import ViewAdmin 8 | from philo.admin.widgets import EmbedWidget 9 | from philo.models.fields import TemplateField 10 | from philo.models.pages import Page, Template, Contentlet, ContentReference 11 | 12 | 13 | class ContainerInline(admin.StackedInline): 14 | extra = 0 15 | max_num = 0 16 | can_delete = False 17 | classes = ('collapse-open', 'collapse','open') 18 | if 'grappelli' in settings.INSTALLED_APPS: 19 | template = 'admin/philo/edit_inline/grappelli_tabular_container.html' 20 | else: 21 | template = 'admin/philo/edit_inline/tabular_container.html' 22 | 23 | 24 | class ContentletInline(ContainerInline): 25 | model = Contentlet 26 | formset = ContentletInlineFormSet 27 | form = ContentletForm 28 | 29 | 30 | class ContentReferenceInline(ContainerInline): 31 | model = ContentReference 32 | formset = ContentReferenceInlineFormSet 33 | form = ContentReferenceForm 34 | 35 | 36 | class PageAdmin(ViewAdmin): 37 | add_form_template = 'admin/philo/page/add_form.html' 38 | fieldsets = ( 39 | (None, { 40 | 'fields': ('title', 'template') 41 | }), 42 | ) 43 | list_display = ('title', 'template') 44 | list_filter = ('template',) 45 | search_fields = ['title', 'contentlets__content'] 46 | inlines = [ContentletInline, ContentReferenceInline] + ViewAdmin.inlines 47 | 48 | def response_add(self, request, obj, post_url_continue='../%s/'): 49 | # Shamelessly cribbed from django/contrib/auth/admin.py:143 50 | if '_addanother' not in request.POST and '_popup' not in request.POST: 51 | request.POST['_continue'] = 1 52 | return super(PageAdmin, self).response_add(request, obj, post_url_continue) 53 | 54 | 55 | class TemplateAdmin(TreeEntityAdmin): 56 | prepopulated_fields = {'slug': ('name',)} 57 | fieldsets = ( 58 | (None, { 59 | 'fields': ('parent', 'name', 'slug') 60 | }), 61 | ('Documentation', { 62 | 'classes': COLLAPSE_CLASSES, 63 | 'fields': ('documentation',) 64 | }), 65 | (None, { 66 | 'fields': ('code',) 67 | }), 68 | ('Advanced', { 69 | 'classes': COLLAPSE_CLASSES, 70 | 'fields': ('mimetype',) 71 | }), 72 | ) 73 | formfield_overrides = { 74 | TemplateField: {'widget': EmbedWidget} 75 | } 76 | save_on_top = True 77 | save_as = True 78 | list_display = ('__unicode__', 'slug', 'get_path',) 79 | 80 | 81 | admin.site.register(Page, PageAdmin) 82 | admin.site.register(Template, TemplateAdmin) -------------------------------------------------------------------------------- /philo/admin/widgets.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf import settings 3 | from django.contrib.admin.widgets import url_params_from_lookup_dict 4 | from django.utils import simplejson as json 5 | from django.utils.html import escape 6 | from django.utils.safestring import mark_safe 7 | from django.utils.text import truncate_words 8 | from django.utils.translation import ugettext as _ 9 | 10 | 11 | class ModelLookupWidget(forms.TextInput): 12 | # is_hidden = False 13 | 14 | def __init__(self, content_type, attrs=None, limit_choices_to=None): 15 | self.content_type = content_type 16 | self.limit_choices_to = limit_choices_to 17 | super(ModelLookupWidget, self).__init__(attrs) 18 | 19 | def render(self, name, value, attrs=None): 20 | related_url = '../../../%s/%s/' % (self.content_type.app_label, self.content_type.model) 21 | params = url_params_from_lookup_dict(self.limit_choices_to) 22 | if params: 23 | url = u'?' + u'&'.join([u'%s=%s' % (k, v) for k, v in params.items()]) 24 | else: 25 | url = u'' 26 | if attrs is None: 27 | attrs = {} 28 | if "class" not in attrs: 29 | attrs['class'] = 'vForeignKeyRawIdAdminField' 30 | output = [super(ModelLookupWidget, self).render(name, value, attrs)] 31 | output.append('' % (related_url, url, name)) 32 | output.append('%s' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))) 33 | output.append('') 34 | if value: 35 | value_class = self.content_type.model_class() 36 | try: 37 | value_object = value_class.objects.get(pk=value) 38 | output.append(' %s' % escape(truncate_words(value_object, 14))) 39 | except value_class.DoesNotExist: 40 | pass 41 | return mark_safe(u''.join(output)) 42 | 43 | 44 | class EmbedWidget(forms.Textarea): 45 | """A form widget with the HTML class embedding and an embedded list of content-types.""" 46 | def __init__(self, attrs=None): 47 | from philo.models import value_content_type_limiter 48 | 49 | content_types = value_content_type_limiter.classes 50 | data = [] 51 | 52 | for content_type in content_types: 53 | 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)}) 54 | 55 | json_ = json.dumps(data) 56 | 57 | default_attrs = {'class': 'embedding vLargeTextField', 'data-content-types': json_ } 58 | 59 | if attrs: 60 | default_attrs.update(attrs) 61 | 62 | super(EmbedWidget, self).__init__(default_attrs) 63 | 64 | class Media: 65 | css = { 66 | 'all': ('philo/css/EmbedWidget.css',), 67 | } 68 | js = ('philo/js/EmbedWidget.js',) 69 | -------------------------------------------------------------------------------- /philo/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | #encoding: utf-8 2 | """ 3 | Following Python and Django’s “batteries included” philosophy, Philo includes a number of optional packages that simplify common website structures: 4 | 5 | * :mod:`~philo.contrib.penfield` — Basic blog and newsletter management. 6 | * :mod:`~philo.contrib.shipherd` — Powerful site navigation. 7 | * :mod:`~philo.contrib.sobol` — Custom web and database searches. 8 | * :mod:`~philo.contrib.waldo` — Custom authentication systems. 9 | * :mod:`~philo.contrib.winer` — Abstract framework for Philo-based syndication. 10 | 11 | """ -------------------------------------------------------------------------------- /philo/contrib/julian/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This version of julian is currently in development and is not considered stable. 3 | 4 | """ -------------------------------------------------------------------------------- /philo/contrib/julian/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from philo.admin import EntityAdmin, COLLAPSE_CLASSES 4 | from philo.contrib.julian.models import Location, Event, Calendar, CalendarView 5 | 6 | 7 | class LocationAdmin(EntityAdmin): 8 | pass 9 | 10 | 11 | class EventAdmin(EntityAdmin): 12 | fieldsets = ( 13 | (None, { 14 | 'fields': ('name', 'slug', 'description', 'tags', 'owner') 15 | }), 16 | ('Location', { 17 | 'fields': ('location_content_type', 'location_pk') 18 | }), 19 | ('Time', { 20 | 'fields': (('start_date', 'start_time'), ('end_date', 'end_time'),), 21 | }), 22 | ('Advanced', { 23 | 'fields': ('parent_event', 'site',), 24 | 'classes': COLLAPSE_CLASSES 25 | }) 26 | ) 27 | filter_horizontal = ['tags'] 28 | raw_id_fields = ['parent_event'] 29 | related_lookup_fields = { 30 | 'fk': raw_id_fields, 31 | 'generic': [["location_content_type", "location_pk"]] 32 | } 33 | prepopulated_fields = {'slug': ('name',)} 34 | 35 | 36 | class CalendarAdmin(EntityAdmin): 37 | prepopulated_fields = {'slug': ('name',)} 38 | filter_horizontal = ['events'] 39 | fieldsets = ( 40 | (None, { 41 | 'fields': ('name', 'description', 'events') 42 | }), 43 | ('Advanced', { 44 | 'fields': ('slug', 'site', 'language',), 45 | 'classes': COLLAPSE_CLASSES 46 | }) 47 | ) 48 | 49 | 50 | class CalendarViewAdmin(EntityAdmin): 51 | fieldsets = ( 52 | (None, { 53 | 'fields': ('calendar',) 54 | }), 55 | ('Pages', { 56 | 'fields': ('index_page', 'event_detail_page') 57 | }), 58 | ('General Settings', { 59 | 'fields': ('tag_permalink_base', 'owner_permalink_base', 'location_permalink_base', 'events_per_page') 60 | }), 61 | ('Event List Pages', { 62 | 'fields': ('timespan_page', 'tag_page', 'location_page', 'owner_page'), 63 | 'classes': COLLAPSE_CLASSES 64 | }), 65 | ('Archive Pages', { 66 | 'fields': ('location_archive_page', 'tag_archive_page', 'owner_archive_page'), 67 | 'classes': COLLAPSE_CLASSES 68 | }), 69 | ('Feed Settings', { 70 | 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'item_title_template', 'item_description_template',), 71 | 'classes': COLLAPSE_CLASSES 72 | }) 73 | ) 74 | raw_id_fields = ('index_page', 'event_detail_page', 'timespan_page', 'tag_page', 'location_page', 'owner_page', 'location_archive_page', 'tag_archive_page', 'owner_archive_page', 'item_title_template', 'item_description_template',) 75 | related_lookup_fields = {'fk': raw_id_fields} 76 | 77 | 78 | admin.site.register(Location, LocationAdmin) 79 | admin.site.register(Event, EventAdmin) 80 | admin.site.register(Calendar, CalendarAdmin) 81 | admin.site.register(CalendarView, CalendarViewAdmin) -------------------------------------------------------------------------------- /philo/contrib/julian/feedgenerator.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.utils.feedgenerator import SyndicationFeed 3 | import vobject 4 | 5 | 6 | # Map the keys in the ICalendarFeed internal dictionary to the names of iCalendar attributes. 7 | FEED_ICAL_MAP = { 8 | 'title': 'x-wr-calname', 9 | 'description': 'x-wr-caldesc', 10 | #'link': ???, 11 | #'language': ???, 12 | #author_email 13 | #author_name 14 | #author_link 15 | #subtitle 16 | #categories 17 | #feed_url 18 | #feed_copyright 19 | 'id': 'prodid', 20 | 'ttl': 'x-published-ttl' 21 | } 22 | 23 | 24 | ITEM_ICAL_MAP = { 25 | 'title': 'summary', 26 | 'description': 'description', 27 | 'link': 'url', 28 | # author_email, author_name, and author_link need special handling. Consider them the 29 | # 'organizer' of the event and 30 | # construct something based on that. 31 | 'pubdate': 'created', 32 | 'last_modified': 'last-modified', 33 | #'comments' require special handling as well 34 | 'unique_id': 'uid', 35 | 'enclosure': 'attach', # does this need special handling? 36 | 'categories': 'categories', # does this need special handling? 37 | # ttl is ignored. 38 | 'start': 'dtstart', 39 | 'end': 'dtend', 40 | } 41 | 42 | 43 | class ICalendarFeed(SyndicationFeed): 44 | mime_type = 'text/calendar' 45 | 46 | def add_item(self, *args, **kwargs): 47 | for kwarg in ['start', 'end', 'last_modified', 'location']: 48 | kwargs.setdefault(kwarg, None) 49 | super(ICalendarFeed, self).add_item(*args, **kwargs) 50 | 51 | def write(self, outfile, encoding): 52 | # TODO: Use encoding... how? Just convert all values when setting them should work... 53 | cal = vobject.iCalendar() 54 | 55 | # IE/Outlook needs this. See 56 | # 57 | cal.add('method').value = 'PUBLISH' 58 | 59 | for key, val in self.feed.items(): 60 | if key in FEED_ICAL_MAP and val: 61 | cal.add(FEED_ICAL_MAP[key]).value = val 62 | 63 | for item in self.items: 64 | # TODO: handle multiple types of events. 65 | event = cal.add('vevent') 66 | for key, val in item.items(): 67 | #TODO: handle the non-standard items like comments and author. 68 | if key in ITEM_ICAL_MAP and val: 69 | event.add(ITEM_ICAL_MAP[key]).value = val 70 | 71 | cal.serialize(outfile) 72 | 73 | # Some special handling for HttpResponses. See link above. 74 | if isinstance(outfile, HttpResponse): 75 | filename = self.feed.get('filename', 'filename.ics') 76 | outfile['Filename'] = filename 77 | outfile['Content-Disposition'] = 'attachment; filename=%s' % filename -------------------------------------------------------------------------------- /philo/contrib/julian/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/julian/migrations/__init__.py -------------------------------------------------------------------------------- /philo/contrib/penfield/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/penfield/__init__.py -------------------------------------------------------------------------------- /philo/contrib/penfield/admin.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib import admin 3 | from django.core.urlresolvers import reverse 4 | from django.http import HttpResponseRedirect, QueryDict 5 | 6 | from philo.admin import EntityAdmin, COLLAPSE_CLASSES 7 | from philo.admin.widgets import EmbedWidget 8 | from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView 9 | from philo.models.fields import TemplateField 10 | 11 | 12 | class DelayedDateForm(forms.ModelForm): 13 | date_field = 'date' 14 | 15 | def __init__(self, *args, **kwargs): 16 | super(DelayedDateForm, self).__init__(*args, **kwargs) 17 | self.fields[self.date_field].required = False 18 | 19 | 20 | class BlogAdmin(EntityAdmin): 21 | prepopulated_fields = {'slug': ('title',)} 22 | list_display = ('title', 'slug') 23 | 24 | 25 | class BlogEntryAdmin(EntityAdmin): 26 | form = DelayedDateForm 27 | list_filter = ['author', 'blog'] 28 | date_hierarchy = 'date' 29 | search_fields = ('content',) 30 | list_display = ['title', 'date', 'author'] 31 | raw_id_fields = ('author',) 32 | fieldsets = ( 33 | (None, { 34 | 'fields': ('title', 'author', 'blog') 35 | }), 36 | ('Content', { 37 | 'fields': ('content', 'excerpt', 'tags'), 38 | }), 39 | ('Advanced', { 40 | 'fields': ('slug', 'date'), 41 | 'classes': COLLAPSE_CLASSES 42 | }) 43 | ) 44 | related_lookup_fields = {'fk': raw_id_fields} 45 | prepopulated_fields = {'slug': ('title',)} 46 | formfield_overrides = { 47 | TemplateField: {'widget': EmbedWidget} 48 | } 49 | 50 | 51 | class BlogViewAdmin(EntityAdmin): 52 | fieldsets = ( 53 | (None, { 54 | 'fields': ('blog',) 55 | }), 56 | ('Pages', { 57 | 'fields': ('index_page', 'entry_page', 'tag_page') 58 | }), 59 | ('Archive Pages', { 60 | 'fields': ('entry_archive_page', 'tag_archive_page') 61 | }), 62 | ('General Settings', { 63 | 'fields': ('entry_permalink_style', 'entry_permalink_base', 'tag_permalink_base', 'entries_per_page'), 64 | 'classes': COLLAPSE_CLASSES 65 | }), 66 | ('Feed Settings', { 67 | 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'feed_length', 'item_title_template', 'item_description_template',), 68 | 'classes': COLLAPSE_CLASSES 69 | }) 70 | ) 71 | raw_id_fields = ('index_page', 'entry_page', 'tag_page', 'entry_archive_page', 'tag_archive_page', 'item_title_template', 'item_description_template',) 72 | related_lookup_fields = {'fk': raw_id_fields} 73 | 74 | 75 | class NewsletterAdmin(EntityAdmin): 76 | prepopulated_fields = {'slug': ('title',)} 77 | list_display = ('title', 'slug') 78 | 79 | 80 | class NewsletterArticleAdmin(EntityAdmin): 81 | form = DelayedDateForm 82 | filter_horizontal = ('authors',) 83 | list_filter = ('newsletter',) 84 | date_hierarchy = 'date' 85 | search_fields = ('title', 'authors__name',) 86 | list_display = ['title', 'date', 'author_names'] 87 | fieldsets = ( 88 | (None, { 89 | 'fields': ('title', 'authors', 'newsletter') 90 | }), 91 | ('Content', { 92 | 'fields': ('full_text', 'lede', 'tags') 93 | }), 94 | ('Advanced', { 95 | 'fields': ('slug', 'date'), 96 | 'classes': COLLAPSE_CLASSES 97 | }) 98 | ) 99 | actions = ['make_issue'] 100 | prepopulated_fields = {'slug': ('title',)} 101 | formfield_overrides = { 102 | TemplateField: {'widget': EmbedWidget} 103 | } 104 | 105 | def author_names(self, obj): 106 | return ', '.join([author.get_full_name() for author in obj.authors.all()]) 107 | author_names.short_description = "Authors" 108 | 109 | def make_issue(self, request, queryset): 110 | opts = NewsletterIssue._meta 111 | info = opts.app_label, opts.module_name 112 | url = reverse("admin:%s_%s_add" % info) 113 | return HttpResponseRedirect("%s?articles=%s" % (url, ",".join([str(a.pk) for a in queryset]))) 114 | make_issue.short_description = u"Create issue from selected %(verbose_name_plural)s" 115 | 116 | 117 | class NewsletterIssueAdmin(EntityAdmin): 118 | filter_horizontal = ('articles',) 119 | prepopulated_fields = {'slug': ('title',)} 120 | list_display = ('title', 'slug') 121 | 122 | 123 | class NewsletterViewAdmin(EntityAdmin): 124 | fieldsets = ( 125 | (None, { 126 | 'fields': ('newsletter',) 127 | }), 128 | ('Pages', { 129 | 'fields': ('index_page', 'article_page', 'issue_page') 130 | }), 131 | ('Archive Pages', { 132 | 'fields': ('article_archive_page', 'issue_archive_page') 133 | }), 134 | ('Permalinks', { 135 | 'fields': ('article_permalink_style', 'article_permalink_base', 'issue_permalink_base'), 136 | 'classes': COLLAPSE_CLASSES 137 | }), 138 | ('Feeds', { 139 | 'fields': ( 'feeds_enabled', 'feed_suffix', 'feed_type', 'feed_length', 'item_title_template', 'item_description_template',), 140 | 'classes': COLLAPSE_CLASSES 141 | }) 142 | ) 143 | raw_id_fields = ('index_page', 'article_page', 'issue_page', 'article_archive_page', 'issue_archive_page', 'item_title_template', 'item_description_template',) 144 | related_lookup_fields = {'fk': raw_id_fields} 145 | 146 | 147 | admin.site.register(Blog, BlogAdmin) 148 | admin.site.register(BlogEntry, BlogEntryAdmin) 149 | admin.site.register(BlogView, BlogViewAdmin) 150 | admin.site.register(Newsletter, NewsletterAdmin) 151 | admin.site.register(NewsletterArticle, NewsletterArticleAdmin) 152 | admin.site.register(NewsletterIssue, NewsletterIssueAdmin) 153 | admin.site.register(NewsletterView, NewsletterViewAdmin) -------------------------------------------------------------------------------- /philo/contrib/penfield/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/penfield/migrations/__init__.py -------------------------------------------------------------------------------- /philo/contrib/penfield/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/penfield/templatetags/__init__.py -------------------------------------------------------------------------------- /philo/contrib/penfield/templatetags/penfield.py: -------------------------------------------------------------------------------- 1 | """ 2 | Penfield supplies two template filters to handle common use cases for blogs and newsletters. 3 | 4 | """ 5 | from django import template 6 | from django.utils.dates import MONTHS, MONTHS_AP 7 | 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.filter 13 | def monthname(value): 14 | """Returns the name of a month with the supplied numeric value.""" 15 | try: 16 | value = int(value) 17 | except: 18 | pass 19 | 20 | try: 21 | return MONTHS[value] 22 | except KeyError: 23 | return value 24 | 25 | 26 | @register.filter 27 | def apmonthname(value): 28 | """Returns the Associated Press abbreviated month name for the supplied numeric value.""" 29 | try: 30 | value = int(value) 31 | except: 32 | pass 33 | 34 | try: 35 | return MONTHS_AP[value] 36 | except KeyError: 37 | return value -------------------------------------------------------------------------------- /philo/contrib/shipherd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/shipherd/__init__.py -------------------------------------------------------------------------------- /philo/contrib/shipherd/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from philo.admin import TreeEntityAdmin, COLLAPSE_CLASSES, NodeAdmin, EntityAdmin 4 | from philo.models import Node 5 | from philo.contrib.shipherd.models import NavigationItem, Navigation 6 | 7 | 8 | NAVIGATION_RAW_ID_FIELDS = ('navigation', 'parent', 'target_node') 9 | 10 | 11 | class NavigationItemInline(admin.StackedInline): 12 | raw_id_fields = NAVIGATION_RAW_ID_FIELDS 13 | model = NavigationItem 14 | extra = 0 15 | sortable_field_name = 'order' 16 | ordering = ('order',) 17 | related_lookup_fields = {'fk': raw_id_fields} 18 | 19 | 20 | class NavigationItemChildInline(NavigationItemInline): 21 | verbose_name = "child" 22 | verbose_name_plural = "children" 23 | fieldsets = ( 24 | (None, { 25 | 'fields': ('text', 'parent') 26 | }), 27 | ('Target', { 28 | 'fields': ('target_node', 'url_or_subpath',) 29 | }), 30 | ('Advanced', { 31 | 'fields': ('reversing_parameters', 'order'), 32 | 'classes': COLLAPSE_CLASSES 33 | }) 34 | ) 35 | 36 | 37 | class NavigationNavigationItemInline(NavigationItemInline): 38 | fieldsets = ( 39 | (None, { 40 | 'fields': ('text', 'navigation') 41 | }), 42 | ('Target', { 43 | 'fields': ('target_node', 'url_or_subpath',) 44 | }), 45 | ('Advanced', { 46 | 'fields': ('reversing_parameters', 'order'), 47 | 'classes': COLLAPSE_CLASSES 48 | }) 49 | ) 50 | 51 | 52 | class NodeNavigationItemInline(NavigationItemInline): 53 | verbose_name_plural = 'targeting navigation' 54 | fieldsets = ( 55 | (None, { 56 | 'fields': ('text',) 57 | }), 58 | ('Target', { 59 | 'fields': ('target_node', 'url_or_subpath',) 60 | }), 61 | ('Advanced', { 62 | 'fields': ('reversing_parameters', 'order'), 63 | 'classes': COLLAPSE_CLASSES 64 | }), 65 | ('Expert', { 66 | 'fields': ('parent', 'navigation') 67 | }), 68 | ) 69 | 70 | 71 | class NodeNavigationInline(admin.TabularInline): 72 | model = Navigation 73 | extra = 0 74 | 75 | 76 | NodeAdmin.inlines = [NodeNavigationInline, NodeNavigationItemInline] + NodeAdmin.inlines 77 | 78 | 79 | class NavigationItemAdmin(TreeEntityAdmin): 80 | list_display = ('__unicode__', 'target_node', 'url_or_subpath', 'reversing_parameters') 81 | fieldsets = ( 82 | (None, { 83 | 'fields': ('text', 'navigation',) 84 | }), 85 | ('Target', { 86 | 'fields': ('target_node', 'url_or_subpath',) 87 | }), 88 | ('Advanced', { 89 | 'fields': ('reversing_parameters',), 90 | 'classes': COLLAPSE_CLASSES 91 | }), 92 | ('Expert', { 93 | 'fields': ('parent', 'order'), 94 | 'classes': COLLAPSE_CLASSES 95 | }) 96 | ) 97 | raw_id_fields = NAVIGATION_RAW_ID_FIELDS 98 | related_lookup_fields = {'fk': raw_id_fields} 99 | inlines = [NavigationItemChildInline] + TreeEntityAdmin.inlines 100 | 101 | 102 | class NavigationAdmin(EntityAdmin): 103 | inlines = [NavigationNavigationItemInline] 104 | raw_id_fields = ['node'] 105 | related_lookup_fields = {'fk': raw_id_fields} 106 | 107 | 108 | admin.site.unregister(Node) 109 | admin.site.register(Node, NodeAdmin) 110 | admin.site.register(Navigation, NavigationAdmin) 111 | admin.site.register(NavigationItem, NavigationItemAdmin) -------------------------------------------------------------------------------- /philo/contrib/shipherd/migrations/0002_auto.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding index on 'Navigation', fields ['key'] 12 | db.create_index('shipherd_navigation', ['key']) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Removing index on 'Navigation', fields ['key'] 18 | db.delete_index('shipherd_navigation', ['key']) 19 | 20 | 21 | models = { 22 | 'contenttypes.contenttype': { 23 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 24 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 25 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 28 | }, 29 | 'philo.attribute': { 30 | 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, 31 | 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), 32 | 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 33 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 34 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), 35 | 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), 36 | 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) 37 | }, 38 | 'philo.node': { 39 | 'Meta': {'object_name': 'Node'}, 40 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 41 | 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 42 | 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 43 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), 44 | 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 45 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), 46 | 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 47 | 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'node_view_set'", 'to': "orm['contenttypes.ContentType']"}), 48 | 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) 49 | }, 50 | 'shipherd.navigation': { 51 | 'Meta': {'unique_together': "(('node', 'key'),)", 'object_name': 'Navigation'}, 52 | 'depth': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '3'}), 53 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 54 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), 55 | 'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'navigation_set'", 'to': "orm['philo.Node']"}) 56 | }, 57 | 'shipherd.navigationitem': { 58 | 'Meta': {'object_name': 'NavigationItem'}, 59 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 61 | 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 62 | 'navigation': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'roots'", 'null': 'True', 'to': "orm['shipherd.Navigation']"}), 63 | 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}), 64 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['shipherd.NavigationItem']"}), 65 | 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}), 66 | 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 67 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), 68 | 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shipherd_navigationitem_related'", 'null': 'True', 'to': "orm['philo.Node']"}), 69 | 'text': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 70 | 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 71 | 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) 72 | } 73 | } 74 | 75 | complete_apps = ['shipherd'] 76 | -------------------------------------------------------------------------------- /philo/contrib/shipherd/migrations/0003_auto__del_field_navigationitem_slug.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Deleting field 'NavigationItem.slug' 12 | db.delete_column('shipherd_navigationitem', 'slug') 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # User chose to not deal with backwards NULL issues for 'NavigationItem.slug' 18 | raise RuntimeError("Cannot reverse this migration. 'NavigationItem.slug' and its values cannot be restored.") 19 | 20 | 21 | models = { 22 | 'contenttypes.contenttype': { 23 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 24 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 25 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 28 | }, 29 | 'philo.attribute': { 30 | 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, 31 | 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), 32 | 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 33 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 34 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), 35 | 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), 36 | 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) 37 | }, 38 | 'philo.node': { 39 | 'Meta': {'object_name': 'Node'}, 40 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 41 | 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 42 | 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 43 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), 44 | 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 45 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), 46 | 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 47 | 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), 48 | 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) 49 | }, 50 | 'shipherd.navigation': { 51 | 'Meta': {'unique_together': "(('node', 'key'),)", 'object_name': 'Navigation'}, 52 | 'depth': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '3'}), 53 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 54 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), 55 | 'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'navigation_set'", 'to': "orm['philo.Node']"}) 56 | }, 57 | 'shipherd.navigationitem': { 58 | 'Meta': {'object_name': 'NavigationItem'}, 59 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 61 | 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 62 | 'navigation': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'roots'", 'null': 'True', 'to': "orm['shipherd.Navigation']"}), 63 | 'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}), 64 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['shipherd.NavigationItem']"}), 65 | 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}), 66 | 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 67 | 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shipherd_navigationitem_related'", 'null': 'True', 'to': "orm['philo.Node']"}), 68 | 'text': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 69 | 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), 70 | 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) 71 | } 72 | } 73 | 74 | complete_apps = ['shipherd'] 75 | -------------------------------------------------------------------------------- /philo/contrib/shipherd/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/shipherd/migrations/__init__.py -------------------------------------------------------------------------------- /philo/contrib/shipherd/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/shipherd/templatetags/__init__.py -------------------------------------------------------------------------------- /philo/contrib/shipherd/templatetags/shipherd.py: -------------------------------------------------------------------------------- 1 | from django import template, VERSION as django_version 2 | from django.conf import settings 3 | from django.utils.safestring import mark_safe 4 | from philo.contrib.shipherd.models import Navigation 5 | from philo.models import Node 6 | from django.utils.safestring import mark_safe 7 | from django.utils.translation import ugettext as _ 8 | 9 | 10 | register = template.Library() 11 | 12 | 13 | class LazyNavigationRecurser(object): 14 | def __init__(self, template_nodes, items, context, request): 15 | self.template_nodes = template_nodes 16 | self.items = items 17 | self.context = context 18 | self.request = request 19 | 20 | def __call__(self): 21 | items = self.items 22 | context = self.context 23 | request = self.request 24 | 25 | if not items: 26 | return '' 27 | 28 | if 'navloop' in context: 29 | parentloop = context['navloop'] 30 | else: 31 | parentloop = {} 32 | context.push() 33 | 34 | depth = items[0].get_level() 35 | len_items = len(items) 36 | 37 | loop_dict = context['navloop'] = { 38 | 'parentloop': parentloop, 39 | 'depth': depth + 1, 40 | 'depth0': depth 41 | } 42 | 43 | bits = [] 44 | 45 | for i, item in enumerate(items): 46 | # First set context variables. 47 | loop_dict['counter0'] = i 48 | loop_dict['counter'] = i + 1 49 | loop_dict['revcounter'] = len_items - i 50 | loop_dict['revcounter0'] = len_items - i - 1 51 | loop_dict['first'] = (i == 0) 52 | loop_dict['last'] = (i == len_items - 1) 53 | 54 | # Set on loop_dict and context for backwards-compatibility. 55 | # Eventually only allow access through the loop_dict. 56 | loop_dict['active'] = context['active'] = item.is_active(request) 57 | loop_dict['active_descendants'] = context['active_descendants'] = item.has_active_descendants(request) 58 | 59 | # Set these directly in the context for easy access. 60 | context['item'] = item 61 | context['children'] = self.__class__(self.template_nodes, item.get_children(), context, request) 62 | 63 | # Then render the nodelist bit by bit. 64 | for node in self.template_nodes: 65 | bits.append(node.render(context)) 66 | context.pop() 67 | return mark_safe(''.join(bits)) 68 | 69 | 70 | class RecurseNavigationNode(template.Node): 71 | def __init__(self, template_nodes, instance_var, key_var): 72 | self.template_nodes = template_nodes 73 | self.instance_var = instance_var 74 | self.key_var = key_var 75 | 76 | def render(self, context): 77 | try: 78 | request = context['request'] 79 | except KeyError: 80 | return '' 81 | 82 | instance = self.instance_var.resolve(context) 83 | key = self.key_var.resolve(context) 84 | 85 | # Fall back on old behavior if the key doesn't seem to be a variable. 86 | if not key: 87 | token = self.key_var.token 88 | if token[0] not in ["'", '"'] and '.' not in token: 89 | key = token 90 | else: 91 | return settings.TEMPLATE_STRING_IF_INVALID 92 | 93 | try: 94 | items = instance.navigation[key] 95 | except: 96 | return settings.TEMPLATE_STRING_IF_INVALID 97 | 98 | return LazyNavigationRecurser(self.template_nodes, items, context, request)() 99 | 100 | 101 | @register.tag 102 | def recursenavigation(parser, token): 103 | """ 104 | The :ttag:`recursenavigation` templatetag takes two arguments: 105 | 106 | * the :class:`.Node` for which the :class:`.Navigation` should be found 107 | * the :class:`.Navigation`'s :attr:`~.Navigation.key`. 108 | 109 | It will then recursively loop over each :class:`.NavigationItem` in the :class:`.Navigation` and render the template 110 | chunk within the block. :ttag:`recursenavigation` sets the following variables in the context: 111 | 112 | ============================== ================================================ 113 | Variable Description 114 | ============================== ================================================ 115 | ``navloop.depth`` The current depth of the loop (1 is the top level) 116 | ``navloop.depth0`` The current depth of the loop (0 is the top level) 117 | ``navloop.counter`` The current iteration of the current level(1-indexed) 118 | ``navloop.counter0`` The current iteration of the current level(0-indexed) 119 | ``navloop.first`` True if this is the first time through the current level 120 | ``navloop.last`` True if this is the last time through the current level 121 | ``navloop.parentloop`` This is the loop one level "above" the current one 122 | 123 | ``item`` The current item in the loop (a :class:`.NavigationItem` instance) 124 | ``children`` If accessed, performs the next level of recursion. 125 | ``navloop.active`` True if the item is active for this request 126 | ``navloop.active_descendants`` True if the item has active descendants for this request 127 | ============================== ================================================ 128 | 129 | Example:: 130 | 131 |
    132 | {% recursenavigation node "main" %} 133 | 134 | {{ item.text }} 135 | {% if item.get_children %} 136 |
      137 | {{ children }} 138 |
    139 | {% endif %} 140 | 141 | {% endrecursenavigation %} 142 |
143 | 144 | .. 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. 145 | 146 | .. _request context processor: https://docs.djangoproject.com/en/dev/ref/templates/api/#django-core-context-processors-request 147 | 148 | """ 149 | bits = token.contents.split() 150 | if len(bits) != 3: 151 | raise template.TemplateSyntaxError(_('%s tag requires two arguments: a node and a navigation section name') % bits[0]) 152 | 153 | instance_var = parser.compile_filter(bits[1]) 154 | key_var = parser.compile_filter(bits[2]) 155 | 156 | template_nodes = parser.parse(('endrecursenavigation',)) 157 | token = parser.delete_first_token() 158 | return RecurseNavigationNode(template_nodes, instance_var, key_var) 159 | 160 | 161 | @register.filter 162 | def has_navigation(node, key=None): 163 | """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.""" 164 | try: 165 | return bool(node.navigation[key]) 166 | except: 167 | return False 168 | 169 | 170 | @register.filter 171 | def navigation_host(node, key): 172 | """Returns the :class:`.Node` which hosts the :class:`.Navigation` which ``node`` has inherited for ``key``. Returns ``node`` if any exceptions are encountered.""" 173 | try: 174 | return node.navigation[key].node 175 | except: 176 | return node -------------------------------------------------------------------------------- /philo/contrib/sobol/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 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. 3 | 4 | Settings 5 | -------- 6 | 7 | :setting:`SOBOL_USE_CACHE` 8 | Whether sobol will use django's cache framework. Defaults to ``True``; this may cause a lot of entries in the cache. 9 | 10 | :setting:`SOBOL_USE_EVENTLET` 11 | 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``. 12 | 13 | Templates 14 | --------- 15 | 16 | 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 ````) simply do the following:: 17 | 18 | {% include "sobol/search/_list.html" with suppress_scripts=1 %} 19 | 20 | """ 21 | 22 | from philo.contrib.sobol.search import * -------------------------------------------------------------------------------- /philo/contrib/sobol/admin.py: -------------------------------------------------------------------------------- 1 | from functools import update_wrapper 2 | 3 | from django.conf import settings 4 | from django.conf.urls.defaults import patterns, url 5 | from django.contrib import admin 6 | from django.core.urlresolvers import reverse 7 | from django.db.models import Count 8 | from django.http import HttpResponseRedirect, Http404 9 | from django.shortcuts import render_to_response 10 | from django.template import RequestContext 11 | from django.utils.translation import ugettext_lazy as _ 12 | 13 | from philo.admin import EntityAdmin 14 | from philo.contrib.sobol.models import Search, ResultURL, SearchView 15 | 16 | 17 | class ResultURLInline(admin.TabularInline): 18 | model = ResultURL 19 | readonly_fields = ('url',) 20 | can_delete = False 21 | extra = 0 22 | max_num = 0 23 | 24 | 25 | class SearchAdmin(admin.ModelAdmin): 26 | readonly_fields = ('string',) 27 | inlines = [ResultURLInline] 28 | list_display = ['string', 'unique_urls', 'total_clicks'] 29 | search_fields = ['string', 'result_urls__url'] 30 | actions = ['results_action'] 31 | if 'grappelli' in settings.INSTALLED_APPS: 32 | change_form_template = 'admin/sobol/search/grappelli_change_form.html' 33 | 34 | def unique_urls(self, obj): 35 | return obj.unique_urls 36 | unique_urls.admin_order_field = 'unique_urls' 37 | 38 | def total_clicks(self, obj): 39 | return obj.total_clicks 40 | total_clicks.admin_order_field = 'total_clicks' 41 | 42 | def queryset(self, request): 43 | qs = super(SearchAdmin, self).queryset(request) 44 | return qs.annotate(total_clicks=Count('result_urls__clicks', distinct=True), unique_urls=Count('result_urls', distinct=True)) 45 | 46 | 47 | class SearchViewAdmin(EntityAdmin): 48 | raw_id_fields = ('results_page',) 49 | related_lookup_fields = {'fk': raw_id_fields} 50 | 51 | 52 | admin.site.register(Search, SearchAdmin) 53 | admin.site.register(SearchView, SearchViewAdmin) -------------------------------------------------------------------------------- /philo/contrib/sobol/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from philo.contrib.sobol.utils import SEARCH_ARG_GET_KEY 4 | 5 | 6 | class BaseSearchForm(forms.BaseForm): 7 | base_fields = { 8 | SEARCH_ARG_GET_KEY: forms.CharField() 9 | } 10 | 11 | 12 | class SearchForm(forms.Form, BaseSearchForm): 13 | pass -------------------------------------------------------------------------------- /philo/contrib/sobol/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/sobol/migrations/__init__.py -------------------------------------------------------------------------------- /philo/contrib/sobol/static/sobol/ajax_search.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | var sobol = window.sobol = {}; 3 | sobol.favoredResults = [] 4 | sobol.favoredResultSearch = null; 5 | sobol.search = function(){ 6 | var searches = sobol.searches = $('article.search'); 7 | if(sobol.favoredResults.length) sobol.favoredResultSearch = searches.eq(0); 8 | for (var i=sobol.favoredResults.length ? 1 : 0;i" + title + ""; 33 | } else { 34 | rendered += "
" + title + "
"; 35 | } 36 | if(content && content != ''){ 37 | rendered += "
" + content + "
" 38 | } 39 | return rendered 40 | } 41 | sobol.addFavoredResult = function(result) { 42 | var dl = sobol.favoredResultSearch.find('dl'); 43 | if(!dl.length){ 44 | dl = $('
'); 45 | dl.appendTo(sobol.favoredResultSearch); 46 | sobol.favoredResultSearch.removeClass('loading'); 47 | } 48 | dl[0].innerHTML += sobol.renderResult(result) 49 | } 50 | sobol.onSuccess = function(ele, data){ 51 | // hook for success! 52 | ele.removeClass('loading'); 53 | if (data['results'].length) { 54 | ele[0].innerHTML += "
"; 55 | $.each(data['results'], function(i, v){ 56 | ele[0].innerHTML += sobol.renderResult(v); 57 | }) 58 | ele[0].innerHTML += "
"; 59 | if(data['hasMoreResults'] && data['moreResultsURL']) ele[0].innerHTML += ""; 60 | } else { 61 | ele.addClass('empty'); 62 | ele[0].innerHTML += "

No results found.

"; 63 | ele.slideUp(); 64 | } 65 | if (sobol.favoredResultSearch){ 66 | for (var i=0;i"; 79 | }; 80 | $(sobol.search); 81 | }(jQuery)); -------------------------------------------------------------------------------- /philo/contrib/sobol/templates/admin/sobol/search/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load i18n %} 3 | 4 | {% block javascripts %}{% endblock %} 5 | {% block object-tools %}{% endblock %} 6 | {% block title %}Results for "{{ original.string }}" | {% trans 'Django site admin' %}{% endblock %} 7 | {% block content_title %}

Results for "{{ original.string }}"

{% endblock %} 8 | {% block extrastyle %} 9 | 17 | {% endblock %} 18 | 19 | {% block content %} 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for result in original.get_weighted_results %} 30 | 31 | 32 | 33 | 34 | {% endfor %} 35 | 36 |
WeightURL
{{ result.weight }}{{ result.url }}
37 |
38 | {% block submit_row %} 39 |
40 | {% if not is_popup and has_delete_permission %}{% if change or show_delete %}{% endif %}{% endif %} 41 |
42 | {% endblock %} 43 | {% endblock %} -------------------------------------------------------------------------------- /philo/contrib/sobol/templates/admin/sobol/search/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_list.html' %} 2 | 3 | {% block object-tools %}{% endblock %} -------------------------------------------------------------------------------- /philo/contrib/sobol/templates/admin/sobol/search/grappelli_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/sobol/search/change_form.html' %} 2 | {% load i18n %} 3 | 4 | {% block extrastyle %} 5 | 15 | {% endblock %} 16 | 17 | {% block submit_row %} 18 | 23 | {% endblock %} -------------------------------------------------------------------------------- /philo/contrib/sobol/templates/sobol/search/_list.html: -------------------------------------------------------------------------------- 1 | {% with node.view.enable_ajax_api as ajax %} 2 | {% if ajax %} 3 | {% if not suppress_scripts %}{% endif %} 4 | 10 | {% endif %} 11 | {% if favored_results %} 12 | 31 | {% endif %} 32 | {% for search in searches %} 33 |
34 |
35 | 36 |

{{ search }}

37 |
38 | {% if not ajax %} 39 | {% if search.results %} 40 |
41 | {% for result in search.results %} 42 | {{ result }} 43 | {% endfor %} 44 |
45 | {% if search.has_more_results and search.more_results_url %} 46 | 49 | {% endif %} 50 | {% else %} 51 |

No results found.

52 | {% endif %} 53 | {% endif %} 54 |
55 | {% endfor %} 56 | {% endwith %} -------------------------------------------------------------------------------- /philo/contrib/sobol/templates/sobol/search/content.html: -------------------------------------------------------------------------------- 1 | {{ result.content|truncatewords_html:20 }} -------------------------------------------------------------------------------- /philo/contrib/sobol/templates/sobol/search/result.html: -------------------------------------------------------------------------------- 1 |
{% if url %}{% endif %}{{ title }}{% if url %}{% endif %}
2 | {% if content %}
{{ content }}
{% endif %} -------------------------------------------------------------------------------- /philo/contrib/sobol/utils.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha1 2 | 3 | from django.conf import settings 4 | from django.http import QueryDict 5 | from django.utils.encoding import smart_str 6 | from django.utils.http import urlquote_plus, urlquote 7 | 8 | 9 | SEARCH_ARG_GET_KEY = 'q' 10 | URL_REDIRECT_GET_KEY = 'url' 11 | HASH_REDIRECT_GET_KEY = 's' 12 | 13 | 14 | def make_redirect_hash(search_arg, url): 15 | """Hashes a redirect for a ``search_arg`` and ``url`` to avoid providing a simple URL spoofing service.""" 16 | return sha1(smart_str(search_arg + url + settings.SECRET_KEY)).hexdigest()[::2] 17 | 18 | 19 | def check_redirect_hash(hash, search_arg, url): 20 | """Checks whether a hash is valid for a given ``search_arg`` and ``url``.""" 21 | return hash == make_redirect_hash(search_arg, url) 22 | 23 | 24 | def make_tracking_querydict(search_arg, url): 25 | """Returns a :class:`QueryDict` instance containing the information necessary for tracking :class:`.Click`\ s on the ``url``.""" 26 | return QueryDict("%s=%s&%s=%s&%s=%s" % ( 27 | SEARCH_ARG_GET_KEY, urlquote_plus(search_arg), 28 | URL_REDIRECT_GET_KEY, urlquote(url), 29 | HASH_REDIRECT_GET_KEY, make_redirect_hash(search_arg, url)) 30 | ) 31 | 32 | 33 | class RegistryIterator(object): 34 | def __init__(self, registry, iterattr='__iter__', transform=lambda x:x): 35 | if not hasattr(registry, iterattr): 36 | raise AttributeError("Registry has no attribute %s" % iterattr) 37 | self.registry = registry 38 | self.iterattr = iterattr 39 | self.transform = transform 40 | 41 | def __iter__(self): 42 | return self 43 | 44 | def next(self): 45 | if not hasattr(self, '_iter'): 46 | self._iter = getattr(self.registry, self.iterattr)() 47 | 48 | return self.transform(self._iter.next()) 49 | 50 | def copy(self): 51 | return self.__class__(self.registry, self.iterattr, self.transform) -------------------------------------------------------------------------------- /philo/contrib/waldo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/contrib/waldo/__init__.py -------------------------------------------------------------------------------- /philo/contrib/waldo/forms.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django import forms 4 | from django.conf import settings 5 | from django.contrib.auth import authenticate 6 | from django.contrib.auth.forms import AuthenticationForm, UserCreationForm 7 | from django.contrib.auth.models import User 8 | from django.core.exceptions import ValidationError 9 | from django.utils.translation import ugettext_lazy as _ 10 | 11 | from philo.contrib.waldo.tokens import REGISTRATION_TIMEOUT_DAYS 12 | 13 | 14 | class EmailInput(forms.TextInput): 15 | """Displays an HTML5 email input on browsers which support it and a normal text input on other browsers.""" 16 | input_type = 'email' 17 | 18 | 19 | class RegistrationForm(UserCreationForm): 20 | """ 21 | 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. 22 | 23 | .. seealso:: `recaptcha-django `_ 24 | 25 | """ 26 | #: An :class:`EmailField` using the :class:`EmailInput` widget. 27 | email = forms.EmailField(widget=EmailInput) 28 | try: 29 | from recaptcha_django import ReCaptchaField 30 | except ImportError: 31 | pass 32 | else: 33 | if 'recaptcha_django.middleware.ReCaptchaMiddleware' in settings.MIDDLEWARE_CLASSES: 34 | recaptcha = ReCaptchaField() 35 | 36 | def clean_username(self): 37 | username = self.cleaned_data['username'] 38 | 39 | # Trivial case: if the username doesn't exist, go for it! 40 | try: 41 | user = User.objects.get(username=username) 42 | except User.DoesNotExist: 43 | return username 44 | 45 | if not user.is_active and (date.today() - user.date_joined.date()).days > REGISTRATION_TIMEOUT_DAYS and user.last_login == user.date_joined: 46 | # Then this is a user who has not confirmed their registration and whose time is up. Delete the old user and return the username. 47 | user.delete() 48 | return username 49 | 50 | raise ValidationError(_("A user with that username already exists.")) 51 | 52 | def clean_email(self): 53 | if User.objects.filter(email__iexact=self.cleaned_data['email']): 54 | raise ValidationError(_('This email is already in use. Please supply a different email address')) 55 | return self.cleaned_data['email'] 56 | 57 | def save(self): 58 | username = self.cleaned_data['username'] 59 | email = self.cleaned_data['email'] 60 | password = self.cleaned_data['password1'] 61 | new_user = User.objects.create_user(username, email, password) 62 | new_user.is_active = False 63 | new_user.save() 64 | return new_user 65 | 66 | 67 | class UserAccountForm(forms.ModelForm): 68 | """Handles a user's account - by default, :attr:`auth.User.first_name`, :attr:`auth.User.last_name`, :attr:`auth.User.email`.""" 69 | first_name = User._meta.get_field('first_name').formfield(required=True) 70 | last_name = User._meta.get_field('last_name').formfield(required=True) 71 | email = User._meta.get_field('email').formfield(required=True, widget=EmailInput) 72 | 73 | def __init__(self, user, *args, **kwargs): 74 | kwargs['instance'] = user 75 | super(UserAccountForm, self).__init__(*args, **kwargs) 76 | 77 | def email_changed(self): 78 | """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`.""" 79 | return 'email' in self.changed_data 80 | 81 | def reset_email(self): 82 | """ 83 | 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`. 84 | 85 | """ 86 | email = self.instance.email 87 | self.instance.email = self.initial['email'] 88 | self.cleaned_data.pop('email') 89 | return email 90 | 91 | @classmethod 92 | def set_email(cls, user, email): 93 | """ 94 | 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`. 95 | 96 | """ 97 | user.email = email 98 | user.save() 99 | 100 | 101 | class Meta: 102 | model = User 103 | fields = ('first_name', 'last_name', 'email') 104 | 105 | 106 | class WaldoAuthenticationForm(AuthenticationForm): 107 | """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.""" 108 | ERROR_MESSAGE = _("Please enter a correct username and password. Note that both fields are case-sensitive.") 109 | 110 | def clean(self): 111 | username = self.cleaned_data.get('username') 112 | password = self.cleaned_data.get('password') 113 | message = self.ERROR_MESSAGE 114 | 115 | if username and password: 116 | self.user_cache = authenticate(username=username, password=password) 117 | if self.user_cache is None: 118 | if u'@' in username: 119 | # Maybe they entered their email? Look it up, but still raise a ValidationError. 120 | try: 121 | user = User.objects.get(email=username) 122 | except (User.DoesNotExist, User.MultipleObjectsReturned): 123 | pass 124 | else: 125 | if user.check_password(password): 126 | message = _("Your e-mail address is not your username. Try '%s' instead.") % user.username 127 | raise ValidationError(message) 128 | elif not self.user_cache.is_active: 129 | raise ValidationError(message) 130 | self.check_for_test_cookie() 131 | return self.cleaned_data -------------------------------------------------------------------------------- /philo/contrib/waldo/tokens.py: -------------------------------------------------------------------------------- 1 | """ 2 | Based on :mod:`django.contrib.auth.tokens`. Supports the following settings: 3 | 4 | :setting:`WALDO_REGISTRATION_TIMEOUT_DAYS` 5 | The number of days a registration link will be valid before expiring. Default: 1. 6 | 7 | :setting:`WALDO_EMAIL_TIMEOUT_DAYS` 8 | The number of days an email change link will be valid before expiring. Default: 1. 9 | 10 | """ 11 | 12 | from hashlib import sha1 13 | from datetime import date 14 | 15 | from django.conf import settings 16 | from django.utils.http import int_to_base36, base36_to_int 17 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 18 | 19 | 20 | REGISTRATION_TIMEOUT_DAYS = getattr(settings, 'WALDO_REGISTRATION_TIMEOUT_DAYS', 1) 21 | EMAIL_TIMEOUT_DAYS = getattr(settings, 'WALDO_EMAIL_TIMEOUT_DAYS', 1) 22 | 23 | 24 | class RegistrationTokenGenerator(PasswordResetTokenGenerator): 25 | """Strategy object used to generate and check tokens for the user registration mechanism.""" 26 | 27 | def check_token(self, user, token): 28 | """Check that a registration token is correct for a given user.""" 29 | # If the user is active, the hash can't be valid. 30 | if user.is_active: 31 | return False 32 | 33 | # Parse the token 34 | try: 35 | ts_b36, hash = token.split('-') 36 | except ValueError: 37 | return False 38 | 39 | try: 40 | ts = base36_to_int(ts_b36) 41 | except ValueError: 42 | return False 43 | 44 | # Check that the timestamp and uid have not been tampered with. 45 | if self._make_token_with_timestamp(user, ts) != token: 46 | return False 47 | 48 | # Check that the timestamp is within limit 49 | if (self._num_days(self._today()) - ts) > REGISTRATION_TIMEOUT_DAYS: 50 | return False 51 | 52 | return True 53 | 54 | def _make_token_with_timestamp(self, user, timestamp): 55 | ts_b36 = int_to_base36(timestamp) 56 | 57 | # By hashing on the internal state of the user and using state that is 58 | # sure to change, we produce a hash that will be invalid as soon as it 59 | # is used. 60 | hash = sha1(settings.SECRET_KEY + unicode(user.id) + unicode(user.is_active) + user.last_login.strftime('%Y-%m-%d %H:%M:%S') + unicode(timestamp)).hexdigest()[::2] 61 | return '%s-%s' % (ts_b36, hash) 62 | 63 | 64 | registration_token_generator = RegistrationTokenGenerator() 65 | 66 | 67 | class EmailTokenGenerator(PasswordResetTokenGenerator): 68 | """Strategy object used to generate and check tokens for a user email change mechanism.""" 69 | 70 | def make_token(self, user, email): 71 | """Returns a token that can be used once to do an email change for the given user and email.""" 72 | return self._make_token_with_timestamp(user, email, self._num_days(self._today())) 73 | 74 | def check_token(self, user, email, token): 75 | if email == user.email: 76 | return False 77 | 78 | # Parse the token 79 | try: 80 | ts_b36, hash = token.split('-') 81 | except ValueError: 82 | return False 83 | 84 | try: 85 | ts = base36_to_int(ts_b36) 86 | except ValueError: 87 | return False 88 | 89 | # Check that the timestamp and uid have not been tampered with. 90 | if self._make_token_with_timestamp(user, email, ts) != token: 91 | return False 92 | 93 | # Check that the timestamp is within limit 94 | if (self._num_days(self._today()) - ts) > EMAIL_TIMEOUT_DAYS: 95 | return False 96 | 97 | return True 98 | 99 | def _make_token_with_timestamp(self, user, email, timestamp): 100 | ts_b36 = int_to_base36(timestamp) 101 | 102 | hash = sha1(settings.SECRET_KEY + unicode(user.id) + user.email + email + unicode(timestamp)).hexdigest()[::2] 103 | return '%s-%s' % (ts_b36, hash) 104 | 105 | 106 | email_token_generator = EmailTokenGenerator() -------------------------------------------------------------------------------- /philo/contrib/winer/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Winer provides the same API as `django's syndication Feed class `_, adapted to a Philo-style :class:`~philo.models.nodes.MultiView` for easy database management. Apps that need syndication can simply subclass :class:`~philo.contrib.winer.models.FeedView`, override a few methods, and start serving RSS and Atom feeds. See :class:`~philo.contrib.penfield.models.BlogView` for a concrete implementation example. 3 | 4 | """ -------------------------------------------------------------------------------- /philo/contrib/winer/exceptions.py: -------------------------------------------------------------------------------- 1 | class HttpNotAcceptable(Exception): 2 | """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.""" 3 | pass -------------------------------------------------------------------------------- /philo/contrib/winer/feeds.py: -------------------------------------------------------------------------------- 1 | from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed 2 | 3 | from philo.utils.registry import Registry 4 | 5 | 6 | DEFAULT_FEED = Atom1Feed 7 | 8 | 9 | registry = Registry() 10 | 11 | 12 | registry.register(Atom1Feed, verbose_name='Atom') 13 | registry.register(Rss201rev2Feed, verbose_name='RSS') -------------------------------------------------------------------------------- /philo/contrib/winer/middleware.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.utils.decorators import decorator_from_middleware 3 | 4 | from philo.contrib.winer.exceptions import HttpNotAcceptable 5 | 6 | 7 | class HttpNotAcceptableMiddleware(object): 8 | """Middleware to catch :exc:`~philo.contrib.winer.exceptions.HttpNotAcceptable` and return an :class:`HttpResponse` with a 406 response code. See :rfc:`2616`.""" 9 | def process_exception(self, request, exception): 10 | if isinstance(exception, HttpNotAcceptable): 11 | return HttpResponse(status=406) 12 | 13 | 14 | http_not_acceptable = decorator_from_middleware(HttpNotAcceptableMiddleware) -------------------------------------------------------------------------------- /philo/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | 3 | 4 | #: 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`. 5 | MIDDLEWARE_NOT_CONFIGURED = ImproperlyConfigured("""Philo requires the RequestNode middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'philo.middleware.RequestNodeMiddleware'.""") 6 | 7 | 8 | class ViewDoesNotProvideSubpaths(Exception): 9 | """Raised by :meth:`.View.reverse` when the :class:`.View` does not provide subpaths (the default).""" 10 | silent_variable_failure = True 11 | 12 | 13 | class ViewCanNotProvideSubpath(Exception): 14 | """Raised by :meth:`.View.reverse` when the :class:`.View` can not provide a subpath for the supplied arguments.""" 15 | silent_variable_failure = True 16 | 17 | 18 | class AncestorDoesNotExist(Exception): 19 | """Raised by :meth:`.TreeEntity.get_path` if the root instance is not an ancestor of the current instance.""" 20 | pass -------------------------------------------------------------------------------- /philo/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from philo.forms.fields import * 2 | from philo.forms.entities import * -------------------------------------------------------------------------------- /philo/forms/entities.py: -------------------------------------------------------------------------------- 1 | from django.forms.models import ModelFormMetaclass, ModelForm, ModelFormOptions 2 | from django.utils.datastructures import SortedDict 3 | 4 | from philo.utils import fattr 5 | 6 | 7 | __all__ = ('EntityForm',) 8 | 9 | 10 | def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=None): 11 | field_list = [] 12 | ignored = [] 13 | opts = entity_model._entity_meta 14 | for f in opts.proxy_fields: 15 | if not f.editable: 16 | continue 17 | if fields and not f.name in fields: 18 | continue 19 | if exclude and f.name in exclude: 20 | continue 21 | if widgets and f.name in widgets: 22 | kwargs = {'widget': widgets[f.name]} 23 | else: 24 | kwargs = {} 25 | 26 | if formfield_callback is None: 27 | formfield = f.formfield(**kwargs) 28 | elif not callable(formfield_callback): 29 | raise TypeError('formfield_callback must be a function or callable') 30 | else: 31 | formfield = formfield_callback(f, **kwargs) 32 | 33 | if formfield: 34 | field_list.append((f.name, formfield)) 35 | else: 36 | ignored.append(f.name) 37 | field_dict = SortedDict(field_list) 38 | if fields: 39 | field_dict = SortedDict( 40 | [(f, field_dict.get(f)) for f in fields 41 | if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)] 42 | ) 43 | return field_dict 44 | 45 | 46 | # HACK until http://code.djangoproject.com/ticket/14082 is resolved. 47 | _old = ModelFormMetaclass.__new__ 48 | def _new(cls, name, bases, attrs): 49 | if cls == ModelFormMetaclass: 50 | m = attrs.get('__metaclass__', None) 51 | if m is None: 52 | parents = [b for b in bases if issubclass(b, ModelForm)] 53 | for c in parents: 54 | if c.__metaclass__ != ModelFormMetaclass: 55 | m = c.__metaclass__ 56 | break 57 | 58 | if m is not None: 59 | return m(name, bases, attrs) 60 | 61 | return _old(cls, name, bases, attrs) 62 | ModelFormMetaclass.__new__ = staticmethod(_new) 63 | # END HACK 64 | 65 | 66 | class EntityFormMetaclass(ModelFormMetaclass): 67 | def __new__(cls, name, bases, attrs): 68 | try: 69 | parents = [b for b in bases if issubclass(b, EntityForm)] 70 | except NameError: 71 | # We are defining EntityForm itself 72 | parents = None 73 | sup = super(EntityFormMetaclass, cls) 74 | 75 | if not parents: 76 | # Then there's no business trying to use proxy fields. 77 | return sup.__new__(cls, name, bases, attrs) 78 | 79 | # Fake a declaration of all proxy fields so they'll be handled correctly. 80 | opts = ModelFormOptions(attrs.get('Meta', None)) 81 | 82 | if opts.model: 83 | formfield_callback = attrs.get('formfield_callback', None) 84 | proxy_fields = proxy_fields_for_entity_model(opts.model, opts.fields, opts.exclude, opts.widgets, formfield_callback) 85 | else: 86 | proxy_fields = {} 87 | 88 | new_attrs = proxy_fields.copy() 89 | new_attrs.update(attrs) 90 | 91 | new_class = sup.__new__(cls, name, bases, new_attrs) 92 | new_class.proxy_fields = proxy_fields 93 | return new_class 94 | 95 | 96 | class EntityForm(ModelForm): 97 | """ 98 | :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. 99 | 100 | """ 101 | __metaclass__ = EntityFormMetaclass 102 | 103 | def __init__(self, *args, **kwargs): 104 | initial = kwargs.pop('initial', None) 105 | instance = kwargs.get('instance', None) 106 | if instance is not None: 107 | new_initial = {} 108 | for f in instance._entity_meta.proxy_fields: 109 | if self._meta.fields and not f.name in self._meta.fields: 110 | continue 111 | if self._meta.exclude and f.name in self._meta.exclude: 112 | continue 113 | new_initial[f.name] = f.value_from_object(instance) 114 | else: 115 | new_initial = {} 116 | if initial is not None: 117 | new_initial.update(initial) 118 | kwargs['initial'] = new_initial 119 | super(EntityForm, self).__init__(*args, **kwargs) 120 | 121 | @fattr(alters_data=True) 122 | def save(self, commit=True): 123 | cleaned_data = self.cleaned_data 124 | instance = super(EntityForm, self).save(commit=False) 125 | 126 | for f in instance._entity_meta.proxy_fields: 127 | if not f.editable or not f.name in cleaned_data: 128 | continue 129 | if self._meta.fields and f.name not in self._meta.fields: 130 | continue 131 | if self._meta.exclude and f.name in self._meta.exclude: 132 | continue 133 | setattr(instance, f.attname, f.get_storage_value(cleaned_data[f.name])) 134 | 135 | if commit: 136 | instance.save() 137 | self.save_m2m() 138 | 139 | return instance -------------------------------------------------------------------------------- /philo/forms/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ValidationError 3 | from django.utils import simplejson as json 4 | 5 | from philo.validators import json_validator 6 | 7 | 8 | __all__ = ('JSONFormField',) 9 | 10 | 11 | class JSONFormField(forms.Field): 12 | """A form field which is validated by :func:`philo.validators.json_validator`.""" 13 | default_validators = [json_validator] 14 | 15 | def clean(self, value): 16 | if value == '' and not self.required: 17 | return None 18 | try: 19 | return json.loads(value) 20 | except Exception, e: 21 | raise ValidationError(u'JSON decode error: %s' % e) -------------------------------------------------------------------------------- /philo/loaders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/loaders/__init__.py -------------------------------------------------------------------------------- /philo/loaders/database.py: -------------------------------------------------------------------------------- 1 | from django.template import TemplateDoesNotExist 2 | from django.template.loader import BaseLoader 3 | from django.utils.encoding import smart_unicode 4 | 5 | from philo.models import Template 6 | 7 | 8 | class Loader(BaseLoader): 9 | """ 10 | :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. 11 | 12 | """ 13 | is_usable=True 14 | 15 | def load_template_source(self, template_name, template_dirs=None): 16 | try: 17 | template = Template.objects.get_with_path(template_name) 18 | except Template.DoesNotExist: 19 | raise TemplateDoesNotExist(template_name) 20 | return (template.code, smart_unicode(template)) -------------------------------------------------------------------------------- /philo/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.sites.models import Site 3 | from django.http import Http404 4 | 5 | from philo.models import Node, View 6 | from philo.utils.lazycompat import SimpleLazyObject 7 | 8 | 9 | def get_node(path): 10 | """Returns a :class:`Node` instance at ``path`` (relative to the current site) or ``None``.""" 11 | try: 12 | current_site = Site.objects.get_current() 13 | except Site.DoesNotExist: 14 | current_site = None 15 | 16 | trailing_slash = False 17 | if path[-1] == '/': 18 | trailing_slash = True 19 | 20 | try: 21 | node, subpath = Node.objects.get_with_path(path, root=getattr(current_site, 'root_node', None), absolute_result=False) 22 | except Node.DoesNotExist: 23 | return None 24 | 25 | if subpath is None: 26 | subpath = "" 27 | subpath = "/" + subpath 28 | 29 | if trailing_slash and subpath[-1] != "/": 30 | subpath += "/" 31 | 32 | node._path = path 33 | node._subpath = subpath 34 | 35 | return node 36 | 37 | 38 | class RequestNodeMiddleware(object): 39 | """ 40 | 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`. 41 | 42 | :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. 43 | 44 | """ 45 | def process_view(self, request, view_func, view_args, view_kwargs): 46 | try: 47 | path = view_kwargs['path'] 48 | except KeyError: 49 | request.node = None 50 | else: 51 | request.node = SimpleLazyObject(lambda: get_node(path)) 52 | 53 | def process_exception(self, request, exception): 54 | if settings.DEBUG or not hasattr(request, 'node') or not request.node: 55 | return 56 | 57 | if isinstance(exception, Http404): 58 | error_view = request.node.attributes.get('Http404', None) 59 | status_code = 404 60 | else: 61 | error_view = request.node.attributes.get('Http500', None) 62 | status_code = 500 63 | 64 | if error_view is None or not isinstance(error_view, View): 65 | # Should this be duck-typing? Perhaps even no testing? 66 | return 67 | 68 | extra_context = {'exception': exception} 69 | response = error_view.render_to_response(request, extra_context) 70 | response.status_code = status_code 71 | return response -------------------------------------------------------------------------------- /philo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | from south.creator.freezer import prep_for_freeze 2 | from django.conf import settings 3 | from django.db import models 4 | 5 | 6 | person_model = getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User') 7 | 8 | 9 | def freeze_person_model(): 10 | try: 11 | app_label, model = person_model.split('.') 12 | except ValueError: 13 | raise ValueError("Invalid PHILO_PERSON_MODULE definition: %s" % person_model) 14 | 15 | model = models.get_model(app_label, model.lower()) 16 | 17 | if model is None: 18 | raise ValueError("PHILO_PERSON_MODULE not found: %s" % person_model) 19 | 20 | return prep_for_freeze(model) 21 | 22 | 23 | frozen_person = freeze_person_model() -------------------------------------------------------------------------------- /philo/models/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User, Group 3 | from django.contrib.sites.models import Site 4 | 5 | from philo.models.base import * 6 | from philo.models.collections import * 7 | from philo.models.nodes import * 8 | from philo.models.pages import * 9 | 10 | 11 | register_value_model(User) 12 | register_value_model(Group) 13 | register_value_model(Site) 14 | 15 | if 'philo' in settings.INSTALLED_APPS: 16 | from django.template import add_to_builtins 17 | add_to_builtins('philo.templatetags.embed') 18 | add_to_builtins('philo.templatetags.containers') 19 | add_to_builtins('philo.templatetags.collections') 20 | add_to_builtins('philo.templatetags.nodes') -------------------------------------------------------------------------------- /philo/models/collections.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes import generic 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.db import models 4 | 5 | from philo.models.base import value_content_type_limiter, register_value_model 6 | from philo.utils import fattr 7 | 8 | 9 | __all__ = ('Collection', 'CollectionMember') 10 | 11 | 12 | class Collection(models.Model): 13 | """ 14 | Collections are curated ordered groupings of arbitrary models. 15 | 16 | """ 17 | #: :class:`CharField` with max_length 255 18 | name = models.CharField(max_length=255) 19 | #: Optional :class:`TextField` 20 | description = models.TextField(blank=True, null=True) 21 | 22 | @fattr(short_description='Members') 23 | def get_count(self): 24 | """Returns the number of items in the collection.""" 25 | return self.members.count() 26 | 27 | def __unicode__(self): 28 | return self.name 29 | 30 | class Meta: 31 | app_label = 'philo' 32 | 33 | 34 | class CollectionMemberManager(models.Manager): 35 | use_for_related_fields = True 36 | 37 | def with_model(self, model): 38 | """ 39 | Given a model class or instance, returns a queryset of all instances of that model which have collection members in this manager's scope. 40 | 41 | Example:: 42 | 43 | >>> from philo.models import Collection 44 | >>> from django.contrib.auth.models import User 45 | >>> collection = Collection.objects.get(name="Foo") 46 | >>> collection.members.all() 47 | [, , ] 48 | >>> collection.members.with_model(User) 49 | [, ] 50 | 51 | """ 52 | 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)) 53 | 54 | 55 | class CollectionMember(models.Model): 56 | """ 57 | The collection member model represents a generic link from a :class:`Collection` to an arbitrary model instance with an attached order. 58 | 59 | """ 60 | #: A :class:`CollectionMemberManager` instance 61 | objects = CollectionMemberManager() 62 | #: :class:`ForeignKey` to a :class:`Collection` instance. 63 | collection = models.ForeignKey(Collection, related_name='members') 64 | #: The numerical index of the item within the collection (optional). 65 | index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True) 66 | member_content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Member type') 67 | member_object_id = models.PositiveIntegerField(verbose_name='Member ID') 68 | #: :class:`GenericForeignKey` to an arbitrary model instance. 69 | member = generic.GenericForeignKey('member_content_type', 'member_object_id') 70 | 71 | def __unicode__(self): 72 | return u'%s - %s' % (self.collection, self.member) 73 | 74 | class Meta: 75 | app_label = 'philo' 76 | 77 | 78 | register_value_model(Collection) -------------------------------------------------------------------------------- /philo/models/fields/__init__.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ValidationError 3 | from django.core.validators import validate_slug 4 | from django.db import models 5 | from django.utils import simplejson as json 6 | from django.utils.text import capfirst 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from philo.forms.fields import JSONFormField 10 | from philo.utils.registry import RegistryIterator 11 | from philo.validators import TemplateValidator, json_validator 12 | #from philo.models.fields.entities import * 13 | 14 | 15 | class TemplateField(models.TextField): 16 | """A :class:`TextField` which is validated with a :class:`.TemplateValidator`. ``allow``, ``disallow``, and ``secure`` will be passed into the validator's construction.""" 17 | def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs): 18 | super(TemplateField, self).__init__(*args, **kwargs) 19 | self.validators.append(TemplateValidator(allow, disallow, secure)) 20 | 21 | 22 | class JSONDescriptor(object): 23 | def __init__(self, field): 24 | self.field = field 25 | 26 | def __get__(self, instance, owner): 27 | if instance is None: 28 | raise AttributeError # ? 29 | 30 | if self.field.name not in instance.__dict__: 31 | json_string = getattr(instance, self.field.attname) 32 | instance.__dict__[self.field.name] = json.loads(json_string) 33 | 34 | return instance.__dict__[self.field.name] 35 | 36 | def __set__(self, instance, value): 37 | instance.__dict__[self.field.name] = value 38 | setattr(instance, self.field.attname, json.dumps(value)) 39 | 40 | def __delete__(self, instance): 41 | del(instance.__dict__[self.field.name]) 42 | setattr(instance, self.field.attname, json.dumps(None)) 43 | 44 | 45 | class JSONField(models.TextField): 46 | """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`.""" 47 | default_validators = [json_validator] 48 | 49 | def get_attname(self): 50 | return "%s_json" % self.name 51 | 52 | def contribute_to_class(self, cls, name): 53 | super(JSONField, self).contribute_to_class(cls, name) 54 | setattr(cls, name, JSONDescriptor(self)) 55 | models.signals.pre_init.connect(self.fix_init_kwarg, sender=cls) 56 | 57 | def fix_init_kwarg(self, sender, args, kwargs, **signal_kwargs): 58 | # Anything passed in as self.name is assumed to come from a serializer and 59 | # will be treated as a json string. 60 | if self.name in kwargs: 61 | value = kwargs.pop(self.name) 62 | 63 | # Hack to handle the xml serializer's handling of "null" 64 | if value is None: 65 | value = 'null' 66 | 67 | kwargs[self.attname] = value 68 | 69 | def formfield(self, *args, **kwargs): 70 | kwargs["form_class"] = JSONFormField 71 | return super(JSONField, self).formfield(*args, **kwargs) 72 | 73 | 74 | class SlugMultipleChoiceField(models.Field): 75 | """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.""" 76 | __metaclass__ = models.SubfieldBase 77 | description = _("Comma-separated slug field") 78 | 79 | def get_internal_type(self): 80 | return "TextField" 81 | 82 | def to_python(self, value): 83 | if not value: 84 | return [] 85 | 86 | if isinstance(value, list): 87 | return value 88 | 89 | return value.split(',') 90 | 91 | def get_prep_value(self, value): 92 | return ','.join(value) 93 | 94 | def formfield(self, **kwargs): 95 | # This is necessary because django hard-codes TypedChoiceField for things with choices. 96 | defaults = { 97 | 'widget': forms.CheckboxSelectMultiple, 98 | 'choices': self.get_choices(include_blank=False), 99 | 'label': capfirst(self.verbose_name), 100 | 'required': not self.blank, 101 | 'help_text': self.help_text 102 | } 103 | if self.has_default(): 104 | if callable(self.default): 105 | defaults['initial'] = self.default 106 | defaults['show_hidden_initial'] = True 107 | else: 108 | defaults['initial'] = self.get_default() 109 | 110 | for k in kwargs.keys(): 111 | if k not in ('coerce', 'empty_value', 'choices', 'required', 112 | 'widget', 'label', 'initial', 'help_text', 113 | 'error_messages', 'show_hidden_initial'): 114 | del kwargs[k] 115 | 116 | defaults.update(kwargs) 117 | form_class = forms.TypedMultipleChoiceField 118 | return form_class(**defaults) 119 | 120 | def validate(self, value, model_instance): 121 | invalid_values = [] 122 | for val in value: 123 | try: 124 | validate_slug(val) 125 | except ValidationError: 126 | invalid_values.append(val) 127 | 128 | if invalid_values: 129 | # should really make a custom message. 130 | raise ValidationError(self.error_messages['invalid_choice'] % invalid_values) 131 | 132 | def _get_choices(self): 133 | if isinstance(self._choices, RegistryIterator): 134 | return self._choices.copy() 135 | elif hasattr(self._choices, 'next'): 136 | choices, self._choices = itertools.tee(self._choices) 137 | return choices 138 | else: 139 | return self._choices 140 | choices = property(_get_choices) 141 | 142 | 143 | try: 144 | from south.modelsinspector import add_introspection_rules 145 | except ImportError: 146 | pass 147 | else: 148 | add_introspection_rules([], ["^philo\.models\.fields\.SlugMultipleChoiceField"]) 149 | add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"]) 150 | add_introspection_rules([], ["^philo\.models\.fields\.JSONField"]) -------------------------------------------------------------------------------- /philo/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | 4 | #: 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`. 5 | #: 6 | #: Arguments that are sent with this signal: 7 | #: 8 | #: ``sender`` 9 | #: The model class. 10 | entity_class_prepared = Signal(providing_args=['class']) 11 | 12 | #: 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. 13 | #: 14 | #: Arguments that are sent with this signal: 15 | #: 16 | #: ``sender`` 17 | #: The :class:`~philo.models.nodes.View` instance 18 | #: 19 | #: ``request`` 20 | #: The :class:`HttpRequest` instance which the :class:`~philo.models.nodes.View` is rendering in response to. 21 | #: 22 | #: ``extra_context`` 23 | #: A dictionary which will be passed into :meth:`~philo.models.nodes.View.actually_render_to_response`. 24 | view_about_to_render = Signal(providing_args=['request', 'extra_context']) 25 | 26 | #: Sent when a view instance has finished rendering. 27 | #: 28 | #: Arguments that are sent with this signal: 29 | #: 30 | #: ``sender`` 31 | #: The :class:`~philo.models.nodes.View` instance 32 | #: 33 | #: ``response`` 34 | #: The :class:`HttpResponse` instance which :class:`~philo.models.nodes.View` view has rendered to. 35 | view_finished_rendering = Signal(providing_args=['response']) 36 | 37 | #: 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. 38 | #: 39 | #: Arguments that are sent with this signal: 40 | #: 41 | #: ``sender`` 42 | #: The :class:`~philo.models.pages.Page` instance 43 | #: 44 | #: ``request`` 45 | #: The :class:`HttpRequest` instance which the :class:`~philo.models.pages.Page` is rendering in response to (if any). 46 | #: 47 | #: ``extra_context`` 48 | #: A dictionary which will be passed into the :class:`Template` context. 49 | page_about_to_render_to_string = Signal(providing_args=['request', 'extra_context']) 50 | 51 | #: 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. 52 | #: 53 | #: Arguments that are sent with this signal: 54 | #: 55 | #: ``sender`` 56 | #: The :class:`~philo.models.pages.Page` instance 57 | #: 58 | #: ``string`` 59 | #: The string which the :class:`~philo.models.pages.Page` has rendered to. 60 | page_finished_rendering_to_string = Signal(providing_args=['string']) -------------------------------------------------------------------------------- /philo/static/philo/css/EmbedWidget.css: -------------------------------------------------------------------------------- 1 | .embed-widget{ 2 | float:left; 3 | } 4 | .embed-toolbar{ 5 | border:1px solid #CCC; 6 | border-bottom:0; 7 | padding:3px 5px; 8 | background:#EEE -webkit-linear-gradient(#F5F5F5, #DDD); 9 | background:#EEE -moz-linear-gradient(#F5F5F5, #DDD); 10 | background-color:#EEE; 11 | } 12 | .embed-widget textarea{ 13 | margin-top:0; 14 | } 15 | .embed-widget button, .embed-widget select{ 16 | vertical-align:middle; 17 | margin-right:3px; 18 | } 19 | .embed-toolbar button{ 20 | background:#FFF; 21 | border:1px solid #CCC; 22 | border-radius:3px; 23 | -webkit-border-radius:3px; 24 | -moz-border-radius:3px; 25 | color:#666; 26 | } 27 | .embed-toolbar button:hover{ 28 | color:#444; 29 | } 30 | .embed-toolbar button:active{ 31 | color:#FFF; 32 | background:#666; 33 | border-color:#666; 34 | } 35 | 36 | .grappelli .embed-widget{ 37 | background:#DDD; 38 | padding:2px; 39 | border:1px solid #CCC; 40 | border-radius:5px; 41 | -webkit-border-radius:5px; 42 | -moz-border-radius:5px; 43 | display:inline-block; 44 | margin:0 -3px; 45 | } 46 | .grappelli .embed-toolbar{ 47 | padding:0; 48 | padding-bottom:3px; 49 | background:none; 50 | border:none; 51 | } -------------------------------------------------------------------------------- /philo/static/philo/js/EmbedWidget.js: -------------------------------------------------------------------------------- 1 | ;(function ($) { 2 | var widget = window.embedWidget; 3 | 4 | widget = { 5 | options: {}, 6 | optgroups: {}, 7 | init: function () { 8 | var EmbedFields = widget.EmbedFields = $('.embedding'), 9 | EmbedWidgets = widget.EmbedWidgets, 10 | EmbedBars = widget.EmbedBars, 11 | EmbedButtons = widget.EmbedButtons, 12 | EmbedSelects = widget.EmbedSelects; 13 | 14 | EmbedFields.wrap($('
')); 15 | EmbedWidgets = $('.embed-widget'); 16 | EmbedWidgets.prepend($('
')); 17 | EmbedBars = $('.embed-toolbar'); 18 | EmbedBars.append(''); 19 | EmbedButtons = $('.embed-button'); 20 | EmbedSelects = $('.embed-select'); 21 | 22 | widget.parseContentTypes(); 23 | EmbedSelects.each(widget.populateSelect); 24 | 25 | EmbedButtons.click(widget.buttonHandler); 26 | 27 | // overload the dismissRelatedLookupPopup function 28 | oldDismissRelatedLookupPopup = window.dismissRelatedLookupPopup; 29 | window.dismissRelatedLookupPopup = function (win, chosenId) { 30 | var name = windowname_to_id(win.name), 31 | elem = $('#'+name), val; 32 | // if the original element was an embed widget, run our script 33 | if (elem.parent().hasClass('embed-widget')) { 34 | contenttype = $('select',elem.parent()).val(); 35 | widget.appendEmbed(elem, contenttype, chosenId); 36 | elem.focus(); 37 | win.close(); 38 | return; 39 | } 40 | // otherwise, do what you usually do 41 | oldDismissRelatedLookupPopup.apply(this, arguments); 42 | } 43 | 44 | // overload the dismissAddAnotherPopup function 45 | oldDismissAddAnotherPopup = window.dismissAddAnotherPopup; 46 | window.dismissAddAnotherPopup = function (win, newId, newRepr) { 47 | var name = windowname_to_id(win.name), 48 | elem = $('#'+win.name), val; 49 | if (elem.parent().hasClass('embed-widget')) { 50 | dismissRelatedLookupPopup(win, newId); 51 | } 52 | // otherwise, do what you usually do 53 | oldDismissAddAnotherPopup.apply(this, arguments); 54 | } 55 | 56 | // Add grappelli to the body class if the admin is grappelli. This will allow us to customize styles accordingly. 57 | if (window.grappelli) { 58 | $(document.body).addClass('grappelli'); 59 | } 60 | }, 61 | parseContentTypes: function () { 62 | var string = widget.EmbedFields.eq(0).attr('data-content-types'), 63 | data = $.parseJSON(string), 64 | i=0, 65 | current_app_label = '', 66 | optgroups = {}; 67 | 68 | // this loop relies on data being clustered by app 69 | for(i=0; i < data.length; i++){ 70 | item = data[i] 71 | // run this next loop every time we encounter a new app label 72 | if (item.app_label !== current_app_label) { 73 | current_app_label = item.app_label; 74 | optgroups[current_app_label] = {} 75 | } 76 | optgroups[current_app_label][item.verbose_name] = [item.app_label,item.object_name].join('.'); 77 | 78 | widget.optgroups = optgroups; 79 | } 80 | }, 81 | populateSelect: function () { 82 | var $this = $(this), 83 | optgroups = widget.optgroups, 84 | optgroup_els = {}, 85 | optgroup_el, group; 86 | 87 | // append a title 88 | $this.append(''); 89 | 90 | // for each group 91 | for (name in optgroups){ 92 | if(optgroups.hasOwnProperty(name)){ 93 | // assign the group to variable group, temporarily 94 | group = optgroups[name]; 95 | // create an element for this group and assign it to optgroup_el, temporarily 96 | optgroup_el = optgroup_els[name] = $(''); 97 | // append this element to the select menu 98 | $this.append(optgroup_el); 99 | // for each item in the group 100 | for (name in group) { 101 | // append an option to the optgroup 102 | optgroup_el.append(''); 103 | } 104 | } 105 | } 106 | }, 107 | buttonHandler: function (e) { 108 | var $this = $(this), 109 | select = $this.prev('select'), 110 | embed_widget = $this.closest('.embed-widget'), 111 | textarea = embed_widget.children('.embedding').eq(0), 112 | val, app_label, object_name, 113 | href, 114 | win; 115 | 116 | // prevent the button from submitting the form 117 | e.preventDefault(); 118 | 119 | // handle the case that they haven't chosen a type to embed 120 | if (select.val()==='') { 121 | alert('Please select a media type to embed.'); 122 | textarea.focus(); 123 | return; 124 | } 125 | 126 | // split the val into app and object 127 | val = select.val(); 128 | app_label = val.split('.')[0]; 129 | object_name = val.split('.')[1]; 130 | 131 | // generate the url for the popup 132 | // TODO: Find a better way to get the admin URL if possible. This will break if the URL patterns for the admin ever change. 133 | href=['../../../', app_label, '/', object_name, '/?pop=1'].join(''); 134 | 135 | // open a new window 136 | win = window.open(href, id_to_windowname(textarea.attr('id')), 'height=500,width=980,resizable=yes,scrollbars=yes'); 137 | }, 138 | appendEmbed: function (textarea, embed_type, embed_id) { 139 | var $textarea = $(textarea), 140 | textarea = $textarea[0], // make sure we're *not* working with a jQuery object 141 | current_selection = [textarea.selectionStart, textarea.selectionEnd], 142 | current_text = $textarea.val(), 143 | embed_string = ['{% embed', embed_type, embed_id, '%}'].join(' '), 144 | new_text = current_text.substring(0, current_selection[0]) + embed_string + current_text.substring(current_selection[1]), 145 | new_cursor_pos = current_selection[0]+embed_string.length; 146 | $textarea.val(new_text); 147 | textarea.setSelectionRange(new_cursor_pos, new_cursor_pos); 148 | } 149 | } 150 | 151 | $(widget.init); 152 | }(django.jQuery)); -------------------------------------------------------------------------------- /philo/templates/admin/philo/edit_inline/grappelli_tabular_container.html: -------------------------------------------------------------------------------- 1 | {% load i18n adminmedia %} 2 | 3 | 4 | {{ inline_admin_formset.formset.management_form }} 5 | {% comment %}Don't render the formset at all if there aren't any forms.{% endcomment %} 6 | {% if inline_admin_formset.formset.forms %} 7 |
8 | {{ inline_admin_formset.opts.verbose_name_plural|capfirst }} 9 | {{ inline_admin_formset.formset.non_form_errors }} 10 | {% for inline_admin_form in inline_admin_formset %} 11 | {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} 12 | {{ inline_admin_form.fk_field.field }} 13 | {% spaceless %} 14 | {% for fieldset in inline_admin_form %} 15 | {% for line in fieldset %} 16 | {% for field in line %} 17 | {% if field.is_hidden %} {{ field.field }} {% endif %} 18 | {% endfor %} 19 | {% endfor %} 20 | {% endfor %}{% endspaceless %} 21 | {% endfor %} 22 | {% for form in inline_admin_formset.formset.forms %} 23 |
24 | {{ form.non_field_errors }} 25 |
26 | {% for field in form %} 27 | {% if not field.is_hidden %} 28 | {% comment %}This will be true for one field: the content/content reference{% endcomment %} 29 |
30 |
31 | {{ field }} 32 | {{ field.errors }} 33 | {% if field.field.help_text %} 34 |

{{ field.field.help_text|safe }}

35 | {% endif %} 36 |
37 | {% endif %} 38 | {% endfor %} 39 |
40 |
41 | {% endfor %} 42 |
43 | {% endif %} 44 | -------------------------------------------------------------------------------- /philo/templates/admin/philo/edit_inline/tabular_attribute.html: -------------------------------------------------------------------------------- 1 | {% load i18n adminmedia %} 2 |
3 | 69 |
70 | 71 | 130 | -------------------------------------------------------------------------------- /philo/templates/admin/philo/edit_inline/tabular_container.html: -------------------------------------------------------------------------------- 1 | {% load i18n adminmedia %} 2 | {{ inline_admin_formset.formset.management_form }} 3 | {% if inline_admin_formset.formset.forms %} 4 |
5 | 48 |
49 | 50 | 109 | {% endif %} 110 | -------------------------------------------------------------------------------- /philo/templates/admin/philo/page/add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block form_top %} 5 | {% if not is_popup %} 6 |

{% trans "First, choose a template. After saving, you'll be able to provide additional content for containers." %}

7 | {% else %} 8 |

{% trans "Choose a template" %}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block after_field_sets %} 13 | 14 | {% endblock %} -------------------------------------------------------------------------------- /philo/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ithinksw/philo/8a772dd4761e3a4b926358d6ebf87c9fc7033ba5/philo/templatetags/__init__.py -------------------------------------------------------------------------------- /philo/templatetags/collections.py: -------------------------------------------------------------------------------- 1 | """ 2 | The collection template tags are automatically included as builtins if :mod:`philo` is an installed app. 3 | 4 | """ 5 | 6 | from django import template 7 | from django.conf import settings 8 | from django.contrib.contenttypes.models import ContentType 9 | 10 | 11 | register = template.Library() 12 | 13 | 14 | class MembersofNode(template.Node): 15 | def __init__(self, collection, model, as_var): 16 | self.collection = template.Variable(collection) 17 | self.model = model 18 | self.as_var = as_var 19 | 20 | def render(self, context): 21 | try: 22 | collection = self.collection.resolve(context) 23 | context[self.as_var] = collection.members.with_model(self.model) 24 | except: 25 | pass 26 | return '' 27 | 28 | 29 | @register.tag 30 | def membersof(parser, token): 31 | """ 32 | 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. 33 | 34 | Usage:: 35 | 36 | {% membersof with . as %} 37 | 38 | """ 39 | params=token.split_contents() 40 | tag = params[0] 41 | 42 | if len(params) < 6: 43 | raise template.TemplateSyntaxError('"%s" template tag requires six parameters' % tag) 44 | 45 | if params[2] != 'with': 46 | raise template.TemplateSyntaxError('"%s" template tag requires the third parameter to be "with"' % tag) 47 | 48 | try: 49 | app_label, model = params[3].strip('"').split('.') 50 | ct = ContentType.objects.get_by_natural_key(app_label, model) 51 | except ValueError: 52 | raise template.TemplateSyntaxError('"%s" template tag option "with" requires an argument of the form app_label.model (see django.contrib.contenttypes)' % tag) 53 | except ContentType.DoesNotExist: 54 | raise template.TemplateSyntaxError('"%s" template tag option "with" requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tag) 55 | 56 | if params[4] != 'as': 57 | raise template.TemplateSyntaxError('"%s" template tag requires the fifth parameter to be "as"' % tag) 58 | 59 | return MembersofNode(collection=params[1], model=ct.model_class(), as_var=params[5]) -------------------------------------------------------------------------------- /philo/templatetags/containers.py: -------------------------------------------------------------------------------- 1 | """ 2 | The container template tags are automatically included as builtins if :mod:`philo` is an installed app. 3 | 4 | """ 5 | 6 | from django import template 7 | from django.conf import settings 8 | from django.contrib.contenttypes.models import ContentType 9 | from django.core.exceptions import ObjectDoesNotExist 10 | from django.db.models import Q 11 | from django.utils.safestring import SafeUnicode, mark_safe 12 | 13 | 14 | register = template.Library() 15 | 16 | 17 | CONTAINER_CONTEXT_KEY = 'philo_container_context' 18 | 19 | 20 | class ContainerContext(object): 21 | def __init__(self, page): 22 | self.page = page 23 | 24 | def get_contentlets(self): 25 | if not hasattr(self, '_contentlets'): 26 | self._contentlets = dict(((c.name, c) for c in self.page.contentlets.all())) 27 | return self._contentlets 28 | 29 | def get_references(self): 30 | if not hasattr(self, '_references'): 31 | references = self.page.contentreferences.all() 32 | self._references = dict((((c.name, ContentType.objects.get_for_id(c.content_type_id)), c) for c in references)) 33 | return self._references 34 | 35 | 36 | class ContainerNode(template.Node): 37 | def __init__(self, name, references=None, as_var=None): 38 | self.name = name 39 | self.as_var = as_var 40 | self.references = references 41 | 42 | def render(self, context): 43 | container_content = self.get_container_content(context) 44 | 45 | if self.as_var: 46 | context[self.as_var] = container_content 47 | return '' 48 | 49 | return container_content 50 | 51 | def get_container_content(self, context): 52 | try: 53 | container_context = context.render_context[CONTAINER_CONTEXT_KEY] 54 | except KeyError: 55 | try: 56 | page = context['page'] 57 | except KeyError: 58 | return settings.TEMPLATE_STRING_IF_INVALID 59 | 60 | container_context = ContainerContext(page) 61 | context.render_context[CONTAINER_CONTEXT_KEY] = container_context 62 | 63 | if self.references: 64 | # Then it's a content reference. 65 | try: 66 | contentreference = container_context.get_references()[(self.name, self.references)] 67 | except KeyError: 68 | content = '' 69 | else: 70 | content = contentreference.content 71 | else: 72 | # Otherwise it's a contentlet. 73 | try: 74 | contentlet = container_context.get_contentlets()[self.name] 75 | except KeyError: 76 | content = '' 77 | else: 78 | content = contentlet.content 79 | return content 80 | 81 | 82 | @register.tag 83 | def container(parser, token): 84 | """ 85 | 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. 86 | 87 | Usage:: 88 | 89 | {% container [[references .] as ] %} 90 | 91 | """ 92 | params = token.split_contents() 93 | if len(params) >= 2: 94 | tag = params[0] 95 | name = params[1].strip('"') 96 | references = None 97 | as_var = None 98 | if len(params) > 2: 99 | remaining_tokens = params[2:] 100 | while remaining_tokens: 101 | option_token = remaining_tokens.pop(0) 102 | if option_token == 'references': 103 | try: 104 | app_label, model = remaining_tokens.pop(0).strip('"').split('.') 105 | references = ContentType.objects.get_by_natural_key(app_label, model) 106 | except IndexError: 107 | raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument specifying a content type' % tag) 108 | except ValueError: 109 | raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument of the form app_label.model (see django.contrib.contenttypes)' % tag) 110 | except ObjectDoesNotExist: 111 | raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tag) 112 | elif option_token == 'as': 113 | try: 114 | as_var = remaining_tokens.pop(0) 115 | except IndexError: 116 | raise template.TemplateSyntaxError('"%s" template tag option "as" requires an argument specifying a variable name' % tag) 117 | if references and not as_var: 118 | raise template.TemplateSyntaxError('"%s" template tags using "references" option require additional use of the "as" option specifying a variable name' % tag) 119 | return ContainerNode(name, references, as_var) 120 | 121 | else: # error 122 | raise template.TemplateSyntaxError('"%s" template tag provided without arguments (at least one required)' % tag) 123 | -------------------------------------------------------------------------------- /philo/templatetags/include_string.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | 5 | register = template.Library() 6 | 7 | 8 | class IncludeStringNode(template.Node): 9 | def __init__(self, string): 10 | self.string = string 11 | 12 | def render(self, context): 13 | try: 14 | t = template.Template(self.string.resolve(context)) 15 | return t.render(context) 16 | except template.TemplateSyntaxError: 17 | if settings.TEMPLATE_DEBUG: 18 | raise 19 | return settings.TEMPLATE_STRING_IF_INVALID 20 | except: 21 | return settings.TEMPLATE_STRING_IF_INVALID 22 | 23 | 24 | @register.tag 25 | def include_string(parser, token): 26 | """ 27 | Include a flat string by interpreting it as a template. The compiled template will be rendered with the current context. 28 | 29 | Usage:: 30 | 31 | {% include_string %} 32 | 33 | """ 34 | bits = token.split_contents() 35 | if len(bits) != 2: 36 | raise TemplateSyntaxError("%r tag takes one argument: the template string to be included" % bits[0]) 37 | string = parser.compile_filter(bits[1]) 38 | return IncludeStringNode(string) -------------------------------------------------------------------------------- /philo/templatetags/nodes.py: -------------------------------------------------------------------------------- 1 | """ 2 | The node template tags are automatically included as builtins if :mod:`philo` is an installed app. 3 | 4 | """ 5 | 6 | from django import template 7 | from django.conf import settings 8 | from django.contrib.sites.models import Site 9 | from django.core.urlresolvers import reverse, NoReverseMatch 10 | from django.template.defaulttags import kwarg_re 11 | from django.utils.encoding import smart_str 12 | 13 | from philo.exceptions import ViewCanNotProvideSubpath 14 | 15 | 16 | register = template.Library() 17 | 18 | 19 | class NodeURLNode(template.Node): 20 | def __init__(self, node, as_var, with_obj=None, view_name=None, args=None, kwargs=None): 21 | self.as_var = as_var 22 | self.view_name = view_name 23 | 24 | # Because the following variables have already been compiled as filters if they exist, they don't need to be re-scanned as template variables. 25 | self.node = node 26 | self.with_obj = with_obj 27 | self.args = args 28 | self.kwargs = kwargs 29 | 30 | def render(self, context): 31 | if self.node: 32 | node = self.node.resolve(context) 33 | else: 34 | node = context.get('node', None) 35 | 36 | if not node: 37 | return settings.TEMPLATE_STRING_IF_INVALID 38 | 39 | if self.with_obj is None and self.view_name is None: 40 | url = node.get_absolute_url() 41 | else: 42 | if not node.accepts_subpath: 43 | return settings.TEMPLATE_STRING_IF_INVALID 44 | 45 | if self.with_obj is not None: 46 | try: 47 | view_name, args, kwargs = node.view.get_reverse_params(self.with_obj.resolve(context)) 48 | except ViewCanNotProvideSubpath: 49 | return settings.TEMPLATE_STRING_IF_INVALID 50 | else: # self.view_name is not None 51 | view_name = self.view_name 52 | args = [arg.resolve(context) for arg in self.args] 53 | kwargs = dict([(smart_str(k, 'ascii'), v.resolve(context)) for k, v in self.kwargs.items()]) 54 | 55 | url = '' 56 | try: 57 | subpath = reverse(view_name, urlconf=node.view, args=args, kwargs=kwargs) 58 | except NoReverseMatch: 59 | if self.as_var is None: 60 | if settings.TEMPLATE_DEBUG: 61 | raise 62 | return settings.TEMPLATE_STRING_IF_INVALID 63 | else: 64 | url = node.construct_url(subpath) 65 | 66 | if self.as_var: 67 | context[self.as_var] = url 68 | return '' 69 | else: 70 | return url 71 | 72 | 73 | @register.tag 74 | def node_url(parser, token): 75 | """ 76 | The :ttag:`node_url` tag allows access to :meth:`.View.reverse` from a template for a :class:`.Node`. By default, the :class:`.Node` that is used for the call is pulled from the context variable ``node``; however, this can be overridden with the ``[for ]`` option. 77 | 78 | Usage:: 79 | 80 | {% node_url [for ] [as ] %} 81 | {% node_url with [for ] [as ] %} 82 | {% node_url [ [ ...] ] [for ] [as ] %} 83 | {% node_url [= [= ...] ] [for ] [as ] %} 84 | 85 | """ 86 | params = token.split_contents() 87 | tag = params[0] 88 | as_var = None 89 | with_obj = None 90 | node = None 91 | params = params[1:] 92 | 93 | if len(params) >= 2 and params[-2] == 'as': 94 | as_var = params[-1] 95 | params = params[:-2] 96 | 97 | if len(params) >= 2 and params[-2] == 'for': 98 | node = parser.compile_filter(params[-1]) 99 | params = params[:-2] 100 | 101 | if len(params) >= 2 and params[-2] == 'with': 102 | with_obj = parser.compile_filter(params[-1]) 103 | params = params[:-2] 104 | 105 | if with_obj is not None: 106 | if params: 107 | raise template.TemplateSyntaxError('`%s` template tag accepts no arguments or keyword arguments if with is specified.' % tag) 108 | return NodeURLNode(with_obj=with_obj, node=node, as_var=as_var) 109 | 110 | if params: 111 | args = [] 112 | kwargs = {} 113 | view_name = params.pop(0) 114 | for param in params: 115 | match = kwarg_re.match(param) 116 | if not match: 117 | raise TemplateSyntaxError("Malformed arguments to `%s` tag" % tag) 118 | name, value = match.groups() 119 | if name: 120 | kwargs[name] = parser.compile_filter(value) 121 | else: 122 | args.append(parser.compile_filter(value)) 123 | return NodeURLNode(view_name=view_name, args=args, kwargs=kwargs, node=node, as_var=as_var) 124 | 125 | return NodeURLNode(node=node, as_var=as_var) -------------------------------------------------------------------------------- /philo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url 2 | 3 | from philo.views import node_view 4 | 5 | 6 | urlpatterns = patterns('', 7 | url(r'^$', node_view, kwargs={'path': '/'}, name='philo-root'), 8 | url(r'^(?P.*)$', node_view, name='philo-node-by-path') 9 | ) 10 | -------------------------------------------------------------------------------- /philo/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.core.paginator import Paginator, EmptyPage 4 | 5 | 6 | def fattr(*args, **kwargs): 7 | """ 8 | 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. 9 | 10 | Example:: 11 | 12 | >>> from philo.utils import fattr 13 | >>> @fattr(short_description="Hello World!") 14 | ... def x(): 15 | ... pass 16 | ... 17 | >>> x.short_description 18 | 'Hello World!' 19 | 20 | """ 21 | def wrapper(function): 22 | for key in kwargs: 23 | setattr(function, key, kwargs[key]) 24 | return function 25 | return wrapper 26 | 27 | 28 | ### ContentTypeLimiters 29 | 30 | 31 | class ContentTypeLimiter(object): 32 | def q_object(self): 33 | return models.Q(pk__in=[]) 34 | 35 | def add_to_query(self, query, *args, **kwargs): 36 | query.add_q(self.q_object(), *args, **kwargs) 37 | 38 | 39 | class ContentTypeRegistryLimiter(ContentTypeLimiter): 40 | """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.""" 41 | def __init__(self): 42 | self.classes = [] 43 | 44 | def register_class(self, cls): 45 | """Registers a model class with this limiter.""" 46 | self.classes.append(cls) 47 | 48 | def unregister_class(self, cls): 49 | """Unregisters a model class from this limiter.""" 50 | self.classes.remove(cls) 51 | 52 | def q_object(self): 53 | contenttype_pks = [] 54 | for cls in self.classes: 55 | try: 56 | if issubclass(cls, models.Model): 57 | if not cls._meta.abstract: 58 | contenttype = ContentType.objects.get_for_model(cls) 59 | contenttype_pks.append(contenttype.pk) 60 | except: 61 | pass 62 | return models.Q(pk__in=contenttype_pks) 63 | 64 | 65 | class ContentTypeSubclassLimiter(ContentTypeLimiter): 66 | """ 67 | 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. 68 | 69 | :param cls: The class whose non-abstract subclasses will be valid choices. 70 | :param inclusive: Whether ``cls`` should also be considered a valid choice (if it is a non-abstract subclass of :class:`models.Model`) 71 | 72 | """ 73 | def __init__(self, cls, inclusive=False): 74 | self.cls = cls 75 | self.inclusive = inclusive 76 | 77 | def q_object(self): 78 | contenttype_pks = [] 79 | def handle_subclasses(cls): 80 | for subclass in cls.__subclasses__(): 81 | try: 82 | if issubclass(subclass, models.Model): 83 | if not subclass._meta.abstract: 84 | if not self.inclusive and subclass is self.cls: 85 | continue 86 | contenttype = ContentType.objects.get_for_model(subclass) 87 | contenttype_pks.append(contenttype.pk) 88 | handle_subclasses(subclass) 89 | except: 90 | pass 91 | handle_subclasses(self.cls) 92 | return models.Q(pk__in=contenttype_pks) 93 | 94 | 95 | ### Pagination 96 | 97 | 98 | def paginate(objects, per_page=None, page_number=1): 99 | """ 100 | Given a list of objects, return a (``paginator``, ``page``, ``objects``) tuple. 101 | 102 | :param objects: The list of objects to be paginated. 103 | :param per_page: The number of objects per page. 104 | :param page_number: The number of the current page. 105 | :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``. 106 | 107 | """ 108 | try: 109 | per_page = int(per_page) 110 | except (TypeError, ValueError): 111 | # Then either it wasn't set or it was set to an invalid value 112 | paginator = page = None 113 | else: 114 | # There also shouldn't be pagination if the list is too short. Try count() 115 | # first - good chance it's a queryset, where count is more efficient. 116 | try: 117 | if objects.count() <= per_page: 118 | paginator = page = None 119 | except AttributeError: 120 | if len(objects) <= per_page: 121 | paginator = page = None 122 | 123 | try: 124 | return paginator, page, objects 125 | except NameError: 126 | pass 127 | 128 | paginator = Paginator(objects, per_page) 129 | try: 130 | page_number = int(page_number) 131 | except: 132 | page_number = 1 133 | 134 | try: 135 | page = paginator.page(page_number) 136 | except EmptyPage: 137 | page = None 138 | else: 139 | objects = page.object_list 140 | 141 | return paginator, page, objects -------------------------------------------------------------------------------- /philo/utils/lazycompat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.utils.functional import empty, LazyObject, SimpleLazyObject 3 | except ImportError: 4 | # Supply LazyObject and SimpleLazyObject for django < r16308 5 | import operator 6 | 7 | 8 | empty = object() 9 | def new_method_proxy(func): 10 | def inner(self, *args): 11 | if self._wrapped is empty: 12 | self._setup() 13 | return func(self._wrapped, *args) 14 | return inner 15 | 16 | class LazyObject(object): 17 | """ 18 | A wrapper for another class that can be used to delay instantiation of the 19 | wrapped class. 20 | 21 | By subclassing, you have the opportunity to intercept and alter the 22 | instantiation. If you don't need to do that, use SimpleLazyObject. 23 | """ 24 | def __init__(self): 25 | self._wrapped = empty 26 | 27 | __getattr__ = new_method_proxy(getattr) 28 | 29 | def __setattr__(self, name, value): 30 | if name == "_wrapped": 31 | # Assign to __dict__ to avoid infinite __setattr__ loops. 32 | self.__dict__["_wrapped"] = value 33 | else: 34 | if self._wrapped is empty: 35 | self._setup() 36 | setattr(self._wrapped, name, value) 37 | 38 | def __delattr__(self, name): 39 | if name == "_wrapped": 40 | raise TypeError("can't delete _wrapped.") 41 | if self._wrapped is empty: 42 | self._setup() 43 | delattr(self._wrapped, name) 44 | 45 | def _setup(self): 46 | """ 47 | Must be implemented by subclasses to initialise the wrapped object. 48 | """ 49 | raise NotImplementedError 50 | 51 | # introspection support: 52 | __members__ = property(lambda self: self.__dir__()) 53 | __dir__ = new_method_proxy(dir) 54 | 55 | 56 | class SimpleLazyObject(LazyObject): 57 | """ 58 | A lazy object initialised from any function. 59 | 60 | Designed for compound objects of unknown type. For builtins or objects of 61 | known type, use django.utils.functional.lazy. 62 | """ 63 | def __init__(self, func): 64 | """ 65 | Pass in a callable that returns the object to be wrapped. 66 | 67 | If copies are made of the resulting SimpleLazyObject, which can happen 68 | in various circumstances within Django, then you must ensure that the 69 | callable can be safely run more than once and will return the same 70 | value. 71 | """ 72 | self.__dict__['_setupfunc'] = func 73 | super(SimpleLazyObject, self).__init__() 74 | 75 | def _setup(self): 76 | self._wrapped = self._setupfunc() 77 | 78 | __str__ = new_method_proxy(str) 79 | __unicode__ = new_method_proxy(unicode) 80 | 81 | def __deepcopy__(self, memo): 82 | if self._wrapped is empty: 83 | # We have to use SimpleLazyObject, not self.__class__, because the 84 | # latter is proxied. 85 | result = SimpleLazyObject(self._setupfunc) 86 | memo[id(self)] = result 87 | return result 88 | else: 89 | import copy 90 | return copy.deepcopy(self._wrapped, memo) 91 | 92 | # Need to pretend to be the wrapped class, for the sake of objects that care 93 | # about this (especially in equality tests) 94 | __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) 95 | __eq__ = new_method_proxy(operator.eq) 96 | __hash__ = new_method_proxy(hash) 97 | __nonzero__ = new_method_proxy(bool) -------------------------------------------------------------------------------- /philo/utils/registry.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import slug_re 2 | from django.template.defaultfilters import slugify 3 | from django.utils.encoding import smart_str 4 | 5 | 6 | class RegistryIterator(object): 7 | """ 8 | 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. 9 | 10 | :param registry: The object which provides the iterator at ``iterattr``. 11 | :param iterattr: The name of the method on ``registry`` that provides the iterator. 12 | :param transform: A function which will be called on each result from the wrapped iterator before it is returned. 13 | 14 | """ 15 | def __init__(self, registry, iterattr='__iter__', transform=lambda x:x): 16 | if not hasattr(registry, iterattr): 17 | raise AttributeError("Registry has no attribute %s" % iterattr) 18 | self.registry = registry 19 | self.iterattr = iterattr 20 | self.transform = transform 21 | 22 | def __iter__(self): 23 | return self 24 | 25 | def next(self): 26 | if not hasattr(self, '_iter'): 27 | self._iter = getattr(self.registry, self.iterattr)() 28 | 29 | return self.transform(self._iter.next()) 30 | 31 | def copy(self): 32 | """Returns a fresh copy of this iterator.""" 33 | return self.__class__(self.registry, self.iterattr, self.transform) 34 | 35 | 36 | class RegistrationError(Exception): 37 | """Raised if there is a problem registering a object with a :class:`Registry`""" 38 | pass 39 | 40 | 41 | class Registry(object): 42 | """Holds a registry of arbitrary objects by slug.""" 43 | 44 | def __init__(self): 45 | self._registry = {} 46 | 47 | def register(self, obj, slug=None, verbose_name=None): 48 | """ 49 | Register an object with the registry. 50 | 51 | :param obj: The object to register. 52 | :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``. 53 | :param verbose_name: The verbose name for the object. If ``verbose_name`` is ``None``, it will be looked for at ``obj.verbose_name``. 54 | :raises: :class:`RegistrationError` if a different object is already registered with ``slug``, or if ``slug`` is not a valid slug. 55 | 56 | """ 57 | verbose_name = verbose_name if verbose_name is not None else obj.verbose_name 58 | 59 | if slug is None: 60 | slug = getattr(obj, 'slug', slugify(verbose_name)) 61 | slug = smart_str(slug) 62 | 63 | if not slug_re.search(slug): 64 | raise RegistrationError(u"%s is not a valid slug." % slug) 65 | 66 | 67 | if slug in self._registry: 68 | reg = self._registry[slug] 69 | if reg['obj'] != obj: 70 | raise RegistrationError(u"A different object is already registered as `%s`" % slug) 71 | else: 72 | self._registry[slug] = { 73 | 'obj': obj, 74 | 'verbose_name': verbose_name 75 | } 76 | 77 | def unregister(self, obj, slug=None): 78 | """ 79 | Unregister an object from the registry. 80 | 81 | :param obj: The object to unregister. 82 | :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. 83 | :raises: :class:`RegistrationError` if ``slug`` is provided and an object other than ``obj`` is registered as ``slug``. 84 | 85 | """ 86 | if slug is not None: 87 | if slug in self._registry: 88 | if self._registry[slug]['obj'] == obj: 89 | del self._registry[slug] 90 | else: 91 | raise RegistrationError(u"`%s` is not registered as `%s`" % (obj, slug)) 92 | else: 93 | for slug, reg in self.items(): 94 | if obj == reg: 95 | del self._registry[slug] 96 | 97 | def items(self): 98 | """Returns a list of (slug, obj) items in the registry.""" 99 | return [(slug, self[slug]) for slug in self._registry] 100 | 101 | def values(self): 102 | """Returns a list of objects in the registry.""" 103 | return [self[slug] for slug in self._registry] 104 | 105 | def iteritems(self): 106 | """Returns a :class:`RegistryIterator` over the (slug, obj) pairs in the registry.""" 107 | return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1]['obj'])) 108 | 109 | def itervalues(self): 110 | """Returns a :class:`RegistryIterator` over the objects in the registry.""" 111 | return RegistryIterator(self._registry, 'itervalues', lambda x: x['obj']) 112 | 113 | def iterchoices(self): 114 | """Returns a :class:`RegistryIterator` over (slug, verbose_name) pairs for the registry.""" 115 | return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1]['verbose_name'])) 116 | choices = property(iterchoices) 117 | 118 | def get(self, key, default=None): 119 | """Returns the object registered with ``key`` or ``default`` if no object was registered.""" 120 | try: 121 | return self[key] 122 | except KeyError: 123 | return default 124 | 125 | def get_slug(self, obj, default=None): 126 | """Returns the slug used to register ``obj`` or ``default`` if ``obj`` was not registered.""" 127 | for slug, reg in self.iteritems(): 128 | if obj == reg: 129 | return slug 130 | return default 131 | 132 | def __getitem__(self, key): 133 | """Returns the obj registered with ``key``.""" 134 | return self._registry[key]['obj'] 135 | 136 | def __iter__(self): 137 | """Returns an iterator over the keys in the registry.""" 138 | return self._registry.__iter__() 139 | 140 | def __contains__(self, item): 141 | return self._registry.__contains__(item) -------------------------------------------------------------------------------- /philo/utils/templates.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from django.template import TextNode, VariableNode, Context 4 | from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext, ConstantIncludeNode 5 | from django.utils.datastructures import SortedDict 6 | 7 | from philo.templatetags.containers import ContainerNode 8 | 9 | 10 | LOADED_TEMPLATE_ATTR = '_philo_loaded_template' 11 | BLANK_CONTEXT = Context() 12 | 13 | 14 | def get_extended(self): 15 | return self.get_parent(BLANK_CONTEXT) 16 | 17 | 18 | def get_included(self): 19 | return self.template 20 | 21 | 22 | # We ignore the IncludeNode because it will never work in a blank context. 23 | setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended)) 24 | setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included)) 25 | 26 | 27 | def get_containers(template): 28 | # Build a tree of the templates we're using, placing the root template first. 29 | levels = build_extension_tree(template.nodelist) 30 | 31 | contentlet_specs = [] 32 | contentreference_specs = SortedDict() 33 | blocks = {} 34 | 35 | for level in reversed(levels): 36 | level.initialize() 37 | contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, level.contentlet_specs)) 38 | contentreference_specs.update(level.contentreference_specs) 39 | for name, block in level.blocks.items(): 40 | if block.block_super: 41 | blocks.setdefault(name, []).append(block) 42 | else: 43 | blocks[name] = [block] 44 | 45 | for block_list in blocks.values(): 46 | for block in block_list: 47 | block.initialize() 48 | contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, block.contentlet_specs)) 49 | contentreference_specs.update(block.contentreference_specs) 50 | 51 | return contentlet_specs, contentreference_specs 52 | 53 | 54 | class LazyContainerFinder(object): 55 | def __init__(self, nodes, extends=False): 56 | self.nodes = nodes 57 | self.initialized = False 58 | self.contentlet_specs = [] 59 | self.contentreference_specs = SortedDict() 60 | self.blocks = {} 61 | self.block_super = False 62 | self.extends = extends 63 | 64 | def process(self, nodelist): 65 | for node in nodelist: 66 | if self.extends: 67 | if isinstance(node, BlockNode): 68 | self.blocks[node.name] = block = LazyContainerFinder(node.nodelist) 69 | block.initialize() 70 | self.blocks.update(block.blocks) 71 | continue 72 | 73 | if isinstance(node, ContainerNode): 74 | if not node.references: 75 | self.contentlet_specs.append(node.name) 76 | else: 77 | if node.name not in self.contentreference_specs.keys(): 78 | self.contentreference_specs[node.name] = node.references 79 | continue 80 | 81 | if isinstance(node, VariableNode): 82 | if node.filter_expression.var.lookups == (u'block', u'super'): 83 | self.block_super = True 84 | 85 | if hasattr(node, 'child_nodelists'): 86 | for nodelist_name in node.child_nodelists: 87 | if hasattr(node, nodelist_name): 88 | nodelist = getattr(node, nodelist_name) 89 | self.process(nodelist) 90 | 91 | # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a 92 | # node as rendering an additional template. Philo monkeypatches the attribute onto 93 | # the relevant default nodes and declares it on any native nodes. 94 | if hasattr(node, LOADED_TEMPLATE_ATTR): 95 | loaded_template = getattr(node, LOADED_TEMPLATE_ATTR) 96 | if loaded_template: 97 | nodelist = loaded_template.nodelist 98 | self.process(nodelist) 99 | 100 | def initialize(self): 101 | if not self.initialized: 102 | self.process(self.nodes) 103 | self.initialized = True 104 | 105 | 106 | def build_extension_tree(nodelist): 107 | nodelists = [] 108 | extends = None 109 | for node in nodelist: 110 | if not isinstance(node, TextNode): 111 | if isinstance(node, ExtendsNode): 112 | extends = node 113 | break 114 | 115 | if extends: 116 | if extends.nodelist: 117 | nodelists.append(LazyContainerFinder(extends.nodelist, extends=True)) 118 | loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR) 119 | nodelists.extend(build_extension_tree(loaded_template.nodelist)) 120 | else: 121 | # Base case: root. 122 | nodelists.append(LazyContainerFinder(nodelist)) 123 | return nodelists -------------------------------------------------------------------------------- /philo/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.template import Template, Parser, Lexer, TOKEN_BLOCK, TOKEN_VAR, TemplateSyntaxError 5 | from django.utils import simplejson as json 6 | from django.utils.html import escape, mark_safe 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from philo.utils.templates import LOADED_TEMPLATE_ATTR 10 | 11 | 12 | #: Tags which are considered insecure and are therefore always disallowed by secure :class:`TemplateValidator` instances. 13 | INSECURE_TAGS = ( 14 | 'load', 15 | 'extends', 16 | 'include', 17 | 'debug', 18 | ) 19 | 20 | 21 | def json_validator(value): 22 | """Validates whether ``value`` is a valid json string.""" 23 | try: 24 | json.loads(value) 25 | except Exception, e: 26 | raise ValidationError(u'JSON decode error: %s' % e) 27 | 28 | 29 | class TemplateValidationParser(Parser): 30 | def __init__(self, tokens, allow=None, disallow=None, secure=True): 31 | super(TemplateValidationParser, self).__init__(tokens) 32 | 33 | allow, disallow = set(allow or []), set(disallow or []) 34 | 35 | if secure: 36 | disallow |= set(INSECURE_TAGS) 37 | 38 | self.allow, self.disallow, self.secure = allow, disallow, secure 39 | 40 | def parse(self, parse_until=None): 41 | if parse_until is None: 42 | parse_until = [] 43 | 44 | nodelist = self.create_nodelist() 45 | while self.tokens: 46 | token = self.next_token() 47 | # We only need to parse var and block tokens. 48 | if token.token_type == TOKEN_VAR: 49 | if not token.contents: 50 | self.empty_variable(token) 51 | 52 | filter_expression = self.compile_filter(token.contents) 53 | var_node = self.create_variable_node(filter_expression) 54 | self.extend_nodelist(nodelist, var_node,token) 55 | elif token.token_type == TOKEN_BLOCK: 56 | if token.contents in parse_until: 57 | # put token back on token list so calling code knows why it terminated 58 | self.prepend_token(token) 59 | return nodelist 60 | 61 | try: 62 | command = token.contents.split()[0] 63 | except IndexError: 64 | self.empty_block_tag(token) 65 | 66 | if (self.allow and command not in self.allow) or (self.disallow and command in self.disallow): 67 | self.disallowed_tag(command) 68 | 69 | self.enter_command(command, token) 70 | 71 | try: 72 | compile_func = self.tags[command] 73 | except KeyError: 74 | self.invalid_block_tag(token, command, parse_until) 75 | 76 | try: 77 | compiled_result = compile_func(self, token) 78 | except TemplateSyntaxError, e: 79 | if not self.compile_function_error(token, e): 80 | raise 81 | 82 | self.extend_nodelist(nodelist, compiled_result, token) 83 | self.exit_command() 84 | 85 | if parse_until: 86 | self.unclosed_block_tag(parse_until) 87 | 88 | return nodelist 89 | 90 | def disallowed_tag(self, command): 91 | if self.secure and command in INSECURE_TAGS: 92 | raise ValidationError('Tag "%s" is not permitted for security reasons.' % command) 93 | raise ValidationError('Tag "%s" is not permitted here.' % command) 94 | 95 | 96 | def linebreak_iter(template_source): 97 | # Cribbed from django/views/debug.py:18 98 | yield 0 99 | p = template_source.find('\n') 100 | while p >= 0: 101 | yield p+1 102 | p = template_source.find('\n', p+1) 103 | yield len(template_source) + 1 104 | 105 | 106 | class TemplateValidator(object): 107 | """ 108 | Validates whether a string represents valid Django template code. 109 | 110 | :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. 111 | :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. 112 | :param secure: If the validator is set to secure, it will automatically disallow the tag names listed in :const:`INSECURE_TAGS`. Defaults to ``True``. 113 | 114 | """ 115 | def __init__(self, allow=None, disallow=None, secure=True): 116 | self.allow = allow 117 | self.disallow = disallow 118 | self.secure = secure 119 | 120 | def __call__(self, value): 121 | try: 122 | self.validate_template(value) 123 | except ValidationError: 124 | raise 125 | except Exception, e: 126 | if hasattr(e, 'source') and isinstance(e, TemplateSyntaxError): 127 | origin, (start, end) = e.source 128 | template_source = origin.reload() 129 | upto = 0 130 | for num, next in enumerate(linebreak_iter(template_source)): 131 | if start >= upto and end <= next: 132 | raise ValidationError(mark_safe("Template code invalid: \"%s\" (%s:%d).
%s" % (escape(template_source[start:end]), origin.loadname, num, e))) 133 | upto = next 134 | raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e)) 135 | 136 | def validate_template(self, template_string): 137 | # We want to tokenize like normal, then use a custom parser. 138 | lexer = Lexer(template_string, None) 139 | tokens = lexer.tokenize() 140 | parser = TemplateValidationParser(tokens, self.allow, self.disallow, self.secure) 141 | 142 | for node in parser.parse(): 143 | template = getattr(node, LOADED_TEMPLATE_ATTR, None) -------------------------------------------------------------------------------- /philo/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.urlresolvers import resolve 3 | from django.http import Http404, HttpResponseRedirect 4 | from django.views.decorators.vary import vary_on_headers 5 | 6 | from philo.exceptions import MIDDLEWARE_NOT_CONFIGURED 7 | 8 | 9 | @vary_on_headers('Accept') 10 | def node_view(request, path=None, **kwargs): 11 | """ 12 | :func:`node_view` handles incoming requests by checking to make sure that: 13 | 14 | - the request has an attached :class:`.Node`. 15 | - the attached :class:`~philo.models.nodes.Node` handles any remaining path beyond its location. 16 | 17 | 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. 18 | 19 | Otherwise, :func:`node_view` will call the :class:`.Node`'s :meth:`~.Node.render_to_response` method, passing ``kwargs`` in as the ``extra_context``. 20 | 21 | """ 22 | if "philo.middleware.RequestNodeMiddleware" not in settings.MIDDLEWARE_CLASSES: 23 | raise MIDDLEWARE_NOT_CONFIGURED 24 | 25 | if not request.node: 26 | if settings.APPEND_SLASH and request.path != "/": 27 | path = request.path 28 | 29 | if path[-1] == "/": 30 | path = path[:-1] 31 | else: 32 | path += "/" 33 | 34 | view, args, kwargs = resolve(path) 35 | if view != node_view: 36 | return HttpResponseRedirect(path) 37 | raise Http404 38 | 39 | node = request.node 40 | subpath = request.node._subpath 41 | 42 | # Explicitly disallow trailing slashes if we are otherwise at a node's url. 43 | if node._path != "/" and node._path[-1] == "/" and subpath == "/": 44 | return HttpResponseRedirect(node.get_absolute_url()) 45 | 46 | if not node.handles_subpath(subpath): 47 | # If the subpath isn't handled, check settings.APPEND_SLASH. If 48 | # it's True, try to correct the subpath. 49 | if not settings.APPEND_SLASH: 50 | raise Http404 51 | 52 | if subpath[-1] == "/": 53 | subpath = subpath[:-1] 54 | else: 55 | subpath += "/" 56 | 57 | redirect_url = node.construct_url(subpath) 58 | 59 | if node.handles_subpath(subpath): 60 | return HttpResponseRedirect(redirect_url) 61 | 62 | # Perhaps there is a non-philo view at this address. Can we 63 | # resolve *something* there besides node_view? If not, 64 | # raise a 404. 65 | view, args, kwargs = resolve(redirect_url) 66 | 67 | if view == node_view: 68 | raise Http404 69 | else: 70 | return HttpResponseRedirect(redirect_url) 71 | 72 | return node.render_to_response(request, kwargs) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup, find_packages 4 | 5 | 6 | version = __import__('philo').VERSION 7 | 8 | 9 | setup( 10 | name = 'philo', 11 | version = '.'.join([str(v) for v in version]), 12 | url = "http://philocms.org/", 13 | description = "A foundation for developing web content management systems.", 14 | long_description = open(os.path.join(os.path.dirname(__file__), 'README')).read(), 15 | maintainer = "iThink Software", 16 | maintainer_email = "contact@ithinksw.com", 17 | packages = find_packages(), 18 | include_package_data=True, 19 | 20 | classifiers = [ 21 | 'Environment :: Web Environment', 22 | 'Framework :: Django', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: ISC License (ISCL)', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 28 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 29 | ], 30 | platforms = ['OS Independent'], 31 | license = 'ISC License (ISCL)', 32 | 33 | install_requires = [ 34 | 'django>=1.3', 35 | 'django-mptt>0.4.2,==dev', 36 | ], 37 | extras_require = { 38 | 'docs': ["sphinx>=1.0"], 39 | 'grappelli': ['django-grappelli>=2.3'], 40 | 'migrations': ['south>=0.7.2'], 41 | 'waldo-recaptcha': ['recaptcha-django'], 42 | 'sobol-eventlet': ['eventlet'], 43 | 'sobol-scrape': ['BeautifulSoup'], 44 | 'penfield': ['django-taggit>=0.9'], 45 | }, 46 | dependency_links = [ 47 | 'https://github.com/django-mptt/django-mptt/tarball/master#egg=django-mptt-dev' 48 | ] 49 | ) 50 | --------------------------------------------------------------------------------