From: Stephen Burrows Date: Sat, 27 Aug 2011 23:04:50 +0000 (-0700) Subject: Merge branch 'taggit' into develop X-Git-Tag: philo-0.9.1^2~3^2 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/cbbff4f681be1a7a1666424d7c2883dfd4a7c30d?hp=-c Merge branch 'taggit' into develop Conflicts: philo/admin/widgets.py philo/contrib/penfield/admin.py --- cbbff4f681be1a7a1666424d7c2883dfd4a7c30d diff --combined README index 6fa1548,4990f8a..69c0c52 --- a/README +++ b/README @@@ -3,11 -3,16 +3,12 @@@ Philo is a foundation for developing we Prerequisites: * Python 2.5.4+ * Django 1.3+ - * django-mptt e734079+ + * django-mptt e734079+ + * (philo.contrib.penfield) django-taggit 0.9.3+ * (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. - - ==== 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 and contribute . diff --combined README.markdown index 5f90ba6,9afbee1..c6a198a --- a/README.markdown +++ b/README.markdown @@@ -5,6 -5,7 +5,7 @@@ Prerequisites * [Python 2.5.4+ <http://www.python.org>](http://www.python.org/) * [Django 1.3+ <http://www.djangoproject.com/>](http://www.djangoproject.com/) * [django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>](https://github.com/django-mptt/django-mptt/) + * (philo.contrib.penfield) [django-taggit 0.9.3+ <https://github.com/alex/django-taggit>](https://github.com/alex/django-taggit) * (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/) @@@ -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 index 7fda164,637475e..3d7d64b --- a/philo/admin/widgets.py +++ b/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(' %s' % 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'\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',) diff --combined philo/contrib/penfield/admin.py index 4cfa1a6,0800be1..31aacb1 --- a/philo/contrib/penfield/admin.py +++ b/philo/contrib/penfield/admin.py @@@ -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()]) diff --combined philo/contrib/penfield/models.py index 6eef5df,eef8974..a57459c --- a/philo/contrib/penfield/models.py +++ b/philo/contrib/penfield/models.py @@@ -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 @@@ -158,8 -164,8 +164,8 @@@ @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_id: urlpatterns += patterns('', @@@ -168,11 -174,11 +174,11 @@@ 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('', @@@ -196,6 -202,9 +202,6 @@@ ) 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()) @@@ -204,62 -213,46 +210,62 @@@ """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 @@@ -292,6 -285,30 +298,6 @@@ }) 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: @@@ -309,25 -326,7 +315,25 @@@ 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: