Commits

David Chambers committed 5ee8341

Swapped out Ian Lewis's disqus-python-client for the "official" Python
bindings, in order to make use of the current version of the Disqus API.
Added `post` and `thread` classes which wrap the `disqusapi` module and
provide the same attributes as were previously available to templates.

  • Participants
  • Parent commits 248d8ad

Comments (0)

Files changed (10)

+from datetime import datetime
+import hashlib
+
+# import `APIError` to provide a convenient alias for `disqusapi.APIError`
+from disqusapi import APIError, DisqusAPI, Paginator
+import pytz
+
+from mango.settings import (COMMENTS_REQUIRE_APPROVAL,
+                            DISQUS_API_KEY, DISQUS_SHORTNAME)
+from mango.templatetags.mango import _convert
+from mango.utils import replace
+
+disqus = DisqusAPI(DISQUS_API_KEY)
+
+api_key = hashlib.sha1(DISQUS_API_KEY).hexdigest()
+
+class post(object):
+    def __init__(self, data, thread_=None):
+        """Initialize a `post` object from an appropriate dictionary."""
+        self.id = data['id']
+        self.author = data['author']
+        self.created_at = datetime.strptime(data['createdAt'],
+                '%Y-%m-%dT%H:%M:%S').replace(tzinfo=pytz.utc)
+        self.message = data['message']
+        self.html = replace(_convert(self.message))
+        self.thread = thread_ or thread.fetch(data['thread'])
+
+    @staticmethod
+    def fetch(post_id):
+        """Fetch the post from Disqus and return it as a `post` object."""
+        return post(disqus.posts.details(post=post_id))
+
+    @staticmethod
+    def create(**kwargs):
+        """Create a new post and return it as a `post` object."""
+        kwargs = dict((k, v) for k, v in kwargs.items() if v)
+        return post(disqus.posts.create(state='unapproved', **kwargs))
+
+    @staticmethod
+    def approve(post_id):
+        """Mark the post as approved."""
+        disqus.posts.approve(post=post_id)
+
+    @staticmethod
+    def remove(post_id):
+        """Mark the post as deleted."""
+        disqus.posts.remove(post=post_id)
+
+    @staticmethod
+    def spam(post_id):
+        """Mark the post as spam."""
+        disqus.posts.spam(post=post_id)
+
+class thread(object):
+    def __init__(self, data):
+        """Initialize a `thread` object from an appropriate dictionary."""
+        self.id = data['id']
+        self.allow_comments = not data['isClosed']
+        self.title = data['title']
+
+        include = ['approved']
+        if not COMMENTS_REQUIRE_APPROVAL:
+            include.append('unapproved')
+
+        self.posts = [post(postdata, thread=self)
+                      for postdata in disqus.threads.listPosts(
+                      thread=self.id, limit=100, include=include)]
+
+        self.posts.sort(key=lambda post: post.created_at)
+
+    @staticmethod
+    def cache_key(thread_id):
+        """Return the thread's cache key."""
+        return 'mango:disqus:thread:%s' % thread_id
+
+    @staticmethod
+    def id_from_identifier(identifier):
+        """Return the id of the first thread matching `identifier`."""
+        paginator = Paginator(disqus.threads.list, forum=DISQUS_SHORTNAME)
+        for data in paginator():
+            if identifier in data['identifiers']:
+                return data['id']
+
+    @staticmethod
+    def fetch(thread_id):
+        """Fetch the thread from Disqus and return it as a `thread` object."""
+        return thread(disqus.threads.details(thread=thread_id))
+
+    @staticmethod
+    def create(**kwargs):
+        """Create a new thread and return it as a `thread` object."""
+        return thread(disqus.threads.create(forum=DISQUS_SHORTNAME, **kwargs))
+
+    @staticmethod
+    def close(thread_id):
+        """Mark the thread as closed."""
+        disqus.threads.close(thread=thread_id)
 # -*- coding: utf-8 -*-
 
