Improved blog and newsletter ModelAdmins. Set BlogEntries and NewsletterArticlesup...
[philo.git] / contrib / penfield / models.py
1 from django.conf import settings
2 from django.conf.urls.defaults import url, patterns, include
3 from django.db import models
4 from django.http import Http404
5 from django.template import loader, Context
6 from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
7 from datetime import date, datetime
8 from philo.contrib.penfield.utils import FeedMultiViewMixin
9 from philo.contrib.penfield.validators import validate_pagination_count
10 from philo.exceptions import ViewCanNotProvideSubpath
11 from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField
12 from philo.utils import paginate
13
14
15 class Blog(Entity, Titled):
16         @property
17         def entry_tags(self):
18                 """ Returns a QuerySet of Tags that are used on any entries in this blog. """
19                 return Tag.objects.filter(blogentries__blog=self).distinct()
20         
21         @property
22         def entry_dates(self):
23                 dates = {'year': self.entries.dates('date', 'year', order='DESC'), 'month': self.entries.dates('date', 'month', order='DESC'), 'day': self.entries.dates('date', 'day', order='DESC')}
24                 return dates
25
26
27 register_value_model(Blog)
28
29
30 class BlogEntry(Entity, Titled):
31         blog = models.ForeignKey(Blog, related_name='entries', blank=True, null=True)
32         author = models.ForeignKey(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='blogentries')
33         date = models.DateTimeField(default=None)
34         content = models.TextField()
35         excerpt = models.TextField(blank=True, null=True)
36         tags = models.ManyToManyField(Tag, related_name='blogentries', blank=True, null=True)
37         
38         def save(self, *args, **kwargs):
39                 if self.date is None:
40                         self.date = datetime.now()
41                 super(BlogEntry, self).save(*args, **kwargs)
42         
43         class Meta:
44                 ordering = ['-date']
45                 verbose_name_plural = "blog entries"
46                 get_latest_by = "date"
47
48
49 register_value_model(BlogEntry)
50
51
52 class BlogView(MultiView, FeedMultiViewMixin):
53         ENTRY_PERMALINK_STYLE_CHOICES = (
54                 ('D', 'Year, month, and day'),
55                 ('M', 'Year and month'),
56                 ('Y', 'Year'),
57                 ('B', 'Custom base'),
58                 ('N', 'No base')
59         )
60         
61         blog = models.ForeignKey(Blog, related_name='blogviews')
62         
63         index_page = models.ForeignKey(Page, related_name='blog_index_related')
64         entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
65         entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
66         tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
67         tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
68         entries_per_page = models.IntegerField(blank=True, validators=[validate_pagination_count], null=True)
69         
70         entry_permalink_style = models.CharField(max_length=1, choices=ENTRY_PERMALINK_STYLE_CHOICES)
71         entry_permalink_base = models.CharField(max_length=255, blank=False, default='entries')
72         tag_permalink_base = models.CharField(max_length=255, blank=False, default='tags')
73         feed_suffix = models.CharField(max_length=255, blank=False, default=FeedMultiViewMixin.feed_suffix)
74         feeds_enabled = models.BooleanField()
75         list_var = 'entries'
76         
77         def __unicode__(self):
78                 return u'BlogView for %s' % self.blog.title
79         
80         @property
81         def per_page(self):
82                 return self.entries_per_page
83         
84         def get_reverse_params(self, obj):
85                 if isinstance(obj, BlogEntry):
86                         if obj.blog == self.blog:
87                                 kwargs = {'slug': obj.slug}
88                                 if self.entry_permalink_style in 'DMY':
89                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
90                                         if self.entry_permalink_style in 'DM':
91                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
92                                                 if self.entry_permalink_style == 'D':
93                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
94                                 return self.entry_view, [], kwargs
95                 elif isinstance(obj, Tag):
96                         if obj in self.blog.entry_tags:
97                                 return 'entries_by_tag', [], {'tag_slugs': obj.slug}
98                 elif isinstance(obj, (date, datetime)):
99                         kwargs = {
100                                 'year': str(obj.year).zfill(4),
101                                 'month': str(obj.month).zfill(2),
102                                 'day': str(obj.day).zfill(2)
103                         }
104                         return 'entries_by_day', [], kwargs
105                 raise ViewCanNotProvideSubpath
106         
107         def get_context(self):
108                 return {'blog': self.blog}
109         
110         @property
111         def urlpatterns(self):
112                 urlpatterns = patterns('',
113                         url(r'^', include(self.feed_patterns(self.get_all_entries, self.index_page, 'index'))),
114                 )
115                 if self.feeds_enabled:
116                         urlpatterns += patterns('',
117                                 url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/%s/' % (self.tag_permalink_base, self.feed_suffix), self.feed_view(self.get_entries_by_tag, 'entries_by_tag_feed'), name='entries_by_tag_feed'),
118                         )
119                 urlpatterns += patterns('',
120                         url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/' % self.tag_permalink_base, self.page_view(self.get_entries_by_tag, self.tag_page), name='entries_by_tag')
121                 )
122                 if self.tag_archive_page:
123                         urlpatterns += patterns('',
124                                 url((r'^(?:%s)/?$' % self.tag_permalink_base), self.tag_archive_view)
125                         )
126                 
127                 if self.entry_archive_page:
128                         if self.entry_permalink_style in 'DMY':
129                                 urlpatterns += patterns('',
130                                         url(r'^(?P<year>\d{4})/', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_year')))
131                                 )
132                                 if self.entry_permalink_style in 'DM':
133                                         urlpatterns += patterns('',
134                                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/?$', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_month'))),
135                                         )
136                                         if self.entry_permalink_style == 'D':
137                                                 urlpatterns += patterns('',
138                                                         url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/?$', include(self.feed_patterns(self.get_entries_by_ymd, self.entry_archive_page, 'entries_by_day')))
139                                                 )
140                 
141                 if self.entry_permalink_style == 'D':
142                         urlpatterns += patterns('',
143                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[-\w]+)/?$', self.entry_view)
144                         )
145                 elif self.entry_permalink_style == 'M':
146                         urlpatterns += patterns('',
147                                 url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)/?$', self.entry_view)
148                         )
149                 elif self.entry_permalink_style == 'Y':
150                         urlpatterns += patterns('',
151                                 url(r'^(?P<year>\d{4})/(?P<slug>[-\w]+)/?$', self.entry_view)
152                         )
153                 elif self.entry_permalink_style == 'B':
154                         urlpatterns += patterns('',
155                                 url((r'^(?:%s)/(?P<slug>[-\w]+)/?$' % self.entry_permalink_base), self.entry_view)
156                         )
157                 else:
158                         urlpatterns = patterns('',
159                                 url(r'^(?P<slug>[-\w]+)/?$', self.entry_view)
160                         )
161                 return urlpatterns
162         
163         def get_all_entries(self, request, extra_context=None):
164                 return self.blog.entries.all(), extra_context
165         
166         def get_entries_by_ymd(self, request, year=None, month=None, day=None, extra_context=None):
167                 if not self.entry_archive_page:
168                         raise Http404
169                 entries = self.blog.entries.all()
170                 if year:
171                         entries = entries.filter(date__year=year)
172                 if month:
173                         entries = entries.filter(date__month=month)
174                 if day:
175                         entries = entries.filter(date__day=day)
176                 
177                 context = extra_context or {}
178                 context.update({'year': year, 'month': month, 'day': day})
179                 return entries, context
180         
181         def get_entries_by_tag(self, request, tag_slugs, extra_context=None):
182                 tags = []
183                 for tag_slug in tag_slugs.replace('+', '/').split('/'):
184                         if tag_slug: # ignore blank slugs, handles for multiple consecutive separators (+ or /)
185                                 try:
186                                         tag = self.blog.entry_tags.get(slug=tag_slug)
187                                 except:
188                                         raise Http404
189                                 tags.append(tag)
190                 if len(tags) <= 0:
191                         raise Http404
192
193                 entries = self.blog.entries.all()
194                 for tag in tags:
195                         entries = entries.filter(tags=tag)
196                 
197                 context = self.get_context()
198                 context.update(extra_context or {})
199                 context.update({'tags': tags})
200                 
201                 return entries, context
202         
203         def add_item(self, feed, obj, kwargs=None):
204                 title = loader.get_template("penfield/feeds/blog_entry/title.html")
205                 description = loader.get_template("penfield/feeds/blog_entry/description.html")
206                 defaults = {
207                         'title': title.render(Context({'entry': obj})),
208                         'description': description.render(Context({'entry': obj})),
209                         'author_name': obj.author.get_full_name(),
210                         'pubdate': obj.date
211                 }
212                 defaults.update(kwargs or {})
213                 super(BlogView, self).add_item(feed, obj, defaults)
214         
215         def get_feed(self, feed_type, extra_context, kwargs=None):
216                 tags = (extra_context or {}).get('tags', None)
217                 title = self.blog.title
218                 
219                 if tags is not None:
220                         title += " - %s" % ', '.join([tag.name for tag in tags])
221                 
222                 defaults = {
223                         'title': title
224                 }
225                 defaults.update(kwargs or {})
226                 return super(BlogView, self).get_feed(feed_type, extra_context, defaults)
227         
228         def entry_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
229                 entries = self.blog.entries.all()
230                 if year:
231                         entries = entries.filter(date__year=year)
232                 if month:
233                         entries = entries.filter(date__month=month)
234                 if day:
235                         entries = entries.filter(date__day=day)
236                 try:
237                         entry = entries.get(slug=slug)
238                 except:
239                         raise Http404
240                 context = self.get_context()
241                 context.update(extra_context or {})
242                 context.update({'entry': entry})
243                 return self.entry_page.render_to_response(request, extra_context=context)
244         
245         def tag_archive_view(self, request, extra_context=None):
246                 if not self.tag_archive_page:
247                         raise Http404
248                 context = {}
249                 context.update(extra_context or {})
250                 context.update({'blog': self.blog})
251                 return self.tag_archive_page.render_to_response(request, extra_context=context)
252
253
254 class Newsletter(Entity, Titled):
255         pass
256
257
258 register_value_model(Newsletter)
259
260
261 class NewsletterArticle(Entity, Titled):
262         newsletter = models.ForeignKey(Newsletter, related_name='articles')
263         authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
264         date = models.DateTimeField(default=None)
265         lede = TemplateField(null=True, blank=True, verbose_name='Summary')
266         full_text = TemplateField(db_index=True)
267         tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
268         
269         def save(self, *args, **kwargs):
270                 if self.date is None:
271                         self.date = datetime.now()
272                 super(NewsletterArticle, self).save(*args, **kwargs)
273         
274         class Meta:
275                 get_latest_by = 'date'
276                 ordering = ['-date']
277                 unique_together = (('newsletter', 'slug'),)
278
279
280 register_value_model(NewsletterArticle)
281
282
283 class NewsletterIssue(Entity, Titled):
284         newsletter = models.ForeignKey(Newsletter, related_name='issues')
285         numbering = models.CharField(max_length=50, help_text='For example, 04.02 for volume 4, issue 2.')
286         articles = models.ManyToManyField(NewsletterArticle, related_name='issues')
287         
288         class Meta:
289                 ordering = ['-numbering']
290                 unique_together = (('newsletter', 'numbering'),)
291
292
293 register_value_model(NewsletterIssue)
294
295
296 class NewsletterView(MultiView, FeedMultiViewMixin):
297         ARTICLE_PERMALINK_STYLE_CHOICES = (
298                 ('D', 'Year, month, and day'),
299                 ('M', 'Year and month'),
300                 ('Y', 'Year'),
301                 ('S', 'Slug only')
302         )
303         
304         newsletter = models.ForeignKey(Newsletter, related_name='newsletterviews')
305         
306         index_page = models.ForeignKey(Page, related_name='newsletter_index_related')
307         article_page = models.ForeignKey(Page, related_name='newsletter_article_related')
308         article_archive_page = models.ForeignKey(Page, related_name='newsletter_article_archive_related', null=True, blank=True)
309         issue_page = models.ForeignKey(Page, related_name='newsletter_issue_related')
310         issue_archive_page = models.ForeignKey(Page, related_name='newsletter_issue_archive_related', null=True, blank=True)
311         
312         article_permalink_style = models.CharField(max_length=1, choices=ARTICLE_PERMALINK_STYLE_CHOICES)
313         article_permalink_base = models.CharField(max_length=255, blank=False, default='articles')
314         issue_permalink_base = models.CharField(max_length=255, blank=False, default='issues')
315         
316         feed_suffix = models.CharField(max_length=255, blank=False, default=FeedMultiViewMixin.feed_suffix)
317         feeds_enabled = models.BooleanField()
318         list_var = 'articles'
319         
320         def __unicode__(self):
321                 return self.newsletter.__unicode__()
322         
323         def get_reverse_params(self, obj):
324                 if isinstance(obj, NewsletterArticle):
325                         if obj.newsletter == self.newsletter:
326                                 kwargs = {'slug': obj.slug}
327                                 if self.article_permalink_style in 'DMY':
328                                         kwargs.update({'year': str(obj.date.year).zfill(4)})
329                                         if self.article_permalink_style in 'DM':
330                                                 kwargs.update({'month': str(obj.date.month).zfill(2)})
331                                                 if self.article_permalink_style == 'D':
332                                                         kwargs.update({'day': str(obj.date.day).zfill(2)})
333                                 return self.article_view, [], kwargs
334                 elif isinstance(obj, NewsletterIssue):
335                         if obj.newsletter == self.newsletter:
336                                 return 'issue', [], {'numbering': obj.numbering}
337                 elif isinstance(obj, (date, datetime)):
338                         kwargs = {
339                                 'year': str(obj.year).zfill(4),
340                                 'month': str(obj.month).zfill(2),
341                                 'day': str(obj.day).zfill(2)
342                         }
343                         return 'articles_by_day', [], kwargs
344                 raise ViewCanNotProvideSubpath
345         
346         @property
347         def urlpatterns(self):
348                 urlpatterns = patterns('',
349                         url(r'^', include(self.feed_patterns(self.get_all_articles, self.index_page, 'index'))),
350                         url(r'^(?:%s)/(?P<numbering>.+)/' % self.issue_permalink_base, include(self.feed_patterns(self.get_articles_by_issue, self.issue_page, 'issue')))
351                 )
352                 if self.issue_archive_page:
353                         urlpatterns += patterns('',
354                                 url(r'^(?:%s)/$' % self.issue_permalink_base, self.issue_archive_view)
355                         )
356                 if self.article_archive_page:
357                         urlpatterns += patterns('',
358                                 url(r'^(?:%s)/' % self.article_permalink_base, include(self.feed_patterns(self.get_all_articles, self.article_archive_page, 'articles')))
359                         )
360                         if self.article_permalink_style in 'DMY':
361                                 urlpatterns += patterns('',
362                                         url(r'^(?:%s)/(?P<year>\d{4})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_year')))
363                                 )
364                                 if self.article_permalink_style in 'DM':
365                                         urlpatterns += patterns('',
366                                                 url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_month')))
367                                         )
368                                         if self.article_permalink_style == 'D':
369                                                 urlpatterns += patterns('',
370                                                         url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/' % self.article_permalink_base, include(self.feed_patterns(self.get_articles_by_ymd, self.article_archive_page, 'articles_by_day')))
371                                                 )
372                 
373                 if self.article_permalink_style == 'Y':
374                         urlpatterns += patterns('',
375                                 url(r'^(?:%s)/(?P<year>\d{4})/(?P<slug>[\w-]+)/$' % self.article_permalink_base, self.article_view)
376                         )
377                 elif self.article_permalink_style == 'M':
378                         urlpatterns += patterns('',
379                                 url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w-]+)/$' % self.article_permalink_base, self.article_view)
380                         )
381                 elif self.article_permalink_style == 'D':
382                         urlpatterns += patterns('',
383                                 url(r'^(?:%s)/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w-]+)/$' % self.article_permalink_base, self.article_view)
384                         )
385                 else:   
386                         urlpatterns += patterns('',
387                                 url(r'^(?:%s)/(?P<slug>[-\w]+)/?$' % self.article_permalink_base, self.article_view)
388                         )
389                 
390                 return urlpatterns
391         
392         def get_context(self):
393                 return {'newsletter': self.newsletter}
394         
395         def get_all_articles(self, request, extra_context=None):
396                 return self.newsletter.articles.all(), extra_context
397         
398         def get_articles_by_ymd(self, request, year, month=None, day=None, extra_context=None):
399                 articles = self.newsletter.articles.filter(dat__year=year)
400                 if month:
401                         articles = articles.filter(date__month=month)
402                 if day:
403                         articles = articles.filter(date__day=day)
404                 return articles
405         
406         def get_articles_by_issue(self, request, numbering, extra_context=None):
407                 try:
408                         issue = self.newsletter.issues.get(numbering=numbering)
409                 except NewsletterIssue.DoesNotExist:
410                         raise Http404
411                 context = extra_context or {}
412                 context.update({'issue': issue})
413                 return issue.articles.all(), context
414         
415         def article_view(self, request, slug, year=None, month=None, day=None, extra_context=None):
416                 articles = self.newsletter.articles.all()
417                 if year:
418                         articles = articles.filter(date__year=year)
419                 if month:
420                         articles = articles.filter(date__month=month)
421                 if day:
422                         articles = articles.filter(date__day=day)
423                 try:
424                         article = articles.get(slug=slug)
425                 except NewsletterArticle.DoesNotExist:
426                         raise Http404
427                 context = self.get_context()
428                 context.update(extra_context or {})
429                 context.update({'article': article})
430                 return self.article_page.render_to_response(request, extra_context=context)
431         
432         def issue_archive_view(self, request, extra_context=None):
433                 if not self.issue_archive_page:
434                         raise Http404
435                 context = {}
436                 context.update(extra_context or {})
437                 context.update({'newsletter': self.newsletter})
438                 return self.issue_archive_page.render_to_response(request, extra_context=context)
439         
440         def add_item(self, feed, obj, kwargs=None):
441                 title = loader.get_template("penfield/feeds/newsletter_article/title.html")
442                 description = loader.get_template("penfield/feeds/newsletter_article/description.html")
443                 defaults = {
444                         'title': title.render(Context({'article': obj})),
445                         'author_name': ', '.join([author.get_full_name() for author in obj.authors.all()]),
446                         'pubdate': obj.date,
447                         'description': description.render(Context({'article': obj})),
448                         'categories': [tag.name for tag in obj.tags.all()]
449                 }
450                 defaults.update(kwargs or {})
451                 super(NewsletterView, self).add_item(feed, obj, defaults)
452         
453         def get_feed(self, feed_type, extra_context, kwargs=None):
454                 title = self.newsletter.title
455                 
456                 defaults = {
457                         'title': title
458                 }
459                 defaults.update(kwargs or {})
460                 return super(NewsletterView, self).get_feed(feed_type, extra_context, defaults)