From: Stephen Burrows Date: Sat, 8 Oct 2011 01:07:09 +0000 (-0700) Subject: Merge remote-tracking branch 'lapilofu/hotfix/manifest' into release X-Git-Tag: philo-0.9.1^2~2^2~1 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/6de576dc999886f300c5abc74f4a2433a884d3b8?hp=38b3b029deb23c1dc242d854fd806c076229e7fe Merge remote-tracking branch 'lapilofu/hotfix/manifest' into release --- diff --git a/README b/README index dbd9cc2..71e8392 100644 --- a/README +++ b/README @@ -3,14 +3,11 @@ Philo is a foundation for developing web content management systems. Prerequisites: * Python 2.5.4+ * Django 1.3+ - * django-mptt e734079+ + * django-mptt e734079+ * (Optional) django-grappelli 2.0+ * (Optional) south 0.7.2+ - * (Optional) recaptcha-django r6 - -To contribute, please visit the project website and/or make a fork of the git repository on GitHub or Gitorious . Feel free to join us on IRC at irc://irc.oftc.net/#philo. - + * (philo.contrib.penfield) django-taggit 0.9.3+ + * (philo.contrib.waldo, optional) recaptcha-django r6 ==== Using philo @@ -22,4 +19,4 @@ After installing philo and mptt on your python path, make sure to complete the f 3. include 'philo.urls' somewhere in your urls.py file. 4. Optionally add a root node to your current Site. -Philo should be ready to go! +Philo should be ready to go! All that's left is to learn more and contribute . diff --git a/README.markdown b/README.markdown index 91a8115..5734eb9 100644 --- a/README.markdown +++ b/README.markdown @@ -7,7 +7,8 @@ Prerequisites: * [django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>](https://github.com/django-mptt/django-mptt/) * (Optional) [django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>](http://code.google.com/p/django-grappelli/) * (Optional) [south 0.7.2+ <http://south.aeracode.org/)](http://south.aeracode.org/) - * (Optional) [recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>](http://code.google.com/p/recaptcha-django/) + * (philo.contrib.penfield) [django-taggit 0.9.3+ <https://github.com/alex/django-taggit>](https://github.com/alex/django-taggit) + * (philo.contrib.waldo, optional) [recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>](http://code.google.com/p/recaptcha-django/) To contribute, please visit the [project website](http://project.philocms.org/) and/or make a fork of the git repository on [GitHub](http://github.com/ithinksw/philo) or [Gitorious](http://gitorious .org/ithinksw/philo). Feel free to join us on IRC at [irc://irc.oftc.net/#philo](irc://irc.oftc.net/#philo). @@ -22,4 +23,4 @@ After installing philo and mptt on your python path, make sure to complete the f 3. include 'philo.urls' somewhere in your urls.py file. 4. Optionally add a root node to your current Site. -Philo should be ready to go! +Philo should be ready to go! All that's left is to [learn more](http://philo.readthedocs.org) and [contribute](http://philo.readthedocs.org/en/latest/contribute.html). diff --git a/docs/cla/ithinksw-ccla.txt b/docs/cla/ithinksw-ccla.txt new file mode 100644 index 0000000..b76d6e4 --- /dev/null +++ b/docs/cla/ithinksw-ccla.txt @@ -0,0 +1,140 @@ + iThink Software + Corporate Contributor License Agreement ("Agreement") v1.0 + +Thank you for your interest in iThink Software. In order to clarify +the intellectual property license granted with Contributions from +any person or entity, iThink Software must have a Contributor +License Agreement ("CLA") on file that has been signed by each +Contributor, indicating agreement to the license terms below. This +license is for your protection as a Contributor as well as the +protection of iThink Software and its users; it does not change +your rights to use your own Contributions for any other purpose. + +This version of the Agreement allows an entity (the "Corporation") +to submit Contributions to iThink Software, to authorize Contributions +submitted by its designated employees to iThink Software, and to grant +copyright and patent licenses thereto. + +If you have not already done so, please complete and sign, then scan +and email a pdf file of this Agreement to contact@ithinksw.com. +Alternatively, you may send an original signed Agreement to +iThink Software, 261 West Lorain Street, Oberlin, OH 44074, U.S.A. +Please read this document carefully before signing and +keep a copy for your records. + + Corporation name: ______________________________________________ + + Corporation address: ______________________________________________ + + ______________________________________________ + + ______________________________________________ + + Point of Contact: ______________________________________________ + + E-Mail: ______________________________________________ + + Telephone: ____________________ Fax: ____________________ + + +You accept and agree to the following terms and conditions for Your +present and future Contributions submitted to iThink Software. Except +for the license granted herein to iThink Software and recipients of +software distributed by iThink Software, You reserve all right, +title, and interest in and to Your Contributions. + +1. Definitions. + + "You" (or "Your") shall mean the copyright owner or legal entity + authorized by the copyright owner that is making this Agreement + with iThink Software. For legal entities, the entity making a + Contribution and all other entities that control, are controlled + by, or are under common control with that entity are considered to + be a single Contributor. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "Contribution" shall mean any original work of authorship, + including any modifications or additions to an existing work, that + is intentionally submitted by You to iThink Software for inclusion + in, or documentation of, any of the products owned or managed by + iThink Software (the "Work"). For the purposes of this definition, + "submitted" means any form of electronic, verbal, or written + communication sent to iThink Software or its representatives, + including but not limited to communication on electronic mailing + lists, source code control systems, and issue tracking systems that + are managed by, or on behalf of, iThink Software for the purpose of + discussing and improving the Work, but excluding communication that + is conspicuously marked or otherwise designated in writing by You + as "Not a Contribution." + +2. Grant of Copyright License. Subject to the terms and conditions of + this Agreement, You hereby grant to iThink Software and to + recipients of software distributed by iThink Software a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare derivative works of, + publicly display, publicly perform, sublicense, and distribute Your + Contributions and such derivative works. + +3. Grant of Patent License. Subject to the terms and conditions of + this Agreement, You hereby grant to iThink Software and to + recipients of software distributed by iThink Software a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have + made, use, offer to sell, sell, import, and otherwise transfer the + Work, where such license applies only to those patent claims + licensable by You that are necessarily infringed by Your + Contribution(s) alone or by combination of Your Contribution(s) + with the Work to which such Contribution(s) were submitted. If any + entity institutes patent litigation against You or any other entity + (including a cross-claim or counterclaim in a lawsuit) alleging + that your Contribution, or the Work to which you have contributed, + constitutes direct or contributory patent infringement, then any + patent licenses granted to that entity under this Agreement for + that Contribution or Work shall terminate as of the date such + litigation is filed. + +4. You represent that You are legally entitled to grant the above + license. You represent further that each employee of the + Corporation designated on Schedule A below (or in a subsequent + written modification to that Schedule) is authorized to submit + Contributions on behalf of the Corporation. + +5. You represent that each of Your Contributions is Your original + creation (see section 7 for submissions on behalf of others). + +6. You are not expected to provide support for Your Contributions, + except to the extent You desire to provide support. You may provide + support for free, for a fee, or not at all. Unless required by + applicable law or agreed to in writing, You provide Your + Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + OF ANY KIND, either express or implied, including, without + limitation, any warranties or conditions of TITLE, NON- + INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +7. Should You wish to submit work that is not Your original creation, + You may submit it to iThink Software separately from any + Contribution, identifying the complete details of its source and of + any license or other restriction (including, but not limited to, + related patents, trademarks, and license agreements) of which you + are personally aware, and conspicuously marking the work as + "Submitted on behalf of a third-party: [named here]". + +8. It is your responsibility to notify iThink Software when any change + is required to the list of designated employees authorized to submit + Contributions on behalf of the Corporation, or to the Corporation's + Point of Contact with iThink Software. + + +Please sign: __________________________________ Date: _______________ + +Title: __________________________________ + +Corporation: __________________________________ + + +Schedule A + + [Initial list of designated employees.] diff --git a/docs/cla/ithinksw-icla.txt b/docs/cla/ithinksw-icla.txt new file mode 100644 index 0000000..9c49363 --- /dev/null +++ b/docs/cla/ithinksw-icla.txt @@ -0,0 +1,130 @@ + iThink Software + Individual Contributor License Agreement ("Agreement") v1.0.1 + +Thank you for your interest in iThink Software. In order to clarify +the intellectual property license granted with Contributions from +any person or entity, iThink Software must have a Contributor +License Agreement ("CLA") on file that has been signed by each +Contributor, indicating agreement to the license terms below. This +license is for your protection as a Contributor as well as the +protection of iThink Software and its users; it does not change +your rights to use your own Contributions for any other purpose. +If you have not already done so, please complete and sign, then scan +and email a pdf file of this Agreement to contact@ithinksw.com. +Alternatively, you may send an original signed Agreement to +iThink Software, 261 West Lorain Street, Oberlin, OH 44074, U.S.A. +Please read this document carefully before signing and +keep a copy for your records. + + Full name: ______________________________________________________ + + Mailing Address: ________________________________________________ + + _________________________________________________________________ + + Country: ______________________________________________________ + + Telephone: ______________________________________________________ + + Facsimile: ______________________________________________________ + + E-Mail: ______________________________________________________ + +You accept and agree to the following terms and conditions for Your +present and future Contributions submitted to iThink Software. Except +for the license granted herein to iThink Software and recipients of +software distributed by iThink Software, You reserve all right, +title, and interest in and to Your Contributions. + +1. Definitions. + + "You" (or "Your") shall mean the copyright owner or legal entity + authorized by the copyright owner that is making this Agreement + with iThink Software. For legal entities, the entity making a + Contribution and all other entities that control, are controlled + by, or are under common control with that entity are considered to + be a single Contributor. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "Contribution" shall mean any original work of authorship, + including any modifications or additions to an existing work, that + is intentionally submitted by You to iThink Software for inclusion + in, or documentation of, any of the products owned or managed by + iThink Software (the "Work"). For the purposes of this definition, + "submitted" means any form of electronic, verbal, or written + communication sent to iThink Software or its representatives, + including but not limited to communication on electronic mailing + lists, source code control systems, and issue tracking systems that + are managed by, or on behalf of, iThink Software for the purpose of + discussing and improving the Work, but excluding communication that + is conspicuously marked or otherwise designated in writing by You + as "Not a Contribution." + +2. Grant of Copyright License. Subject to the terms and conditions of + this Agreement, You hereby grant to iThink Software and to + recipients of software distributed by iThink Software a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare derivative works of, + publicly display, publicly perform, sublicense, and distribute Your + Contributions and such derivative works. + +3. Grant of Patent License. Subject to the terms and conditions of + this Agreement, You hereby grant to iThink Software and to + recipients of software distributed by iThink Software a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have + made, use, offer to sell, sell, import, and otherwise transfer the + Work, where such license applies only to those patent claims + licensable by You that are necessarily infringed by Your + Contribution(s) alone or by combination of Your Contribution(s) + with the Work to which such Contribution(s) were submitted. If any + entity institutes patent litigation against You or any other entity + (including a cross-claim or counterclaim in a lawsuit) alleging + that your Contribution, or the Work to which you have contributed, + constitutes direct or contributory patent infringement, then any + patent licenses granted to that entity under this Agreement for + that Contribution or Work shall terminate as of the date such + litigation is filed. + +4. You represent that you are legally entitled to grant the above + license. If your employer(s) has rights to intellectual property + that you create that includes your Contributions, you represent + that you have received permission to make Contributions on behalf + of that employer, that your employer has waived such rights for + your Contributions to iThink Software, or that your employer has + executed a separate Corporate CLA with iThink Software. + +5. You represent that each of Your Contributions is Your original + creation (see section 7 for submissions on behalf of others). You + represent that Your Contribution submissions include complete + details of any third-party license or other restriction (including, + but not limited to, related patents and trademarks) of which you + are personally aware and which are associated with any part of Your + Contributions. + +6. You are not expected to provide support for Your Contributions, + except to the extent You desire to provide support. You may provide + support for free, for a fee, or not at all. Unless required by + applicable law or agreed to in writing, You provide Your + Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + OF ANY KIND, either express or implied, including, without + limitation, any warranties or conditions of TITLE, NON- + INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +7. Should You wish to submit work that is not Your original creation, + You may submit it to iThink Software separately from any + Contribution, identifying the complete details of its source and of + any license or other restriction (including, but not limited to, + related patents, trademarks, and license agreements) of which you + are personally aware, and conspicuously marking the work as + "Submitted on behalf of a third-party: [named here]". + +8. You agree to notify iThink Software of any facts or circumstances of + which you become aware that would make these representations + inaccurate in any respect. + + +Please sign: __________________________________ Date: ________________ diff --git a/docs/conf.py b/docs/conf.py index 4e6a624..2e703d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,7 +62,7 @@ copyright = u'2009-2011, iThink Software' # # The short X.Y version. from philo import VERSION -version = '%s.%s' % (VERSION[0], VERSION[1]) +version = '.'.join([str(v) for v in VERSION]) # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/contrib/intro.rst b/docs/contrib/intro.rst index 3b97ecd..e833317 100644 --- a/docs/contrib/intro.rst +++ b/docs/contrib/intro.rst @@ -9,5 +9,6 @@ Contrib apps shipherd sobol waldo + winer .. automodule:: philo.contrib diff --git a/docs/contrib/penfield.rst b/docs/contrib/penfield.rst index d774dcb..87073b9 100644 --- a/docs/contrib/penfield.rst +++ b/docs/contrib/penfield.rst @@ -27,18 +27,6 @@ Newsletters .. autoclass:: philo.contrib.penfield.models.NewsletterView :members: -Abstract Syndication -++++++++++++++++++++ - -.. autoclass:: philo.contrib.penfield.models.FeedView - :members: - -.. automodule:: philo.contrib.penfield.exceptions - :members: - -.. automodule:: philo.contrib.penfield.middleware - :members: - Template filters ++++++++++++++++ diff --git a/docs/contrib/shipherd.rst b/docs/contrib/shipherd.rst index 7d2eaf7..9e03f67 100644 --- a/docs/contrib/shipherd.rst +++ b/docs/contrib/shipherd.rst @@ -31,18 +31,9 @@ Models :members: Navigation, NavigationItem, NavigationMapper :show-inheritance: -Navigation caching ------------------- - .. autoclass:: NavigationManager :members: -.. autoclass:: NavigationItemManager - :members: - -.. autoclass:: NavigationCacheQuerySet - :members: - Template tags +++++++++++++ diff --git a/docs/contrib/winer.rst b/docs/contrib/winer.rst new file mode 100644 index 0000000..4b8a670 --- /dev/null +++ b/docs/contrib/winer.rst @@ -0,0 +1,15 @@ +Winer +===== + +.. automodule:: philo.contrib.winer + +.. automodule:: philo.contrib.winer.models + + .. autoclass:: FeedView + :members: + +.. automodule:: philo.contrib.winer.exceptions + :members: + +.. automodule:: philo.contrib.winer.middleware + :members: \ No newline at end of file diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..8d905b1 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,33 @@ +Contributing to Philo +===================== + +So you want to contribute to Philo? That's great! Here's some ways you can get started: + +* **Report bugs and request features.** :mod:`philo` uses a Redmine installation located at `http://ithinksw.org/projects/philo/issues `_ for issue tracking. In order to report an issue, you will need to register for an account with the tracker. +* **Contribute code.** Philo uses git to manage its code. You can fork philo's repository either on `GitHub `_ or `Gitorious `_. If you are contributing to Philo, you will need to submit a :ref:`Contributor License Agreement `. +* **Join the discussion** on IRC at `irc://irc.oftc.net/#philo `_ if you have any questions or suggestions or just want to chat about the project. You can also keep in touch via :mod:`philo`'s mailing lists: `philo@ithinksw.org `_ and `philo-devel@ithinksw.org `_. + + +Branches and Code Style ++++++++++++++++++++++++ +We use `A successful Git branching model`__ with the blessed repository. To make things easier, you probably should too. This means that you should work on and against the develop branch in most cases, and leave it to the release manager to create the commits on the master branch if and when necessary. Regardless of what you do, the release manager will usually merge your changes into the develop branch unless you explicitly note they should be treated otherwise. + +__ http://nvie.com/posts/a-successful-git-branching-model/ + +Philo adheres to PEP8 for its code style, with two exceptions: tabs are used rather than spaces, and lines are not truncated at 79 characters. + +.. _cla: + +Licensing and Legal ++++++++++++++++++++ + +In order for the release manager to merge your changes into the blessed repository, you will need to submit a signed CLA. Our CLAs are based on the Apache Software Foundation's CLAs, which is the same source as the `Django Project's CLAs`_. You might, therefore, find the `Django Project's CLA FAQ`_. helpful. + +.. _`Django Project's CLAs`: https://www.djangoproject.com/foundation/cla/ +.. _`Django Project's CLA FAQ`: https://www.djangoproject.com/foundation/cla/faq/ + +If you are an individual not doing work for an employer, then you can simply submit the :download:`Individual CLA `. + +If you are doing work for an employer, they will need to submit the :download:`Corporate CLA ` and you will need to submit the Individual CLA :download:`Individual CLA ` as well. + +Both documents include information on how to submit them. diff --git a/docs/index.rst b/docs/index.rst index 7e960a0..26fe771 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,18 +8,17 @@ Welcome to Philo's documentation! ================================= -Philo is a foundation for developing web content management systems. +Philo is a foundation for developing web content management systems. Please, read the :doc:`notes for our latest release `. Prerequisites: * `Python 2.5.4+ `_ -* `Django 1.2+ `_ +* `Django 1.3+ `_ * `django-mptt e734079+ `_ * (Optional) `django-grappelli 2.0+ `_ * (Optional) `south 0.7.2+ `_ -* (Optional) `recaptcha-django r6 `_ - -To contribute, please visit the `project website `_ and/or make a fork of the git repository on `GitHub `_ or `Gitorious `_. Feel free to join us on IRC at `irc://irc.oftc.net/#philo `_. +* (:mod:`philo.contrib.penfield`) `django-taggit 0.9.3+ `_ +* (:mod:`philo.contrib.waldo`, optional) `recaptcha-django r6 `_ Contents ++++++++ @@ -39,6 +38,7 @@ Contents forms loaders contrib/intro + contributing Indices and tables ++++++++++++++++++ diff --git a/docs/models/miscellaneous.rst b/docs/models/miscellaneous.rst index 80b654b..005e112 100644 --- a/docs/models/miscellaneous.rst +++ b/docs/models/miscellaneous.rst @@ -2,7 +2,4 @@ Miscellaneous Models ============================= .. autoclass:: philo.models.nodes.TargetURLModel :members: - :exclude-members: get_target_url - -.. autoclass:: philo.models.base.Tag - :members: \ No newline at end of file + :exclude-members: get_target_url \ No newline at end of file diff --git a/docs/releases/0.9.1.rst b/docs/releases/0.9.1.rst new file mode 100644 index 0000000..2003350 --- /dev/null +++ b/docs/releases/0.9.1.rst @@ -0,0 +1,13 @@ +Philo version 0.9.1 release notes +================================= + +The primary focus of the 0.9.1 release has been streamlining and optimization. Requests in 0.9.1 are served two to three times faster than in 0.9. A number of bugs in code, documentation, and migrations have also been corrected. + +New Features and backwards-incompatible changes ++++++++++++++++++++++++++++++++++++++++++++++++ + +* :class:`.FeedView` and related syndication code has been migrated to :mod:`philo.contrib.winer` so it can be used independently of :mod:`philo.contrib.penfield`. +* :class:`.FeedView` has been refactored; the result of :meth:`.FeedView.get_object` is now passed into :meth:`.FeedView.get_items` to allow for more flexibility and for :class:`.FeedView`\ s which do not have a :class:`ForeignKey` relationship to the items that the feed is for. +* :class:`.BlogView` has been refactored to take advantage of the more flexible :meth:`~.BlogView.get_object` method. Many of its former entry-fetching methods have been removed. +* :class:`.EmbedWidget` is now used for text fields on, for example, :class:`BlogEntry`. The widget allows javascript-based generation of embed tags for model instances, using the same popup interface as raw id fields. +* :class:`philo.models.Tag` has been removed in favor of an optional requirement for ``django-taggit``. This will allow :mod:`philo` to remain more focused. Migrations are provided for :mod:`philo.contrib.penfield` which losslessly convert :mod:`philo` :class:`~philo.models.Tag`\ s to ``django-taggit`` :class:`Tags`. diff --git a/docs/tutorials/shipherd.rst b/docs/tutorials/shipherd.rst index 3a62cb0..914a6bb 100644 --- a/docs/tutorials/shipherd.rst +++ b/docs/tutorials/shipherd.rst @@ -3,7 +3,21 @@ Using Shipherd in the Admin The navigation mechanism is fairly complex; unfortunately, there's no real way around that - without a lot of equally complex code that you are quite welcome to write and contribute! ;-) -For this guide, we'll assume that you have the setup described in :doc:`getting-started`. We'll be adding a main :class:`.Navigation` to the root :class:`.Node` and making it display as part of the :class:`.Template`. Before getting started, make sure that you've added :mod:`philo.contrib.shipherd` to your :setting:`INSTALLED_APPS`. +For this guide, we'll assume that you have the setup described in :doc:`getting-started`. We'll be adding a main :class:`.Navigation` to the root :class:`.Node` and making it display as part of the :class:`.Template`. + +Before getting started, make sure that you've added :mod:`philo.contrib.shipherd` to your :setting:`INSTALLED_APPS`. :mod:`~philo.contrib.shipherd` template tags also require the request context processor, so make sure to set :setting:`TEMPLATE_CONTEXT_PROCESSORS` appropriately:: + + TEMPLATE_CONTEXT_PROCESSORS = ( + # Defaults + "django.contrib.auth.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.contrib.messages.context_processors.messages" + ... + "django.core.context_processors.request" + ) Creating the Navigation +++++++++++++++++++++++ @@ -19,7 +33,7 @@ Displaying the Navigation All you need to do now is show the navigation in the template! This is quite easy, using the :ttag:`~philo.contrib.shipherd.templatetags.shipherd.recursenavigation` templatetag. For now we'll keep it simple. Adjust the "Hello World Template" to look like this:: - + {% load shipherd %} {% container page_title %} @@ -27,9 +41,9 @@ All you need to do now is show the navigation in the template! This is quite eas
    {% recursenavigation node "main" %} - {{ item.text }} + {{ item.text }} - {% endnavigation %} + {% endrecursenavigation %}
{% container page_body as content %} {% if content %} diff --git a/philo/__init__.py b/philo/__init__.py index c07c373..2ad4062 100644 --- a/philo/__init__.py +++ b/philo/__init__.py @@ -1 +1 @@ -VERSION = (0, 9) +VERSION = (0, 9, "1rc") diff --git a/philo/admin/base.py b/philo/admin/base.py index 81916ab..d966c39 100644 --- a/philo/admin/base.py +++ b/philo/admin/base.py @@ -6,10 +6,9 @@ from django.utils import simplejson as json from django.utils.html import escape from mptt.admin import MPTTModelAdmin -from philo.models import Tag, Attribute +from philo.models import Attribute from philo.models.fields.entities import ForeignKeyAttribute, ManyToManyAttribute from philo.admin.forms.attributes import AttributeForm, AttributeInlineFormSet -from philo.admin.widgets import TagFilteredSelectMultiple from philo.forms.entities import EntityForm, proxy_fields_for_entity_model @@ -137,38 +136,4 @@ class EntityAdmin(admin.ModelAdmin): class TreeEntityAdmin(EntityAdmin, MPTTModelAdmin): - pass - - -class TagAdmin(admin.ModelAdmin): - list_display = ('name', 'slug') - prepopulated_fields = {"slug": ("name",)} - search_fields = ["name"] - - def response_add(self, request, obj, post_url_continue='../%s/'): - # If it's an ajax request, return a json response containing the necessary information. - if request.is_ajax(): - return HttpResponse(json.dumps({'pk': escape(obj._get_pk_val()), 'unicode': escape(obj)})) - return super(TagAdmin, self).response_add(request, obj, post_url_continue) - - -class AddTagAdmin(admin.ModelAdmin): - def formfield_for_manytomany(self, db_field, request=None, **kwargs): - """ - Get a form Field for a ManyToManyField. - """ - # If it uses an intermediary model that isn't auto created, don't show - # a field in admin. - if not db_field.rel.through._meta.auto_created: - return None - - if db_field.rel.to == Tag and db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): - opts = Tag._meta - if request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()): - kwargs['widget'] = TagFilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) - return db_field.formfield(**kwargs) - - return super(AddTagAdmin, self).formfield_for_manytomany(db_field, request, **kwargs) - - -admin.site.register(Tag, TagAdmin) \ No newline at end of file + pass \ No newline at end of file diff --git a/philo/admin/forms/attributes.py b/philo/admin/forms/attributes.py index 5372ab3..4a6dd67 100644 --- a/philo/admin/forms/attributes.py +++ b/philo/admin/forms/attributes.py @@ -21,7 +21,7 @@ class AttributeForm(ModelForm): # This is necessary because model forms store changes to self.instance in their clean method. # Mutter mutter. value = self.instance.value - self._cached_value_ct = self.instance.value_content_type + self._cached_value_ct_id = self.instance.value_content_type_id self._cached_value = value # If there is a value, pull in its fields. @@ -32,7 +32,7 @@ class AttributeForm(ModelForm): def save(self, *args, **kwargs): # At this point, the cleaned_data has already been stored on self.instance. - if self.instance.value_content_type != self._cached_value_ct: + if self.instance.value_content_type_id != self._cached_value_ct_id: # The value content type has changed. Clear the old value, if there was one. if self._cached_value: self._cached_value.delete() @@ -42,8 +42,8 @@ class AttributeForm(ModelForm): # Now create a new value instance so that on next instantiation, the form will # know what fields to add. - if self.instance.value_content_type is not None: - self.instance.value = self.instance.value_content_type.model_class().objects.create() + if self.instance.value_content_type_id is not None: + self.instance.value = ContentType.objects.get_for_id(self.instance.value_content_type_id).model_class().objects.create() elif self.instance.value is not None: # The value content type is the same, but one of the value fields has changed. diff --git a/philo/admin/forms/containers.py b/philo/admin/forms/containers.py index 987524f..0f8d117 100644 --- a/philo/admin/forms/containers.py +++ b/philo/admin/forms/containers.py @@ -1,12 +1,11 @@ from django import forms -from django.contrib.admin.widgets import AdminTextareaWidget from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from django.forms.models import ModelForm, BaseInlineFormSet, BaseModelFormSet from django.forms.formsets import TOTAL_FORM_COUNT from django.utils.datastructures import SortedDict -from philo.admin.widgets import ModelLookupWidget +from philo.admin.widgets import ModelLookupWidget, EmbedWidget from philo.models import Contentlet, ContentReference @@ -26,7 +25,7 @@ class ContainerForm(ModelForm): class ContentletForm(ContainerForm): - content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content') + content = forms.CharField(required=False, widget=EmbedWidget, label='Content') def should_delete(self): # Delete iff: the data has changed and is now empty. diff --git a/philo/admin/pages.py b/philo/admin/pages.py index 3e8f0f1..4cdd37b 100644 --- a/philo/admin/pages.py +++ b/philo/admin/pages.py @@ -5,15 +5,14 @@ from django.contrib import admin from philo.admin.base import COLLAPSE_CLASSES, TreeEntityAdmin from philo.admin.forms.containers import * from philo.admin.nodes import ViewAdmin +from philo.admin.widgets import EmbedWidget +from philo.models.fields import TemplateField from philo.models.pages import Page, Template, Contentlet, ContentReference -class ContentletInline(admin.StackedInline): - model = Contentlet +class ContainerInline(admin.StackedInline): extra = 0 max_num = 0 - formset = ContentletInlineFormSet - form = ContentletForm can_delete = False classes = ('collapse-open', 'collapse','open') if 'grappelli' in settings.INSTALLED_APPS: @@ -22,18 +21,16 @@ class ContentletInline(admin.StackedInline): template = 'admin/philo/edit_inline/tabular_container.html' -class ContentReferenceInline(admin.StackedInline): +class ContentletInline(ContainerInline): + model = Contentlet + formset = ContentletInlineFormSet + form = ContentletForm + + +class ContentReferenceInline(ContainerInline): model = ContentReference - extra = 0 - max_num = 0 formset = ContentReferenceInlineFormSet form = ContentReferenceForm - can_delete = False - classes = ('collapse-open', 'collapse','open') - if 'grappelli' in settings.INSTALLED_APPS: - template = 'admin/philo/edit_inline/grappelli_tabular_container.html' - else: - template = 'admin/philo/edit_inline/tabular_container.html' class PageAdmin(ViewAdmin): @@ -73,6 +70,9 @@ class TemplateAdmin(TreeEntityAdmin): 'fields': ('mimetype',) }), ) + formfield_overrides = { + TemplateField: {'widget': EmbedWidget} + } save_on_top = True save_as = True list_display = ('__unicode__', 'slug', 'get_path',) diff --git a/philo/admin/widgets.py b/philo/admin/widgets.py index c753850..3d7d64b 100644 --- a/philo/admin/widgets.py +++ b/philo/admin/widgets.py @@ -1,6 +1,7 @@ from django import forms from django.conf import settings -from django.contrib.admin.widgets import FilteredSelectMultiple, url_params_from_lookup_dict +from django.contrib.admin.widgets import url_params_from_lookup_dict +from django.utils import simplejson as json from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.text import truncate_words @@ -40,29 +41,28 @@ class ModelLookupWidget(forms.TextInput): return mark_safe(u''.join(output)) -class TagFilteredSelectMultiple(FilteredSelectMultiple): - """ - A SelectMultiple with a JavaScript filter interface. - - Note that the resulting JavaScript assumes that the jsi18n - catalog has been loaded in the page - """ +class EmbedWidget(forms.Textarea): + """A form widget with the HTML class embedding and an embedded list of content-types.""" + def __init__(self, attrs=None): + from philo.models import value_content_type_limiter + + content_types = value_content_type_limiter.classes + data = [] + + for content_type in content_types: + data.append({'app_label': content_type._meta.app_label, 'object_name': content_type._meta.object_name.lower(), 'verbose_name': unicode(content_type._meta.verbose_name)}) + + json_ = json.dumps(data) + + default_attrs = {'class': 'embedding vLargeTextField', 'data-content-types': json_ } + + if attrs: + default_attrs.update(attrs) + + super(EmbedWidget, self).__init__(default_attrs) + class Media: - js = ( - settings.ADMIN_MEDIA_PREFIX + "js/core.js", - settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js", - settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js", - "philo/js/TagCreation.js", - ) - - def render(self, name, value, attrs=None, choices=()): - if attrs is None: attrs = {} - attrs['class'] = 'selectfilter' - if self.is_stacked: attrs['class'] += 'stacked' - output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)] - output.append(u'\n' % \ - (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX, name)) - return mark_safe(u''.join(output)) \ No newline at end of file + css = { + 'all': ('philo/css/EmbedWidget.css',), + } + js = ('philo/js/EmbedWidget.js',) diff --git a/philo/contrib/__init__.py b/philo/contrib/__init__.py index d6c4be4..0cde6d5 100644 --- a/philo/contrib/__init__.py +++ b/philo/contrib/__init__.py @@ -2,9 +2,10 @@ """ Following Python and Django’s “batteries included” philosophy, Philo includes a number of optional packages that simplify common website structures: -* :mod:`~philo.contrib.penfield` — Basic philo syndication, and blog and newsletter management. +* :mod:`~philo.contrib.penfield` — Basic blog and newsletter management. * :mod:`~philo.contrib.shipherd` — Powerful site navigation. * :mod:`~philo.contrib.sobol` — Custom web and database searches. * :mod:`~philo.contrib.waldo` — Custom authentication systems. +* :mod:`~philo.contrib.winer` — Abstract framework for Philo-based syndication. """ \ No newline at end of file diff --git a/philo/contrib/julian/migrations/0001_initial.py b/philo/contrib/julian/migrations/0001_initial.py index 3236095..21e8778 100644 --- a/philo/contrib/julian/migrations/0001_initial.py +++ b/philo/contrib/julian/migrations/0001_initial.py @@ -219,14 +219,6 @@ class Migration(SchemaMigration): 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) }, - 'oberlin.locationcoordinates': { - 'Meta': {'unique_together': "(('location_ct', 'location_pk'),)", 'object_name': 'LocationCoordinates'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'latitude': ('django.db.models.fields.FloatField', [], {}), - 'location_ct': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'location_pk': ('django.db.models.fields.TextField', [], {}), - 'longitude': ('django.db.models.fields.FloatField', [], {}) - }, 'philo.attribute': { 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), diff --git a/philo/contrib/julian/models.py b/philo/contrib/julian/models.py index 62b938a..df49da5 100644 --- a/philo/contrib/julian/models.py +++ b/philo/contrib/julian/models.py @@ -13,9 +13,11 @@ from django.db import models from django.db.models.query import QuerySet from django.http import HttpResponse, Http404 from django.utils.encoding import force_unicode +from taggit.managers import TaggableManager from philo.contrib.julian.feedgenerator import ICalendarFeed -from philo.contrib.penfield.models import FeedView, FEEDS +from philo.contrib.winer.models import FeedView +from philo.contrib.winer.feeds import registry from philo.exceptions import ViewCanNotProvideSubpath from philo.models import Tag, Entity, Page from philo.models.fields import TemplateField @@ -25,8 +27,7 @@ from philo.utils import ContentTypeRegistryLimiter __all__ = ('register_location_model', 'unregister_location_model', 'Location', 'TimedModel', 'Event', 'Calendar', 'CalendarView',) -ICALENDAR = ICalendarFeed.mime_type -FEEDS[ICALENDAR] = ICalendarFeed +registry.register(ICalendarFeed, verbose_name="iCalendar") try: DEFAULT_SITE = Site.objects.get_current() except: @@ -223,17 +224,17 @@ class CalendarView(FeedView): # or per-calendar-view basis. #url(r'^%s/(?P[\w-]+)' % self.location_permalink_base, ...) - if self.tag_archive_page: + if self.tag_archive_page_id: urlpatterns += patterns('', url(r'^%s$' % self.tag_permalink_base, self.tag_archive_view, name='tag_archive') ) - if self.owner_archive_page: + if self.owner_archive_page_id: urlpatterns += patterns('', url(r'^%s$' % self.owner_permalink_base, self.owner_archive_view, name='owner_archive') ) - if self.location_archive_page: + if self.location_archive_page_id: urlpatterns += patterns('', url(r'^%s$' % self.location_permalink_base, self.location_archive_view, name='location_archive') ) @@ -334,7 +335,7 @@ class CalendarView(FeedView): def get_events_by_location(self, request, app_label, model, pk, extra_context=None): try: - ct = ContentType.objects.get(app_label=app_label, model=model) + ct = ContentType.objects.get_by_natural_key(app_label, model) location = ct.model_class()._default_manager.get(pk=pk) except ObjectDoesNotExist: raise Http404 @@ -461,5 +462,4 @@ class CalendarView(FeedView): return u"%s for %s" % (self.__class__.__name__, self.calendar) field = CalendarView._meta.get_field('feed_type') -field._choices += ((ICALENDAR, 'iCalendar'),) -field.default = ICALENDAR \ No newline at end of file +field.default = registry.get_slug(ICalendarFeed, field.default) \ No newline at end of file diff --git a/philo/contrib/penfield/admin.py b/philo/contrib/penfield/admin.py index d350303..31aacb1 100644 --- a/philo/contrib/penfield/admin.py +++ b/philo/contrib/penfield/admin.py @@ -3,8 +3,10 @@ from django.contrib import admin from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect, QueryDict -from philo.admin import EntityAdmin, AddTagAdmin, COLLAPSE_CLASSES +from philo.admin import EntityAdmin, COLLAPSE_CLASSES +from philo.admin.widgets import EmbedWidget from philo.contrib.penfield.models import BlogEntry, Blog, BlogView, Newsletter, NewsletterArticle, NewsletterIssue, NewsletterView +from philo.models.fields import TemplateField class DelayedDateForm(forms.ModelForm): @@ -20,9 +22,8 @@ class BlogAdmin(EntityAdmin): list_display = ('title', 'slug') -class BlogEntryAdmin(AddTagAdmin): +class BlogEntryAdmin(EntityAdmin): form = DelayedDateForm - filter_horizontal = ['tags'] list_filter = ['author', 'blog'] date_hierarchy = 'date' search_fields = ('content',) @@ -42,6 +43,9 @@ class BlogEntryAdmin(AddTagAdmin): ) related_lookup_fields = {'fk': raw_id_fields} prepopulated_fields = {'slug': ('title',)} + formfield_overrides = { + TemplateField: {'widget': EmbedWidget} + } class BlogViewAdmin(EntityAdmin): @@ -73,9 +77,9 @@ class NewsletterAdmin(EntityAdmin): list_display = ('title', 'slug') -class NewsletterArticleAdmin(AddTagAdmin): +class NewsletterArticleAdmin(EntityAdmin): form = DelayedDateForm - filter_horizontal = ('tags', 'authors') + filter_horizontal = ('authors',) list_filter = ('newsletter',) date_hierarchy = 'date' search_fields = ('title', 'authors__name',) @@ -94,6 +98,9 @@ class NewsletterArticleAdmin(AddTagAdmin): ) actions = ['make_issue'] prepopulated_fields = {'slug': ('title',)} + formfield_overrides = { + TemplateField: {'widget': EmbedWidget} + } def author_names(self, obj): return ', '.join([author.get_full_name() for author in obj.authors.all()]) diff --git a/philo/contrib/penfield/exceptions.py b/philo/contrib/penfield/exceptions.py deleted file mode 100644 index 96b96ed..0000000 --- a/philo/contrib/penfield/exceptions.py +++ /dev/null @@ -1,3 +0,0 @@ -class HttpNotAcceptable(Exception): - """This will be raised if an Http-Accept header will not accept the feed content types that are available.""" - pass \ No newline at end of file diff --git a/philo/contrib/penfield/migrations/0003_auto__add_field_newsletterview_feed_type__add_field_newsletterview_ite.py b/philo/contrib/penfield/migrations/0003_auto__add_field_newsletterview_feed_type__add_field_newsletterview_ite.py index eae496e..72df39b 100644 --- a/philo/contrib/penfield/migrations/0003_auto__add_field_newsletterview_feed_type__add_field_newsletterview_ite.py +++ b/philo/contrib/penfield/migrations/0003_auto__add_field_newsletterview_feed_type__add_field_newsletterview_ite.py @@ -3,6 +3,7 @@ import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models +from philo.migrations import person_model, frozen_person class Migration(SchemaMigration): @@ -85,13 +86,7 @@ class Migration(SchemaMigration): 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, - 'oberlin.person': { - 'Meta': {'object_name': 'Person'}, - 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '70', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) - }, + person_model: frozen_person, 'penfield.blog': { 'Meta': {'object_name': 'Blog'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), @@ -100,7 +95,7 @@ class Migration(SchemaMigration): }, 'penfield.blogentry': { 'Meta': {'object_name': 'BlogEntry'}, - 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['oberlin.Person']"}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['%s']" % person_model}), 'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}), 'content': ('django.db.models.fields.TextField', [], {}), 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), @@ -137,7 +132,7 @@ class Migration(SchemaMigration): }, 'penfield.newsletterarticle': { 'Meta': {'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'}, - 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['oberlin.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['%s']" % person_model}), 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), 'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), diff --git a/philo/contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py b/philo/contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py index 9b9ffa7..e48e0d7 100644 --- a/philo/contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py +++ b/philo/contrib/penfield/migrations/0004_auto__add_field_newsletterview_feed_length__add_field_blogview_feed_le.py @@ -3,6 +3,7 @@ import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models +from philo.migrations import person_model, frozen_person class Migration(SchemaMigration): @@ -61,13 +62,7 @@ class Migration(SchemaMigration): 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, - 'oberlin.person': { - 'Meta': {'object_name': 'Person'}, - 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '70', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) - }, + person_model: frozen_person, 'penfield.blog': { 'Meta': {'object_name': 'Blog'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), @@ -76,7 +71,7 @@ class Migration(SchemaMigration): }, 'penfield.blogentry': { 'Meta': {'object_name': 'BlogEntry'}, - 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['oberlin.Person']"}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['%s']" % person_model}), 'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}), 'content': ('django.db.models.fields.TextField', [], {}), 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), @@ -114,7 +109,7 @@ class Migration(SchemaMigration): }, 'penfield.newsletterarticle': { 'Meta': {'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'}, - 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['oberlin.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['%s']" % person_model}), 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), 'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), diff --git a/philo/contrib/penfield/migrations/0005_to_taggit.py b/philo/contrib/penfield/migrations/0005_to_taggit.py new file mode 100644 index 0000000..52090c6 --- /dev/null +++ b/philo/contrib/penfield/migrations/0005_to_taggit.py @@ -0,0 +1,248 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from philo.migrations import person_model, frozen_person + +class Migration(DataMigration): + + depends_on = ( + ("philo", "0019_to_taggit"), + ) + + needed_by = ( + ("philo", "0020_from_taggit"), + ) + + def forwards(self, orm): + "Write your forwards methods here." + BlogEntry = orm['penfield.BlogEntry'] + NewsletterArticle = orm['penfield.NewsletterArticle'] + TaggitTag = orm['taggit.Tag'] + TaggedItem = orm['taggit.TaggedItem'] + ContentType = orm['contenttypes.contenttype'] + + entry_ct = ContentType.objects.get(app_label="penfield", model="blogentry") + article_ct = ContentType.objects.get(app_label="penfield", model="newsletterarticle") + + for entry in BlogEntry.objects.all(): + for tag in entry.tags.all(): + ttag = TaggitTag.objects.get(slug=tag.slug) + TaggedItem.objects.get_or_create(tag=ttag, content_type=entry_ct, object_id=entry.pk) + + for article in NewsletterArticle.objects.all(): + for tag in article.tags.all(): + ttag = TaggitTag.objects.get(slug=tag.slug) + TaggedItem.objects.get_or_create(tag=ttag, content_type=article_ct, object_id=article.pk) + + + def backwards(self, orm): + "Write your backwards methods here." + BlogEntry = orm['penfield.BlogEntry'] + NewsletterArticle = orm['penfield.NewsletterArticle'] + Tag = orm['philo.Tag'] + TaggitTag = orm['taggit.Tag'] + TaggedItem = orm['taggit.TaggedItem'] + ContentType = orm['contenttypes.contenttype'] + + entry_ct = ContentType.objects.get(app_label="penfield", model="blogentry") + article_ct = ContentType.objects.get(app_label="penfield", model="newsletterarticle") + + for entry in BlogEntry.objects.all(): + tag_slugs = list(TaggitTag.objects.filter(taggit_taggeditem_items__content_type=entry_ct, taggit_taggeditem_items__object_id=entry.pk).values_list('slug', flat=True).distinct()) + entry.tags = Tag.objects.filter(slug__in=tag_slugs) + entry.save() + + for article in NewsletterArticle.objects.all(): + tag_slugs = list(TaggitTag.objects.filter(taggit_taggeditem_items__content_type=article_ct, taggit_taggeditem_items__object_id=article.pk).values_list('slug', flat=True).distinct()) + article.tags = Tag.objects.filter(slug__in=tag_slugs) + article.save() + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + person_model: frozen_person, + 'penfield.blog': { + 'Meta': {'object_name': 'Blog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.blogentry': { + 'Meta': {'ordering': "['-date']", 'object_name': 'BlogEntry'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['%s']" % person_model}), + 'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}), + 'content': ('django.db.models.fields.TextField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'excerpt': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'blogentries'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['philo.Tag']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.blogview': { + 'Meta': {'object_name': 'BlogView'}, + 'blog': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogviews'", 'to': "orm['penfield.Blog']"}), + 'entries_per_page': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'entry_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_entry_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}), + 'entry_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_entry_related'", 'to': "orm['philo.Page']"}), + 'entry_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'entries'", 'max_length': '255'}), + 'entry_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}), + 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", 'max_length': '50'}), + 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_index_related'", 'to': "orm['philo.Page']"}), + 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'tag_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_tag_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}), + 'tag_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_tag_related'", 'to': "orm['philo.Page']"}), + 'tag_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'tags'", 'max_length': '255'}) + }, + 'penfield.newsletter': { + 'Meta': {'object_name': 'Newsletter'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.newsletterarticle': { + 'Meta': {'ordering': "['-date']", 'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'}, + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['%s']" % person_model}), + 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lede': ('philo.models.fields.TemplateField', [], {'null': 'True', 'blank': 'True'}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'articles'", 'to': "orm['penfield.Newsletter']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'newsletterarticles'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['philo.Tag']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.newsletterissue': { + 'Meta': {'ordering': "['-numbering']", 'unique_together': "(('newsletter', 'numbering'),)", 'object_name': 'NewsletterIssue'}, + 'articles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'issues'", 'symmetrical': 'False', 'to': "orm['penfield.NewsletterArticle']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issues'", 'to': "orm['penfield.Newsletter']"}), + 'numbering': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.newsletterview': { + 'Meta': {'object_name': 'NewsletterView'}, + 'article_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_article_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}), + 'article_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_article_related'", 'to': "orm['philo.Page']"}), + 'article_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'articles'", 'max_length': '255'}), + 'article_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}), + 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", 'max_length': '50'}), + 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_index_related'", 'to': "orm['philo.Page']"}), + 'issue_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_issue_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}), + 'issue_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_issue_related'", 'to': "orm['philo.Page']"}), + 'issue_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'issues'", 'max_length': '255'}), + 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletterviews'", 'to': "orm['penfield.Newsletter']"}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'philo.page': { + 'Meta': {'object_name': 'Page'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.tag': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + 'philo.template': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['penfield', 'taggit'] + symmetrical = True diff --git a/philo/contrib/penfield/migrations/0006_delete_tag_rels.py b/philo/contrib/penfield/migrations/0006_delete_tag_rels.py new file mode 100644 index 0000000..d3bba00 --- /dev/null +++ b/philo/contrib/penfield/migrations/0006_delete_tag_rels.py @@ -0,0 +1,218 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models +from philo.migrations import person_model, frozen_person + +class Migration(SchemaMigration): + + needed_by = ( + ('philo', '0021_auto__del_tag'), + ) + + def forwards(self, orm): + + # Removing M2M table for field tags on 'BlogEntry' + db.delete_table('penfield_blogentry_tags') + + # Removing M2M table for field tags on 'NewsletterArticle' + db.delete_table('penfield_newsletterarticle_tags') + + + def backwards(self, orm): + + # Adding M2M table for field tags on 'BlogEntry' + db.create_table('penfield_blogentry_tags', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('blogentry', models.ForeignKey(orm['penfield.blogentry'], null=False)), + ('tag', models.ForeignKey(orm['philo.tag'], null=False)) + )) + db.create_unique('penfield_blogentry_tags', ['blogentry_id', 'tag_id']) + + # Adding M2M table for field tags on 'NewsletterArticle' + db.create_table('penfield_newsletterarticle_tags', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('newsletterarticle', models.ForeignKey(orm['penfield.newsletterarticle'], null=False)), + ('tag', models.ForeignKey(orm['philo.tag'], null=False)) + )) + db.create_unique('penfield_newsletterarticle_tags', ['newsletterarticle_id', 'tag_id']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + person_model: frozen_person, + 'penfield.blog': { + 'Meta': {'object_name': 'Blog'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.blogentry': { + 'Meta': {'object_name': 'BlogEntry'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogentries'", 'to': "orm['%s']" % person_model}), + 'blog': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'entries'", 'null': 'True', 'to': "orm['penfield.Blog']"}), + 'content': ('django.db.models.fields.TextField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'excerpt': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.blogview': { + 'Meta': {'object_name': 'BlogView'}, + 'blog': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blogviews'", 'to': "orm['penfield.Blog']"}), + 'entries_per_page': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'entry_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_entry_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}), + 'entry_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_entry_related'", 'to': "orm['philo.Page']"}), + 'entry_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'entries'", 'max_length': '255'}), + 'entry_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}), + 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", 'max_length': '50'}), + 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_index_related'", 'to': "orm['philo.Page']"}), + 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_blogview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'tag_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'blog_tag_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}), + 'tag_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blog_tag_related'", 'to': "orm['philo.Page']"}), + 'tag_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'tags'", 'max_length': '255'}) + }, + 'penfield.newsletter': { + 'Meta': {'object_name': 'Newsletter'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.newsletterarticle': { + 'Meta': {'ordering': "['-date']", 'unique_together': "(('newsletter', 'slug'),)", 'object_name': 'NewsletterArticle'}, + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'newsletterarticles'", 'symmetrical': 'False', 'to': "orm['%s']" % person_model}), + 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'full_text': ('philo.models.fields.TemplateField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lede': ('philo.models.fields.TemplateField', [], {'null': 'True', 'blank': 'True'}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'articles'", 'to': "orm['penfield.Newsletter']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.newsletterissue': { + 'Meta': {'ordering': "['-numbering']", 'unique_together': "(('newsletter', 'numbering'),)", 'object_name': 'NewsletterIssue'}, + 'articles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'issues'", 'symmetrical': 'False', 'to': "orm['penfield.NewsletterArticle']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issues'", 'to': "orm['penfield.Newsletter']"}), + 'numbering': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'penfield.newsletterview': { + 'Meta': {'object_name': 'NewsletterView'}, + 'article_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_article_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}), + 'article_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_article_related'", 'to': "orm['philo.Page']"}), + 'article_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'articles'", 'max_length': '255'}), + 'article_permalink_style': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'feed_length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '15', 'null': 'True', 'blank': 'True'}), + 'feed_suffix': ('django.db.models.fields.CharField', [], {'default': "'feed'", 'max_length': '255'}), + 'feed_type': ('django.db.models.fields.CharField', [], {'default': "'atom'", 'max_length': '50'}), + 'feeds_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_index_related'", 'to': "orm['philo.Page']"}), + 'issue_archive_page': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'newsletter_issue_archive_related'", 'null': 'True', 'to': "orm['philo.Page']"}), + 'issue_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletter_issue_related'", 'to': "orm['philo.Page']"}), + 'issue_permalink_base': ('django.db.models.fields.CharField', [], {'default': "'issues'", 'max_length': '255'}), + 'item_description_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_description_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'item_title_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'penfield_newsletterview_title_related'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'newsletterviews'", 'to': "orm['penfield.Newsletter']"}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'philo.page': { + 'Meta': {'object_name': 'Page'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.template': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['penfield', 'taggit'] diff --git a/philo/contrib/penfield/models.py b/philo/contrib/penfield/models.py index 3632ff6..a57459c 100644 --- a/philo/contrib/penfield/models.py +++ b/philo/contrib/penfield/models.py @@ -1,328 +1,19 @@ +# encoding: utf-8 from datetime import date, datetime from django.conf import settings from django.conf.urls.defaults import url, patterns, include -from django.contrib.sites.models import Site, RequestSite -from django.contrib.syndication.views import add_domain from django.db import models from django.http import Http404, HttpResponse -from django.template import RequestContext, Template as DjangoTemplate -from django.utils import feedgenerator, tzinfo -from django.utils.datastructures import SortedDict -from django.utils.encoding import smart_unicode, force_unicode -from django.utils.html import escape +from taggit.managers import TaggableManager +from taggit.models import Tag, TaggedItem -from philo.contrib.penfield.exceptions import HttpNotAcceptable -from philo.contrib.penfield.middleware import http_not_acceptable +from philo.contrib.winer.models import FeedView from philo.exceptions import ViewCanNotProvideSubpath -from philo.models import Tag, Entity, MultiView, Page, register_value_model, Template +from philo.models import Entity, Page, register_value_model from philo.models.fields import TemplateField from philo.utils import paginate -try: - import mimeparse -except: - mimeparse = None - - -ATOM = feedgenerator.Atom1Feed.mime_type -RSS = feedgenerator.Rss201rev2Feed.mime_type -FEEDS = SortedDict([ - (ATOM, feedgenerator.Atom1Feed), - (RSS, feedgenerator.Rss201rev2Feed), -]) -FEED_CHOICES = ( - (ATOM, "Atom"), - (RSS, "RSS"), -) - - -class FeedView(MultiView): - """ - :class:`FeedView` handles a number of pages and related feeds for a single object such as a blog or newsletter. In addition to all other methods and attributes, :class:`FeedView` supports the same generic API as `django.contrib.syndication.views.Feed `_. - - """ - #: The type of feed which should be served by the :class:`FeedView`. - feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM) - #: The suffix which will be appended to a page URL for a feed of its items. Default: "feed" - feed_suffix = models.CharField(max_length=255, blank=False, default="feed") - #: A :class:`BooleanField` - whether or not feeds are enabled. - feeds_enabled = models.BooleanField(default=True) - #: A :class:`PositiveIntegerField` - the maximum number of items to return for this feed. All items will be returned if this field is blank. Default: 15. - feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.") - - #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided. - item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related") - #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided. - item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related") - - #: The name of the context variable to be populated with the items managed by the :class:`FeedView`. - item_context_var = 'items' - #: The attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`Blog`.) - object_attr = 'object' - - #: A description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API. - description = "" - - def feed_patterns(self, base, get_items_attr, page_attr, reverse_name): - """ - Given the name to be used to reverse this view and the names of the attributes for the function that fetches the objects, returns patterns suitable for inclusion in urlpatterns. - - :param base: The base of the returned patterns - that is, the subpath pattern which will reference the page for the items. The :attr:`feed_suffix` will be appended to this subpath. - :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` which will return an (``items``, ``extra_context``) tuple. This will be passed directly to :meth:`feed_view` and :meth:`page_view`. - :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be passed directly to :meth:`page_view` and will be rendered with the items from ``get_items_attr``. - :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters. - :returns: Patterns suitable for use in urlpatterns. - - Example:: - - @property - def urlpatterns(self): - urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') - urlpatterns += self.feed_patterns(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day') - return urlpatterns - - """ - urlpatterns = patterns('') - if self.feeds_enabled: - feed_reverse_name = "%s_feed" % reverse_name - feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name)) - feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix) - urlpatterns += patterns('', - url(feed_pattern, feed_view, name=feed_reverse_name), - ) - urlpatterns += patterns('', - url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name) - ) - return urlpatterns - - def get_object(self, request, **kwargs): - """By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API.""" - return getattr(self, self.object_attr) - - def feed_view(self, get_items_attr, reverse_name): - """ - Returns a view function that renders a list of items as a feed. - - :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments. - :param reverse_name: The name which can be used reverse this feed using the :class:`FeedView` as the urlconf. - - :returns: A view function that renders a list of items as a feed. - - """ - get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr) - - def inner(request, extra_context=None, *args, **kwargs): - obj = self.get_object(request, *args, **kwargs) - feed = self.get_feed(obj, request, reverse_name) - items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs) - self.populate_feed(feed, items, request) - - response = HttpResponse(mimetype=feed.mime_type) - feed.write(response, 'utf-8') - return response - - return inner - - def page_view(self, get_items_attr, page_attr): - """ - :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments. - :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be rendered with the items from ``get_items_attr``. - - :returns: A view function that renders a list of items as an :class:`HttpResponse`. - - """ - get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr) - page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr) - - def inner(request, extra_context=None, *args, **kwargs): - items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs) - items, item_context = self.process_page_items(request, items) - - context = self.get_context() - context.update(extra_context or {}) - context.update(item_context or {}) - - return page.render_to_response(request, extra_context=context) - return inner - - def process_page_items(self, request, items): - """ - Hook for handling any extra processing of ``items`` based on an :class:`HttpRequest`, such as pagination or searching. This method is expected to return a list of items and a dictionary to be added to the page context. - - """ - item_context = { - self.item_context_var: items - } - return items, item_context - - def get_feed_type(self, request): - """ - Intelligently chooses a feed type for a given request. Tries to return :attr:`feed_type`, but if the Accept header does not include that mimetype, tries to return the best match from the feed types that are offered by the :class:`FeedView`. If none of the offered feed types are accepted by the :class:`HttpRequest`, then this method will raise :exc:`philo.contrib.penfield.exceptions.HttpNotAcceptable`. - - """ - feed_type = self.feed_type - if feed_type not in FEEDS: - feed_type = FEEDS.keys()[0] - accept = request.META.get('HTTP_ACCEPT') - if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept: - # Wups! They aren't accepting the chosen format. Is there another format we can use? - if mimeparse: - feed_type = mimeparse.best_match(FEEDS.keys(), accept) - else: - for feed_type in FEEDS.keys(): - if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept: - break - else: - feed_type = None - if not feed_type: - raise HttpNotAcceptable - return FEEDS[feed_type] - - def get_feed(self, obj, request, reverse_name): - """ - Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object. - - """ - try: - current_site = Site.objects.get_current() - except Site.DoesNotExist: - current_site = RequestSite(request) - - feed_type = self.get_feed_type(request) - node = request.node - link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure()) - - feed = feed_type( - title = self.__get_dynamic_attr('title', obj), - subtitle = self.__get_dynamic_attr('subtitle', obj), - link = link, - description = self.__get_dynamic_attr('description', obj), - language = settings.LANGUAGE_CODE.decode(), - feed_url = add_domain( - current_site.domain, - self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node._subpath, with_domain=True, request=request, secure=request.is_secure()), - request.is_secure() - ), - author_name = self.__get_dynamic_attr('author_name', obj), - author_link = self.__get_dynamic_attr('author_link', obj), - author_email = self.__get_dynamic_attr('author_email', obj), - categories = self.__get_dynamic_attr('categories', obj), - feed_copyright = self.__get_dynamic_attr('feed_copyright', obj), - feed_guid = self.__get_dynamic_attr('feed_guid', obj), - ttl = self.__get_dynamic_attr('ttl', obj), - **self.feed_extra_kwargs(obj) - ) - return feed - - def populate_feed(self, feed, items, request): - """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``.""" - if self.item_title_template: - title_template = DjangoTemplate(self.item_title_template.code) - else: - title_template = None - if self.item_description_template: - description_template = DjangoTemplate(self.item_description_template.code) - else: - description_template = None - - node = request.node - try: - current_site = Site.objects.get_current() - except Site.DoesNotExist: - current_site = RequestSite(request) - - if self.feed_length is not None: - items = items[:self.feed_length] - - for item in items: - if title_template is not None: - title = title_template.render(RequestContext(request, {'obj': item})) - else: - title = self.__get_dynamic_attr('item_title', item) - if description_template is not None: - description = description_template.render(RequestContext(request, {'obj': item})) - else: - description = self.__get_dynamic_attr('item_description', item) - - link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure()) - - enc = None - enc_url = self.__get_dynamic_attr('item_enclosure_url', item) - if enc_url: - enc = feedgenerator.Enclosure( - url = smart_unicode(add_domain( - current_site.domain, - enc_url, - request.is_secure() - )), - length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)), - mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item)) - ) - author_name = self.__get_dynamic_attr('item_author_name', item) - if author_name is not None: - author_email = self.__get_dynamic_attr('item_author_email', item) - author_link = self.__get_dynamic_attr('item_author_link', item) - else: - author_email = author_link = None - - pubdate = self.__get_dynamic_attr('item_pubdate', item) - if pubdate and not pubdate.tzinfo: - ltz = tzinfo.LocalTimezone(pubdate) - pubdate = pubdate.replace(tzinfo=ltz) - - feed.add_item( - title = title, - link = link, - description = description, - unique_id = self.__get_dynamic_attr('item_guid', item, link), - enclosure = enc, - pubdate = pubdate, - author_name = author_name, - author_email = author_email, - author_link = author_link, - categories = self.__get_dynamic_attr('item_categories', item), - item_copyright = self.__get_dynamic_attr('item_copyright', item), - **self.item_extra_kwargs(item) - ) - - def __get_dynamic_attr(self, attname, obj, default=None): - try: - attr = getattr(self, attname) - except AttributeError: - return default - if callable(attr): - # Check func_code.co_argcount rather than try/excepting the - # function and catching the TypeError, because something inside - # the function may raise the TypeError. This technique is more - # accurate. - if hasattr(attr, 'func_code'): - argcount = attr.func_code.co_argcount - else: - argcount = attr.__call__.func_code.co_argcount - if argcount == 2: # one argument is 'self' - return attr(obj) - else: - return attr() - return attr - - def feed_extra_kwargs(self, obj): - """Returns an extra keyword arguments dictionary that is used when initializing the feed generator.""" - return {} - - def item_extra_kwargs(self, item): - """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator.""" - return {} - - def item_title(self, item): - return escape(force_unicode(item)) - - def item_description(self, item): - return force_unicode(item) - - class Meta: - abstract=True - class Blog(Entity): """Represents a blog which can be posted to.""" @@ -338,7 +29,11 @@ class Blog(Entity): @property def entry_tags(self): """Returns a :class:`QuerySet` of :class:`.Tag`\ s that are used on any entries in this blog.""" - return Tag.objects.filter(blogentries__blog=self).distinct() + entry_pks = list(self.entries.values_list('pk', flat=True)) + kwargs = { + '%s__object_id__in' % TaggedItem.tag_relname(): entry_pks + } + return TaggedItem.tags_for(BlogEntry).filter(**kwargs) @property def entry_dates(self): @@ -368,13 +63,13 @@ class BlogEntry(Entity): date = models.DateTimeField(default=None) #: The content of the :class:`BlogEntry`. - content = models.TextField() + content = TemplateField() #: An optional brief excerpt from the :class:`BlogEntry`. - excerpt = models.TextField(blank=True, null=True) + excerpt = TemplateField(blank=True, null=True) - #: :class:`.Tag`\ s for this :class:`BlogEntry`. - tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True) + #: A ``django-taggit`` :class:`TaggableManager`. + tags = TaggableManager() def save(self, *args, **kwargs): if self.date is None: @@ -395,7 +90,7 @@ register_value_model(BlogEntry) class BlogView(FeedView): """ - A subclass of :class:`FeedView` which handles patterns and feeds for a :class:`Blog` and its related :class:`entries `. + A subclass of :class:`.FeedView` which handles patterns and feeds for a :class:`Blog` and its related :class:`entries `. """ ENTRY_PERMALINK_STYLE_CHOICES = ( @@ -437,14 +132,13 @@ class BlogView(FeedView): tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags') item_context_var = 'entries' - object_attr = 'blog' def __unicode__(self): return u'BlogView for %s' % self.blog.title def get_reverse_params(self, obj): if isinstance(obj, BlogEntry): - if obj.blog == self.blog: + if obj.blog_id == self.blog_id: kwargs = {'slug': obj.slug} if self.entry_permalink_style in 'DMY': kwargs.update({'year': str(obj.date.year).zfill(4)}) @@ -456,7 +150,7 @@ class BlogView(FeedView): elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj): if isinstance(obj, Tag): obj = [obj] - slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()] + slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset(self.blog)] if slugs: return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)} elif isinstance(obj, (date, datetime)): @@ -470,21 +164,21 @@ class BlogView(FeedView): @property def urlpatterns(self): - urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\ - self.feed_patterns(r'^%s/(?P[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag') + urlpatterns = self.feed_patterns(r'^', 'get_entries', 'index_page', 'index') +\ + self.feed_patterns(r'^%s/(?P[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries', 'tag_page', 'entries_by_tag') - if self.tag_archive_page: + if self.tag_archive_page_id: urlpatterns += patterns('', url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive') ) - if self.entry_archive_page: + if self.entry_archive_page_id: if self.entry_permalink_style in 'DMY': - urlpatterns += self.feed_patterns(r'^(?P\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year') + urlpatterns += self.feed_patterns(r'^(?P\d{4})', 'get_entries', 'entry_archive_page', 'entries_by_year') if self.entry_permalink_style in 'DM': - urlpatterns += self.feed_patterns(r'^(?P\d{4})/(?P\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month') + urlpatterns += self.feed_patterns(r'^(?P\d{4})/(?P\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_month') if self.entry_permalink_style == 'D': - urlpatterns += self.feed_patterns(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day') + urlpatterns += self.feed_patterns(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_day') if self.entry_permalink_style == 'D': urlpatterns += patterns('', @@ -508,63 +202,76 @@ class BlogView(FeedView): ) return urlpatterns - def get_context(self): - return {'blog': self.blog} - - def get_entry_queryset(self): + def get_entry_queryset(self, obj): """Returns the default :class:`QuerySet` of :class:`BlogEntry` instances for the :class:`BlogView` - all entries that are considered posted in the past. This allows for scheduled posting of entries.""" - return self.blog.entries.filter(date__lte=datetime.now()) + return obj.entries.filter(date__lte=datetime.now()) - def get_tag_queryset(self): + def get_tag_queryset(self, obj): """Returns the default :class:`QuerySet` of :class:`.Tag`\ s for the :class:`BlogView`'s :meth:`get_entries_by_tag` and :meth:`tag_archive_view`.""" - return self.blog.entry_tags - - def get_all_entries(self, request, extra_context=None): - """Used to generate :meth:`~FeedView.feed_patterns` for all entries.""" - return self.get_entry_queryset(), extra_context + return obj.entry_tags - def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None): - """Used to generate :meth:`~FeedView.feed_patterns` for entries with a specific year, month, and day.""" - if not self.entry_archive_page: - raise Http404 - entries = self.get_entry_queryset() - if year: - entries = entries.filter(date__year=year) - if month: - entries = entries.filter(date__month=month) - if day: - entries = entries.filter(date__day=day) + def get_object(self, request, year=None, month=None, day=None, tag_slugs=None): + """Returns a dictionary representing the parameters for a feed which will be exposed.""" + if tag_slugs is None: + tags = None + else: + tag_slugs = tag_slugs.replace('+', '/').split('/') + tags = self.get_tag_queryset(self.blog).filter(slug__in=tag_slugs) + if not tags: + raise Http404 + + # Raise a 404 on an incorrect slug. + found_slugs = set([tag.slug for tag in tags]) + for slug in tag_slugs: + if slug and slug not in found_slugs: + raise Http404 - context = extra_context or {} - context.update({'year': year, 'month': month, 'day': day}) - return entries, context + try: + if year and month and day: + context_date = date(int(year), int(month), int(day)) + elif year and month: + context_date = date(int(year), int(month), 1) + elif year: + context_date = date(int(year), 1, 1) + else: + context_date = None + except TypeError, ValueError: + context_date = None + + return { + 'blog': self.blog, + 'tags': tags, + 'year': year, + 'month': month, + 'day': day, + 'date': context_date + } - def get_entries_by_tag(self, request, tag_slugs, extra_context=None): - """Used to generate :meth:`~FeedView.feed_patterns` for entries with all of the given tags.""" - tag_slugs = tag_slugs.replace('+', '/').split('/') - tags = self.get_tag_queryset().filter(slug__in=tag_slugs) + def get_entries(self, obj, request, year=None, month=None, day=None, tag_slugs=None, extra_context=None): + """Returns the :class:`BlogEntry` objects which will be exposed for the given object, as returned from :meth:`get_object`.""" + entries = self.get_entry_queryset(obj['blog']) - if not tags: - raise Http404 + if obj['tags'] is not None: + tags = obj['tags'] + for tag in tags: + entries = entries.filter(tags=tag) - # Raise a 404 on an incorrect slug. - found_slugs = [tag.slug for tag in tags] - for slug in tag_slugs: - if slug and slug not in found_slugs: - raise Http404 - - entries = self.get_entry_queryset() - for tag in tags: - entries = entries.filter(tags=tag) + if obj['date'] is not None: + if year: + entries = entries.filter(date__year=year) + if month: + entries = entries.filter(date__month=month) + if day: + entries = entries.filter(date__day=day) context = extra_context or {} - context.update({'tags': tags}) + context.update(obj) return entries, context def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None): """Renders :attr:`entry_page` with the entry specified by the given parameters.""" - entries = self.get_entry_queryset() + entries = self.get_entry_queryset(self.blog) if year: entries = entries.filter(date__year=year) if month: @@ -587,36 +294,12 @@ class BlogView(FeedView): context = self.get_context() context.update(extra_context or {}) context.update({ - 'tags': self.get_tag_queryset() + 'tags': self.get_tag_queryset(self.blog) }) return self.tag_archive_page.render_to_response(request, extra_context=context) - def feed_view(self, get_items_attr, reverse_name): - """Overrides :meth:`FeedView.feed_view` to add :class:`.Tag`\ s to the feed as categories.""" - get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr) - - def inner(request, extra_context=None, *args, **kwargs): - obj = self.get_object(request, *args, **kwargs) - feed = self.get_feed(obj, request, reverse_name) - items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs) - self.populate_feed(feed, items, request) - - if 'tags' in extra_context: - tags = extra_context['tags'] - feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure()) - else: - tags = obj.entry_tags - - feed.feed['categories'] = [tag.name for tag in tags] - - response = HttpResponse(mimetype=feed.mime_type) - feed.write(response, 'utf-8') - return response - - return inner - def process_page_items(self, request, items): - """Overrides :meth:`FeedView.process_page_items` to add pagination.""" + """Overrides :meth:`.FeedView.process_page_items` to add pagination.""" if self.entries_per_page: page_num = request.GET.get('page', 1) paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num) @@ -632,7 +315,25 @@ class BlogView(FeedView): return items, item_context def title(self, obj): - return obj.title + title = obj['blog'].title + if obj['tags']: + title += u" – %s" % u", ".join((tag.name for tag in obj['tags'])) + date = obj['date'] + if date: + if obj['day']: + datestr = date.strftime("%F %j, %Y") + elif obj['month']: + datestr = date.strftime("%F, %Y") + elif obj['year']: + datestr = date.strftime("%Y") + title += u" – %s" % datestr + return title + + def categories(self, obj): + tags = obj['tags'] + if tags: + return (tag.name for tag in tags) + return None def item_title(self, item): return item.title @@ -680,8 +381,8 @@ class NewsletterArticle(Entity): lede = TemplateField(null=True, blank=True, verbose_name='Summary') #: A :class:`.TemplateField` containing the full text of the article. full_text = TemplateField(db_index=True) - #: A :class:`ManyToManyField` to :class:`.Tag`\ s for the :class:`NewsletterArticle`. - tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True) + #: A ``django-taggit`` :class:`TaggableManager`. + tags = TaggableManager() def save(self, *args, **kwargs): if self.date is None: @@ -725,7 +426,7 @@ register_value_model(NewsletterIssue) class NewsletterView(FeedView): - """A subclass of :class:`FeedView` which handles patterns and feeds for a :class:`Newsletter` and its related :class:`articles `.""" + """A subclass of :class:`.FeedView` which handles patterns and feeds for a :class:`Newsletter` and its related :class:`articles `.""" ARTICLE_PERMALINK_STYLE_CHOICES = ( ('D', 'Year, month, and day'), ('M', 'Year and month'), @@ -767,7 +468,7 @@ class NewsletterView(FeedView): def get_reverse_params(self, obj): if isinstance(obj, NewsletterArticle): - if obj.newsletter == self.newsletter: + if obj.newsletter_id == self.newsletter_id: kwargs = {'slug': obj.slug} if self.article_permalink_style in 'DMY': kwargs.update({'year': str(obj.date.year).zfill(4)}) @@ -777,7 +478,7 @@ class NewsletterView(FeedView): kwargs.update({'day': str(obj.date.day).zfill(2)}) return self.article_view, [], kwargs elif isinstance(obj, NewsletterIssue): - if obj.newsletter == self.newsletter: + if obj.newsletter_id == self.newsletter_id: return 'issue', [], {'numbering': obj.numbering} elif isinstance(obj, (date, datetime)): kwargs = { @@ -793,14 +494,12 @@ class NewsletterView(FeedView): urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('', url(r'^%s/(?P.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue') ) - if self.issue_archive_page: + if self.issue_archive_page_id: urlpatterns += patterns('', url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive') ) - if self.article_archive_page: - urlpatterns += patterns('', - url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles'))) - ) + if self.article_archive_page_id: + urlpatterns += self.feed_patterns(r'^%s' % self.article_permalink_base, 'get_all_articles', 'article_archive_page', 'articles') if self.article_permalink_style in 'DMY': urlpatterns += self.feed_patterns(r'^%s/(?P\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year') if self.article_permalink_style in 'DM': @@ -830,40 +529,40 @@ class NewsletterView(FeedView): def get_context(self): return {'newsletter': self.newsletter} - def get_article_queryset(self): + def get_article_queryset(self, obj): """Returns the default :class:`QuerySet` of :class:`NewsletterArticle` instances for the :class:`NewsletterView` - all articles that are considered posted in the past. This allows for scheduled posting of articles.""" - return self.newsletter.articles.filter(date__lte=datetime.now()) + return obj.articles.filter(date__lte=datetime.now()) - def get_issue_queryset(self): + def get_issue_queryset(self, obj): """Returns the default :class:`QuerySet` of :class:`NewsletterIssue` instances for the :class:`NewsletterView`.""" - return self.newsletter.issues.all() + return obj.issues.all() - def get_all_articles(self, request, extra_context=None): - """Used to generate :meth:`FeedView.feed_patterns` for all entries.""" - return self.get_article_queryset(), extra_context + def get_all_articles(self, obj, request, extra_context=None): + """Used to generate :meth:`~.FeedView.feed_patterns` for all entries.""" + return self.get_article_queryset(obj), extra_context - def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None): - """Used to generate :meth:`FeedView.feed_patterns` for a specific year, month, and day.""" - articles = self.get_article_queryset().filter(date__year=year) + def get_articles_by_ymd(self, obj, request, year, month=None, day=None, extra_context=None): + """Used to generate :meth:`~.FeedView.feed_patterns` for a specific year, month, and day.""" + articles = self.get_article_queryset(obj).filter(date__year=year) if month: articles = articles.filter(date__month=month) if day: articles = articles.filter(date__day=day) return articles, extra_context - def get_articles_by_issue(self, request, numbering, extra_context=None): - """Used to generate :meth:`FeedView.feed_patterns` for articles from a certain issue.""" + def get_articles_by_issue(self, obj, request, numbering, extra_context=None): + """Used to generate :meth:`~.FeedView.feed_patterns` for articles from a certain issue.""" try: - issue = self.get_issue_queryset().get(numbering=numbering) + issue = self.get_issue_queryset(obj).get(numbering=numbering) except NewsletterIssue.DoesNotExist: raise Http404 context = extra_context or {} context.update({'issue': issue}) - return self.get_article_queryset().filter(issues=issue), context + return self.get_article_queryset(obj).filter(issues=issue), context def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None): """Renders :attr:`article_page` with the article specified by the given parameters.""" - articles = self.get_article_queryset() + articles = self.get_article_queryset(self.newsletter) if year: articles = articles.filter(date__year=year) if month: @@ -886,7 +585,7 @@ class NewsletterView(FeedView): context = self.get_context() context.update(extra_context or {}) context.update({ - 'issues': self.get_issue_queryset() + 'issues': self.get_issue_queryset(self.newsletter) }) return self.issue_archive_page.render_to_response(request, extra_context=context) diff --git a/philo/contrib/shipherd/admin.py b/philo/contrib/shipherd/admin.py index be31a43..246693e 100644 --- a/philo/contrib/shipherd/admin.py +++ b/philo/contrib/shipherd/admin.py @@ -11,8 +11,9 @@ NAVIGATION_RAW_ID_FIELDS = ('navigation', 'parent', 'target_node') class NavigationItemInline(admin.StackedInline): raw_id_fields = NAVIGATION_RAW_ID_FIELDS model = NavigationItem - extra = 1 + extra = 0 sortable_field_name = 'order' + ordering = ('order',) related_lookup_fields = {'fk': raw_id_fields} @@ -69,7 +70,7 @@ class NodeNavigationItemInline(NavigationItemInline): class NodeNavigationInline(admin.TabularInline): model = Navigation - extra = 1 + extra = 0 NodeAdmin.inlines = [NodeNavigationInline, NodeNavigationItemInline] + NodeAdmin.inlines diff --git a/philo/contrib/shipherd/models.py b/philo/contrib/shipherd/models.py index 429faaa..95be501 100644 --- a/philo/contrib/shipherd/models.py +++ b/philo/contrib/shipherd/models.py @@ -1,6 +1,9 @@ #encoding: utf-8 from UserDict import DictMixin +from hashlib import sha1 +from django.contrib.sites.models import Site +from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.urlresolvers import NoReverseMatch from django.core.validators import RegexValidator, MinValueValidator @@ -16,17 +19,20 @@ DEFAULT_NAVIGATION_DEPTH = 3 class NavigationMapper(object, DictMixin): """ - The :class:`NavigationMapper` is a dictionary-like object which allows easy fetching of the root items of a navigation for a node according to a key. The fetching goes through the :class:`NavigationManager` and can thus take advantage of the navigation cache. A :class:`NavigationMapper` instance will be available on each node instance as :attr:`Node.navigation` if :mod:`~philo.contrib.shipherd` is in the :setting:`INSTALLED_APPS` + The :class:`NavigationMapper` is a dictionary-like object which allows easy fetching of the root items of a navigation for a node according to a key. A :class:`NavigationMapper` instance will be available on each node instance as :attr:`Node.navigation` if :mod:`~philo.contrib.shipherd` is in the :setting:`INSTALLED_APPS` """ def __init__(self, node): self.node = node + self._cache = {} def __getitem__(self, key): - return Navigation.objects.get_cache_for(self.node)[key]['root_items'] - - def keys(self): - return Navigation.objects.get_cache_for(self.node).keys() + if key not in self._cache: + try: + self._cache[key] = Navigation.objects.get_for_node(self.node, key) + except Navigation.DoesNotExist: + self._cache[key] = None + return self._cache[key] def navigation(self): @@ -38,141 +44,68 @@ def navigation(self): Node.navigation = property(navigation) -class NavigationCacheQuerySet(models.query.QuerySet): - """ - This subclass will trigger general cache clearing for Navigation.objects when a mass - update or deletion is performed. As there is no convenient way to iterate over the - changed or deleted instances, there's no way to be more precise about what gets cleared. - - """ - def update(self, *args, **kwargs): - super(NavigationCacheQuerySet, self).update(*args, **kwargs) - Navigation.objects.clear_cache() - - def delete(self, *args, **kwargs): - super(NavigationCacheQuerySet, self).delete(*args, **kwargs) - Navigation.objects.clear_cache() - - class NavigationManager(models.Manager): - """ - Since navigation on a site will be hit frequently, is relatively costly to compute, and is changed relatively infrequently, the NavigationManager maintains a cache which maps nodes to navigations. - - """ use_for_related = True - _cache = {} - def get_query_set(self): - """ - Returns a :class:`NavigationCacheQuerySet` instance. - - """ - return NavigationCacheQuerySet(self.model, using=self._db) - - def get_cache_for(self, node, update_targets=True): - """Returns the navigation cache for a given :class:`.Node`. If update_targets is ``True``, then :meth:`update_targets_for` will be run with the :class:`.Node`.""" - created = False - if not self.has_cache_for(node): - self.create_cache_for(node) - created = True - - if update_targets and not created: - self.update_targets_for(node) - - return self.__class__._cache[self.db][node] - - def has_cache_for(self, node): - """Returns ``True`` if a cache exists for the :class:`.Node` and ``False`` otherwise.""" - return self.db in self.__class__._cache and node in self.__class__._cache[self.db] - - def create_cache_for(self, node): - """This method loops through the :class:`.Node`\ s ancestors and caches all unique navigation keys.""" - ancestors = node.get_ancestors(ascending=True, include_self=True) - - nodes_to_cache = [] - - for node in ancestors: - if self.has_cache_for(node): - cache = self.get_cache_for(node).copy() - break - else: - nodes_to_cache.insert(0, node) - else: - cache = {} - - for node in nodes_to_cache: - cache = cache.copy() - cache.update(self._build_cache_for(node)) - self.__class__._cache.setdefault(self.db, {})[node] = cache - - def _build_cache_for(self, node): - cache = {} - tree_id_attr = NavigationItem._mptt_meta.tree_id_attr - level_attr = NavigationItem._mptt_meta.level_attr - - for navigation in node.navigation_set.all(): - tree_ids = navigation.roots.values_list(tree_id_attr) - items = list(NavigationItem.objects.filter(**{'%s__in' % tree_id_attr: tree_ids, '%s__lt' % level_attr: navigation.depth}).order_by('order', 'lft')) + def get_for_node(self, node, key): + cache_key = self._get_cache_key(node, key) + cached = cache.get(cache_key) + + if cached is None: + opts = Node._mptt_meta + left = getattr(node, opts.left_attr) + right = getattr(node, opts.right_attr) + tree_id = getattr(node, opts.tree_id_attr) + kwargs = { + "node__%s__lte" % opts.left_attr: left, + "node__%s__gte" % opts.right_attr: right, + "node__%s" % opts.tree_id_attr: tree_id + } + navs = self.filter(key=key, **kwargs).select_related('node').order_by('-node__%s' % opts.level_attr) + nav = navs[0] + roots = nav.roots.all().select_related('target_node').order_by('order') + item_opts = NavigationItem._mptt_meta + by_pk = {} + tree_ids = [] - root_items = [] + site_root_node = Site.objects.get_current().root_node - for item in items: - item._is_cached = True - - if not hasattr(item, '_cached_children'): - item._cached_children = [] - - if item.parent: - # alternatively, if I don't want to force it to a list, I could keep track of - # instances where the parent hasn't yet been met and do this step later for them. - # delayed action. - item.parent = items[items.index(item.parent)] - if not hasattr(item.parent, '_cached_children'): - item.parent._cached_children = [] - item.parent._cached_children.append(item) - else: - root_items.append(item) + for root in roots: + by_pk[root.pk] = root + tree_ids.append(getattr(root, item_opts.tree_id_attr)) + root._cached_children = [] + if root.target_node: + root.target_node.get_path(root=site_root_node) + root.navigation = nav - cache[navigation.key] = { - 'navigation': navigation, - 'root_items': root_items, - 'items': items + kwargs = { + '%s__in' % item_opts.tree_id_attr: tree_ids, + '%s__lt' % item_opts.level_attr: nav.depth, + '%s__gt' % item_opts.level_attr: 0 } + items = NavigationItem.objects.filter(**kwargs).select_related('target_node').order_by('level', 'order') + for item in items: + by_pk[item.pk] = item + item._cached_children = [] + parent_pk = getattr(item, '%s_id' % item_opts.parent_attr) + item.parent = by_pk[parent_pk] + item.parent._cached_children.append(item) + if item.target_node: + item.target_node.get_path(root=site_root_node) + + cached = roots + cache.set(cache_key, cached) - return cache - - def clear_cache_for(self, node): - """Clear the cache for the :class:`.Node` and all its descendants. The navigation for this node has probably changed, and it isn't worth it to figure out which descendants were actually affected by this.""" - if not self.has_cache_for(node): - # Already cleared. - return - - descendants = node.get_descendants(include_self=True) - cache = self.__class__._cache[self.db] - for node in descendants: - cache.pop(node, None) + return cached - def update_targets_for(self, node): - """Manually updates the target nodes for the :class:`.Node`'s cache in case something's changed there. This is a less complex operation than rebuilding the :class:`.Node`'s cache.""" - caches = self.__class__._cache[self.db][node].values() - - target_pks = set() - - for cache in caches: - target_pks |= set([item.target_node_id for item in cache['items']]) - - # A distinct query is not strictly necessary. TODO: benchmark the efficiency - # with/without distinct. - targets = list(Node.objects.filter(pk__in=target_pks).distinct()) + def _get_cache_key(self, node, key): + opts = Node._mptt_meta + left = getattr(node, opts.left_attr) + right = getattr(node, opts.right_attr) + tree_id = getattr(node, opts.tree_id_attr) + parent_id = getattr(node, "%s_id" % opts.parent_attr) - for cache in caches: - for item in cache['items']: - if item.target_node_id: - item.target_node = targets[targets.index(item.target_node)] - - def clear_cache(self): - """Clears the manager's entire navigation cache.""" - self.__class__._cache.pop(self.db, None) + return sha1(unicode(left) + unicode(right) + unicode(tree_id) + unicode(parent_id) + unicode(node.pk) + unicode(key)).hexdigest() class Navigation(Entity): @@ -199,43 +132,14 @@ class Navigation(Entity): #: There is no limit to the depth of a tree of :class:`NavigationItem`\ s, but ``depth`` will limit how much of the tree will be displayed. depth = models.PositiveSmallIntegerField(default=DEFAULT_NAVIGATION_DEPTH, validators=[MinValueValidator(1)], help_text="Defines the maximum display depth of this navigation.") - def __init__(self, *args, **kwargs): - super(Navigation, self).__init__(*args, **kwargs) - self._initial_data = model_to_dict(self) - def __unicode__(self): return "%s[%s]" % (self.node, self.key) - def _has_changed(self): - return self._initial_data != model_to_dict(self) - - def save(self, *args, **kwargs): - super(Navigation, self).save(*args, **kwargs) - - if self._has_changed(): - Navigation.objects.clear_cache_for(self.node) - self._initial_data = model_to_dict(self) - - def delete(self, *args, **kwargs): - super(Navigation, self).delete(*args, **kwargs) - Navigation.objects.clear_cache_for(self.node) - class Meta: unique_together = ('node', 'key') -class NavigationItemManager(TreeEntityManager): - use_for_related = True - - def get_query_set(self): - """Returns a :class:`NavigationCacheQuerySet` instance.""" - return NavigationCacheQuerySet(self.model, using=self._db) - - class NavigationItem(TreeEntity, TargetURLModel): - #: A :class:`NavigationItemManager` instance - objects = NavigationItemManager() - #: A :class:`ForeignKey` to a :class:`Navigation` instance. If this is not null, then the :class:`NavigationItem` will be a root node of the :class:`Navigation` instance. navigation = models.ForeignKey(Navigation, blank=True, null=True, related_name='roots', help_text="Be a root in this navigation tree.") #: The text which will be displayed in the navigation. This is a :class:`CharField` instance with max length 50. @@ -244,11 +148,6 @@ class NavigationItem(TreeEntity, TargetURLModel): #: The order in which the :class:`NavigationItem` will be displayed. order = models.PositiveSmallIntegerField(default=0) - def __init__(self, *args, **kwargs): - super(NavigationItem, self).__init__(*args, **kwargs) - self._initial_data = model_to_dict(self) - self._is_cached = False - def get_path(self, root=None, pathsep=u' › ', field='text'): return super(NavigationItem, self).get_path(root, pathsep, field) path = property(get_path) @@ -275,13 +174,15 @@ class NavigationItem(TreeEntity, TargetURLModel): # the same as the request path, check whether the target node is an ancestor # of the requested node. If so, this is active unless the target node # is the same as the ``host node`` for this navigation structure. - try: - host_node = self.get_root().navigation.node - except AttributeError: - pass - else: - if self.target_node != host_node and self.target_node.is_ancestor_of(request.node): - return True + root = self + + # The common case will be cached items, whose parents are cached with them. + while root.parent is not None: + root = root.parent + + host_node_id = root.navigation.node_id + if self.target_node.pk != host_node_id and self.target_node.is_ancestor_of(request.node): + return True return False @@ -290,27 +191,4 @@ class NavigationItem(TreeEntity, TargetURLModel): for child in self.get_children(): if child.is_active(request) or child.has_active_descendants(request): return True - return False - - def _has_changed(self): - if model_to_dict(self) == self._initial_data: - return False - return True - - def _clear_cache(self): - try: - root = self.get_root() - if self.get_level() < root.navigation.depth: - Navigation.objects.clear_cache_for(self.get_root().navigation.node) - except AttributeError: - pass - - def save(self, *args, **kwargs): - super(NavigationItem, self).save(*args, **kwargs) - - if self._has_changed(): - self._clear_cache() - - def delete(self, *args, **kwargs): - super(NavigationItem, self).delete(*args, **kwargs) - self._clear_cache() \ No newline at end of file + return False \ No newline at end of file diff --git a/philo/contrib/shipherd/templatetags/shipherd.py b/philo/contrib/shipherd/templatetags/shipherd.py index 85a0bc5..4fae9c4 100644 --- a/philo/contrib/shipherd/templatetags/shipherd.py +++ b/philo/contrib/shipherd/templatetags/shipherd.py @@ -131,7 +131,7 @@ def recursenavigation(parser, token):
    {% recursenavigation node "main" %} - {{ item.text }} + {{ item.text }} {% if item.get_children %}
      {{ children }} @@ -140,6 +140,11 @@ def recursenavigation(parser, token): {% endrecursenavigation %}
    + + .. note:: {% recursenavigation %} requires that the current :class:`HttpRequest` be present in the context as ``request``. The simplest way to do this is with the `request context processor`_. Simply make sure that ``django.core.context_processors.request`` is included in your :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting. + + .. _request context processor: https://docs.djangoproject.com/en/dev/ref/templates/api/#django-core-context-processors-request + """ bits = token.contents.split() if len(bits) != 3: @@ -157,13 +162,7 @@ def recursenavigation(parser, token): def has_navigation(node, key=None): """Returns ``True`` if the node has a :class:`.Navigation` with the given key and ``False`` otherwise. If ``key`` is ``None``, returns whether the node has any :class:`.Navigation`\ s at all.""" try: - nav = node.navigation - if key is not None: - if key in nav and bool(node.navigation[key]): - return True - elif key not in node.navigation: - return False - return bool(node.navigation) + return bool(node.navigation[key]) except: return False @@ -172,6 +171,6 @@ def has_navigation(node, key=None): def navigation_host(node, key): """Returns the :class:`.Node` which hosts the :class:`.Navigation` which ``node`` has inherited for ``key``. Returns ``node`` if any exceptions are encountered.""" try: - return Navigation.objects.filter(node__in=node.get_ancestors(include_self=True), key=key).order_by('-node__level')[0].node + return node.navigation[key].node except: return node \ No newline at end of file diff --git a/philo/contrib/sobol/models.py b/philo/contrib/sobol/models.py index b35133e..ffe5871 100644 --- a/philo/contrib/sobol/models.py +++ b/philo/contrib/sobol/models.py @@ -153,18 +153,6 @@ class Click(models.Model): get_latest_by = 'datetime' -class RegistryChoiceField(SlugMultipleChoiceField): - def _get_choices(self): - if isinstance(self._choices, RegistryIterator): - return self._choices.copy() - elif hasattr(self._choices, 'next'): - choices, self._choices = itertools.tee(self._choices) - return choices - else: - return self._choices - choices = property(_get_choices) - - try: from south.modelsinspector import add_introspection_rules except ImportError: @@ -177,8 +165,8 @@ class SearchView(MultiView): """Handles a view for the results of a search, anonymously tracks the selections made by end users, and provides an AJAX API for asynchronous search result loading. This can be particularly useful if some searches are slow.""" #: :class:`ForeignKey` to a :class:`.Page` which will be used to render the search results. results_page = models.ForeignKey(Page, related_name='search_results_related') - #: A :class:`.SlugMultipleChoiceField` whose choices are the contents of the :class:`.SearchRegistry` - searches = RegistryChoiceField(choices=registry.iterchoices()) + #: A :class:`.SlugMultipleChoiceField` whose choices are the contents of :obj:`.sobol.search.registry` + searches = SlugMultipleChoiceField(choices=registry.iterchoices()) #: A :class:`BooleanField` which controls whether or not the AJAX API is enabled. #: #: .. note:: If the AJAX API is enabled, a ``ajax_api_url`` attribute will be added to each search instance containing the url and get parameters for an AJAX request to retrieve results for that search. diff --git a/philo/contrib/sobol/search.py b/philo/contrib/sobol/search.py index eb2a333..a79030a 100644 --- a/philo/contrib/sobol/search.py +++ b/philo/contrib/sobol/search.py @@ -12,7 +12,8 @@ from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.template import loader, Context, Template, TemplateDoesNotExist -from philo.contrib.sobol.utils import make_tracking_querydict, RegistryIterator +from philo.contrib.sobol.utils import make_tracking_querydict +from philo.utils.registry import Registry if getattr(settings, 'SOBOL_USE_EVENTLET', False): @@ -25,7 +26,7 @@ else: __all__ = ( - 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'SearchRegistry', 'registry', 'get_search_instance' + 'Result', 'BaseSearch', 'DatabaseSearch', 'URLSearch', 'JSONSearch', 'GoogleSearch', 'registry', 'get_search_instance' ) @@ -33,74 +34,8 @@ SEARCH_CACHE_SEED = 'philo_sobol_search_results' USE_CACHE = getattr(settings, 'SOBOL_USE_CACHE', True) -class RegistrationError(Exception): - """Raised if there is a problem registering a search with a :class:`SearchRegistry`""" - pass - - -class SearchRegistry(object): - """Holds a registry of search types by slug.""" - - def __init__(self): - self._registry = {} - - def register(self, search, slug=None): - """ - Register a search with the registry. - - :param search: The search class to register - generally a subclass of :class:`BaseSearch` - :param slug: The slug which will be used to register the search class. If ``slug`` is ``None``, the search's default slug will be used. - :raises: :class:`RegistrationError` if a different search is already registered with ``slug``. - - """ - slug = slug or search.slug - if slug in self._registry: - registered = self._registry[slug] - if registered.__module__ != search.__module__: - raise RegistrationError("A different search is already registered as `%s`" % slug) - else: - self._registry[slug] = search - - def unregister(self, search, slug=None): - """ - Unregister a search from the registry. - - :param search: The search class to unregister - generally a subclass of :class:`BaseSearch` - :param slug: If provided, the search will only be removed if it was registered with ``slug``. If not provided, the search class will be unregistered no matter what slug it was registered with. - :raises: :class:`RegistrationError` if a slug is provided but the search registered with that slug is not ``search``. - - """ - if slug is not None: - if slug in self._registry and self._registry[slug] == search: - del self._registry[slug] - raise RegistrationError("`%s` is not registered as `%s`" % (search, slug)) - else: - for slug, search in self._registry.items(): - if search == search: - del self._registry[slug] - - def items(self): - """Returns a list of (slug, search) items in the registry.""" - return self._registry.items() - - def iteritems(self): - """Returns an iterator over the (slug, search) pairs in the registry.""" - return RegistryIterator(self._registry, 'iteritems') - - def iterchoices(self): - """Returns an iterator over (slug, search.verbose_name) pairs for the registry.""" - return RegistryIterator(self._registry, 'iteritems', lambda x: (x[0], x[1].verbose_name)) - - def __getitem__(self, key): - """Returns the search registered with ``key``.""" - return self._registry[key] - - def __iter__(self): - """Returns an iterator over the keys in the registry.""" - return self._registry.__iter__() - - -registry = SearchRegistry() +#: A registry for :class:`BaseSearch` subclasses that should be available in the admin. +registry = Registry() def _make_cache_key(search, search_arg): @@ -119,7 +54,6 @@ def get_search_instance(slug, search_arg): instance = search(search_arg) instance.slug = slug return instance - class Result(object): diff --git a/philo/contrib/waldo/forms.py b/philo/contrib/waldo/forms.py index eb53598..8e14ba5 100644 --- a/philo/contrib/waldo/forms.py +++ b/philo/contrib/waldo/forms.py @@ -74,6 +74,30 @@ class UserAccountForm(forms.ModelForm): kwargs['instance'] = user super(UserAccountForm, self).__init__(*args, **kwargs) + def email_changed(self): + """Returns ``True`` if the email field changed value and ``False`` if it did not, or if there is no email field on the form. This method must be supplied by account forms used with :mod:`~philo.contrib.waldo`.""" + return 'email' in self.changed_data + + def reset_email(self): + """ + ModelForms modify their instances in-place during :meth:`_post_clean`; this method resets the email value to its initial state and returns the altered value. This is a method on the form to allow unusual behavior such as storing email on a :class:`UserProfile`. + + """ + email = self.instance.email + self.instance.email = self.initial['email'] + self.cleaned_data.pop('email') + return email + + @classmethod + def set_email(cls, user, email): + """ + Given a valid instance and an email address, correctly set the email address for that instance and save the changes. This is a class method in order to allow unusual behavior such as storing email on a :class:`UserProfile`. + + """ + user.email = email + user.save() + + class Meta: model = User fields = ('first_name', 'last_name', 'email') diff --git a/philo/contrib/waldo/models.py b/philo/contrib/waldo/models.py index 411cf8e..cdadead 100644 --- a/philo/contrib/waldo/models.py +++ b/philo/contrib/waldo/models.py @@ -107,7 +107,7 @@ class LoginMultiView(MultiView): return HttpResponseRedirect(redirect) else: - form = self.login_form() + form = self.login_form(request) request.session.set_test_cookie() @@ -164,13 +164,13 @@ class PasswordMultiView(LoginMultiView): def urlpatterns(self): urlpatterns = super(PasswordMultiView, self).urlpatterns - if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page: + if self.password_reset_page_id and self.password_reset_confirmation_email_id and self.password_set_page_id: urlpatterns += patterns('', url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'), url(r'^password/reset/(?P\w+)/(?P[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'), ) - if self.password_change_page: + if self.password_change_page_id: urlpatterns += patterns('', url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'), ) @@ -329,7 +329,7 @@ class RegistrationMultiView(PasswordMultiView): @property def urlpatterns(self): urlpatterns = super(RegistrationMultiView, self).urlpatterns - if self.register_page and self.register_confirmation_email: + if self.register_page_id and self.register_confirmation_email_id: urlpatterns += patterns('', url(r'^register$', csrf_protect(self.register), name='register'), url(r'^register/(?P\w+)/(?P[^/]+)$', self.register_confirm, name='register_confirm') @@ -421,11 +421,11 @@ class AccountMultiView(RegistrationMultiView): @property def urlpatterns(self): urlpatterns = super(AccountMultiView, self).urlpatterns - if self.manage_account_page: + if self.manage_account_page_id: urlpatterns += patterns('', url(r'^account$', self.login_required(self.account_view), name='account'), ) - if self.email_change_confirmation_email: + if self.email_change_confirmation_email_id: urlpatterns += patterns('', url(r'^account/email/(?P\w+)/(?P[\w.]+[+][\w.]+)/(?P[^/]+)$', self.email_change_confirm, name='email_change_confirm') ) @@ -444,15 +444,8 @@ class AccountMultiView(RegistrationMultiView): if form.is_valid(): message = "Account information saved." redirect = self.get_requirement_redirect(request, default='') - if 'email' in form.changed_data and self.email_change_confirmation_email: - # ModelForms modify their instances in-place during - # validation, so reset the instance's email to its - # previous value here, then remove the new value - # from cleaned_data. We only do this if an email - # change confirmation email is available. - request.user.email = form.initial['email'] - - email = form.cleaned_data.pop('email') + if form.email_changed() and self.email_change_confirmation_email: + email = form.reset_email() current_site = Site.objects.get_current() @@ -464,7 +457,7 @@ class AccountMultiView(RegistrationMultiView): } self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context) - message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "") + message = "An email has be sent to %s to confirm the email%s." % (email, " change" if bool(request.user.email) else "") if not request.user.email: message += " You will need to confirm the email before accessing pages that require a valid account." redirect = '' @@ -501,7 +494,7 @@ class AccountMultiView(RegistrationMultiView): self.set_requirement_redirect(request, redirect=request.path) redirect = self.reverse('account', node=request.node) else: - redirect = node.get_absolute_url() + redirect = request.node.get_absolute_url() return HttpResponseRedirect(redirect) return view(request, *args, **kwargs) @@ -538,8 +531,7 @@ class AccountMultiView(RegistrationMultiView): raise Http404 if token_generator.check_token(user, email, token): - user.email = email - user.save() + self.account_form.set_email(user, email) messages.add_message(request, messages.SUCCESS, 'Email changed successfully.') if self.manage_account_page: redirect = self.reverse('account', node=request.node) diff --git a/philo/contrib/winer/__init__.py b/philo/contrib/winer/__init__.py new file mode 100644 index 0000000..83fb303 --- /dev/null +++ b/philo/contrib/winer/__init__.py @@ -0,0 +1,4 @@ +""" +Winer provides the same API as `django's syndication Feed class `_, adapted to a Philo-style :class:`~philo.models.nodes.MultiView` for easy database management. Apps that need syndication can simply subclass :class:`~philo.contrib.winer.models.FeedView`, override a few methods, and start serving RSS and Atom feeds. See :class:`~philo.contrib.penfield.models.BlogView` for a concrete implementation example. + +""" \ No newline at end of file diff --git a/philo/contrib/winer/exceptions.py b/philo/contrib/winer/exceptions.py new file mode 100644 index 0000000..e2045f9 --- /dev/null +++ b/philo/contrib/winer/exceptions.py @@ -0,0 +1,3 @@ +class HttpNotAcceptable(Exception): + """This will be raised in :meth:`.FeedView.get_feed_type` if an Http-Accept header will not accept any of the feed content types that are available.""" + pass \ No newline at end of file diff --git a/philo/contrib/winer/feeds.py b/philo/contrib/winer/feeds.py new file mode 100644 index 0000000..0554591 --- /dev/null +++ b/philo/contrib/winer/feeds.py @@ -0,0 +1,13 @@ +from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed + +from philo.utils.registry import Registry + + +DEFAULT_FEED = Atom1Feed + + +registry = Registry() + + +registry.register(Atom1Feed, verbose_name='Atom') +registry.register(Rss201rev2Feed, verbose_name='RSS') \ No newline at end of file diff --git a/philo/contrib/penfield/middleware.py b/philo/contrib/winer/middleware.py similarity index 58% rename from philo/contrib/penfield/middleware.py rename to philo/contrib/winer/middleware.py index a0cd649..89a5bd2 100644 --- a/philo/contrib/penfield/middleware.py +++ b/philo/contrib/winer/middleware.py @@ -1,11 +1,11 @@ from django.http import HttpResponse from django.utils.decorators import decorator_from_middleware -from philo.contrib.penfield.exceptions import HttpNotAcceptable +from philo.contrib.winer.exceptions import HttpNotAcceptable class HttpNotAcceptableMiddleware(object): - """Middleware to catch :exc:`~philo.contrib.penfield.exceptions.HttpNotAcceptable` and return an :class:`HttpResponse` with a 406 response code. See :rfc:`2616`.""" + """Middleware to catch :exc:`~philo.contrib.winer.exceptions.HttpNotAcceptable` and return an :class:`HttpResponse` with a 406 response code. See :rfc:`2616`.""" def process_exception(self, request, exception): if isinstance(exception, HttpNotAcceptable): return HttpResponse(status=406) diff --git a/philo/contrib/winer/models.py b/philo/contrib/winer/models.py new file mode 100644 index 0000000..4acf5d1 --- /dev/null +++ b/philo/contrib/winer/models.py @@ -0,0 +1,347 @@ +from django.conf import settings +from django.conf.urls.defaults import url, patterns, include +from django.contrib.sites.models import Site, RequestSite +from django.contrib.syndication.views import add_domain +from django.db import models +from django.http import HttpResponse +from django.template import RequestContext, Template as DjangoTemplate +from django.utils import feedgenerator, tzinfo +from django.utils.encoding import smart_unicode, force_unicode +from django.utils.html import escape + +from philo.contrib.winer.exceptions import HttpNotAcceptable +from philo.contrib.winer.feeds import registry, DEFAULT_FEED +from philo.contrib.winer.middleware import http_not_acceptable +from philo.models import Page, Template, MultiView + +try: + import mimeparse +except: + mimeparse = None + + +class FeedView(MultiView): + """ + :class:`FeedView` is an abstract model which handles a number of pages and related feeds for a single object such as a blog or newsletter. In addition to all other methods and attributes, :class:`FeedView` supports the same generic API as `django.contrib.syndication.views.Feed `_. + + """ + #: The type of feed which should be served by the :class:`FeedView`. + feed_type = models.CharField(max_length=50, choices=registry.choices, default=registry.get_slug(DEFAULT_FEED)) + #: The suffix which will be appended to a page URL for a :attr:`feed_type` feed of its items. Default: "feed". Note that RSS and Atom feeds will always be available at ``/rss`` and ``/atom`` regardless of the value of this setting. + #: + #: .. seealso:: :meth:`get_feed_type`, :meth:`feed_patterns` + feed_suffix = models.CharField(max_length=255, blank=False, default="feed") + #: A :class:`BooleanField` - whether or not feeds are enabled. + feeds_enabled = models.BooleanField(default=True) + #: A :class:`PositiveIntegerField` - the maximum number of items to return for this feed. All items will be returned if this field is blank. Default: 15. + feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.") + + #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the title of each item in the feed if provided. + item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related") + #: A :class:`ForeignKey` to a :class:`.Template` which will be used to render the description of each item in the feed if provided. + item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related") + + #: An attribute holding the name of the context variable to be populated with the items managed by the :class:`FeedView`. Default: "items" + item_context_var = 'items' + #: An attribute holding the name of the attribute on a subclass of :class:`FeedView` which will contain the main object of a feed (such as a :class:`~philo.contrib.penfield.models.Blog`.) Default: "object" + #: + #: Example:: + #: + #: class BlogView(FeedView): + #: blog = models.ForeignKey(Blog) + #: + #: object_attr = 'blog' + #: item_context_var = 'entries' + object_attr = 'object' + + #: An attribute holding a description of the feeds served by the :class:`FeedView`. This is a required part of the :class:`django.contrib.syndication.view.Feed` API. + description = "" + + def feed_patterns(self, base, get_items_attr, page_attr, reverse_name): + """ + Given the name to be used to reverse this view and the names of the attributes for the function that fetches the objects, returns patterns suitable for inclusion in urlpatterns. In addition to ``base`` (which will serve the page at ``page_attr``) and ``base`` + :attr:`feed_suffix` (which will serve a :attr:`feed_type` feed), patterns will be provided for each registered feed type as ``base`` + ``slug``. + + :param base: The base of the returned patterns - that is, the subpath pattern which will reference the page for the items. The :attr:`feed_suffix` will be appended to this subpath. + :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` which will return an (``items``, ``extra_context``) tuple. This will be passed directly to :meth:`feed_view` and :meth:`page_view`. + :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be passed directly to :meth:`page_view` and will be rendered with the items from ``get_items_attr``. + :param reverse_name: The string which is considered the "name" of the view function returned by :meth:`page_view` for the given parameters. + :returns: Patterns suitable for use in urlpatterns. + + Example:: + + class BlogView(FeedView): + blog = models.ForeignKey(Blog) + entry_archive_page = models.ForeignKey(Page) + + @property + def urlpatterns(self): + urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') + urlpatterns += self.feed_patterns(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day') + return urlpatterns + + def get_entries_by_ymd(request, year, month, day, extra_context=None): + entries = Blog.entries.all() + # filter entries based on the year, month, and day. + return entries, extra_context + + .. seealso:: :meth:`get_feed_type` + + """ + feed_patterns = () + if self.feeds_enabled: + suffixes = [(self.feed_suffix, None)] + [(slug, slug) for slug in registry] + for suffix, feed_type in suffixes: + feed_view = http_not_acceptable(self.feed_view(get_items_attr, reverse_name, feed_type)) + feed_pattern = r'%s%s%s$' % (base, "/" if base and base[-1] != "^" else "", suffix) + feed_patterns += (url(feed_pattern, feed_view, name="%s_%s" % (reverse_name, suffix)),) + feed_patterns += (url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name),) + return patterns('', *feed_patterns) + + def get_object(self, request, **kwargs): + """By default, returns the object stored in the attribute named by :attr:`object_attr`. This can be overridden for subclasses that publish different data for different URL parameters. It is part of the :class:`django.contrib.syndication.views.Feed` API.""" + return getattr(self, self.object_attr) + + def feed_view(self, get_items_attr, reverse_name, feed_type=None): + """ + Returns a view function that renders a list of items as a feed. + + :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with the object for the feed and view arguments. + :param reverse_name: The name which can be used reverse the page for this feed using the :class:`FeedView` as the urlconf. + :param feed_type: The slug used to render the feed class which will be used by the returned view function. + + :returns: A view function that renders a list of items as a feed. + + """ + get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr) + + def inner(request, extra_context=None, *args, **kwargs): + obj = self.get_object(request, *args, **kwargs) + feed = self.get_feed(obj, request, reverse_name, feed_type, *args, **kwargs) + items, xxx = get_items(obj, request, extra_context=extra_context, *args, **kwargs) + self.populate_feed(feed, items, request) + + response = HttpResponse(mimetype=feed.mime_type) + feed.write(response, 'utf-8') + return response + + return inner + + def page_view(self, get_items_attr, page_attr): + """ + :param get_items_attr: A callable or the name of a callable on the :class:`FeedView` that will return a (items, extra_context) tuple when called with view arguments. + :param page_attr: A :class:`.Page` instance or the name of an attribute on the :class:`FeedView` which contains a :class:`.Page` instance. This will be rendered with the items from ``get_items_attr``. + + :returns: A view function that renders a list of items as an :class:`HttpResponse`. + + """ + get_items = get_items_attr if callable(get_items_attr) else getattr(self, get_items_attr) + + def inner(request, extra_context=None, *args, **kwargs): + obj = self.get_object(request, *args, **kwargs) + items, extra_context = get_items(obj, request, extra_context=extra_context, *args, **kwargs) + items, item_context = self.process_page_items(request, items) + + context = self.get_context() + context.update(extra_context or {}) + context.update(item_context or {}) + + page = page_attr if isinstance(page_attr, Page) else getattr(self, page_attr) + return page.render_to_response(request, extra_context=context) + return inner + + def process_page_items(self, request, items): + """ + Hook for handling any extra processing of ``items`` based on an :class:`HttpRequest`, such as pagination or searching. This method is expected to return a list of items and a dictionary to be added to the page context. + + """ + item_context = { + self.item_context_var: items + } + return items, item_context + + def get_feed_type(self, request, feed_type=None): + """ + If ``feed_type`` is not ``None``, returns the corresponding class from the registry or raises :exc:`.HttpNotAcceptable`. + + Otherwise, intelligently chooses a feed type for a given request. Tries to return :attr:`feed_type`, but if the Accept header does not include that mimetype, tries to return the best match from the feed types that are offered by the :class:`FeedView`. If none of the offered feed types are accepted by the :class:`HttpRequest`, raises :exc:`.HttpNotAcceptable`. + + If `mimeparse `_ is installed, it will be used to select the best matching accepted format; otherwise, the first available format that is accepted will be selected. + + """ + if feed_type is not None: + feed_type = registry[feed_type] + loose = False + else: + feed_type = registry.get(self.feed_type, DEFAULT_FEED) + loose = True + mt = feed_type.mime_type + accept = request.META.get('HTTP_ACCEPT') + if accept and mt not in accept and "*/*" not in accept and "%s/*" % mt.split("/")[0] not in accept: + # Wups! They aren't accepting the chosen format. + feed_type = None + if loose: + # Is there another format we can use? + accepted_mts = dict([(obj.mime_type, obj) for obj in registry.values()]) + if mimeparse: + mt = mimeparse.best_match(accepted_mts.keys(), accept) + if mt: + feed_type = accepted_mts[mt] + else: + for mt in accepted_mts: + if mt in accept or "%s/*" % mt.split("/")[0] in accept: + feed_type = accepted_mts[mt] + break + if not feed_type: + raise HttpNotAcceptable + return feed_type + + def get_feed(self, obj, request, reverse_name, feed_type=None, *args, **kwargs): + """ + Returns an unpopulated :class:`django.utils.feedgenerator.DefaultFeed` object for this object. + + :param obj: The object for which the feed should be generated. + :param request: The current request. + :param reverse_name: The name which can be used to reverse the URL of the page corresponding to this feed. + :param feed_type: The slug used to register the feed class that will be instantiated and returned. + + :returns: An instance of the feed class registered as ``feed_type``, falling back to :attr:`feed_type` if ``feed_type`` is ``None``. + + """ + try: + current_site = Site.objects.get_current() + except Site.DoesNotExist: + current_site = RequestSite(request) + + feed_type = self.get_feed_type(request, feed_type) + node = request.node + link = node.construct_url(self.reverse(reverse_name, args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure()) + + feed = feed_type( + title = self.__get_dynamic_attr('title', obj), + subtitle = self.__get_dynamic_attr('subtitle', obj), + link = link, + description = self.__get_dynamic_attr('description', obj), + language = settings.LANGUAGE_CODE.decode(), + feed_url = add_domain( + current_site.domain, + self.__get_dynamic_attr('feed_url', obj) or node.construct_url(self.reverse("%s_%s" % (reverse_name, registry.get_slug(feed_type)), args=args, kwargs=kwargs), with_domain=True, request=request, secure=request.is_secure()), + request.is_secure() + ), + author_name = self.__get_dynamic_attr('author_name', obj), + author_link = self.__get_dynamic_attr('author_link', obj), + author_email = self.__get_dynamic_attr('author_email', obj), + categories = self.__get_dynamic_attr('categories', obj), + feed_copyright = self.__get_dynamic_attr('feed_copyright', obj), + feed_guid = self.__get_dynamic_attr('feed_guid', obj), + ttl = self.__get_dynamic_attr('ttl', obj), + **self.feed_extra_kwargs(obj) + ) + return feed + + def populate_feed(self, feed, items, request): + """Populates a :class:`django.utils.feedgenerator.DefaultFeed` instance as is returned by :meth:`get_feed` with the passed-in ``items``.""" + if self.item_title_template: + title_template = DjangoTemplate(self.item_title_template.code) + else: + title_template = None + if self.item_description_template: + description_template = DjangoTemplate(self.item_description_template.code) + else: + description_template = None + + node = request.node + try: + current_site = Site.objects.get_current() + except Site.DoesNotExist: + current_site = RequestSite(request) + + if self.feed_length is not None: + items = items[:self.feed_length] + + for item in items: + if title_template is not None: + title = title_template.render(RequestContext(request, {'obj': item})) + else: + title = self.__get_dynamic_attr('item_title', item) + if description_template is not None: + description = description_template.render(RequestContext(request, {'obj': item})) + else: + description = self.__get_dynamic_attr('item_description', item) + + link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure()) + + enc = None + enc_url = self.__get_dynamic_attr('item_enclosure_url', item) + if enc_url: + enc = feedgenerator.Enclosure( + url = smart_unicode(add_domain( + current_site.domain, + enc_url, + request.is_secure() + )), + length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)), + mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item)) + ) + author_name = self.__get_dynamic_attr('item_author_name', item) + if author_name is not None: + author_email = self.__get_dynamic_attr('item_author_email', item) + author_link = self.__get_dynamic_attr('item_author_link', item) + else: + author_email = author_link = None + + pubdate = self.__get_dynamic_attr('item_pubdate', item) + if pubdate and not pubdate.tzinfo: + ltz = tzinfo.LocalTimezone(pubdate) + pubdate = pubdate.replace(tzinfo=ltz) + + feed.add_item( + title = title, + link = link, + description = description, + unique_id = self.__get_dynamic_attr('item_guid', item, link), + enclosure = enc, + pubdate = pubdate, + author_name = author_name, + author_email = author_email, + author_link = author_link, + categories = self.__get_dynamic_attr('item_categories', item), + item_copyright = self.__get_dynamic_attr('item_copyright', item), + **self.item_extra_kwargs(item) + ) + + def __get_dynamic_attr(self, attname, obj, default=None): + try: + attr = getattr(self, attname) + except AttributeError: + return default + if callable(attr): + # Check func_code.co_argcount rather than try/excepting the + # function and catching the TypeError, because something inside + # the function may raise the TypeError. This technique is more + # accurate. + if hasattr(attr, 'func_code'): + argcount = attr.func_code.co_argcount + else: + argcount = attr.__call__.func_code.co_argcount + if argcount == 2: # one argument is 'self' + return attr(obj) + else: + return attr() + return attr + + def feed_extra_kwargs(self, obj): + """Returns an extra keyword arguments dictionary that is used when initializing the feed generator.""" + return {} + + def item_extra_kwargs(self, item): + """Returns an extra keyword arguments dictionary that is used with the `add_item` call of the feed generator.""" + return {} + + def item_title(self, item): + return escape(force_unicode(item)) + + def item_description(self, item): + return force_unicode(item) + + class Meta: + abstract=True \ No newline at end of file diff --git a/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py b/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py new file mode 100644 index 0000000..75a3dee --- /dev/null +++ b/philo/migrations/0018_auto__chg_field_node_view_object_id__chg_field_node_view_content_type.py @@ -0,0 +1,145 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Node.view_object_id' + db.alter_column('philo_node', 'view_object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)) + + # Changing field 'Node.view_content_type' + db.alter_column('philo_node', 'view_content_type_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['contenttypes.ContentType'])) + + + def backwards(self, orm): + + # User chose to not deal with backwards NULL issues for 'Node.view_object_id' + raise RuntimeError("Cannot reverse this migration. 'Node.view_object_id' and its values cannot be restored.") + + # User chose to not deal with backwards NULL issues for 'Node.view_content_type' + raise RuntimeError("Cannot reverse this migration. 'Node.view_content_type' and its values cannot be restored.") + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.collection': { + 'Meta': {'object_name': 'Collection'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.collectionmember': { + 'Meta': {'object_name': 'CollectionMember'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'philo.contentlet': { + 'Meta': {'object_name': 'Contentlet'}, + 'content': ('philo.models.fields.TemplateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"}) + }, + 'philo.contentreference': { + 'Meta': {'object_name': 'ContentReference'}, + 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"}) + }, + 'philo.file': { + 'Meta': {'object_name': 'File'}, + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.foreignkeyvalue': { + 'Meta': {'object_name': 'ForeignKeyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.jsonvalue': { + 'Meta': {'object_name': 'JSONValue'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'}) + }, + 'philo.manytomanyvalue': { + 'Meta': {'object_name': 'ManyToManyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'philo.page': { + 'Meta': {'object_name': 'Page'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.redirect': { + 'Meta': {'object_name': 'Redirect'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}), + 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}), + 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'philo.tag': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + 'philo.template': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + } + } + + complete_apps = ['philo'] diff --git a/philo/migrations/0019_to_taggit.py b/philo/migrations/0019_to_taggit.py new file mode 100644 index 0000000..fb5e8f0 --- /dev/null +++ b/philo/migrations/0019_to_taggit.py @@ -0,0 +1,155 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # If any tags are longer than 100, this will result in some data loss. + PhiloTag = orm['philo.Tag'] + Tag = orm['taggit.Tag'] + + for tag in PhiloTag.objects.all(): + Tag.objects.get_or_create(name=tag.name, slug=tag.slug) + + + def backwards(self, orm): + "Write your backwards methods here." + pass + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.collection': { + 'Meta': {'object_name': 'Collection'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.collectionmember': { + 'Meta': {'object_name': 'CollectionMember'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'philo.contentlet': { + 'Meta': {'object_name': 'Contentlet'}, + 'content': ('philo.models.fields.TemplateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"}) + }, + 'philo.contentreference': { + 'Meta': {'object_name': 'ContentReference'}, + 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"}) + }, + 'philo.file': { + 'Meta': {'object_name': 'File'}, + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.foreignkeyvalue': { + 'Meta': {'object_name': 'ForeignKeyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.jsonvalue': { + 'Meta': {'object_name': 'JSONValue'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'}) + }, + 'philo.manytomanyvalue': { + 'Meta': {'object_name': 'ManyToManyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'philo.page': { + 'Meta': {'object_name': 'Page'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.redirect': { + 'Meta': {'object_name': 'Redirect'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}), + 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}), + 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'philo.tag': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + 'philo.template': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['taggit', 'philo'] diff --git a/philo/migrations/0020_from_taggit.py b/philo/migrations/0020_from_taggit.py new file mode 100644 index 0000000..9a43df9 --- /dev/null +++ b/philo/migrations/0020_from_taggit.py @@ -0,0 +1,154 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + pass + + + def backwards(self, orm): + "Write your backwards methods here." + PhiloTag = orm['philo.Tag'] + Tag = orm['taggit.Tag'] + + for tag in Tag.objects.all(): + PhiloTag.objects.get_or_create(name=tag.name, slug=tag.slug) + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.collection': { + 'Meta': {'object_name': 'Collection'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.collectionmember': { + 'Meta': {'object_name': 'CollectionMember'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'philo.contentlet': { + 'Meta': {'object_name': 'Contentlet'}, + 'content': ('philo.models.fields.TemplateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"}) + }, + 'philo.contentreference': { + 'Meta': {'object_name': 'ContentReference'}, + 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"}) + }, + 'philo.file': { + 'Meta': {'object_name': 'File'}, + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.foreignkeyvalue': { + 'Meta': {'object_name': 'ForeignKeyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.jsonvalue': { + 'Meta': {'object_name': 'JSONValue'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'}) + }, + 'philo.manytomanyvalue': { + 'Meta': {'object_name': 'ManyToManyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'philo.page': { + 'Meta': {'object_name': 'Page'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.redirect': { + 'Meta': {'object_name': 'Redirect'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}), + 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}), + 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'philo.tag': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + 'philo.template': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + }, + 'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + } + } + + complete_apps = ['taggit', 'philo'] diff --git a/philo/migrations/0021_auto__del_tag.py b/philo/migrations/0021_auto__del_tag.py new file mode 100644 index 0000000..f63b906 --- /dev/null +++ b/philo/migrations/0021_auto__del_tag.py @@ -0,0 +1,138 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting model 'Tag' + db.delete_table('philo_tag') + + + def backwards(self, orm): + + # Adding model 'Tag' + db.create_table('philo_tag', ( + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255, unique=True, db_index=True)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal('philo', ['Tag']) + + + models = { + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'philo.attribute': { + 'Meta': {'unique_together': "(('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))", 'object_name': 'Attribute'}, + 'entity_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_entity_set'", 'to': "orm['contenttypes.ContentType']"}), + 'entity_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'value_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attribute_value_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'value_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.collection': { + 'Meta': {'object_name': 'Collection'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.collectionmember': { + 'Meta': {'object_name': 'CollectionMember'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'members'", 'to': "orm['philo.Collection']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'member_content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'member_object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + 'philo.contentlet': { + 'Meta': {'object_name': 'Contentlet'}, + 'content': ('philo.models.fields.TemplateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentlets'", 'to': "orm['philo.Page']"}) + }, + 'philo.contentreference': { + 'Meta': {'object_name': 'ContentReference'}, + 'content_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contentreferences'", 'to': "orm['philo.Page']"}) + }, + 'philo.file': { + 'Meta': {'object_name': 'File'}, + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.foreignkeyvalue': { + 'Meta': {'object_name': 'ForeignKeyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'philo.jsonvalue': { + 'Meta': {'object_name': 'JSONValue'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'value': ('philo.models.fields.JSONField', [], {'default': "'null'", 'db_index': 'True'}) + }, + 'philo.manytomanyvalue': { + 'Meta': {'object_name': 'ManyToManyValue'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'values': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['philo.ForeignKeyValue']", 'null': 'True', 'blank': 'True'}) + }, + 'philo.node': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Node'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'view_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'node_view_set'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), + 'view_object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'philo.page': { + 'Meta': {'object_name': 'Page'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': "orm['philo.Template']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'philo.redirect': { + 'Meta': {'object_name': 'Redirect'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'reversing_parameters': ('philo.models.fields.JSONField', [], {'blank': 'True'}), + 'status_code': ('django.db.models.fields.IntegerField', [], {'default': '302'}), + 'target_node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'philo_redirect_related'", 'null': 'True', 'to': "orm['philo.Node']"}), + 'url_or_subpath': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'philo.template': { + 'Meta': {'unique_together': "(('parent', 'slug'),)", 'object_name': 'Template'}, + 'code': ('philo.models.fields.TemplateField', [], {}), + 'documentation': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'default': "'text/html'", 'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['philo.Template']"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + } + } + + complete_apps = ['philo'] diff --git a/philo/models/base.py b/philo/models/base.py index 2f798ae..e7918f5 100644 --- a/philo/models/base.py +++ b/philo/models/base.py @@ -16,23 +16,7 @@ from philo.utils.entities import AttributeMapper, TreeAttributeMapper from philo.validators import json_validator -__all__ = ('Tag', 'value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity', 'SlugTreeEntity') - - -class Tag(models.Model): - """A simple, generic model for tagging.""" - #: A CharField (max length 255) which contains the name of the tag. - name = models.CharField(max_length=255) - #: A CharField (max length 255) which contains the tag's unique slug. - slug = models.SlugField(max_length=255, unique=True) - - def __unicode__(self): - """Returns the value of the :attr:`name` field""" - return self.name - - class Meta: - app_label = 'philo' - ordering = ('name',) +__all__ = ('value_content_type_limiter', 'register_value_model', 'unregister_value_model', 'JSONValue', 'ForeignKeyValue', 'ManyToManyValue', 'Attribute', 'Entity', 'TreeEntity', 'SlugTreeEntity') #: An instance of :class:`.ContentTypeRegistryLimiter` which is used to track the content types which can be related to by :class:`ForeignKeyValue`\ s and :class:`ManyToManyValue`\ s. @@ -44,9 +28,6 @@ def register_value_model(model): value_content_type_limiter.register_class(model) -register_value_model(Tag) - - def unregister_value_model(model): """Registers a model as a valid content type for a :class:`ForeignKeyValue` or :class:`ManyToManyValue` through the :data:`value_content_type_limiter`.""" value_content_type_limiter.unregister_class(model) @@ -458,11 +439,12 @@ class TreeEntity(Entity, MPTTModel): objects = TreeEntityManager() parent = models.ForeignKey('self', related_name='children', null=True, blank=True) - def get_path(self, root=None, pathsep='/', field='slug'): + def get_path(self, root=None, pathsep='/', field='pk', memoize=True): """ :param root: Only return the path since this object. :param pathsep: The path separator to use when constructing an instance's path :param field: The field to pull path information from for each ancestor. + :param memoize: Whether to use memoized results. Since, in most cases, the ancestors of a TreeEntity will not change over the course of an instance's lifetime, this defaults to ``True``. :returns: A string representation of an object's path. """ @@ -470,18 +452,33 @@ class TreeEntity(Entity, MPTTModel): if root == self: return '' - if root is None and self.is_root_node(): + parent_id = getattr(self, "%s_id" % self._mptt_meta.parent_attr) + if getattr(root, 'pk', None) == parent_id: return getattr(self, field, '?') if root is not None and not self.is_descendant_of(root): raise AncestorDoesNotExist(root) + if memoize: + memo_args = (parent_id, getattr(root, 'pk', None), pathsep, getattr(self, field, '?')) + try: + return self._path_memo[memo_args] + except AttributeError: + self._path_memo = {} + except KeyError: + pass + qs = self.get_ancestors(include_self=True) if root is not None: qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()}) - return pathsep.join([getattr(parent, field, '?') for parent in qs]) + path = pathsep.join([getattr(parent, field, '?') for parent in qs]) + + if memoize: + self._path_memo[memo_args] = path + + return path path = property(get_path) def get_attribute_mapper(self, mapper=None): @@ -500,7 +497,7 @@ class TreeEntity(Entity, MPTTModel): """ if mapper is None: - if self.parent: + if getattr(self, "%s_id" % self._mptt_meta.parent_attr): mapper = TreeAttributeMapper else: mapper = AttributeMapper @@ -522,12 +519,12 @@ class SlugTreeEntity(TreeEntity): objects = SlugTreeEntityManager() slug = models.SlugField(max_length=255) - def get_path(self, root=None, pathsep='/', field='slug'): - return super(SlugTreeEntity, self).get_path(root, pathsep, field) + def get_path(self, root=None, pathsep='/', field='slug', memoize=True): + return super(SlugTreeEntity, self).get_path(root, pathsep, field, memoize) path = property(get_path) def clean(self): - if self.parent is None: + if getattr(self, "%s_id" % self._mptt_meta.parent_attr) is None: try: self._default_manager.exclude(pk=self.pk).get(slug=self.slug, parent__isnull=True) except self.DoesNotExist: diff --git a/philo/models/fields/__init__.py b/philo/models/fields/__init__.py index efd315f..7ab4326 100644 --- a/philo/models/fields/__init__.py +++ b/philo/models/fields/__init__.py @@ -7,6 +7,7 @@ from django.utils.text import capfirst from django.utils.translation import ugettext_lazy as _ from philo.forms.fields import JSONFormField +from philo.utils.registry import RegistryIterator from philo.validators import TemplateValidator, json_validator #from philo.models.fields.entities import * @@ -71,7 +72,7 @@ class JSONField(models.TextField): class SlugMultipleChoiceField(models.Field): - """Stores a selection of multiple items with unique slugs in the form of a comma-separated list.""" + """Stores a selection of multiple items with unique slugs in the form of a comma-separated list. Also knows how to correctly handle :class:`RegistryIterator`\ s passed in as choices.""" __metaclass__ = models.SubfieldBase description = _("Comma-separated slug field") @@ -127,6 +128,16 @@ class SlugMultipleChoiceField(models.Field): if invalid_values: # should really make a custom message. raise ValidationError(self.error_messages['invalid_choice'] % invalid_values) + + def _get_choices(self): + if isinstance(self._choices, RegistryIterator): + return self._choices.copy() + elif hasattr(self._choices, 'next'): + choices, self._choices = itertools.tee(self._choices) + return choices + else: + return self._choices + choices = property(_get_choices) try: diff --git a/philo/models/nodes.py b/philo/models/nodes.py index 93f772a..647ba81 100644 --- a/philo/models/nodes.py +++ b/philo/models/nodes.py @@ -2,9 +2,11 @@ from inspect import getargspec import mimetypes from os.path import basename +from django.conf import settings from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site, RequestSite +from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.servers.basehttp import FileWrapper from django.core.urlresolvers import resolve, clear_url_caches, reverse, NoReverseMatch @@ -24,6 +26,7 @@ __all__ = ('Node', 'View', 'MultiView', 'Redirect', 'File') _view_content_type_limiter = ContentTypeSubclassLimiter(None) +CACHE_PHILO_ROOT = getattr(settings, "PHILO_CACHE_PHILO_ROOT", True) class Node(SlugTreeEntity): @@ -31,24 +34,30 @@ class Node(SlugTreeEntity): :class:`Node`\ s are the basic building blocks of a website using Philo. They define the URL hierarchy and connect each URL to a :class:`View` subclass instance which is used to generate an HttpResponse. """ - view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter) - view_object_id = models.PositiveIntegerField() + view_content_type = models.ForeignKey(ContentType, related_name='node_view_set', limit_choices_to=_view_content_type_limiter, blank=True, null=True) + view_object_id = models.PositiveIntegerField(blank=True, null=True) #: :class:`GenericForeignKey` to a non-abstract subclass of :class:`View` view = generic.GenericForeignKey('view_content_type', 'view_object_id') @property def accepts_subpath(self): """A property shortcut for :attr:`self.view.accepts_subpath `""" - if self.view: - return self.view.accepts_subpath + if self.view_object_id and self.view_content_type_id: + return ContentType.objects.get_for_id(self.view_content_type_id).model_class().accepts_subpath return False def handles_subpath(self, subpath): - return self.view.handles_subpath(subpath) + if self.view_object_id and self.view_content_type_id: + return ContentType.objects.get_for_id(self.view_content_type_id).model_class().handles_subpath(subpath) + return False def render_to_response(self, request, extra_context=None): """This is a shortcut method for :meth:`View.render_to_response`""" - return self.view.render_to_response(request, extra_context) + if self.view_object_id and self.view_content_type_id: + view_model = ContentType.objects.get_for_id(self.view_content_type_id).model_class() + self.view = view_model._default_manager.get(pk=self.view_object_id) + return self.view.render_to_response(request, extra_context) + raise Http404 def get_absolute_url(self, request=None, with_domain=False, secure=False): """ @@ -65,6 +74,8 @@ class Node(SlugTreeEntity): Node urls will not contain a trailing slash unless a subpath is provided which ends with a trailing slash. Subpaths are expected to begin with a slash, as if returned by :func:`django.core.urlresolvers.reverse`. + Because this method will be called frequently and will always try to reverse ``philo-root``, the results of that reversal will be cached by default. This can be disabled by setting :setting:`PHILO_CACHE_PHILO_ROOT` to ``False``. + :meth:`construct_url` may raise the following exceptions: - :class:`NoReverseMatch` if "philo-root" is not reversable -- for example, if :mod:`philo.urls` is not included anywhere in your urlpatterns. @@ -79,7 +90,14 @@ class Node(SlugTreeEntity): """ # Try reversing philo-root first, since we can't do anything if that fails. - root_url = reverse('philo-root') + if CACHE_PHILO_ROOT: + key = "CACHE_PHILO_ROOT__" + settings.ROOT_URLCONF + root_url = cache.get(key) + if root_url is None: + root_url = reverse('philo-root') + cache.set(key, root_url) + else: + root_url = reverse('philo-root') try: current_site = Site.objects.get_current() @@ -122,12 +140,13 @@ class View(Entity): #: A generic relation back to nodes. nodes = generic.GenericRelation(Node, content_type_field='view_content_type', object_id_field='view_object_id') - #: Property or attribute which defines whether this :class:`View` can handle subpaths. Default: ``False`` + #: An attribute on the class which defines whether this :class:`View` can handle subpaths. Default: ``False`` accepts_subpath = False - def handles_subpath(self, subpath): + @classmethod + def handles_subpath(cls, subpath): """Returns True if the :class:`View` handles the given subpath, and False otherwise.""" - if not self.accepts_subpath and subpath != "/": + if not cls.accepts_subpath and subpath != "/": return False return True @@ -222,15 +241,6 @@ class MultiView(View): """Returns urlpatterns that point to views (generally methods on the class). :class:`MultiView`\ s can be thought of as "managing" these subpaths.""" raise NotImplementedError("MultiView subclasses must implement urlpatterns.") - def handles_subpath(self, subpath): - if not super(MultiView, self).handles_subpath(subpath): - return False - try: - resolve(subpath, urlconf=self) - except Http404: - return False - return True - def actually_render_to_response(self, request, extra_context=None): """ Resolves the remaining subpath left after finding this :class:`View`'s node using :attr:`self.urlpatterns ` and renders the view function (or method) found with the appropriate args and kwargs. @@ -325,8 +335,17 @@ class TargetURLModel(models.Model): kwargs = dict([(smart_str(k, 'ascii'), v) for k, v in params.items()]) return self.url_or_subpath, args, kwargs - def get_target_url(self): - """Calculates and returns the target url based on the :attr:`target_node`, :attr:`url_or_subpath`, and :attr:`reversing_parameters`.""" + def get_target_url(self, memoize=True): + """Calculates and returns the target url based on the :attr:`target_node`, :attr:`url_or_subpath`, and :attr:`reversing_parameters`. The results will be memoized by default; this can be prevented by passing in ``memoize=False``.""" + if memoize: + memo_args = (self.target_node_id, self.url_or_subpath, self.reversing_parameters_json) + try: + return self._target_url_memo[memo_args] + except AttributeError: + self._target_url_memo = {} + except KeyError: + pass + node = self.target_node if node is not None and node.accepts_subpath and self.url_or_subpath: if self.reversing_parameters is not None: @@ -336,14 +355,19 @@ class TargetURLModel(models.Model): subpath = self.url_or_subpath if subpath[0] != '/': subpath = '/' + subpath - return node.construct_url(subpath) + target_url = node.construct_url(subpath) elif node is not None: - return node.get_absolute_url() + target_url = node.get_absolute_url() else: if self.reversing_parameters is not None: view_name, args, kwargs = self.get_reverse_params() - return reverse(view_name, args=args, kwargs=kwargs) - return self.url_or_subpath + target_url = reverse(view_name, args=args, kwargs=kwargs) + else: + target_url = self.url_or_subpath + + if memoize: + self._target_url_memo[memo_args] = target_url + return target_url target_url = property(get_target_url) class Meta: diff --git a/philo/models/pages.py b/philo/models/pages.py index ea3bb64..350bce5 100644 --- a/philo/models/pages.py +++ b/philo/models/pages.py @@ -4,102 +4,24 @@ """ -import itertools - from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic from django.core.exceptions import ValidationError from django.db import models from django.http import HttpResponse -from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, TextNode, VariableNode -from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext -from django.utils.datastructures import SortedDict +from django.template import Context, RequestContext, Template as DjangoTemplate from philo.models.base import SlugTreeEntity, register_value_model from philo.models.fields import TemplateField from philo.models.nodes import View from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string -from philo.templatetags.containers import ContainerNode -from philo.utils import fattr -from philo.validators import LOADED_TEMPLATE_ATTR +from philo.utils import templates __all__ = ('Template', 'Page', 'Contentlet', 'ContentReference') -class LazyContainerFinder(object): - def __init__(self, nodes, extends=False): - self.nodes = nodes - self.initialized = False - self.contentlet_specs = [] - self.contentreference_specs = SortedDict() - self.blocks = {} - self.block_super = False - self.extends = extends - - def process(self, nodelist): - for node in nodelist: - if self.extends: - if isinstance(node, BlockNode): - self.blocks[node.name] = block = LazyContainerFinder(node.nodelist) - block.initialize() - self.blocks.update(block.blocks) - continue - - if isinstance(node, ContainerNode): - if not node.references: - self.contentlet_specs.append(node.name) - else: - if node.name not in self.contentreference_specs.keys(): - self.contentreference_specs[node.name] = node.references - continue - - if isinstance(node, VariableNode): - if node.filter_expression.var.lookups == (u'block', u'super'): - self.block_super = True - - if hasattr(node, 'child_nodelists'): - for nodelist_name in node.child_nodelists: - if hasattr(node, nodelist_name): - nodelist = getattr(node, nodelist_name) - self.process(nodelist) - - # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a - # node as rendering an additional template. Philo monkeypatches the attribute onto - # the relevant default nodes and declares it on any native nodes. - if hasattr(node, LOADED_TEMPLATE_ATTR): - loaded_template = getattr(node, LOADED_TEMPLATE_ATTR) - if loaded_template: - nodelist = loaded_template.nodelist - self.process(nodelist) - - def initialize(self): - if not self.initialized: - self.process(self.nodes) - self.initialized = True - - -def build_extension_tree(nodelist): - nodelists = [] - extends = None - for node in nodelist: - if not isinstance(node, TextNode): - if isinstance(node, ExtendsNode): - extends = node - break - - if extends: - if extends.nodelist: - nodelists.append(LazyContainerFinder(extends.nodelist, extends=True)) - loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR) - nodelists.extend(build_extension_tree(loaded_template.nodelist)) - else: - # Base case: root. - nodelists.append(LazyContainerFinder(nodelist)) - return nodelists - - class Template(SlugTreeEntity): """Represents a database-driven django template.""" #: The name of the template. Used for organization and debugging. @@ -111,38 +33,14 @@ class Template(SlugTreeEntity): #: An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template. code = TemplateField(secure=False, verbose_name='django template code') - @property - def containers(self): + def get_containers(self): """ Returns a tuple where the first item is a list of names of contentlets referenced by containers, and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers. This will break if there is a recursive extends or includes in the template code. Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work. """ template = DjangoTemplate(self.code) - - # Build a tree of the templates we're using, placing the root template first. - levels = build_extension_tree(template.nodelist) - - contentlet_specs = [] - contentreference_specs = SortedDict() - blocks = {} - - for level in reversed(levels): - level.initialize() - contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, level.contentlet_specs)) - contentreference_specs.update(level.contentreference_specs) - for name, block in level.blocks.items(): - if block.block_super: - blocks.setdefault(name, []).append(block) - else: - blocks[name] = [block] - - for block_list in blocks.values(): - for block in block_list: - block.initialize() - contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, block.contentlet_specs)) - contentreference_specs.update(block.contentreference_specs) - - return contentlet_specs, contentreference_specs + return templates.get_containers(template) + containers = property(get_containers) def __unicode__(self): """Returns the value of the :attr:`name` field.""" diff --git a/philo/static/philo/css/EmbedWidget.css b/philo/static/philo/css/EmbedWidget.css new file mode 100644 index 0000000..525e5e3 --- /dev/null +++ b/philo/static/philo/css/EmbedWidget.css @@ -0,0 +1,51 @@ +.embed-widget{ + float:left; +} +.embed-toolbar{ + border:1px solid #CCC; + border-bottom:0; + padding:3px 5px; + background:#EEE -webkit-linear-gradient(#F5F5F5, #DDD); + background:#EEE -moz-linear-gradient(#F5F5F5, #DDD); + background-color:#EEE; +} +.embed-widget textarea{ + margin-top:0; +} +.embed-widget button, .embed-widget select{ + vertical-align:middle; + margin-right:3px; +} +.embed-toolbar button{ + background:#FFF; + border:1px solid #CCC; + border-radius:3px; + -webkit-border-radius:3px; + -moz-border-radius:3px; + color:#666; +} +.embed-toolbar button:hover{ + color:#444; +} +.embed-toolbar button:active{ + color:#FFF; + background:#666; + border-color:#666; +} + +.grappelli .embed-widget{ + background:#DDD; + padding:2px; + border:1px solid #CCC; + border-radius:5px; + -webkit-border-radius:5px; + -moz-border-radius:5px; + display:inline-block; + margin:0 -3px; +} +.grappelli .embed-toolbar{ + padding:0; + padding-bottom:3px; + background:none; + border:none; +} \ No newline at end of file diff --git a/philo/static/philo/js/EmbedWidget.js b/philo/static/philo/js/EmbedWidget.js new file mode 100644 index 0000000..7293125 --- /dev/null +++ b/philo/static/philo/js/EmbedWidget.js @@ -0,0 +1,152 @@ +;(function ($) { + var widget = window.embedWidget; + + widget = { + options: {}, + optgroups: {}, + init: function () { + var EmbedFields = widget.EmbedFields = $('.embedding'), + EmbedWidgets = widget.EmbedWidgets, + EmbedBars = widget.EmbedBars, + EmbedButtons = widget.EmbedButtons, + EmbedSelects = widget.EmbedSelects; + + EmbedFields.wrap($('
    ')); + EmbedWidgets = $('.embed-widget'); + EmbedWidgets.prepend($('
    ')); + EmbedBars = $('.embed-toolbar'); + EmbedBars.append(''); + EmbedButtons = $('.embed-button'); + EmbedSelects = $('.embed-select'); + + widget.parseContentTypes(); + EmbedSelects.each(widget.populateSelect); + + EmbedButtons.click(widget.buttonHandler); + + // overload the dismissRelatedLookupPopup function + oldDismissRelatedLookupPopup = window.dismissRelatedLookupPopup; + window.dismissRelatedLookupPopup = function (win, chosenId) { + var name = windowname_to_id(win.name), + elem = $('#'+name), val; + // if the original element was an embed widget, run our script + if (elem.parent().hasClass('embed-widget')) { + contenttype = $('select',elem.parent()).val(); + widget.appendEmbed(elem, contenttype, chosenId); + elem.focus(); + win.close(); + return; + } + // otherwise, do what you usually do + oldDismissRelatedLookupPopup.apply(this, arguments); + } + + // overload the dismissAddAnotherPopup function + oldDismissAddAnotherPopup = window.dismissAddAnotherPopup; + window.dismissAddAnotherPopup = function (win, newId, newRepr) { + var name = windowname_to_id(win.name), + elem = $('#'+win.name), val; + if (elem.parent().hasClass('embed-widget')) { + dismissRelatedLookupPopup(win, newId); + } + // otherwise, do what you usually do + oldDismissAddAnotherPopup.apply(this, arguments); + } + + // Add grappelli to the body class if the admin is grappelli. This will allow us to customize styles accordingly. + if (window.grappelli) { + $(document.body).addClass('grappelli'); + } + }, + parseContentTypes: function () { + var string = widget.EmbedFields.eq(0).attr('data-content-types'), + data = $.parseJSON(string), + i=0, + current_app_label = '', + optgroups = {}; + + // this loop relies on data being clustered by app + for(i=0; i < data.length; i++){ + item = data[i] + // run this next loop every time we encounter a new app label + if (item.app_label !== current_app_label) { + current_app_label = item.app_label; + optgroups[current_app_label] = {} + } + optgroups[current_app_label][item.verbose_name] = [item.app_label,item.object_name].join('.'); + + widget.optgroups = optgroups; + } + }, + populateSelect: function () { + var $this = $(this), + optgroups = widget.optgroups, + optgroup_els = {}, + optgroup_el, group; + + // append a title + $this.append(''); + + // for each group + for (name in optgroups){ + if(optgroups.hasOwnProperty(name)){ + // assign the group to variable group, temporarily + group = optgroups[name]; + // create an element for this group and assign it to optgroup_el, temporarily + optgroup_el = optgroup_els[name] = $(''); + // append this element to the select menu + $this.append(optgroup_el); + // for each item in the group + for (name in group) { + // append an option to the optgroup + optgroup_el.append(''); + } + } + } + }, + buttonHandler: function (e) { + var $this = $(this), + select = $this.prev('select'), + embed_widget = $this.closest('.embed-widget'), + textarea = embed_widget.children('.embedding').eq(0), + val, app_label, object_name, + href, + win; + + // prevent the button from submitting the form + e.preventDefault(); + + // handle the case that they haven't chosen a type to embed + if (select.val()==='') { + alert('Please select a media type to embed.'); + textarea.focus(); + return; + } + + // split the val into app and object + val = select.val(); + app_label = val.split('.')[0]; + object_name = val.split('.')[1]; + + // generate the url for the popup + // TODO: Find a better way to get the admin URL if possible. This will break if the URL patterns for the admin ever change. + href=['../../../', app_label, '/', object_name, '/?pop=1'].join(''); + + // open a new window + win = window.open(href, id_to_windowname(textarea.attr('id')), 'height=500,width=980,resizable=yes,scrollbars=yes'); + }, + appendEmbed: function (textarea, embed_type, embed_id) { + var $textarea = $(textarea), + textarea = $textarea[0], // make sure we're *not* working with a jQuery object + current_selection = [textarea.selectionStart, textarea.selectionEnd], + current_text = $textarea.val(), + embed_string = ['{% embed', embed_type, embed_id, '%}'].join(' '), + new_text = current_text.substring(0, current_selection[0]) + embed_string + current_text.substring(current_selection[1]), + new_cursor_pos = current_selection[0]+embed_string.length; + $textarea.val(new_text); + textarea.setSelectionRange(new_cursor_pos, new_cursor_pos); + } + } + + $(widget.init); +}(django.jQuery)); \ No newline at end of file diff --git a/philo/static/philo/js/TagCreation.js b/philo/static/philo/js/TagCreation.js deleted file mode 100644 index a23e609..0000000 --- a/philo/static/philo/js/TagCreation.js +++ /dev/null @@ -1,111 +0,0 @@ -var tagCreation = window.tagCreation; - -(function($) { - location_re = new RegExp("^https?:\/\/" + window.location.host + "/") - - $('html').ajaxSend(function(event, xhr, settings) { - function getCookie(name) { - var cookieValue = null; - if (document.cookie && document.cookie != '') { - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var cookie = $.trim(cookies[i]); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) == (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; - } - if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url)) || location_re.test(settings.url)) { - // Only send the token to relative URLs i.e. locally. - xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); - } - }); - tagCreation = { - 'cache': {}, - 'addTagFromSlug': function(triggeringLink) { - var id = triggeringLink.id.replace(/^ajax_add_/, '') + '_input'; - var slug = document.getElementById(id).value; - - var name = slug.split(' '); - for(var i=0;i=1.3', - 'python>=2.5.4', 'django-mptt>0.4.2,==dev', ], extras_require = { @@ -75,6 +74,7 @@ setup( 'waldo-recaptcha': ['recaptcha-django'], 'sobol-eventlet': ['eventlet'], 'sobol-scrape': ['BeautifulSoup'], + 'penfield': ['django-taggit>=0.9'], }, dependency_links = [ 'https://github.com/django-mptt/django-mptt/tarball/master#egg=django-mptt-dev'