Skip to content

Commit

Permalink
RSS feed for recent articles.
Browse files Browse the repository at this point in the history
  • Loading branch information
taehoon-kang committed Mar 13, 2012
1 parent 33361d3 commit df15fb9
Show file tree
Hide file tree
Showing 14 changed files with 181 additions and 105 deletions.
1 change: 1 addition & 0 deletions action/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ["feed"]
11 changes: 8 additions & 3 deletions action/category.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
from lib.controller import Action
from lib.decorators import rss
import action
from action import feed
import models

class ArticleList(Action):
LIST_PER_PAGE = 20

@rss(action.feed.CategoryArticleList)
@rss(feed.Category)
def get(self, category_name):
page = int(self.request.get('page', 1))
offset = (page - 1) * self.LIST_PER_PAGE
category = models.Category.get_by_name(category_name)
self.list = models.Article.get_list(category, self.LIST_PER_PAGE, offset) if category else None
self.count = category.article_count if category else 0
return Action.Result.DEFAULT
return Action.Result.DEFAULT

class Top(Action):
def get(self):
self.list = models.Category.get_top_level()
return Action.Result.JSON
56 changes: 33 additions & 23 deletions action/feed.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
'''
@see https://developers.google.com/feed/v1/jsondevguide
'''
from gettext import gettext as _
from lib.controller import Action
import models

class CategoryArticleList(Action):
def get(self, category_name):
def get_entry(item):
media_group_contents = []
if item['image']:
media_group_contents.append({'url': item['image'], 'medium': 'image', 'type': '', 'height':'', 'width': ''})
if item['video']:
media_group_contents.append({'url': item['video'], 'medium': 'video', 'type': '', 'height':'', 'width': ''})

entry = {
class Best(Action):
def get(self, period):
offset = int(self.request.get('offset', 0))
limit = int(self.request.get('limit', 20))
return {
'feedUrl': self.request.uri,
'title': _('%s Best Articles' % period),
'link': '/best/%s' % period,
'type': 'rss20',
'description': '',
'entries': [{
'title':item['title'],
'link':'%s/%s?page=%s' % (link, item['id'], page),
'contentSnippet': item['excerpt'],
'link':'%s/#!/%s/%s?page=%s' % (self.request.host_url, item['category'], item['id'], 1),
'comments':'%s/#!/%s/%s?page=%s#comments' % (self.request.host_url, item['category'], item['id'], 1),
'contentSnippet': item['excerpt'],
'content': item['excerpt'],
'publishedDate':item['created'],
'author': item['author']['nickname'],
'categories': category.path,
'mediaGroups': [{'contents': media_group_contents}],
}

return entry
'categories': [item['category']],
'enclosure': {'url': item['video'] if item['video'] else item['image'], 'type': 'video' if item['video'] else 'image', 'length': 10000} if item['video'] or item['image'] else None,
} for item in models.Article.get_best_list(period, offset=offset, limit=limit)]
}

class Category(Action):
def get(self, category_name):
page = int(self.request.get('page', 1))
limit = int(self.request.get('limit', 20))
offset = (page - 1) * limit
Expand All @@ -39,9 +43,15 @@ def get_entry(item):
'link': link,
'type': 'rss20',
'description': category.description,
'entries': [get_entry(item) for item in models.Article.get_list(category=category, offset=offset, limit=limit)]
}

class ArticleCommentList(Action):
def get(self, category_name):
pass
'entries': [{
'title':item['title'],
'link':'%s/%s?page=%s' % (link, item['id'], page),
'comments':'%s/%s?page=%s#comments' % (link, item['id'], page),
'contentSnippet': item['excerpt'],
'content': item['excerpt'],
'publishedDate':item['created'],
'author': item['author']['nickname'],
'categories': [item['category']],
'enclosure': {'url': item['video'] if item['video'] else item['image'], 'type': 'video' if item['video'] else 'image', 'length': 10000} if item['video'] or item['image'] else None,
} for item in models.Article.get_list(category=category, offset=offset, limit=limit)]
}
5 changes: 2 additions & 3 deletions action/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from lib.decorators import login_required, rss
from lib.recaptcha.client import captcha
import models
import action
from action import feed
import settings

try:
Expand All @@ -18,7 +18,7 @@ def _captcha_validation(self, challenge, response):
return True
captcha_result = captcha.submit(challenge, response, settings.RECAPTCHA_PRIVATE_KEY, self.request.remote_addr)
if not captcha_result.is_valid:
self.response.set_status(412, _('Captcha code mismatch: %s' % captcha_result.error_code))
self.response.set_status(412, _('Captcha code mismatch: %s') % captcha_result.error_code)
return False
return True

Expand Down Expand Up @@ -81,7 +81,6 @@ def get(self, article_id):
return Action.Result.DEFAULT