-import hashlib
-import urllib2
-
 from django.core.cache import cache
 from django.core.mail import EmailMultiAlternatives
 from django.core.urlresolvers import reverse
 
 import mango.settings
 from mango.decorators import baseurl
+from mango import disqus
 from mango.main import Index
-from mango.utils import akismet_request, logger, text_response
+from mango.utils import logger, text_response
 if mango.settings.SUBSCRIPTIONS:
     from mango.models import Subscription
 
 
 @baseurl
 def moderate(request, action):
-    if not mango.settings.DISQUS:
-        return text_response('Invalid DISQUS settings.', 500)
-
-    if request.GET.get('api_key') != hashlib.sha1(mango.settings.DISQUS_API_KEY).hexdigest():
+    if request.GET.get('api_key') != disqus.api_key:
         return text_response('Invalid API key.', 400)
 
     post_id = request.GET.get('post_id')
     thread_id = request.GET.get('thread_id')
-
-    if thread_id:
-        if not mango.settings.FORUM:
-            return text_response('Invalid DISQUS settings.', 500)
-
-        for thread in mango.settings.DISQUS.get_thread_list(mango.settings.FORUM, limit=9999):
-            if thread.id == thread_id:
-                break
-        else:
-            return text_response('Invalid thread id.', 400)
+    thread_url = request.GET.get('url')
 
     if action == 'close':
-        mango.settings.DISQUS.update_thread(mango.settings.FORUM, thread, allow_comments=False)
-        message = 'Thread closed.'
+        disqus.thread.close(thread_id)
+        return text_response('Thread closed.')
 
-    elif action == 'approve':
-        comment = mango.settings.DISQUS.moderate_post(post_id, 'approve')
-        message = 'Comment approved.'
-        comment.thread.url = request.GET.get('url', comment.thread.url)
+    comment = disqus.post(post_id)
+    cache_key = disqus.thread.cache_key(comment.thread.id)
 
-        cache_key = 'mango:disqus:%s' % comment.thread.id
+    if action == 'approve':
+        disqus.post.approve(post_id)
         cache.delete(cache_key)
         logger.debug('Cache key invalidated: %s' % cache_key)
 
-        if mango.settings.SUBSCRIPTIONS and comment.thread.url: # notify subscribers
+        if mango.settings.SUBSCRIPTIONS and thread_url: # notify subscribers
+            comment = disqus.post.fetch(post_id)
             subject = u'New comment on "%s"' % comment.thread.title
             text_template = loader.get_template('email/subscriber.dtext')
             html_template = loader.get_template('email/subscriber.dhtml')
-            for sub in Subscription.objects.filter(url=comment.thread.url):
+
+            for sub in Subscription.objects.filter(url=thread_url):
                 context = Context({'comment': comment, 'subscription_id': sub.id})
                 msg = EmailMultiAlternatives(subject, text_template.render(context),
                         to=[u'%s <%s>' % (sub.subscriber_name, sub.subscriber_email)])
                 msg.attach_alternative(html_template.render(context), 'text/html')
                 msg.send(fail_silently=False)
+
+        return text_response('Comment approved.')
+
+    if action == 'spam':
+        disqus.post.spam(post_id)
+        message = 'Comment marked as spam.'
     else:
-        comment = mango.settings.DISQUS.moderate_post(post_id, 'kill')
+        disqus.post.remove(post_id)
         message = 'Comment deleted.'
 
