diff --git a/action/__init__.py b/action/__init__.py index e69de29..aecaf34 100644 --- a/action/__init__.py +++ b/action/__init__.py @@ -0,0 +1 @@ +__all__ = ["feed"] \ No newline at end of file diff --git a/action/category.py b/action/category.py index cf9464f..3ba53b1 100644 --- a/action/category.py +++ b/action/category.py @@ -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 \ No newline at end of file + return Action.Result.DEFAULT + +class Top(Action): + def get(self): + self.list = models.Category.get_top_level() + return Action.Result.JSON \ No newline at end of file diff --git a/action/feed.py b/action/feed.py index f342a94..17dbc41 100644 --- a/action/feed.py +++ b/action/feed.py @@ -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 @@ -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 \ No newline at end of file + '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)] + } \ No newline at end of file diff --git a/action/service.py b/action/service.py index 5ec8d9f..ff3377c 100644 --- a/action/service.py +++ b/action/service.py @@ -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: @@ -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 @@ -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) diff --git a/bootstrap.py b/bootstrap.py index 6298738..52264d8 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -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')), diff --git a/lib/controller.py b/lib/controller.py index eb213da..d6c2dd8 100644 --- a/lib/controller.py +++ b/lib/controller.py @@ -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 @@ -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] @@ -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: @@ -107,7 +109,7 @@ 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): @@ -115,7 +117,9 @@ def _execute(self, method_name, *args): 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() @@ -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) @@ -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. @@ -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 @@ -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)) \ No newline at end of file diff --git a/lib/decorators.py b/lib/decorators.py index 7dd1fc8..cbe34a4 100644 --- a/lib/decorators.py +++ b/lib/decorators.py @@ -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. @@ -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 \ No newline at end of file + return wrap diff --git a/models.py b/models.py index 06dd59e..202d7d0 100644 --- a/models.py +++ b/models.py @@ -248,11 +248,18 @@ def get_list(cls, category): return result @classmethod - def get_all_categories(cls): + def get_all(cls): q = cls.all() q.filter('is_active =', True) return [item.name for item in q.fetch(DEFAULT_FETCH_COUNT)] + @classmethod + def get_top_level(cls): + q = cls.all() + q.filter('is_active = ', True) + q.filter('parent_category = ', None) + return [item.name for item in q.fetch(DEFAULT_FETCH_COUNT)] + def increase_article_count(self): self.article_count += 1 super(self.__class__, self).put() @@ -360,7 +367,23 @@ def save(self): found.delete() return self - + + @classmethod + def get_best_list(cls, period, limit=20, offset=0): + q = cls.all() + delta = None + if period == 'weekly': + delta = datetime.timedelta(weeks = 1) + elif period == 'monthly': + delta = datetime.timedelta(days = 30) + elif period == 'quaterly': + delta = datetime.timedelta(days = 90) + else: + delta = datetime.timedelta(days = 1) + q.filter('created > ', datetime.datetime.now() - delta) + q.order('-like_count') + return [item.to_dict() for item in q.fetch(limit, offset)] + @classmethod def get_list(cls, category, limit=20, offset=0, orderby='created'): q = Category.all() diff --git a/static/css/openalive.css b/static/css/openalive.css index 33f0b62..155564f 100644 --- a/static/css/openalive.css +++ b/static/css/openalive.css @@ -363,6 +363,11 @@ iframe { display: none } +#video-player { + width: 670px; + margin-left: -335px +} + #video-player .modal-body { text-align: center } diff --git a/static/js/hash-url.js b/static/js/hash-url.js index 26e2063..66493c4 100644 --- a/static/js/hash-url.js +++ b/static/js/hash-url.js @@ -52,6 +52,7 @@ $(function() { case "!": case "category": models.Category.select(tokens[0], callback); + tokens.length == 1 && !tokens[0] && models.Category.showTopLevelRecent(); break; case "tag": models.Tag.select(tokens[0], callback); diff --git a/static/js/service.js b/static/js/service.js index e5e41c8..16a9e92 100644 --- a/static/js/service.js +++ b/static/js/service.js @@ -20,19 +20,30 @@ $.ajaxSetup({ } }); -var getRss = function(url) { - new google.feeds.Feed("http://localhost:8080/service/article-list/%EC%A0%95%EC%B9%98?output=rss").load(function(data) { - if (!data.error) { - console.log(data.feed); - $(data.feed.entries).each(function(i, item) { - console.log(item); - }); - } - }); -} - var models; var initializeModels = function() { + var getRss = function(uri, callback) { + if(location.host.indexOf('localhost') > -1 || location.host.indexOf("dev.") > -1) { + $.ajax({ + url: uri, + data: {output: "rss_json_xml"}, + dataType: "json", + success: function(data) { + callback(data); + } + }); + } else { + $.ajax({ + url: 'https://ajax.googleapis.com/ajax/services/feed/load?v=1.0&output=json_xml&callback=?&q=' + encodeURI(location.protocol + "//" + location.host + uri + "?output=rss"), + crossDomain: true, + dataType: "jsonp", + success: function(data) { + callback(data); + } + }); + } + } + models || (models = { Category : (function() { var self = { @@ -42,6 +53,7 @@ var initializeModels = function() { $("#nav li.active").removeClass("active"); $("#nav li:has(a.home-link)").addClass("active"); $("#sidebar-wrapper, #loading, .btn-post-article").show(); + name && $("#recent").hide(); if(self.getCurrent() == name) { models.Article.loadList('category', name, callback); return; @@ -153,6 +165,20 @@ var initializeModels = function() { }, dataType: "json" }); + }, + showTopLevelRecent: function() { + $.getJSON("/category/top", function(data) { + $("#recent").show(); + $(data.list).each(function(i, item) { + getRss("/feed/category/" + item, function(data) { + if(data.responseStatus != 200) { + return; + } + var feed = data.responseData.feed; + var xml = $(data.responseData.xmlString); + }); + }); + }); } } self.loadStarred(); @@ -904,6 +930,7 @@ var initializeModels = function() { $("#starred-wrapper, #sidebar-wrapper, .btn-post-article").hide(); $("#container .breadcrumb li:gt(0)").remove(); $("#container .breadcrumb li:eq(0) .divider").show(); + $("#recent").hide(); $("#container .breadcrumb").append(formatString('
  • {{ label }} /
  • {{ tag }}
  • ', { label: gettext("Tags"), tag: gettext(name) diff --git a/static/tiny_mce/themes/advanced/js/video.js b/static/tiny_mce/themes/advanced/js/video.js index 72b49ad..23af656 100644 --- a/static/tiny_mce/themes/advanced/js/video.js +++ b/static/tiny_mce/themes/advanced/js/video.js @@ -67,7 +67,7 @@ var VideoDialog = { } else { var src = this.parseV(f.src.value); if(src) { - ed.execCommand('mceInsertContent', false, '', {skip_undo : 1}); + ed.execCommand('mceInsertContent', false, '', {skip_undo : 1}); // ed.dom.setAttribs('__mce_tmp', args); //ed.dom.setAttrib('__mce_tmp', 'id', ''); ed.undoManager.add(); diff --git a/templates/index.html b/templates/index.html index 209b449..0bf1321 100644 --- a/templates/index.html +++ b/templates/index.html @@ -19,6 +19,7 @@
    {% trans 'Category Explorer' %}
    +
    Recent