Permalink
Browse files

Made Admin links visible to everyone. They will only work for actual …

…admin users though. Other users will get an error message if they click on the links.

Refactored HubSubscriber to support unsubscription at the hub.
Added tests for deleting feeds and unsubscribing from the hub.
Added support for correctly dealing with hub challenges during unsubscription
  • Loading branch information...
1 parent b0db10a commit 96b0ca6280b375e3eafd7f3d0b251b52c6d76182 @adewale committed Sep 15, 2010
Showing with 124 additions and 35 deletions.
  1. +2 −5 TODO
  2. +13 −11 pshb.py
  3. +3 −2 sidemenu_fragment.html
  4. +27 −8 streamer.py
  5. +42 −0 streamer_functional_tests.py
  6. +37 −9 streamer_tests.py
View
@@ -1,11 +1,8 @@
- Adding a feed should send you back to the addSubscription page
- Add a handler for exporting a textual list of the feeds of the blogs that are being consumed.
- The handle* function calls should all be made into background tasks by putting them into task queues.
-
-- List all the things needed to be a valid PSHB subscriber. Consider extracting that out as a reusable interface based on callbacks. Make that into its own configurable library.
- OPML import/export
-
- Expose an atom feed with support for PSHB and atom:source
-
- Store the entirety of the feedarser entry and expose all of it in the Atom feed that's generated. We should support MustIgnore by making sure that anything that comes in to Streamer goes out.
-- Change all the class, variable and method names to be PEP 8 compatible
+- Change all the class, variable and method names to be PEP 8 compatible
+- Correctly implement support for verification tokens and authenticated content distribution. This will remove the need to deal with feeds, like Google Buzz, that use a slightly different URL in their rel='self' element.
View
@@ -196,22 +196,24 @@ def extractSourceUrl(self):
class HubSubscriber(object):
- def __init__(self, url, hub):
- self.url = url
- self.hub = hub
+ def subscribe(self, url, hub, callback_url):
+ self._talk_to_hub('subscribe', url, hub, callback_url)
+
+ def unsubscribe(self, url, hub, callback_url):
+ self._talk_to_hub('unsubscribe', url, hub, callback_url)
- def subscribe(self):
- parameters = {"hub.callback": "http://%s.appspot.com/posts" % settings.APP_NAME,
- "hub.mode": "subscribe",
- "hub.topic": self.url,
- "hub.verify": "async", # We don't want subscriptions to block until verification happens
+ def _talk_to_hub(self, mode, url, hub, callback_url):
+ parameters = {"hub.callback": callback_url,
+ "hub.mode": mode,
+ "hub.topic": url,
+ "hub.verify": "async", # We don't want un/subscriptions to block until verification happens
"hub.verify_token": settings.SECRET_TOKEN, #TODO Must generate a token based on some secret value
}
payload = urllib.urlencode(parameters)
- response = urlfetch.fetch(self.hub,
+ response = urlfetch.fetch(hub,
payload=payload,
method=urlfetch.POST,
headers={'Content-Type': 'application/x-www-form-urlencoded'})
- logging.info("Status of subscription for feed: %s at hub: %s is: %d" % (self.url, self.hub, response.status_code))
+ logging.info("Status of %s for feed: %s at hub: %s is: %d" % (mode, url, hub, response.status_code))
if response.status_code != 202:
- logging.info(response.content)
+ logging.info(response.content)
@@ -6,12 +6,13 @@
<li><h3><a href="/about">About</a></h3></li>
<li><h3><a href="/subscriptions">Subscriptions</a></h3></li>
<li><h3><a href="/posts">Posts</a></h3></li>
+ <li></li>
+ <li></li>
- {% if admin%}
+ <li><h3>Admin</h3></li>
<li><h3><a href="/admin/addSubscription">Admin::Add Subscription</a></h3></li>
<li><h3><a href="/admin/deleteSubscription">Admin::Delete Subscription</a></h3></li>
<li><h3><a href="/admin/refreshSubscriptions">Admin::Refresh Subscriptions</a></h3></li>
- {% endif %}
<li></li>
<li></li>
</ul>
View
@@ -100,6 +100,7 @@ def get(self):
self.redirect('/subscriptions')
else:
self.error(403)
+ self.response.out.write("You are not the Admin")
class AdminAddSubscriptionHandler(webapp.RequestHandler):
@login_required
@@ -110,6 +111,7 @@ def get(self):
render(self.response.out, 'add_subscriptions.html', templateValues)
else:
self.error(403)
+ self.response.out.write("You are not the Admin")
class AdminDeleteSubscriptionHandler(webapp.RequestHandler):
@login_required
@@ -120,6 +122,7 @@ def get(self):
render(self.response.out, 'delete_subscriptions.html', templateValues)
else:
self.error(403)
+ self.response.out.write("You are not the Admin")
def post(self):
# Only admin users can see this page
@@ -131,20 +134,27 @@ def post(self):
self.redirect('/admin/deleteSubscription')
else:
self.error(403)
+ self.response.out.write("You are not the Admin")
class AboutHandler(webapp.RequestHandler):
def get(self):
render(self.response.out, 'about.html')
# TODO work out to make the handle* functions deferred
-def handleDeleteSubscription(url):
+def handleDeleteSubscription(url, hubSubscriber=pshb.HubSubscriber()):
logging.info("Deleting subscription: %s" % url)
+
pshb.Post.deleteAllPostsWithMatchingFeedUrl(url)
+ subscription = Subscription.get_by_key_name(url)
+ logging.info('Found: %s' % str(subscription))
+
Subscription.deleteSubscriptionWithMatchingUrl(url)
+ hubSubscriber.unsubscribe(url, subscription.hub, "http://%s.appspot.com/posts" % settings.APP_NAME)
def handleNewSubscription(url, nickname):
logging.info("Subscription added: <%s> by <%s>" % (url, nickname))
+ # TODO test this function directly just like we do for handleDeleteSubscription
try:
parser = pshb.ContentParser(None, settings.DEFAULT_HUB, settings.ALWAYS_USE_DEFAULT_HUB, urlToFetch=url)
@@ -160,8 +170,8 @@ def handleNewSubscription(url, nickname):
subscription.put()
# Tell the hub about the url
- hubSubscriber = pshb.HubSubscriber(url, hub)
- hubSubscriber.subscribe()
+ hubSubscriber = pshb.HubSubscriber()
+ hubSubscriber.subscribe(url, hub, "http://%s.appspot.com/posts" % settings.APP_NAME)
# Store the current content of the feed
posts = parser.extractPosts()
@@ -180,6 +190,8 @@ def post(self):
# Only admins can add new subscriptions
if not userIsAdmin():
self.error(403)
+ self.response.out.write("You are not the Admin")
+ return
# Extract the url from the request
url = self.request.get('url')
@@ -197,14 +209,21 @@ def get(self):
"""Show all the resources in this collection"""
# If this is a hub challenge
if self.request.get('hub.challenge'):
- # If this is a subscription and the url is one we have in our database
- if self.request.get('hub.mode') == "subscribe" and Subscription.exists(self.request.get('hub.topic')):
+ mode = self.request.get('hub.mode')
+ topic = self.request.get('hub.topic')
+ if mode == "subscribe" and Subscription.exists(topic):
+ # If this is a subscription and the URL is one we have in our database
+ self.response.out.write(self.request.get('hub.challenge'))
+ logging.info("Successfully accepted challenge for subscription to feed: %s" % topic)
+ elif mode == "unsubscribe" and not Subscription.exists(topic):
+ # If this is an unsubscription then we shouldn't have the URL in our database since it should already have been
+ # deleted.
self.response.out.write(self.request.get('hub.challenge'))
- logging.info("Successfully accepted challenge for feed: %s" % self.request.get('hub.topic'))
+ logging.info("Successfully accepted challenge for unsubscription to feed: %s" % topic)
else:
self.response.set_status(404)
- self.response.out.write("Challenge failed")
- logging.info("Challenge failed for feed: %s" % self.request.get('hub.topic'))
+ self.response.out.write("Challenge failed for feed: %s with mode: %s" % (topic, mode))
+ logging.info("Challenge failed for feed: %s with mode: %s" % (topic, mode))
# Once a challenge has been issued there's no point in returning anything other than challenge passed or failed
return
@@ -53,6 +53,15 @@ def testAddingNoneExistentFeedsDoesNotRaiseAnException(self):
response = self.post('/bgtasks', data=data, expect_errors=True)
self.assertEquals('200 OK', response.status)
+ def testDeletingFeedDoesNotRaiseAnException(self):
+ url = "http://example.org/atom"
+ f = streamer.Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
+ f.put()
+
+ data = {'function':'handleDeleteSubscription', 'url': url, 'nickname':'ade'}
+ response = self.post('/bgtasks', data=data, expect_errors=True)
+ self.assertEquals('200 OK', response.status)
+
def testEnqueuesTaskForNewSubscription(self):
data = {'url':'http://blog.oshineye.com/feeds/posts/default'}
self.assertTasksInQueue(0)
@@ -74,6 +83,39 @@ def testGoingToSlashIsTheSameAsPostsPage(self):
self.assertEquals(responseFromPostsPage.body, responseFromSlash.body)
self.assertEquals(responseFromPostsPage.status, responseFromSlash.status)
+ def testCanAcceptHubChallengeForSubscriptionToExistingFeed(self):
+ url = "http://example.org/atom"
+ hub="http://hub.example.org/"
+ s = streamer.Subscription(url=url, hub=hub, sourceUrl="http://example.org/", key_name=url)
+ s.put()
+
+ challenge = 'some hub challenge message'
+ response = self.get('/posts?hub.mode=subscribe&hub.topic=%s&hub.challenge=%s' % (url, challenge))
+ self.assertOK(response)
+ response.mustcontain(challenge)
+
+ def testAcceptsHubChallengeForUnsubscriptionToDeletedFeed(self):
+ # If the hub wants us to unsubscribe and we don't have the subscription then we should accept it
+ url = "http://example.org/atom"
+
+ challenge = 'some hub challenge message'
+ response = self.get('/posts?hub.mode=unsubscribe&hub.topic=%s&hub.challenge=%s' % (url, challenge))
+ self.assertOK(response)
+ response.mustcontain(challenge)
+
+ def testRejectsHubChallengeForUnsubscriptionToExistingFeed(self):
+ # If the hub wants us to unsubscribe and we have the subscription then the hub is probably confused
+ # so we honour the intentions of our users since they'll think we're still subscribed
+ url = "http://example.org/atom"
+ hub="http://hub.example.org/"
+ s = streamer.Subscription(url=url, hub=hub, sourceUrl="http://example.org/", key_name=url)
+ s.put()
+
+ challenge = 'some hub challenge message'
+ response = self.get('/posts?hub.mode=unsubscribe&hub.topic=%s&hub.challenge=%s' % (url, challenge), expect_errors=True)
+ self.assertEquals('404 Not Found', response.status)
+ response.mustcontain("Challenge failed for feed: %s with mode: %s" % (url, 'unsubscribe'))
+
class AboutHandlerTest(FunctionalTestCase, unittest.TestCase):
APPLICATION = streamer.application
View
@@ -3,9 +3,16 @@
import datetime
import feedparser
-import settings
+import pshb
+import streamer
import unittest
+class StubHubSubscriber(pshb.HubSubscriber):
+ def unsubscribe(self, url, hub, callback_url):
+ self.url = url
+ self.hub = hub
+ self.callback_url = callback_url
+
class SubscriptionTest(unittest.TestCase):
def setUp(self):
subscriptions = Subscription.all()
@@ -14,8 +21,8 @@ def setUp(self):
def testCanTellIfFeedIsAlreadyStored(self):
url = "http://example.org/atom"
- f = Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
- f.put()
+ s = Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
+ s.put()
self.assertTrue(Subscription.exists(url))
@@ -25,22 +32,43 @@ def testCanTellIfFeedIsNew(self):
def testAddingSubscriptionTwiceOnlyAddsOneRecordToDataStore(self):
url = "http://example.org/atom"
- f = Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
- f.put()
+ s = Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
+ s.put()
self.assertEquals(1, len(Subscription.find(url).fetch(1000)))
- f2 = Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
- f2.put()
+ s2 = Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
+ s2.put()
self.assertEquals(1, len(Subscription.find(url).fetch(1000)))
self.assertEquals(1, Subscription.all().count())
def testCanDeleteSubscription(self):
url = "http://example.org/atom"
- f = Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
- f.put()
+ s = Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
+ s.put()
self.assertTrue(Subscription.exists(url))
Subscription.deleteSubscriptionWithMatchingUrl(url)
self.assertFalse(Subscription.exists(url))
+class BackgroundHandlerTest(unittest.TestCase):
+ def testCanDeleteFeed(self):
+ url = "http://example.org/atom"
+ s = streamer.Subscription(url=url, hub="http://hub.example.org/", sourceUrl="http://example.org/", key_name=url)
+ s.put()
+
+ streamer.handleDeleteSubscription(url, hubSubscriber=StubHubSubscriber())
+ self.assertFalse(Subscription.exists(url))
+
+ def testDeletingFeedUnsubscribesFromHub(self):
+ url = "http://example.org/atom"
+ hub="http://hub.example.org/"
+ s = streamer.Subscription(url=url, hub=hub, sourceUrl="http://example.org/", key_name=url)
+ s.put()
+
+ hubSubscriber=StubHubSubscriber()
+ streamer.handleDeleteSubscription(url, hubSubscriber=hubSubscriber)
+ self.assertEquals(url, hubSubscriber.url)
+ self.assertEquals(hub, hubSubscriber.hub)
+ self.assertEquals('http://streamer-ade.appspot.com/posts', hubSubscriber.callback_url)
+
class PostTest(unittest.TestCase):
def testCanDeleteMatchingPostsCreatedUsingPostFactory(self):
feedUrl = "some feed url"

0 comments on commit 96b0ca6

Please sign in to comment.