-        if not mango.settings.COMMENTS_REQUIRE_APPROVAL:
-            cache_key = 'mango:disqus:%s' % comment.thread.id
-            cache.delete(cache_key)
-            logger.debug('Cache key invalidated: %s' % cache_key)
-
-        if action == 'spam':
-            mango.settings.DISQUS.moderate_post(post_id, 'spam')
-            message = 'Comment marked as spam and deleted.'
-
-            if mango.settings.AKISMET_API_KEY and thread_id: # inform Akismet of its failure
-                for comment in mango.settings.DISQUS.get_thread_posts(mango.settings.FORUM, thread, limit=9999):
-                    if comment.id == post_id:
-                        break
-                else:
-                    return text_response('Comment not found.', 500)
-
-                args = {
-                    'blog': baseurl(),
-                    'user_ip': comment.ip_address,
-                    'user_agent': '',
-                    'referrer': comment.thread.url,
-                    'permalink': comment.thread.url,
-                    'comment_type': 'comment',
-                    'comment_content': comment.message,
-                }
-                if comment.is_anonymous:
-                    args['comment_author'] = comment.anonymous_author.name
-                    url = comment.anonymous_author.url
-                else:
-                    args['comment_author'] = comment.author.username
-                    url = comment.author.url
-
-                if url:
-                    args['comment_author_url'] = url
-
-                f = urllib2.urlopen(akismet_request('submit-spam', args))
-                f.close()
+    if not mango.settings.COMMENTS_REQUIRE_APPROVAL:
+        cache.delete(cache_key)
+        logger.debug('Cache key invalidated: %s' % cache_key)
 
     return text_response(message)
 
 import os
 import re
 
-import disqus
 import lxml.html
 import markdown
 import pytz
 
 import mango.settings
 from mango import decorators
+from mango import disqus
 from mango.template import Script, StyleSheet
 from mango.template.code import supported_code_sites
 from mango.template.video import supported_video_sites
-from mango.templatetags.mango import _convert, convert_html_chars
+from mango.templatetags.mango import convert_html_chars
 from mango.utils import canonicalize, logger, replace, slugify
 
 
         self._aliases = aliases
         self._slug = canon_fragments[-1]
 
+    def identifier(self):
+        return self.urls['canon']['rel']
+
     def permalink(self):
         return self.urls['canon']['abs']
 
             if title:
                 self.name = title
 
-        for name in [f for f in os.listdir(dirpath) if not _invalid_filename(f)]:
+        for name in [fn for fn in os.listdir(dirpath) if not _invalid_filename(fn)]:
             path = os.path.join(dirpath, name)
             if os.path.isdir(path):
                 self.subcategories.append(Category(path))
 
         self._filepath = filepath
         self._parent = parent
-        self._thread = None
         self._tags = None
+        self._thread_id = None
 
         self.lastmod = None
         self.previous = None
             self.excerpt = mark_safe('\n'.join([Excerpt(parent=self).convert(s).html for s in snippets])) if snippets else self.html
             self.type = self.meta.get('type', 'post' if self.datetime else 'page')
 
-        # attach comments thread
-        if mango.settings.FORUM and hasattr(self, 'urls'):
-            cache_key = 'mango:disqus:%s' % self.permalink()
-            cached = cache.get(cache_key)
-            if cached is not None:
-                self._thread = cached
-                logger.debug('Disqus thread for "%s" retrieved from cache' % self._thread.title)
-            else:
-                try:
-                    # accommodate old threads (Mango-made threads must be accessed by identifier)
-                    self._thread = (
-                        mango.settings.DISQUS.get_thread_by_url(
-                            mango.settings.FORUM, self.permalink()) or
-                        mango.settings.DISQUS.thread_by_identifier(
-                            mango.settings.FORUM, self.title_text, self.urls['canon']['rel'])['thread']
-                    )
-                except disqus.APIError, error:
-                    logger.warning('Disqus API error: %s' % error)
-                else:
-                    cache.set(cache_key, self._thread, 24*60*60)
-                    logger.debug('Disqus thread for "%s" cached' % self._thread.title)
-
         md.reset() # clear cached link definitions
 
         return self
 
-    def comments(self):
-        comments = []
-        if not self._thread:
-            return comments
+    def _thread(self):
+        if self.type == 'page':
+            return None
 
-        cache_key = 'mango:disqus:%s' % self._thread.id
-        cached = cache.get(cache_key)
-        if cached is not None:
-            logger.debug('Disqus comments for "%s" retrieved from cache' % self._thread.title)
-            return cached
+        identifier = self.identifier()
+        title = self.title_text
+
+        if self._thread_id is None:
+            self._thread_id = disqus.thread.id_from_identifier(identifier)
+
+        cache_key = disqus.thread.cache_key(self._thread_id)
+        thread = cache.get(cache_key)
+
+        if thread is not None:
+            logger.debug('Disqus thread for "%s" retrieved from cache' % title)
+            return thread
 
         try:
-            thread_posts = mango.settings.DISQUS.get_thread_posts(mango.settings.FORUM, self._thread, limit=9999, exclude='killed')
+            thread = disqus.thread.fetch(self._thread_id)
         except disqus.APIError, error:
             logger.warning('Disqus API error: %s' % error)
         else:
-            for comment in thread_posts:
-                if comment.has_been_moderated or not mango.settings.COMMENTS_REQUIRE_APPROVAL:
-                    comment.html = replace(_convert(comment.message))
-                    comments.append(comment)
-            comments.sort(key=lambda comment: comment.created_at)
-            cache.set(cache_key, comments, 24*60*60)
-            logger.debug('Disqus comments for "%s" cached' % self._thread.title)
+            if thread is None:
+                thread = disqus.thread.create(title=title, slug=self._slug,
+                                              identifier=identifier)
+                logger.debug('Disqus thread for "%s" created' % title)
+            # In theory, threads could be cached indefinitely since cache keys
+            # are invalidated when required in response to comment moderation.
+            cache.set(cache_key, thread, 24*60*60)
+            logger.debug('Disqus thread for "%s" cached' % title)
+            return thread
 
-        return comments
+    def comments(self):
+        thread = self._thread()
+        return thread.posts if thread else []
 
     def has_tag(self, tag):
         return tag in self.meta.get('tags', [])

management/commands/bake.py

         documents = index.descendants(include_pages=True)
 
         for document in documents:
-            dirpath = os.path.join(path, document.urls['canon']['rel'].strip('/'))
+            dirpath = os.path.join(path, document.identifier().strip('/'))
 
             try:
                 os.makedirs(dirpath)
             f.close()
 
             # Markdown document
-            components = document.urls['canon']['rel'].strip('/').split('/')
+            components = document.identifier().strip('/').split('/')
             f = codecs.open(os.path.join(path, *components[:-1] + [components[-1] + '.text']), 'w', 'utf-8')
             f.write(u'%s\n' % document.body.rstrip())
             f.close()
+disqusapi
 django>=1.2
 html2text
 lxml
 markdown
 pytz
-simplejson # required by disqus-python-client
-hg+http://bitbucket.org/IanLewis/disqus-python-client
+simplejson # required by disqus-python

settings/__init__.py

 MARKDOWN_DATE_FORMAT = u'%d %B %Y' # e.g. 2 April 2010
 MARKDOWN_TIME_FORMAT = u'%I:%M%p' # e.g. 6:50pm
 
+DISQUS = bool(DISQUS_API_KEY)
+
 DOCUMENTS_PATH = os.path.expanduser(smart_unicode(DOCUMENTS_PATH.rstrip('/')))
 PATH_TO_STATIC = os.path.expanduser(smart_unicode(PATH_TO_STATIC.rstrip('/')))
 
 
 TAGS_PAGE = 'tags' in META_LISTS
 
-try:
-    DISQUS_API_KEY, DISQUS_SHORTNAME
-except NameError:
-    DISQUS = DISQUS_API_KEY = FORUM = None
-else:
-    import disqus as _disqus
-    DISQUS = _disqus.DisqusService('1.1')
-    DISQUS.login(DISQUS_API_KEY)
-    for FORUM in DISQUS.get_forum_list():
-        if FORUM.shortname == DISQUS_SHORTNAME:
-            break
-    else:
-        FORUM = None
-
 MANGO_PATH = os.path.dirname(dirname)
 
 PROJECT_PATH = os.path.dirname(MANGO_PATH)
         return lastmod
 
     def location(self, document):
-        return document.urls['canon']['rel']
+        return document.identifier()

templates/email/moderator.dhtml

 Delete comment</a></li>
 <li><a style="text-decoration:none" href="{{ urls.spam }}">
 <img style="border:none;padding-right:5px" src="http://mango.io/static/e/spam.png" />
-Mark comment as spam (and delete it)</a></li>
+Mark comment as spam</a></li>
 <li><a style="text-decoration:none" href="{{ urls.close }}">
 <img style="border:none;padding-right:5px" src="http://mango.io/static/e/close.png" />
 Close thread</a></li>

templatetags/mango_extras.py

 
 @register.filter
 def author_name(comment):
-    if comment.is_anonymous:
-        return comment.anonymous_author.name
-    else:
-        return comment.author.display_name or comment.author.username
+    return comment.author['name']
 
 @register.filter
 def author_email_hash(comment):
-    if comment.is_anonymous:
-        return comment.anonymous_author.email_hash
-    else:
-        return comment.author.email_hash
+    return comment.author['emailHash']
 
 @register.filter
 def author_url(comment):
-    if comment.is_anonymous:
-        return comment.anonymous_author.url
-    else:
-        return comment.author.url
+    return comment.author['url']
 
 @register.filter
 def combine(list1, list2):
 # -*- coding: utf-8 -*-
 
-import hashlib
-
-import disqus
-
 from django.core.mail import EmailMultiAlternatives
 from django.core.urlresolvers import reverse
 from django.http import HttpResponseRedirect, HttpResponseServerError
 
 import mango.settings
 from mango.decorators import baseurl
+from mango import disqus
 from mango.exceptions import EmptySettingError
 from mango.forms import CommentForm, ContactForm
 from mango.main import Category, Document, Index
 if mango.settings.SUBSCRIPTIONS:
     from mango.models import Subscription
-from mango.templatetags.mango import _convert, convert
+from mango.templatetags.mango import convert
 from mango.utils import (html_response, logger, primary_author_email, replace,
         slugify, text_response)
 
     if not isinstance(match, (Category, Document)):
         return page_not_found(request)
 
-    if path != match.urls['canon']['rel']:
+    if path != match.identifier():
         return HttpResponseRedirect(match.permalink())
 
     if isinstance(match, Category):
         return category(request, match)
 
     document = match # we've confirmed that the we're to serve a Document
-    thread = document._thread
+    thread = document._thread()
 
     comment = request.session.pop('comment', None) if thread else None
     if comment is not None:
 
         try:
             # send request to Disqus
-            comment = mango.settings.DISQUS.create_post(
-                    mango.settings.FORUM, thread,
+            comment = disqus.post.create(
+                    thread=thread.id,
                     message=message,
                     author_name=author_name,
                     author_email=author_email,
 
         # store comment so that it can be displayed to
         # its author even if withheld for moderation
-        comment.html = _convert(comment.message)
         request.session['comment'] = comment
 
         # send e-mail notification
         post_url = request.build_absolute_uri(reverse(post, args=(path_,)))
         params = {
-            'api_key': hashlib.sha1(mango.settings.DISQUS_API_KEY).hexdigest(),
+            'api_key': disqus.api_key,
             'post_id': comment.id,
             'thread_id': thread.id,
             'url': post_url,
         }
         links = {
-            'approve':  ('api_key', 'post_id', 'url'),
+            'approve':  ('api_key', 'post_id'),
             'close':    ('api_key', 'thread_id'),
             'delete':   ('api_key', 'post_id'),
-            'spam':     ('api_key', 'post_id', 'thread_id'),
+            'spam':     ('api_key', 'post_id'),
         }
         author = u'%s <%s>' % (author_name, author_email)
         subject = u'[%s] Comment: "%s"' % (mango.settings.SITE_TITLE, document.title_text)
 
     if isinstance(match, Document):
         document = match
-        if path == document.urls['canon']['rel']:
+        if path == document.identifier():
             return text_response(document.body)
         else:
             return HttpResponseRedirect('%s%s' % (document.permalink(),