X-Git-Url: http://git.ithinksw.org/philo.git/blobdiff_plain/bb938c5fa1db5fe9133c12385526b99fce343f87..ee5fbe5df5817578911fbf0c2e1fc2698e701b73:/contrib/penfield/models.py diff --git a/contrib/penfield/models.py b/contrib/penfield/models.py index bf5178e..a03bed8 100644 --- a/contrib/penfield/models.py +++ b/contrib/penfield/models.py @@ -1,13 +1,297 @@ -from django.db import models from django.conf import settings -from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model -from philo.exceptions import ViewCanNotProvideSubpath -from django.conf.urls.defaults import url, patterns -from django.core.urlresolvers import reverse +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 datetime import datetime -from philo.utils import paginate +from django.template import RequestContext, Template as DjangoTemplate +from django.utils import feedgenerator, tzinfo +from django.utils.datastructures import SortedDict +from django.utils.encoding import smart_unicode, force_unicode +from django.utils.html import escape +from datetime import date, datetime +from philo.contrib.penfield.exceptions import HttpNotAcceptable +from philo.contrib.penfield.middleware import http_not_acceptable from philo.contrib.penfield.validators import validate_pagination_count +from philo.exceptions import ViewCanNotProvideSubpath +from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField, Template +from philo.utils import paginate +try: + import mimeparse +except: + mimeparse = None + + +ATOM = feedgenerator.Atom1Feed.mime_type +RSS = feedgenerator.Rss201rev2Feed.mime_type +FEEDS = SortedDict([ + (ATOM, feedgenerator.Atom1Feed), + (RSS, feedgenerator.Rss201rev2Feed), +]) +FEED_CHOICES = ( + (ATOM, "Atom"), + (RSS, "RSS"), +) + + +class FeedView(MultiView): + """ + The FeedView expects to handle a number of different feeds for the + same object - i.e. patterns for a blog to handle all entries or + just entries for a certain year/month/day. + + This class would subclass django.contrib.syndication.views.Feed, but + that would make it callable, which causes problems. + """ + feed_type = models.CharField(max_length=50, choices=FEED_CHOICES, default=ATOM) + feed_suffix = models.CharField(max_length=255, blank=False, default="feed") + feeds_enabled = models.BooleanField(default=True) + feed_length = models.PositiveIntegerField(blank=True, null=True, default=15, help_text="The maximum number of items to return for this feed. All items will be returned if this field is blank.") + + item_title_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_title_related") + item_description_template = models.ForeignKey(Template, blank=True, null=True, related_name="%(app_label)s_%(class)s_description_related") + + item_context_var = 'items' + object_attr = 'object' + + description = "" + + def feed_patterns(self, base, get_items_attr, page_attr, reverse_name): + """ + Given the name to be used to reverse this view and the names of + the attributes for the function that fetches the objects, returns + patterns suitable for inclusion in urlpatterns. + """ + urlpatterns = patterns('') + if self.feeds_enabled: + feed_reverse_name = "%s_feed" % reverse_name + feed_view = http_not_acceptable(self.feed_view(get_items_attr, feed_reverse_name)) + feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix) + urlpatterns += patterns('', + url(feed_pattern, feed_view, name=feed_reverse_name), + ) + urlpatterns += patterns('', + url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name) + ) + return urlpatterns + + def get_object(self, request, **kwargs): + return getattr(self, self.object_attr) + + def feed_view(self, get_items_attr, reverse_name): + """ + Returns a view function that renders a list of items as a feed. + """ + get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr) + + def inner(request, extra_context=None, *args, **kwargs): + obj = self.get_object(request, *args, **kwargs) + feed = self.get_feed(obj, request, reverse_name) + items, xxx = get_items(request, extra_context=extra_context, *args, **kwargs) + self.populate_feed(feed, items, request) + + response = HttpResponse(mimetype=feed.mime_type) + feed.write(response, 'utf-8') + return response + + return inner + + def page_view(self, get_items_attr, page_attr): + """ + Returns a view function that renders a list of items as a page. + """ + get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr) + page = isinstance(page_attr, Page) and page_attr or getattr(self, page_attr) + + def inner(request, extra_context=None, *args, **kwargs): + items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs) + items, item_context = self.process_page_items(request, items) + + context = self.get_context() + context.update(extra_context or {}) + context.update(item_context or {}) + + return page.render_to_response(request, extra_context=context) + return inner + + def process_page_items(self, request, items): + """ + Hook for handling any extra processing of items based on a + request, such as pagination or searching. This method is + expected to return a list of items and a dictionary to be + added to the page context. + """ + item_context = { + self.item_context_var: items + } + return items, item_context + + def get_feed_type(self, request): + feed_type = self.feed_type + if feed_type not in FEEDS: + feed_type = FEEDS.keys()[0] + accept = request.META.get('HTTP_ACCEPT') + if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept: + # Wups! They aren't accepting the chosen format. Is there another format we can use? + if mimeparse: + feed_type = mimeparse.best_match(FEEDS.keys(), accept) + else: + for feed_type in FEEDS.keys(): + if feed_type in accept or "%s/*" % feed_type.split("/")[0] in accept: + break + else: + feed_type = None + if not feed_type: + raise HttpNotAcceptable + return FEEDS[feed_type] + + def get_feed(self, obj, request, reverse_name): + """ + Returns an unpopulated feedgenerator.DefaultFeed object for this object. + """ + try: + current_site = Site.objects.get_current() + except Site.DoesNotExist: + current_site = RequestSite(request) + + feed_type = self.get_feed_type(request) + node = request.node + link = node.get_absolute_url(with_domain=True, request=request, secure=request.is_secure()) + + feed = feed_type( + title = self.__get_dynamic_attr('title', obj), + subtitle = self.__get_dynamic_attr('subtitle', obj), + link = link, + description = self.__get_dynamic_attr('description', obj), + language = settings.LANGUAGE_CODE.decode(), + feed_url = add_domain( + current_site.domain, + self.__get_dynamic_attr('feed_url', obj) or node.construct_url(node.subpath, with_domain=True, request=request, secure=request.is_secure()), + request.is_secure() + ), + author_name = self.__get_dynamic_attr('author_name', obj), + author_link = self.__get_dynamic_attr('author_link', obj), + author_email = self.__get_dynamic_attr('author_email', obj), + categories = self.__get_dynamic_attr('categories', obj), + feed_copyright = self.__get_dynamic_attr('feed_copyright', obj), + feed_guid = self.__get_dynamic_attr('feed_guid', obj), + ttl = self.__get_dynamic_attr('ttl', obj), + **self.feed_extra_kwargs(obj) + ) + return feed + + def populate_feed(self, feed, items, request): + if self.item_title_template: + title_template = DjangoTemplate(self.item_title_template.code) + else: + title_template = None + if self.item_description_template: + description_template = DjangoTemplate(self.item_description_template.code) + else: + description_template = None + + node = request.node + try: + current_site = Site.objects.get_current() + except Site.DoesNotExist: + current_site = RequestSite(request) + + if self.feed_length is not None: + items = items[:self.feed_length] + + for item in items: + if title_template is not None: + title = title_template.render(RequestContext(request, {'obj': item})) + else: + title = self.__get_dynamic_attr('item_title', item) + if description_template is not None: + description = description_template.render(RequestContext(request, {'obj': item})) + else: + description = self.__get_dynamic_attr('item_description', item) + + link = node.construct_url(self.reverse(obj=item), with_domain=True, request=request, secure=request.is_secure()) + + enc = None + enc_url = self.__get_dynamic_attr('item_enclosure_url', item) + if enc_url: + enc = feedgenerator.Enclosure( + url = smart_unicode(add_domain( + current_site.domain, + enc_url, + request.is_secure() + )), + length = smart_unicode(self.__get_dynamic_attr('item_enclosure_length', item)), + mime_type = smart_unicode(self.__get_dynamic_attr('item_enclosure_mime_type', item)) + ) + author_name = self.__get_dynamic_attr('item_author_name', item) + if author_name is not None: + author_email = self.__get_dynamic_attr('item_author_email', item) + author_link = self.__get_dynamic_attr('item_author_link', item) + else: + author_email = author_link = None + + pubdate = self.__get_dynamic_attr('item_pubdate', item) + if pubdate and not pubdate.tzinfo: + ltz = tzinfo.LocalTimezone(pubdate) + pubdate = pubdate.replace(tzinfo=ltz) + + feed.add_item( + title = title, + link = link, + description = description, + unique_id = self.__get_dynamic_attr('item_guid', item, link), + enclosure = enc, + pubdate = pubdate, + author_name = author_name, + author_email = author_email, + author_link = author_link, + categories = self.__get_dynamic_attr('item_categories', item), + item_copyright = self.__get_dynamic_attr('item_copyright', item), + **self.item_extra_kwargs(item) + ) + + def __get_dynamic_attr(self, attname, obj, default=None): + try: + attr = getattr(self, attname) + except AttributeError: + return default + if callable(attr): + # Check func_code.co_argcount rather than try/excepting the + # function and catching the TypeError, because something inside + # the function may raise the TypeError. This technique is more + # accurate. + if hasattr(attr, 'func_code'): + argcount = attr.func_code.co_argcount + else: + argcount = attr.__call__.func_code.co_argcount + if argcount == 2: # one argument is 'self' + return attr(obj) + else: + return attr() + return attr + + def feed_extra_kwargs(self, obj): + """ + Returns an extra keyword arguments dictionary that is used when + initializing the feed generator. + """ + return {} + + def item_extra_kwargs(self, item): + """ + Returns an extra keyword arguments dictionary that is used with + the `add_item` call of the feed generator. + """ + return {} + + def item_title(self, item): + return escape(force_unicode(item)) + + def item_description(self, item): + return force_unicode(item) + + class Meta: + abstract=True class Blog(Entity, Titled): @@ -28,20 +312,26 @@ register_value_model(Blog) class BlogEntry(Entity, Titled): blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True) author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries') - date = models.DateTimeField(default=datetime.now) + date = models.DateTimeField(default=None) content = models.TextField() excerpt = models.TextField(blank=True, null=True) tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True) + def save(self, *args, **kwargs): + if self.date is None: + self.date = datetime.now() + super(BlogEntry, self).save(*args, **kwargs) + class Meta: ordering = ['-date'] verbose_name_plural = "blog entries" + get_latest_by = "date" register_value_model(BlogEntry) -class BlogView(MultiView): +class BlogView(FeedView): ENTRY_PERMALINK_STYLE_CHOICES = ( ('D', 'Year, month, and day'), ('M', 'Year and month'), @@ -54,6 +344,7 @@ class BlogView(MultiView): index_page = models.ForeignKey(Page, related_name='blog_index_related') entry_page = models.ForeignKey(Page, related_name='blog_entry_related') + # TODO: entry_archive is misleading. Rename to ymd_page or timespan_page. entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True) tag_page = models.ForeignKey(Page, related_name='blog_tag_related') tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True) @@ -63,100 +354,94 @@ class BlogView(MultiView): entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries') 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_subpath(self, obj): + def get_reverse_params(self, obj): if isinstance(obj, BlogEntry): if obj.blog == self.blog: - entry_view_args = {'slug': obj.slug} + kwargs = {'slug': obj.slug} if self.entry_permalink_style in 'DMY': - entry_view_args.update({'year': str(obj.date.year).zfill(4)}) + kwargs.update({'year': str(obj.date.year).zfill(4)}) if self.entry_permalink_style in 'DM': - entry_view_args.update({'month': str(obj.date.month).zfill(2)}) + kwargs.update({'month': str(obj.date.month).zfill(2)}) if self.entry_permalink_style == 'D': - entry_view_args.update({'day': str(obj.date.day).zfill(2)}) - return reverse(self.entry_view, urlconf=self, kwargs=entry_view_args) - elif isinstance(obj, Tag): - if obj in self.blog.entry_tags: - return reverse(self.tag_view, urlconf=self, kwargs={'tag_slugs': obj.slug}) - elif isinstance(obj, (str, unicode)): - split_obj = obj.split(':') - if len(split_obj) > 1: - entry_archive_view_args = {} - if split_obj[0].lower() == 'archives': - entry_archive_view_args.update({'year': str(int(split_obj[1])).zfill(4)}) - if len(split_obj) > 2: - entry_archive_view_args.update({'month': str(int(split_obj[2])).zfill(2)}) - if len(split_obj) > 3: - entry_archive_view_args.update({'day': str(int(split_obj[3])).zfill(2)}) - return reverse(self.entry_archive_view, urlconf=self, kwargs=entry_archive_view_args) + kwargs.update({'day': str(obj.date.day).zfill(2)}) + return self.entry_view, [], kwargs + 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()] + if slugs: + return 'entries_by_tag', [], {'tag_slugs': "/".join(slugs)} + elif isinstance(obj, (date, datetime)): + kwargs = { + 'year': str(obj.year).zfill(4), + 'month': str(obj.month).zfill(2), + 'day': str(obj.day).zfill(2) + } + return 'entries_by_day', [], kwargs raise ViewCanNotProvideSubpath @property def urlpatterns(self): - base_patterns = patterns('', - url(r'^$', self.index_view), - url((r'^(?:%s)/?$' % self.tag_permalink_base), self.tag_archive_view), - url((r'^(?:%s)/(?P[-\w]+[-+/\w]*)/?$' % self.tag_permalink_base), self.tag_view) - ) + 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') + + if self.tag_archive_page: + urlpatterns += patterns('', + url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive') + ) + + if self.entry_archive_page: + 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') + 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') + 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') + if self.entry_permalink_style == 'D': - entry_patterns = patterns('', - url(r'^(?P\d{4})/?$', self.entry_archive_view), - url(r'^(?P\d{4})/(?P\d{2})/?$', self.entry_archive_view), - url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/?$', self.entry_archive_view), - url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)/?$', self.entry_view) + urlpatterns += patterns('', + url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)$', self.entry_view) ) elif self.entry_permalink_style == 'M': - entry_patterns = patterns('', - url(r'^(?P\d{4})/?$', self.entry_archive_view), - url(r'^(?P\d{4})/(?P\d{2})/?$', self.entry_archive_view), - url(r'^(?P\d{4})/(?P\d{2})/(?P[-\w]+)/?$', self.entry_view) + urlpatterns += patterns('', + url(r'^(?P\d{4})/(?P\d{2})/(?P[-\w]+)$', self.entry_view) ) elif self.entry_permalink_style == 'Y': - entry_patterns = patterns('', - url(r'^(?P\d{4})/?$', self.entry_archive_view), - url(r'^(?P\d{4})/(?P[-\w]+)/?$', self.entry_view) + urlpatterns += patterns('', + url(r'^(?P\d{4})/(?P[-\w]+)$', self.entry_view) ) elif self.entry_permalink_style == 'B': - entry_patterns = patterns('', - url((r'^(?:%s)/?$' % self.entry_permalink_base), self.entry_archive_view), - url((r'^(?:%s)/(?P[-\w]+)/?$' % self.entry_permalink_base), self.entry_view) + urlpatterns += patterns('', + url((r'^%s/(?P[-\w]+)$' % self.entry_permalink_base), self.entry_view) ) else: - entry_patterns = patterns('', - url(r'^(?P[-\w]+)/?$', self.entry_view) + urlpatterns = patterns('', + url(r'^(?P[-\w]+)$', self.entry_view) ) - return base_patterns + entry_patterns + return urlpatterns - def index_view(self, request, node=None, extra_context=None): - paginator, page, entries = paginate(self.blog.entries.all(), self.entries_per_page, request.GET.get('page', 1)) - context = {} - context.update(extra_context or {}) - context.update({'blog': self.blog, 'paginator': paginator, 'paginated_page': page, 'entries': entries}) - return self.index_page.render_to_response(node, request, extra_context=context) + def get_context(self): + return {'blog': self.blog} - def entry_view(self, request, slug, year=None, month=None, day=None, node=None, extra_context=None): - entries = self.blog.entries.all() - if year: - entries = entries.filter(date__year=year) - if month: - entries = entries.filter(date__month=month) - if day: - entries = entries.filter(date__day=day) - try: - entry = entries.get(slug=slug) - except: - raise Http404 - context = {} - context.update(extra_context or {}) - context.update({'blog': self.blog, 'entry': entry}) - return self.entry_page.render_to_response(node, request, extra_context=context) + def get_entry_queryset(self): + return self.blog.entries.all() + + def get_tag_queryset(self): + return self.blog.entry_tags - def entry_archive_view(self, request, year=None, month=None, day=None, node=None, extra_context=None): + def get_all_entries(self, request, extra_context=None): + return self.get_entry_queryset(), extra_context + + def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None): if not self.entry_archive_page: raise Http404 - entries = self.blog.entries.all() + entries = self.get_entry_queryset() if year: entries = entries.filter(date__year=year) if month: @@ -164,43 +449,114 @@ class BlogView(MultiView): if day: entries = entries.filter(date__day=day) - paginator, page, entries = paginate(entries, self.entries_per_page, request.GET.get('page', 1)) - context = {} - context.update(extra_context or {}) - context.update({'blog': self.blog, 'year': year, 'month': month, 'day': day, 'paginator': paginator, 'paginated_page': page, 'entries': entries}) - return self.entry_archive_page.render_to_response(node, request, extra_context=context) - - def tag_view(self, request, tag_slugs, node=None, extra_context=None): - tags = [] - for tag_slug in tag_slugs.replace('+', '/').split('/'): - if tag_slug: # ignore blank slugs, handles for multiple consecutive separators (+ or /) - try: - tag = self.blog.entry_tags.get(slug=tag_slug) - except: - raise Http404 - tags.append(tag) - if len(tags) <= 0: + context = extra_context or {} + context.update({'year': year, 'month': month, 'day': day}) + return entries, context + + def get_entries_by_tag(self, request, tag_slugs, extra_context=None): + tag_slugs = tag_slugs.replace('+', '/').split('/') + tags = self.get_tag_queryset().filter(slug__in=tag_slugs) + + if not tags: raise Http404 - entries = self.blog.entries.all() + # 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 entries.count() <= 0: - raise Http404 - paginator, page, entries = paginate(entries, self.entries_per_page, request.GET.get('page', 1)) - context = {} + context = extra_context or {} + context.update({'tags': tags}) + + return entries, context + + def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None): + 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) + try: + entry = entries.get(slug=slug) + except: + raise Http404 + context = self.get_context() context.update(extra_context or {}) - context.update({'blog': self.blog, 'tags': tags, 'paginator': paginator, 'paginated_page': page, 'entries': entries}) - return self.tag_page.render_to_response(node, request, extra_context=context) + context.update({'entry': entry}) + return self.entry_page.render_to_response(request, extra_context=context) - def tag_archive_view(self, request, node=None, extra_context=None): + def tag_archive_view(self, request, extra_context=None): if not self.tag_archive_page: raise Http404 - context = {} + context = self.get_context() context.update(extra_context or {}) - context.update({'blog': self.blog}) - return self.tag_archive_page.render_to_response(node, request, extra_context=context) + context.update({ + 'tags': self.get_tag_queryset() + }) + return self.tag_archive_page.render_to_response(request, extra_context=context) + + def feed_view(self, get_items_attr, reverse_name): + get_items = callable(get_items_attr) and get_items_attr or getattr(self, get_items_attr) + + def inner(request, extra_context=None, *args, **kwargs): + obj = self.get_object(request, *args, **kwargs) + feed = self.get_feed(obj, request, reverse_name) + items, extra_context = get_items(request, extra_context=extra_context, *args, **kwargs) + self.populate_feed(feed, items, request) + + if 'tags' in extra_context: + tags = extra_context['tags'] + feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure()) + else: + tags = obj.entry_tags + + feed.feed['categories'] = [tag.name for tag in tags] + + response = HttpResponse(mimetype=feed.mime_type) + feed.write(response, 'utf-8') + return response + + return inner + + def process_page_items(self, request, items): + if self.entries_per_page: + page_num = request.GET.get('page', 1) + paginator, paginated_page, items = paginate(items, self.entries_per_page, page_num) + item_context = { + 'paginator': paginator, + 'paginated_page': paginated_page, + self.item_context_var: items + } + else: + item_context = { + self.item_context_var: items + } + return items, item_context + + def title(self, obj): + return obj.title + + def item_title(self, item): + return item.title + + def item_description(self, item): + return item.content + + def item_author_name(self, item): + return item.author.get_full_name() + + def item_pubdate(self, item): + return item.date + + def item_categories(self, item): + return [tag.name for tag in item.tags.all()] class Newsletter(Entity, Titled): @@ -213,9 +569,15 @@ register_value_model(Newsletter) class NewsletterArticle(Entity, Titled): newsletter = models.ForeignKey(Newsletter, related_name='articles') authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles') - date = models.DateTimeField(default=datetime.now) - lede = models.TextField(null=True, blank=True) - full_text = models.TextField() + date = models.DateTimeField(default=None) + lede = TemplateField(null=True, blank=True, verbose_name='Summary') + full_text = TemplateField(db_index=True) + tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True) + + def save(self, *args, **kwargs): + if self.date is None: + self.date = datetime.now() + super(NewsletterArticle, self).save(*args, **kwargs) class Meta: get_latest_by = 'date' @@ -228,18 +590,18 @@ register_value_model(NewsletterArticle) class NewsletterIssue(Entity, Titled): newsletter = models.ForeignKey(Newsletter, related_name='issues') - number = models.PositiveIntegerField() + numbering = models.CharField(max_length=50, help_text='For example, 04.02 for volume 4, issue 2.') articles = models.ManyToManyField(NewsletterArticle, related_name='issues') class Meta: - ordering = ['-number'] - unique_together = (('newsletter', 'number'),) + ordering = ['-numbering'] + unique_together = (('newsletter', 'numbering'),) register_value_model(NewsletterIssue) -class NewsletterView(MultiView): +class NewsletterView(FeedView): ARTICLE_PERMALINK_STYLE_CHOICES = ( ('D', 'Year, month, and day'), ('M', 'Year and month'), @@ -259,111 +621,150 @@ class NewsletterView(MultiView): article_permalink_base = models.CharField(max_length=255, blank=False, default='articles') issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues') - def get_subpath(self, obj): + item_context_var = 'articles' + object_attr = 'newsletter' + + def __unicode__(self): + return "NewsletterView for %s" % self.newsletter.__unicode__() + + def get_reverse_params(self, obj): if isinstance(obj, NewsletterArticle): if obj.newsletter == self.newsletter: - article_view_args = {'slug': obj.slug} + kwargs = {'slug': obj.slug} if self.article_permalink_style in 'DMY': - article_view_args.update({'year': str(obj.date.year).zfill(4)}) + kwargs.update({'year': str(obj.date.year).zfill(4)}) if self.article_permalink_style in 'DM': - article_view_args.update({'month': str(obj.date.month).zfill(2)}) + kwargs.update({'month': str(obj.date.month).zfill(2)}) if self.article_permalink_style == 'D': - article_view_args.update({'day': str(obj.date.day).zfill(2)}) - return reverse(self.article_view, urlconf=self, kwargs=article_view_args) + kwargs.update({'day': str(obj.date.day).zfill(2)}) + return self.article_view, [], kwargs elif isinstance(obj, NewsletterIssue): if obj.newsletter == self.newsletter: - return reverse(self.issue_view, urlconf=self, kwargs={'number': str(obj.number)}) + return 'issue', [], {'numbering': obj.numbering} + elif isinstance(obj, (date, datetime)): + kwargs = { + 'year': str(obj.year).zfill(4), + 'month': str(obj.month).zfill(2), + 'day': str(obj.day).zfill(2) + } + return 'articles_by_day', [], kwargs raise ViewCanNotProvideSubpath @property def urlpatterns(self): - base_patterns = patterns('', - url(r'^$', self.index_view), - url((r'^(?:%s)/?$' % self.issue_permalink_base), self.issue_archive_view), - url((r'^(?:%s)/(?P\d+)/?$' % self.issue_permalink_base), self.issue_view) - ) - article_patterns = patterns('', - url((r'^(?:%s)/?$' % self.article_permalink_base), self.article_archive_view) + 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.article_permalink_style in 'DMY': - article_patterns += patterns('', - url((r'^(?:%s)/(?P\d{4})/?$' % self.article_permalink_base), self.article_archive_view) + if self.issue_archive_page: + urlpatterns += patterns('', + url(r'^%s$' % self.issue_permalink_base, self.issue_archive_view, 'issue_archive') ) - if self.article_permalink_style in 'DM': - article_patterns += patterns('', - url((r'^(?:%s)/(?P\d{4})/(?P\d{2})/?$' % self.article_permalink_base), self.article_archive_view) - ) - if self.article_permalink_style == 'D': - article_patterns += patterns('', - url((r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P\d{2})/?$' % self.article_permalink_base), self.article_archive_view), - url((r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)/?$' % self.article_permalink_base), self.article_view) - ) - else: - article_patterns += patterns('', - url((r'^(?:%s)/(?P\d{4})/(?P\d{2})/(?P[-\w]+)/?$' % self.article_permalink_base), self.article_view) - ) - else: - article_patterns += patterns('', - url((r'^(?:%s)/(?P\d{4})/(?P[-\w]+)/?$' % self.article_permalink_base), self.article_view) - ) - else: - article_patterns += patterns('', - url((r'^(?:%s)/(?P[-\w]+)/?$' % self.article_permalink_base), self.article_view) + 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_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': + urlpatterns += self.feed_patterns(r'^%s/(?P\d{4})/(?P\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_month') + if self.article_permalink_style == 'D': + urlpatterns += self.feed_patterns(r'^%s/(?P\d{4})/(?P\d{2})/(?P\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_day') + + if self.article_permalink_style == 'Y': + urlpatterns += patterns('', + url(r'^%s/(?P\d{4})/(?P[\w-]+)$' % self.article_permalink_base, self.article_view) ) - return base_patterns + article_patterns + elif self.article_permalink_style == 'M': + urlpatterns += patterns('', + url(r'^%s/(?P\d{4})/(?P\d{2})/(?P[\w-]+)$' % self.article_permalink_base, self.article_view) + ) + elif self.article_permalink_style == 'D': + urlpatterns += patterns('', + url(r'^%s/(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w-]+)$' % self.article_permalink_base, self.article_view) + ) + else: + urlpatterns += patterns('', + url(r'^%s/(?P[-\w]+)$' % self.article_permalink_base, self.article_view) + ) + + return urlpatterns - def index_view(self, request, node=None, extra_context=None): - context = {} - context.update(extra_context or {}) - context.update({'newsletter': self.newsletter}) - return self.index_page.render_to_response(node, request, extra_context=context) + def get_context(self): + return {'newsletter': self.newsletter} - def article_view(self, request, slug, year=None, month=None, day=None, node=None, extra_context=None): - articles = self.newsletter.articles.all() - if year: - articles = articles.filter(date__year=year) + def get_article_queryset(self): + return self.newsletter.articles.all() + + def get_issue_queryset(self): + return self.newsletter.issues.all() + + def get_all_articles(self, request, extra_context=None): + return self.get_article_queryset(), extra_context + + def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None): + articles = self.get_article_queryset().filter(date__year=year) 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): try: - article = articles.get(slug=slug) - except: + issue = self.get_issue_queryset().get(numbering=numbering) + except NewsletterIssue.DoesNotExist: raise Http404 - context = {} - context.update(extra_context or {}) - context.update({'newsletter': self.newsletter, 'article': article}) - return self.article_page.render_to_response(node, request, extra_context=context) + context = extra_context or {} + context.update({'issue': issue}) + return self.get_article_queryset().filter(issues=issue), context - def article_archive_view(self, request, year=None, month=None, day=None, node=None, extra_context=None): - if not self.article_archive_page: - raise Http404 - articles = self.newsletter.articles.all() + def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None): + articles = self.get_article_queryset() if year: articles = articles.filter(date__year=year) if month: articles = articles.filter(date__month=month) if day: articles = articles.filter(date__day=day) - context = {} - context.update(extra_context or {}) - context.update({'newsletter': self.newsletter, 'year': year, 'month': month, 'day': day, 'articles': articles}) - return self.article_archive_page.render_to_response(node, request, extra_context=context) - - def issue_view(self, request, number, node=None, extra_context=None): try: - issue = self.newsletter.issues.get(number=number) - except: + article = articles.get(slug=slug) + except NewsletterArticle.DoesNotExist: raise Http404 - context = {} + context = self.get_context() context.update(extra_context or {}) - context.update({'newsletter': self.newsletter, 'issue': issue}) - return self.issue_page.render_to_response(node, request, extra_context=context) + context.update({'article': article}) + return self.article_page.render_to_response(request, extra_context=context) - def issue_archive_view(self, request, node=None, extra_context=None): + def issue_archive_view(self, request, extra_context): if not self.issue_archive_page: raise Http404 - context = {} + context = self.get_context() context.update(extra_context or {}) - context.update({'newsletter': self.newsletter}) - return self.issue_archive_page.render_to_response(node, request, extra_context=context) \ No newline at end of file + context.update({ + 'issues': self.get_issue_queryset() + }) + return self.issue_archive_page.render_to_response(request, extra_context=context) + + def title(self, obj): + return obj.title + + def item_title(self, item): + return item.title + + def item_description(self, item): + return item.full_text + + def item_author_name(self, item): + authors = list(item.authors.all()) + if len(authors) > 1: + return "%s and %s" % (", ".join([author.get_full_name() for author in authors[:-1]]), authors[-1].get_full_name()) + elif authors: + return authors[0].get_full_name() + else: + return '' + + def item_pubdate(self, item): + return item.date + + def item_categories(self, item): + return [tag.name for tag in item.tags.all()] \ No newline at end of file