class Comment(Action):
@rss(action.feed.ArticleCommentList)
def get(self, article_id):
offset = int(self.request.get('offset'))
self.comment_list = models.Comment.get_list(article=models.Article.get_by_id(int(article_id)), offset=offset)
Expand Down
2 changes: 1 addition & 1 deletion bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import webapp2
import settings

categories = '|'.join(Category.get_all_categories())
categories = '|'.join(Category.get_all())
Controller.url_mapping = [
(r'^/([0-9]+)$', ('service', 'Article')),
(r'^/user/([0-9]+)$', ('user', 'Index')),
Expand Down
69 changes: 51 additions & 18 deletions lib/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from django import template
from django.template import loader
from lib.json_encoder import encode
import action
from lib import PyRSS2Gen
import json
import datetime
import logging
import os
import re
Expand Down Expand Up @@ -37,17 +39,17 @@ def initialize(self, request, response):
super(self.__class__, self).initialize(request, response)

action_module = None
action_class = None
action_instance = None

if Controller.url_mapping:
for regex, action_location in Controller.url_mapping:
m = re.match(regex, urllib.unquote_plus(request.path).decode('utf-8'))
if m:
action_module, action_class = action_location
action_module, action_instance = action_location
self._current_request_args = m.groups()
break

if not action_module and not action_class:
if not action_module and not action_instance:
'''supports 2 depth path'''
path = request.path[1:].split('/')
action_module = path[0]
Expand All @@ -58,14 +60,14 @@ def initialize(self, request, response):
self._current_request_args = {}
path_len = len(path)
if path_len > 1:
action_class = ''.join([x.title() for x in path[1].split('-')])
action_instance = ''.join([x.title() for x in path[1].split('-')])
self._current_request_args = [urllib.unquote_plus(item).decode('utf-8') for item in path[2:]]
else:
action_class = 'Index'
action_instance = 'Index'
del path

logging.debug('Current action module : %s, class : %s' % (action_module, action_class))
self._import_action(action_module, action_class)
logging.debug('Current action module : %s, class : %s' % (action_module, action_instance))
self._import_action(action_module, action_instance)

def _execute(self, method_name, *args):
if not self.response:
Expand Down Expand Up @@ -107,15 +109,17 @@ def _execute(self, method_name, *args):

output = self.request.get('output')

if output == 'json' or (output != 'html' and result == Action.Result.DEFAULT and self.__action.is_ajax) or result is Action.Result.JSON:
if output == Action.Result.JSON or (output != Action.Result.HTML and result == Action.Result.DEFAULT and self.__action.is_ajax) or result is Action.Result.JSON:
context = self.__action._get_context()
for key in NON_AJAX_CONTEXT_KEYS:
if hasattr(self.__action, key):
del context[key]
logging.debug('Context data for JSON Serialize : %s' % context)
self.response.headers['Content-type'] = 'application/json'
self.response.out.write(encode(context))
elif output == 'html' or result is not None:
elif result and output in [Action.Result.RSS, Action.Result.RSS_JSON, Action.Result.RSS_JSON_XML, Action.Result.RSS_XML]:
print_rss(output, result, self.__action)
elif output == Action.Result.HTML or result is not None:
template_path = self._find_template(result)
if template_path:
context = self.__action._get_context()
Expand All @@ -129,7 +133,7 @@ def handle_exception(self, e, debug):
self.response.set_status(500, e)
if debug:
if not self.__action.is_ajax:
raise e
raise
else:
sys.stderr.write(e)

Expand All @@ -139,16 +143,16 @@ def _find_template(self, result_name):
if result_name.startswith('/'):
return result_name[1:]
result = [self.__action.__module__.replace('%s.' % ACTION_PACKAGE, '')]
action_class = self.__action.__class__.__name__
if action_class is not 'Index':
result.append(action_class.lower())
action_instance = self.__action.__class__.__name__
if action_instance is not 'Index':
result.append(action_instance.lower())

if result_name is not '' and result_name != Action.Result.HTML:
result.append(result_name)

return '%s%s' % (os.path.sep.join(result), TEMPLATE_SUFFIX)

def _import_action(self, action_name, action_class='Index'):
def _import_action(self, action_name, action_instance='Index'):
module_name = '%s.%s' % (ACTION_PACKAGE, action_name)

# Fast path: see if the module has already been imported.
Expand All @@ -159,7 +163,7 @@ def _import_action(self, action_name, action_class='Index'):
logging.debug('Newer import of %s' % module)

try:
cls = getattr(module, action_class)
cls = getattr(module, action_instance)
self.__action = cls(self.request, self.response)
except Exception:
import traceback
Expand Down Expand Up @@ -287,5 +291,34 @@ class Result(object):
HTML = 'html'
INPUT = 'input'
RSS = 'rss'
RSSJSON = 'rssjson'
JSON = '__json__'
RSS_JSON = 'rss_json'
RSS_XML = 'rss_xml'
RSS_JSON_XML = 'rss_json_xml'
JSON = 'json'

def print_rss(output, result, action_instance):
json_result = {'responseData': {}, 'responseDetails': None, 'responseStatus': 200}
if output in [Action.Result.RSS, Action.Result.RSS_JSON_XML, Action.Result.RSS_XML]:
feed = PyRSS2Gen.RSS2(
title=result['title'],
link=result['link'],
description=result['description'],
lastBuildDate=datetime.datetime.utcnow(),
items=[PyRSS2Gen.RSSItem(
title=item['title'],
link=item['link'],
description=item['content'],
pubDate=item['publishedDate'],
author=item['author'],
categories=item['categories'],
enclosure=PyRSS2Gen.Enclosure(url=item['enclosure']['url'], length=item['enclosure']['length'], type=item['enclosure']['type']) if item.has_key('enclosure') and item['enclosure'] else None
) for item in result['entries']],
)
if output == Action.Result.RSS:
action_instance.response.headers['Content-type'] = 'text/xml'
feed.write_xml(action_instance.response.out, 'utf-8')
return
json_result['responseData']['xmlString'] = feed.to_xml(encoding='UTF-8')
if output in [Action.Result.RSS_JSON, Action.Result.RSS_JSON_XML]:
json_result['responseData']['feed'] = result
action_instance.response.out.write(json.dumps(json_result, default=lambda obj: obj.strftime('%a, %d %b %Y %H:%M:%S %z') if isinstance(obj, datetime.datetime) else None))
55 changes: 13 additions & 42 deletions lib/decorators.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from google.appengine.api import users
from lib import PyRSS2Gen
from lib.controller import Action
import json
import datetime
from lib.controller import Action, print_rss
import logging


def login_required(method):
"""A decorator to require that a user be logged in to access a handler.
Expand All @@ -23,52 +19,27 @@ def get(self):
def new(*args):
if not users.get_current_user():
logging.debug('Current user not found')
action = args[0]
action.response.set_status(302)
action.response.headers['Location'] = str(users.create_login_url(action.request.uri))
action.response.clear()
action_instance = args[0]
action_instance.response.set_status(302)
action_instance.response.headers['Location'] = str(users.create_login_url(action_instance.request.uri))
action_instance.response.clear()
else:
logging.debug('Current user found')
return method(*args)
return new

def rss(rss_action_class):
def wrap(action_method):
def get_enclosure(item):
if not item.has_key('mediaGroups') or len(item['mediaGroups']) == 0 or not item['mediaGroups'][0].has_key('contents') or len(item['mediaGroups'][0]['contents']) == 0:
return None
enclosure = item['mediaGroups'][0]['contents'][0]
return PyRSS2Gen.Enclosure(url = enclosure['url'], length = 10000, type = enclosure['medium'])

def wrap(action_method):
def wrapped_f(*args):
action_class = args[0]
output = action_class.request.get('output')
if output == Action.Result.RSS or output == Action.Result.RSSJSON:
result = getattr(rss_action_class(action_class.request, action_class.response, action_class._get_context()), 'get')(*args[1:])
action_instance = args[0]
output = action_instance.request.get('output')
if output in [Action.Result.RSS, Action.Result.RSS_JSON, Action.Result.RSS_XML, Action.Result.RSS_JSON_XML]:
result = getattr(rss_action_class(action_instance.request, action_instance.response, action_instance._get_context()), 'get')(*args[1:])
if result:
if output == Action.Result.RSS:
feed = PyRSS2Gen.RSS2(
title = result['title'],
link = result['link'],
description = result['description'],
lastBuildDate = datetime.datetime.utcnow(),
items = [PyRSS2Gen.RSSItem(
title=item['title'],
link=item['link'],
description=item['contentSnippet'],
pubDate=item['publishedDate'],
author=item['author'],
categories=item['categories'],
enclosure=get_enclosure(item)
) for item in result['entries']],
)
action_class.response.headers['Content-type'] = 'text/xml'
feed.write_xml(action_class.response.out, 'utf-8')
elif output == Action.Result.RSSJSON:
action_class.response.out.write(json.dumps(result, default=lambda obj: obj.strftime('%a, %d %b %Y %H:%M:%S 0000') if isinstance(obj, datetime.datetime) else None))
print_rss(output, result, action_instance)
else:
action_class.response.set_status(404)
action_instance.response.set_status(404)
else:
return action_method(*args)
return wrapped_f
return wrap
return wrap
Loading

0 comments on commit df15fb9

Please sign in to comment.