Merge branch 'taggit' into develop
authorStephen Burrows <stephen.r.burrows@gmail.com>
Sat, 27 Aug 2011 23:04:50 +0000 (16:04 -0700)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Sat, 27 Aug 2011 23:04:50 +0000 (16:04 -0700)
Conflicts:
philo/admin/widgets.py
philo/contrib/penfield/admin.py

1  2 
README
README.markdown
philo/admin/widgets.py
philo/contrib/penfield/admin.py
philo/contrib/penfield/models.py

diff --combined README
--- 1/README
--- 2/README
+++ b/README
@@@ -3,11 -3,16 +3,12 @@@ Philo is a foundation for developing we
  Prerequisites:
        * Python 2.5.4+ <http://www.python.org/>
        * Django 1.3+ <http://www.djangoproject.com/>
-       * django-mptt e734079+ <https://github.com/django-mptt/django-mptt/> 
+       * django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>
+       * (philo.contrib.penfield) django-taggit 0.9.3+ <https://github.com/alex/django-taggit>
        * (Optional) django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>
        * (Optional) south 0.7.2+ <http://south.aeracode.org/>
        * (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
  
 -To contribute, please visit the project website <http://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.
 -
 -
  ====
  Using philo
  ====
@@@ -18,4 -23,4 +19,4 @@@ After installing philo and mptt on you
  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 --combined README.markdown
@@@ -5,6 -5,7 +5,7 @@@ Prerequisites
   * [Python 2.5.4+ &lt;http://www.python.org&gt;](http://www.python.org/)
   * [Django 1.3+ &lt;http://www.djangoproject.com/&gt;](http://www.djangoproject.com/)
   * [django-mptt e734079+ &lt;https://github.com/django-mptt/django-mptt/&gt;](https://github.com/django-mptt/django-mptt/)
+  * (philo.contrib.penfield) [django-taggit 0.9.3+ &lt;https://github.com/alex/django-taggit&gt;](https://github.com/alex/django-taggit)
   * (Optional) [django-grappelli 2.0+ &lt;http://code.google.com/p/django-grappelli/&gt;](http://code.google.com/p/django-grappelli/)
   * (Optional) [south 0.7.2+ &lt;http://south.aeracode.org/)](http://south.aeracode.org/)
   * (Optional) [recaptcha-django r6 &lt;http://code.google.com/p/recaptcha-django/&gt;](http://code.google.com/p/recaptcha-django/)
@@@ -22,4 -23,4 +23,4 @@@ After installing philo and mptt on you
  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 --combined philo/admin/widgets.py
@@@ -1,7 -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
@@@ -38,59 -37,4 +38,31 @@@ class ModelLookupWidget(forms.TextInput
                                output.append('&nbsp;<strong>%s</strong>' % escape(truncate_words(value_object, 14)))
                        except value_class.DoesNotExist:
                                pass
 -              return mark_safe(u''.join(output))
 +              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 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'<script type="text/javascript">addEvent(window, "load", function(e) {')
-               # TODO: "id_" is hard-coded here. This should instead use the correct
-               # API to determine the ID dynamically.
-               output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); tagCreation.init("id_%s"); });</script>\n' % \
-                       (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX, name))
-               return mark_safe(u''.join(output))
 +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:
 +              css = {
 +                      'all': ('philo/css/EmbedWidget.css',),
 +              }
-               js = ('philo/js/EmbedWidget.js',)
++              js = ('philo/js/EmbedWidget.js',)
@@@ -3,10 -3,8 +3,10 @@@ from django.contrib import admi
  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):
@@@ -22,9 -20,8 +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',)
@@@ -44,9 -41,6 +43,9 @@@
        )
        related_lookup_fields = {'fk': raw_id_fields}
        prepopulated_fields = {'slug': ('title',)}
 +      formfield_overrides = {
 +              TemplateField: {'widget': EmbedWidget}
 +      }
  
  
  class BlogViewAdmin(EntityAdmin):
@@@ -78,9 -72,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',)
@@@ -99,9 -93,6 +98,9 @@@
        )
        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()])
@@@ -1,14 -1,15 +1,16 @@@
 +# encoding: utf-8
  from datetime import date, datetime
  
  from django.conf import settings
  from django.conf.urls.defaults import url, patterns, include
  from django.db import models
  from django.http import Http404, HttpResponse
+ from taggit.managers import TaggableManager
+ from taggit.models import Tag, TaggedItem
  
  from philo.contrib.winer.models import FeedView
  from philo.exceptions import ViewCanNotProvideSubpath
- from philo.models import Tag, Entity, Page, register_value_model
+ from philo.models import Entity, Page, register_value_model
  from philo.models.fields import TemplateField
  from philo.utils import paginate
  
@@@ -27,7 -28,11 +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):
@@@ -57,13 -62,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:
@@@ -126,6 -131,7 +132,6 @@@ 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
        
        @property
        def urlpatterns(self):
 -              urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index') +\
 -                      self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'entries_by_tag')
 +              urlpatterns = self.feed_patterns(r'^', 'get_entries', 'index_page', 'index') +\
 +                      self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)' % self.tag_permalink_base, 'get_entries', 'tag_page', 'entries_by_tag')
                
                if self.tag_archive_page_id:
                        urlpatterns += patterns('',
                
                if self.entry_archive_page_id:
                        if self.entry_permalink_style in 'DMY':
 -                              urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
 +                              urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries', 'entry_archive_page', 'entries_by_year')
                                if self.entry_permalink_style in 'DM':
 -                                      urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
 +                                      urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_month')
                                        if self.entry_permalink_style == 'D':
 -                                              urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
 +                                              urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries', 'entry_archive_page', 'entries_by_day')
                
                if self.entry_permalink_style == 'D':
                        urlpatterns += patterns('',
                        )
                return urlpatterns
        
 -      def get_context(self):
 -              return {'blog': self.blog}
 -      
        def get_entry_queryset(self, obj):
                """Returns the default :class:`QuerySet` of :class:`BlogEntry` instances for the :class:`BlogView` - all entries that are considered posted in the past. This allows for scheduled posting of entries."""
                return obj.entries.filter(date__lte=datetime.now())
                """Returns the default :class:`QuerySet` of :class:`.Tag`\ s for the :class:`BlogView`'s :meth:`get_entries_by_tag` and :meth:`tag_archive_view`."""
                return obj.entry_tags
        
 -      def get_all_entries(self, obj, request, extra_context=None):
 -              """Used to generate :meth:`~.FeedView.feed_patterns` for all entries."""
 -              return self.get_entry_queryset(obj), extra_context
 -      
 -      def get_entries_by_ymd(self, obj, 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(obj)
 -              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, obj, 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(obj).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(obj)
 -              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
        
                })
                return self.tag_archive_page.render_to_response(request, extra_context=context)
        
 -      def feed_view(self, get_items_attr, reverse_name, feed_type=None):
 -              """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, feed_type, *args, **kwargs)
 -                      items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs)
 -                      self.populate_feed(feed, items, request)
 -                      
 -                      if 'tags' in extra_context:
 -                              tags = extra_context['tags']
 -                              feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
 -                      else:
 -                              tags = obj.entry_tags
 -                      
 -                      feed.feed['categories'] = [tag.name for tag in tags]
 -                      
 -                      response = HttpResponse(mimetype=feed.mime_type)
 -                      feed.write(response, 'utf-8')
 -                      return response
 -              
 -              return inner
 -      
        def process_page_items(self, request, items):
                """Overrides :meth:`.FeedView.process_page_items` to add pagination."""
                if self.entries_per_page:
                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
@@@ -375,8 -374,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: