Merge branch 'master' into readstories

* master: (78 commits)
  Adding 401 status code for invalid token on user/info.
  Adding new preference for 24 hour clock. Moving to client-side generated dates.
  Adding IFTTT logging.
  All dates in iso 8601 format.
  Use strings for feed_id.
  Fixing unread trigger for IFTTT.
  Updating shared story ifttt entries.
  Fixing ReadKit flat folders by leaving out removed feeds.
  Adding handlers for all stories.
  Fixing ifttt timestamp.
  Adding share story action for IFTTT.
  Adding site title, website, and address to each IFTTT trigger.
  Adding share username to ifttt share trigger.
  Upping access token expiration to 10 years.
  Using TitleCase instead of snake_case.
  Fixing exception dialogs to correctly update on fixed addresses and links.
  New saved story as per IFTTT action.
  Updating IFTTT urls.
  IFTTT action for new subscriptions.
  Adding folder list for dynamic action fields.
  ...
This commit is contained in:
Samuel Clay 2014-01-30 18:42:48 -08:00
commit 6094e28fac
83 changed files with 1642 additions and 500 deletions

View file

@ -101,6 +101,30 @@ class MClassifierFeed(mongo.Document):
feed = User.objects.get(pk=self.social_user_id)
return "%s - %s/%s: (%s) %s" % (user, self.feed_id, self.social_user_id, self.score, feed)
def compute_story_score(story, classifier_titles, classifier_authors, classifier_tags, classifier_feeds):
intelligence = {
'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id']),
'author': apply_classifier_authors(classifier_authors, story),
'tags': apply_classifier_tags(classifier_tags, story),
'title': apply_classifier_titles(classifier_titles, story),
}
score = 0
score_max = max(intelligence['title'],
intelligence['author'],
intelligence['tags'])
score_min = min(intelligence['title'],
intelligence['author'],
intelligence['tags'])
if score_max > 0:
score = score_max
elif score_min < 0:
score = score_min
if score == 0:
score = intelligence['feed']
return score
def apply_classifier_titles(classifiers, story):
score = 0

View file

@ -1,5 +1,6 @@
from django.conf.urls import url, patterns
from apps.oauth import views
from oauth2_provider import views as op_views
urlpatterns = patterns('',
url(r'^twitter_connect/?$', views.twitter_connect, name='twitter-connect'),
@ -10,4 +11,25 @@ urlpatterns = patterns('',
url(r'^appdotnet_disconnect/?$', views.appdotnet_disconnect, name='appdotnet-disconnect'),
url(r'^follow_twitter_account/?$', views.follow_twitter_account, name='social-follow-twitter'),
url(r'^unfollow_twitter_account/?$', views.unfollow_twitter_account, name='social-unfollow-twitter'),
# Django OAuth Toolkit
url(r'^status/?$', views.ifttt_status, name="ifttt-status"),
url(r'^oauth2/authorize/?$', op_views.AuthorizationView.as_view(), name="ifttt-authorize"),
url(r'^oauth2/token/?$', op_views.TokenView.as_view(), name="ifttt-token"),
url(r'^user/info/?$', views.api_user_info, name="ifttt-user-info"),
url(r'^triggers/(?P<trigger_slug>(new-unread-story|new-focus-story))/fields/feed_or_folder/options/?$',
views.api_feed_list, name="ifttt-trigger-feedlist"),
url(r'^triggers/(?P<unread_score>(new-unread-story|new-focus-story))/?$',
views.api_unread_story, name="ifttt-trigger-unreadstory"),
url(r'^triggers/new-saved-story/fields/story_tag/options/?$',
views.api_saved_tag_list, name="ifttt-trigger-taglist"),
url(r'^triggers/new-saved-story/?$', views.api_saved_story, name="ifttt-trigger-saved"),
url(r'^triggers/new-shared-story/fields/blurblog_user/options/?$',
views.api_shared_usernames, name="ifttt-trigger-blurbloglist"),
url(r'^triggers/new-shared-story/?$', views.api_shared_story, name="ifttt-trigger-shared"),
url(r'^actions/post-new-shared-story/?$', views.api_share_new_story, name="ifttt-action-share"),
url(r'^actions/save-new-saved-story/?$', views.api_save_new_story, name="ifttt-action-saved"),
url(r'^actions/add-new-subscription/?$', views.api_save_new_subscription, name="ifttt-action-subscription"),
url(r'^actions/add-new-subscription/fields/folder/options/?$',
views.api_folder_list, name="ifttt-action-folderlist"),
)

View file

@ -1,13 +1,21 @@
import urllib
import urlparse
import datetime
import lxml.html
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.http import HttpResponse
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.conf import settings
from apps.social.models import MSocialServices
from mongoengine.queryset import OperationError
from apps.social.models import MSocialServices, MSocialSubscription, MSharedStory
from apps.social.tasks import SyncTwitterFriends, SyncFacebookFriends, SyncAppdotnetFriends
from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
from apps.analyzer.models import compute_story_score
from apps.rss_feeds.models import Feed, MStory, MStarredStoryCounts, MStarredStory
from utils import log as logging
from utils.user_functions import ajax_login_required
from utils.view_functions import render_to
@ -180,9 +188,7 @@ def appdotnet_connect(request):
social_services.syncing_appdotnet = True
social_services.save()
# SyncAppdotnetFriends.delay(user_id=request.user.pk)
# XXX TODO: Remove below and uncomment above. Only for www->dev.
social_services.sync_appdotnet_friends()
SyncAppdotnetFriends.delay(user_id=request.user.pk)
logging.user(request, "~BB~FRFinishing App.net connect")
return {}
@ -260,3 +266,471 @@ def unfollow_twitter_account(request):
message = e
return {'code': code, 'message': message}
def api_user_info(request):
user = request.user
if user.is_anonymous():
return HttpResponse(content="{}", status=401)
return json.json_response(request, {"data": {
"name": user.username,
"id": user.pk,
}})
@login_required
@json.json_view
def api_feed_list(request, trigger_slug=None):
user = request.user
usf = UserSubscriptionFolders.objects.get(user=user)
flat_folders = usf.flatten_folders()
titles = [dict(label=" - Folder: All Site Stories", value="all")]
feeds = {}
user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True)
for sub in user_subs:
feeds[sub.feed_id] = sub.canonical()
for folder_title in sorted(flat_folders.keys()):
if folder_title and folder_title != " ":
titles.append(dict(label=" - Folder: %s" % folder_title, value=folder_title, optgroup=True))
else:
titles.append(dict(label=" - Folder: Top Level", value="Top Level", optgroup=True))
folder_contents = []
for feed_id in flat_folders[folder_title]:
if feed_id not in feeds: continue
feed = feeds[feed_id]
folder_contents.append(dict(label=feed['feed_title'], value=str(feed['id'])))
folder_contents = sorted(folder_contents, key=lambda f: f['label'].lower())
titles.extend(folder_contents)
return {"data": titles}
@login_required
@json.json_view
def api_folder_list(request, trigger_slug=None):
user = request.user
usf = UserSubscriptionFolders.objects.get(user=user)
flat_folders = usf.flatten_folders()
titles = [dict(label="All Site Stories", value="all")]
for folder_title in sorted(flat_folders.keys()):
if folder_title and folder_title != " ":
titles.append(dict(label=folder_title, value=folder_title))
else:
titles.append(dict(label="Top Level", value="Top Level"))
return {"data": titles}
@login_required
@json.json_view
def api_saved_tag_list(request):
user = request.user
starred_counts, starred_count = MStarredStoryCounts.user_counts(user.pk, include_total=True)
tags = []
for tag in starred_counts:
if tag['tag'] == "": continue
tags.append(dict(label="%s (%s %s)" % (tag['tag'], tag['count'],
'story' if tag['count'] == 1 else 'stories'),
value=tag['tag']))
tags = sorted(tags, key=lambda t: t['value'].lower())
catchall = dict(label="All Saved Stories (%s %s)" % (starred_count,
'story' if starred_count == 1 else 'stories'),
value="all")
tags.insert(0, catchall)
return {"data": tags}
@login_required
@json.json_view
def api_shared_usernames(request):
user = request.user
social_feeds = MSocialSubscription.feeds(user_id=user.pk)
blurblogs = []
for social_feed in social_feeds:
if not social_feed['shared_stories_count']: continue
blurblogs.append(dict(label="%s (%s %s)" % (social_feed['username'],
social_feed['shared_stories_count'],
'story' if social_feed['shared_stories_count'] == 1 else 'stories'),
value=social_feed['user_id']))
blurblogs = sorted(blurblogs, key=lambda b: b['label'].lower())
catchall = dict(label="All Shared Stories",
value="all")
blurblogs.insert(0, catchall)
return {"data": blurblogs}
@login_required
@json.json_view
def api_unread_story(request, unread_score=None):
user = request.user
body = json.decode(request.body)
after = body.get('after', None)
before = body.get('before', None)
limit = body.get('limit', 50)
fields = body.get('triggerFields')
feed_or_folder = fields['feed_or_folder']
entries = []
if feed_or_folder.isdigit():
feed_id = int(feed_or_folder)
usersub = UserSubscription.objects.get(user=user, feed_id=feed_id)
found_feed_ids = [feed_id]
found_trained_feed_ids = [feed_id] if usersub.is_trained else []
stories = usersub.get_stories(order="newest", read_filter="unread",
offset=0, limit=limit,
default_cutoff_date=user.profile.unread_cutoff)
else:
folder_title = feed_or_folder
if folder_title == "Top Level":
folder_title = " "
usf = UserSubscriptionFolders.objects.get(user=user)
flat_folders = usf.flatten_folders()
feed_ids = None
if folder_title != "all":
feed_ids = flat_folders.get(folder_title)
usersubs = UserSubscription.subs_for_feeds(user.pk, feed_ids=feed_ids,
read_filter="unread")
feed_ids = [sub.feed_id for sub in usersubs]
params = {
"user_id": user.pk,
"feed_ids": feed_ids,
"offset": 0,
"limit": limit,
"order": "newest",
"read_filter": "unread",
"usersubs": usersubs,
"cutoff_date": user.profile.unread_cutoff,
}
story_hashes, unread_feed_story_hashes = UserSubscription.feed_stories(**params)
mstories = MStory.objects(story_hash__in=story_hashes).order_by('-story_date')
stories = Feed.format_stories(mstories)
found_feed_ids = list(set([story['story_feed_id'] for story in stories]))
trained_feed_ids = [sub.feed_id for sub in usersubs if sub.is_trained]
found_trained_feed_ids = list(set(trained_feed_ids) & set(found_feed_ids))
if found_trained_feed_ids:
classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk,
feed_id__in=found_trained_feed_ids))
classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk,
feed_id__in=found_trained_feed_ids))
classifier_titles = list(MClassifierTitle.objects(user_id=user.pk,
feed_id__in=found_trained_feed_ids))
classifier_tags = list(MClassifierTag.objects(user_id=user.pk,
feed_id__in=found_trained_feed_ids))
feeds = dict([(f.pk, {
"title": f.feed_title,
"website": f.feed_link,
"address": f.feed_address,
}) for f in Feed.objects.filter(pk__in=found_feed_ids)])
for story in stories:
if before and int(story['story_date'].strftime("%s")) > before: continue
if after and int(story['story_date'].strftime("%s")) < after: continue
score = 0
if found_trained_feed_ids and story['story_feed_id'] in found_trained_feed_ids:
score = compute_story_score(story, classifier_titles=classifier_titles,
classifier_authors=classifier_authors,
classifier_tags=classifier_tags,
classifier_feeds=classifier_feeds)
if score < 0: continue
if unread_score == "new-focus-story" and score < 1: continue
feed = feeds.get(story['story_feed_id'], None)
entries.append({
"StoryTitle": story['story_title'],
"StoryContent": story['story_content'],
"StoryUrl": story['story_permalink'],
"StoryAuthor": story['story_authors'],
"StoryDate": story['story_date'].isoformat(),
"StoryScore": score,
"SiteTitle": feed and feed['title'],
"SiteWebsite": feed and feed['website'],
"SiteFeedAddress": feed and feed['address'],
"ifttt": {
"id": story['story_hash'],
"timestamp": int(story['story_date'].strftime("%s"))
},
})
logging.user(request, "~FYChecking unread%s stories with ~SB~FCIFTTT~SN~FY: ~SB%s~SN - ~SB%s~SN stories" % (" ~SBfocus~SN" if unread_score == "new-focus-story" else "", feed_or_folder, len(entries)))
return {"data": entries}
@login_required
@json.json_view
def api_saved_story(request):
user = request.user
body = json.decode(request.body)
after = body.get('after', None)
before = body.get('before', None)
limit = body.get('limit', 50)
fields = body.get('triggerFields')
story_tag = fields['story_tag']
entries = []
if story_tag == "all":
story_tag = ""
mstories = MStarredStory.objects(
user_id=user.pk,
user_tags__contains=story_tag
).order_by('-starred_date')[:limit]
stories = Feed.format_stories(mstories)
found_feed_ids = list(set([story['story_feed_id'] for story in stories]))
feeds = dict([(f.pk, {
"title": f.feed_title,
"website": f.feed_link,
"address": f.feed_address,
}) for f in Feed.objects.filter(pk__in=found_feed_ids)])
for story in stories:
if before and int(story['story_date'].strftime("%s")) > before: continue
if after and int(story['story_date'].strftime("%s")) < after: continue
feed = feeds.get(story['story_feed_id'], None)
entries.append({
"StoryTitle": story['story_title'],
"StoryContent": story['story_content'],
"StoryUrl": story['story_permalink'],
"StoryAuthor": story['story_authors'],
"StoryDate": story['story_date'].isoformat(),
"SavedDate": story['starred_date'].isoformat(),
"SavedTags": ', '.join(story['user_tags']),
"SiteTitle": feed and feed['title'],
"SiteWebsite": feed and feed['website'],
"SiteFeedAddress": feed and feed['address'],
"ifttt": {
"id": story['story_hash'],
"timestamp": int(story['starred_date'].strftime("%s"))
},
})
logging.user(request, "~FCChecking saved stories from ~SBIFTTT~SB: ~SB%s~SN - ~SB%s~SN stories" % (story_tag if story_tag else "[All stories]", len(entries)))
return {"data": entries}
@login_required
@json.json_view
def api_shared_story(request):
user = request.user
body = json.decode(request.body)
after = body.get('after', None)
before = body.get('before', None)
limit = body.get('limit', 50)
fields = body.get('triggerFields')
blurblog_user = fields['blurblog_user']
entries = []
if blurblog_user.isdigit():
social_user_ids = [int(blurblog_user)]
elif blurblog_user == "all":
socialsubs = MSocialSubscription.objects.filter(user_id=user.pk)
social_user_ids = [ss.subscription_user_id for ss in socialsubs]
mstories = MSharedStory.objects(
user_id__in=social_user_ids
).order_by('-shared_date')[:limit]
stories = Feed.format_stories(mstories)
found_feed_ids = list(set([story['story_feed_id'] for story in stories]))
share_user_ids = list(set([story['user_id'] for story in stories]))
users = dict([(u.pk, u.username)
for u in User.objects.filter(pk__in=share_user_ids).only('pk', 'username')])
feeds = dict([(f.pk, {
"title": f.feed_title,
"website": f.feed_link,
"address": f.feed_address,
}) for f in Feed.objects.filter(pk__in=found_feed_ids)])
classifier_feeds = list(MClassifierFeed.objects(user_id=user.pk,
social_user_id__in=social_user_ids))
classifier_authors = list(MClassifierAuthor.objects(user_id=user.pk,
social_user_id__in=social_user_ids))
classifier_titles = list(MClassifierTitle.objects(user_id=user.pk,
social_user_id__in=social_user_ids))
classifier_tags = list(MClassifierTag.objects(user_id=user.pk,
social_user_id__in=social_user_ids))
# Merge with feed specific classifiers
classifier_feeds = classifier_feeds + list(MClassifierFeed.objects(user_id=user.pk,
feed_id__in=found_feed_ids))
classifier_authors = classifier_authors + list(MClassifierAuthor.objects(user_id=user.pk,
feed_id__in=found_feed_ids))
classifier_titles = classifier_titles + list(MClassifierTitle.objects(user_id=user.pk,
feed_id__in=found_feed_ids))
classifier_tags = classifier_tags + list(MClassifierTag.objects(user_id=user.pk,
feed_id__in=found_feed_ids))
for story in stories:
if before and int(story['shared_date'].strftime("%s")) > before: continue
if after and int(story['shared_date'].strftime("%s")) < after: continue
score = compute_story_score(story, classifier_titles=classifier_titles,
classifier_authors=classifier_authors,
classifier_tags=classifier_tags,
classifier_feeds=classifier_feeds)
if score < 0: continue
feed = feeds.get(story['story_feed_id'], None)
entries.append({
"StoryTitle": story['story_title'],
"StoryContent": story['story_content'],
"StoryUrl": story['story_permalink'],
"StoryAuthor": story['story_authors'],
"StoryDate": story['story_date'].isoformat(),
"StoryScore": score,
"SharedComments": story['comments'],
"ShareUsername": users.get(story['user_id']),
"SharedDate": story['shared_date'].isoformat(),
"SiteTitle": feed and feed['title'],
"SiteWebsite": feed and feed['website'],
"SiteFeedAddress": feed and feed['address'],
"ifttt": {
"id": story['story_hash'],
"timestamp": int(story['shared_date'].strftime("%s"))
},
})
logging.user(request, "~FMChecking shared stories from ~SB~FCIFTTT~SN~FM: ~SB~FM%s~FM~SN - ~SB%s~SN stories" % (blurblog_user, len(entries)))
return {"data": entries}
@json.json_view
def ifttt_status(request):
logging.user(request, "~FCChecking ~SBIFTTT~SN status")
return {"data": {
"status": "OK",
"time": datetime.datetime.now().isoformat()
}}
@login_required
@json.json_view
def api_share_new_story(request):
user = request.user
body = json.decode(request.body)
fields = body.get('actionFields')
story_url = fields['story_url']
content = fields.get('story_content', "")
story_title = fields.get('story_title', "[Untitled]")
story_author = fields.get('story_author', "")
comments = fields.get('comments', None)
feed = Feed.get_feed_from_url(story_url, create=True, fetch=True)
content = lxml.html.fromstring(content)
content.make_links_absolute(story_url)
content = lxml.html.tostring(content)
shared_story = MSharedStory.objects.filter(user_id=user.pk,
story_feed_id=feed and feed.pk or 0,
story_guid=story_url).limit(1).first()
if not shared_story:
story_db = {
"story_guid": story_url,
"story_permalink": story_url,
"story_title": story_title,
"story_feed_id": feed and feed.pk or 0,
"story_content": content,
"story_author": story_author,
"story_date": datetime.datetime.now(),
"user_id": user.pk,
"comments": comments,
"has_comments": bool(comments),
}
shared_story = MSharedStory.objects.create(**story_db)
socialsubs = MSocialSubscription.objects.filter(subscription_user_id=user.pk)
for socialsub in socialsubs:
socialsub.needs_unread_recalc = True
socialsub.save()
logging.user(request, "~BM~FYSharing story from ~SB~FCIFTTT~FY: ~SB%s: %s" % (story_url, comments))
else:
logging.user(request, "~BM~FY~SBAlready~SN shared story from ~SB~FCIFTTT~FY: ~SB%s: %s" % (story_url, comments))
try:
socialsub = MSocialSubscription.objects.get(user_id=user.pk,
subscription_user_id=user.pk)
except MSocialSubscription.DoesNotExist:
socialsub = None
if socialsub:
socialsub.mark_story_ids_as_read([shared_story.story_hash],
shared_story.story_feed_id,
request=request)
else:
RUserStory.mark_read(user.pk, shared_story.story_feed_id, shared_story.story_hash)
shared_story.publish_update_to_subscribers()
return {"data": [{
"id": shared_story and shared_story.story_guid,
"url": shared_story and shared_story.blurblog_permalink()
}]}
@login_required
@json.json_view
def api_save_new_story(request):
user = request.user
body = json.decode(request.body)
fields = body.get('actionFields')
story_url = fields['story_url']
story_content = fields.get('story_content', "")
story_title = fields.get('story_title', "[Untitled]")
story_author = fields.get('story_author', "")
user_tags = fields.get('user_tags', "")
story = None
try:
original_feed = Feed.get_feed_from_url(story_url)
story_db = {
"user_id": user.pk,
"starred_date": datetime.datetime.now(),
"story_date": datetime.datetime.now(),
"story_title": story_title or '[Untitled]',
"story_permalink": story_url,
"story_guid": story_url,
"story_content": story_content,
"story_author_name": story_author,
"story_feed_id": original_feed and original_feed.pk or 0,
"user_tags": [tag for tag in user_tags.split(',')]
}
story = MStarredStory.objects.create(**story_db)
logging.user(request, "~FCStarring by ~SBIFTTT~SN: ~SB%s~SN in ~SB%s" % (story_db['story_title'][:50], original_feed and original_feed))
MStarredStoryCounts.count_tags_for_user(user.pk)
except OperationError:
logging.user(request, "~FCAlready starred by ~SBIFTTT~SN: ~SB%s" % (story_db['story_title'][:50]))
pass
return {"data": [{
"id": story and story.id,
"url": story and story.story_permalink
}]}
@login_required
@json.json_view
def api_save_new_subscription(request):
user = request.user
body = json.decode(request.body)
fields = body.get('actionFields')
url = fields['url']
folder = fields['folder']
if folder == "Top Level":
folder = " "
code, message, us = UserSubscription.add_subscription(
user=user,
feed_address=url,
folder=folder,
bookmarklet=True
)
logging.user(request, "~FRAdding URL from ~FC~SBIFTTT~SN~FR: ~SB%s (in %s)" % (url, folder))
if us and us.feed:
url = us.feed.feed_address
return {"data": [{
"id": us and us.feed_id,
"url": url,
}]}

View file

@ -7,6 +7,7 @@ from django.http import HttpResponse
from django.conf import settings
from django.db import connection
from django.template import Template, Context
from apps.profile.tasks import CleanupUser
from utils import json_functions as json
class LastSeenMiddleware(object):
@ -22,6 +23,7 @@ class LastSeenMiddleware(object):
if request.user.profile.last_seen_on < hour_ago:
logging.user(request, "~FG~BBRepeat visitor: ~SB%s (%s)" % (
request.user.profile.last_seen_on, ip))
CleanupUser.delay(user_id=request.user.pk)
elif settings.DEBUG:
logging.user(request, "~FG~BBRepeat visitor (ignored): ~SB%s (%s)" % (
request.user.profile.last_seen_on, ip))
@ -181,12 +183,14 @@ BANNED_USER_AGENTS = (
'feed reader-background',
'missing',
)
class UserAgentBanMiddleware:
def process_request(self, request):
user_agent = request.environ.get('HTTP_USER_AGENT', 'missing').lower()
if 'profile' in request.path: return
if 'haproxy' in request.path: return
if 'account' in request.path: return
if getattr(settings, 'TEST_DEBUG'): return
if any(ua in user_agent for ua in BANNED_USER_AGENTS):
@ -195,7 +199,6 @@ class UserAgentBanMiddleware:
'code': -1
}
logging.user(request, "~FB~SN~BBBanned UA: ~SB%s" % (user_agent))
return HttpResponse(json.encode(data), status=403, mimetype='text/json')
return HttpResponse(json.encode(data), status=403, mimetype='text/json')

View file

@ -2,7 +2,7 @@ import datetime
from celery.task import Task
from apps.profile.models import Profile, RNewUserQueue
from utils import log as logging
from apps.reader.models import UserSubscription
class EmailNewUser(Task):
@ -42,3 +42,9 @@ class ActivateNextNewUser(Task):
def run(self):
RNewUserQueue.activate_next()
class CleanupUser(Task):
name = 'cleanup-user'
def run(self, user_id):
UserSubscription.trim_user_read_stories(user_id)

View file

@ -3,12 +3,11 @@ import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from apps.reader.models import MUserStory
class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
from apps.reader.models import MUserStory
userstories = MUserStory.objects.all()
print "%s userstories" % userstories.count()

View file

@ -3,11 +3,11 @@ import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from apps.reader.models import MUserStory
class Migration(DataMigration):
def forwards(self, orm):
from apps.reader.models import MUserStory
userstories = MUserStory.objects.all()
print "%s userstories" % userstories.count()

View file

@ -429,23 +429,33 @@ class UserSubscription(models.Model):
@classmethod
def trim_user_read_stories(self, user_id):
user = User.objects.get(pk=user_id)
r = redis.Redis(connection_pool=settings.REDIS_STORY_HASH_POOL)
subs = UserSubscription.objects.filter(user_id=user_id).only('feed')
if not subs: return
feeds = [f.feed_id for f in subs]
old_rs = r.smembers("RS:%s" % user_id)
old_count = len(old_rs)
# new_rs = r.sunionstore("RS:%s" % user_id, *["RS:%s:%s" % (user_id, f) for f in feeds])
new_rs = r.sunion(*["RS:%s:%s" % (user_id, f) for f in feeds])
if not old_count: return
r.sunionstore("RS:%s:backup" % user_id, "RS:%s" % user_id)
r.expire("RS:%s:backup" % user_id, 60*60*24)
key = "RS:%s" % user_id
feeds = [f.feed_id for f in subs]
old_rs = r.smembers(key)
old_count = len(old_rs)
if not old_count:
logging.user(user, "~FBTrimming all read stories, ~SBnone found~SN.")
return
r.sunionstore("%s:backup" % key, key)
r.expire("%s:backup" % key, 60*60*24)
r.sunionstore(key, *["%s:%s" % (key, f) for f in feeds])
new_rs = r.smembers(key)
missing_rs = []
missing_count = 0
feed_re = re.compile(r'(\d+):.*?')
for rs in old_rs:
for i, rs in enumerate(old_rs):
if i and i % 1000 == 0:
if missing_rs:
r.sadd(key, *missing_rs)
missing_count += len(missing_rs)
missing_rs = []
found = feed_re.search(rs)
if not found:
print " ---> Not found: %s" % rs
@ -453,13 +463,13 @@ class UserSubscription(models.Model):
rs_feed_id = found.groups()[0]
if int(rs_feed_id) not in feeds:
missing_rs.append(rs)
# r.sadd("RS:%s" % user_id, *missing_rs)
if missing_rs:
r.sadd(key, *missing_rs)
missing_count += len(missing_rs)
new_count = len(new_rs)
missing_count = len(missing_rs)
new_total = new_count + missing_count
user = User.objects.get(pk=user_id)
logging.user(user, "~FBTrimming ~FR%s~FB/%s (~SB%s~SN+~SB%s~SN saved) user read stories..." %
logging.user(user, "~FBTrimming ~FR%s~FB/%s (~SB%s sub'ed ~SN+ ~SB%s unsub'ed~SN saved) user read stories..." %
(old_count - new_total, old_count, new_count, missing_count))
@ -969,6 +979,34 @@ class UserSubscriptionFolders(models.Model):
return _arrange_folder(user_sub_folders)
def flatten_folders(self, feeds=None):
folders = json.decode(self.folders)
flat_folders = {" ": []}
def _flatten_folders(items, parent_folder="", depth=0):
for item in items:
if isinstance(item, int) and ((not feeds) or (feeds and item in feeds)):
if not parent_folder:
parent_folder = ' '
if parent_folder in flat_folders:
flat_folders[parent_folder].append(item)
else:
flat_folders[parent_folder] = [item]
elif isinstance(item, dict):
for folder_name in item:
folder = item[folder_name]
flat_folder_name = "%s%s%s" % (
parent_folder if parent_folder and parent_folder != ' ' else "",
" - " if parent_folder and parent_folder != ' ' else "",
folder_name
)
flat_folders[flat_folder_name] = []
_flatten_folders(folder, flat_folder_name, depth+1)
_flatten_folders(folders)
return flat_folders
def delete_feed(self, feed_id, in_folder, commit_delete=True):
def _find_feed_in_folders(old_folders, folder_name='', multiples_found=False, deleted=False):
new_folders = []

View file

@ -4,10 +4,10 @@ from apps.reader import views
urlpatterns = patterns('',
url(r'^$', views.index),
url(r'^login_as', views.login_as, name='login_as'),
url(r'^logout', views.logout, name='logout'),
url(r'^login', views.login, name='login'),
url(r'^logout', views.logout, name='welcome-logout'),
url(r'^login', views.login, name='welcome-login'),
url(r'^autologin/(?P<username>\w+)/(?P<secret>\w+)/?', views.autologin, name='autologin'),
url(r'^signup', views.signup, name='signup'),
url(r'^signup', views.signup, name='welcome-signup'),
url(r'^feeds/?$', views.load_feeds, name='load-feeds'),
url(r'^feed/(?P<feed_id>\d+)', views.load_single_feed, name='load-single-feed'),
url(r'^page/(?P<feed_id>\d+)', views.load_feed_page, name='load-feed-page'),

View file

@ -23,6 +23,8 @@ from django.core.mail import EmailMultiAlternatives
from django.contrib.sites.models import Site
from django.utils import feedgenerator
from mongoengine.queryset import OperationError
from mongoengine.queryset import NotUniqueError
from oauth2_provider.decorators import protected_resource
from apps.recommendations.models import RecommendedFeed
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds
@ -311,7 +313,6 @@ def load_feeds_flat(request):
update_counts = is_true(request.REQUEST.get('update_counts', True))
feeds = {}
flat_folders = {" ": []}
iphone_version = "2.1"
if include_favicons == 'false': include_favicons = False
@ -344,32 +345,11 @@ def load_feeds_flat(request):
logging.user(request, "~SN~FMTasking the scheduling immediate fetch of ~SB%s~SN feeds..." %
len(scheduled_feeds))
ScheduleImmediateFetches.apply_async(kwargs=dict(feed_ids=scheduled_feeds))
flat_folders = []
if folders:
folders = json.decode(folders.folders)
def make_feeds_folder(items, parent_folder="", depth=0):
for item in items:
if isinstance(item, int) and item in feeds:
if not parent_folder:
parent_folder = ' '
if parent_folder in flat_folders:
flat_folders[parent_folder].append(item)
else:
flat_folders[parent_folder] = [item]
elif isinstance(item, dict):
for folder_name in item:
folder = item[folder_name]
flat_folder_name = "%s%s%s" % (
parent_folder if parent_folder and parent_folder != ' ' else "",
" - " if parent_folder and parent_folder != ' ' else "",
folder_name
)
flat_folders[flat_folder_name] = []
make_feeds_folder(folder, flat_folder_name, depth+1)
flat_folders = folders.flatten_folders(feeds=feeds)
make_feeds_folder(folders)
social_params = {
'user_id': user.pk,
'include_favicon': include_favicons,
@ -743,7 +723,7 @@ def load_feed_page(request, feed_id):
logging.user(request, "~FYLoading original page, from the db")
return HttpResponse(data, mimetype="text/html; charset=utf-8")
@json.json_view
def load_starred_stories(request):
user = get_user(request)
@ -1855,17 +1835,20 @@ def mark_story_as_starred(request):
story_db.pop('id', None)
story_db.pop('user_tags', None)
now = datetime.datetime.now()
story_values = dict(user_id=request.user.pk, starred_date=now, user_tags=user_tags, **story_db)
starred_story, created = MStarredStory.objects.get_or_create(
story_hash=story.story_hash,
user_id=story_values.pop('user_id'),
defaults=story_values)
if created:
story_values = dict(starred_date=now, user_tags=user_tags, **story_db)
params = dict(story_guid=story.story_guid, user_id=request.user.pk)
starred_story = MStarredStory.objects(**params).limit(1)
created = False
if not starred_story:
params.update(story_values)
starred_story = MStarredStory.objects.create(**params)
created = True
MActivity.new_starred_story(user_id=request.user.pk,
story_title=story.story_title,
story_feed_id=feed_id,
story_id=starred_story.story_guid)
else:
starred_story = starred_story[0]
starred_story.user_tags = user_tags
starred_story.save()
@ -1896,7 +1879,11 @@ def mark_story_as_unstarred(request):
MActivity.remove_starred_story(user_id=request.user.pk,
story_feed_id=starred_story.story_feed_id,
story_id=starred_story.story_guid)
starred_story.delete()
starred_story.user_id = 0
try:
starred_story.save()
except NotUniqueError:
starred_story.delete()
MStarredStoryCounts.count_tags_for_user(request.user.pk)
starred_counts = MStarredStoryCounts.user_counts(request.user.pk)
else:

View file

@ -826,7 +826,8 @@ class Feed(models.Model):
if getattr(settings, 'TEST_DEBUG', False):
self.feed_address = self.feed_address.replace("%(NEWSBLUR_DIR)s", settings.NEWSBLUR_DIR)
self.feed_link = self.feed_link.replace("%(NEWSBLUR_DIR)s", settings.NEWSBLUR_DIR)
if self.feed_link:
self.feed_link = self.feed_link.replace("%(NEWSBLUR_DIR)s", settings.NEWSBLUR_DIR)
self.save()
options = {
@ -1220,6 +1221,10 @@ class Feed(models.Model):
story['user_tags'] = story_db.user_tags
if hasattr(story_db, 'shared_date'):
story['shared_date'] = story_db.shared_date
if hasattr(story_db, 'comments'):
story['comments'] = story_db.comments
if hasattr(story_db, 'user_id'):
story['user_id'] = story_db.user_id
if include_permalinks and hasattr(story_db, 'blurblog_permalink'):
story['blurblog_permalink'] = story_db.blurblog_permalink()
if text:

View file

@ -2530,6 +2530,23 @@ class MSocialServices(mongo.Document):
profile.save()
return profile
@classmethod
def sync_all_twitter_photos(cls, days=14):
week_ago = datetime.datetime.now() - datetime.timedelta(days=days)
shares = MSharedStory.objects.filter(shared_date__gte=week_ago)
sharers = sorted(set([s.user_id for s in shares]))
print " ---> %s sharing user_ids" % len(sorted(sharers))
for user_id in sharers:
profile = MSocialProfile.objects.get(user_id=user_id)
if not profile.photo_service == 'twitter': continue
ss = MSocialServices.objects.get(user_id=user_id)
try:
ss.sync_twitter_photo()
print " ---> Syncing %s" % user_id
except Exception, e:
print " ***> Exception on %s: %s" % (user_id, e)
def sync_twitter_photo(self):
profile = MSocialProfile.get_user(self.user_id)

View file

@ -53,6 +53,7 @@ javascripts:
- media/js/vendor/jquery.chosen.js
- media/js/vendor/jquery.effects.core.js
- media/js/vendor/jquery.effects.slideOffscreen.js
- media/js/vendor/date.format.js
- media/js/vendor/tag-it.js
- media/js/vendor/chart.js
- media/js/vendor/audio.js

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.newsblur"
android:versionCode="57"
android:versionName="3.5.5" >
android:versionCode="60"
android:versionName="3.6.2" >
<uses-sdk
android:minSdkVersion="11"

View file

@ -142,6 +142,6 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="5dp"
android:background="#F0F0F0" />
android:background="@color/story_comment_divider" />
</LinearLayout>

View file

@ -15,8 +15,8 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentTop="true"
android:layout_marginBottom="5dp"
android:background="#A6A6A6" />
android:layout_marginBottom="10dp"
android:background="@color/story_comment_divider" />
<ImageView
android:id="@+id/reply_user_image"

View file

@ -31,13 +31,13 @@
android:layout_margin="10dp"
android:layout_weight="1"
android:gravity="center"
android:text="S"
android:text=" S "
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="M"
android:text=" M "
android:textSize="14sp"
android:layout_margin="10dp"
android:layout_weight="1"
@ -46,7 +46,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="L"
android:text=" L "
android:textSize="14sp"
android:layout_margin="10dp"
android:layout_weight="1"
@ -55,7 +55,16 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="XL"
android:text=" XL"
android:textSize="14sp"
android:layout_margin="10dp"
android:layout_weight="1"
android:gravity="center" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="XXL"
android:textSize="14sp"
android:layout_margin="10dp"
android:layout_weight="1"
@ -67,10 +76,9 @@
android:id="@+id/textSizeSlider"
android:background="@android:color/transparent"
android:layout_width="match_parent"
android:progress="2"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:max="4"
android:max="5"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View file

@ -30,6 +30,7 @@
<color name="story_author_read">#C0C0C0</color>
<color name="story_date_unread">#262C6C</color>
<color name="story_date_read">#BABDD1</color>
<color name="story_comment_divider">#F0F0F0</color>
<color name="twitter_blue">#4099FF</color>
<color name="facebook_blue">#3B5998</color>

View file

@ -1,6 +1,5 @@
package com.newsblur.activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.content.CursorLoader;
@ -10,7 +9,6 @@ import com.newsblur.R;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefConstants;
import com.newsblur.util.PrefsUtils;
@ -24,7 +22,7 @@ public class AllSharedStoriesReading extends Reading {
setTitle(getResources().getString(R.string.all_shared_stories));
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), defaultFeedView);
getSupportLoaderManager().initLoader(0, null, this);
}

View file

@ -1,6 +1,5 @@
package com.newsblur.activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.content.CursorLoader;
@ -10,7 +9,6 @@ import com.newsblur.R;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefConstants;
import com.newsblur.util.PrefsUtils;
@ -26,7 +24,7 @@ public class AllStoriesReading extends Reading {
stories = contentResolver.query(FeedProvider.ALL_STORIES_URI, null, DatabaseConstants.getStorySelectionFromState(currentState), null, DatabaseConstants.getStorySortOrder(storyOrder));
setTitle(getResources().getString(R.string.all_stories_row_title));
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), defaultFeedView);
getSupportLoaderManager().initLoader(0, null, this);
}

View file

@ -1,6 +1,5 @@
package com.newsblur.activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
@ -12,8 +11,6 @@ import com.newsblur.database.FeedProvider;
import com.newsblur.database.FeedReadingAdapter;
import com.newsblur.domain.Classifier;
import com.newsblur.domain.Feed;
import com.newsblur.fragment.SyncUpdateFragment;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StoryOrder;
@ -38,7 +35,7 @@ public class FeedReading extends Reading {
feedCursor.close();
setTitle(feed.title);
readingAdapter = new FeedReadingAdapter(getSupportFragmentManager(), feed, classifier);
readingAdapter = new FeedReadingAdapter(getSupportFragmentManager(), feed, classifier, defaultFeedView);
getSupportLoaderManager().initLoader(0, null, this);
}

View file

@ -1,8 +1,6 @@
package com.newsblur.activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
@ -10,7 +8,6 @@ import android.support.v4.content.Loader;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
@ -27,7 +24,7 @@ public class FolderReading extends Reading {
folderName = getIntent().getStringExtra(Reading.EXTRA_FOLDERNAME);
setTitle(folderName);
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), defaultFeedView);
getSupportLoaderManager().initLoader(0, null, this);
}

View file

@ -144,7 +144,6 @@ public class Main extends NbFragmentActivity implements StateChangedListener, Sy
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
Log.d(this.getClass().getName(), "onActivityResult:RESULT_OK" );
folderFeedList.hasUpdated();
}
}
@ -165,7 +164,8 @@ public class Main extends NbFragmentActivity implements StateChangedListener, Sy
*/
@Override
public void updatePartialSync() {
folderFeedList.hasUpdated();
// TODO: move 2-step sync to new async lib and remove this method entirely
// folderFeedList.hasUpdated();
}
@Override

View file

@ -34,7 +34,6 @@ import com.newsblur.fragment.ReadingItemFragment;
import com.newsblur.fragment.ShareDialogFragment;
import com.newsblur.fragment.TextSizeDialogFragment;
import com.newsblur.network.APIManager;
import com.newsblur.network.domain.StoryTextResponse;
import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedUtils;
@ -93,8 +92,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
private List<Story> pageHistory;
private DefaultFeedView defaultFeedView;
private DefaultFeedView currentFeedView;
protected DefaultFeedView defaultFeedView;
@Override
protected void onCreate(Bundle savedInstanceBundle) {
@ -114,7 +112,6 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
passedPosition = getIntent().getIntExtra(EXTRA_POSITION, 0);
currentState = getIntent().getIntExtra(ItemsList.EXTRA_STATE, 0);
defaultFeedView = (DefaultFeedView)getIntent().getSerializableExtra(EXTRA_DEFAULT_FEED_VIEW);
currentFeedView = defaultFeedView;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
contentResolver = getContentResolver();
@ -168,15 +165,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
fragment.updateStory(readingAdapter.getStory(pager.getCurrentItem()));
fragment.updateSaveButton();
// make sure we start in default mode and the ui reflects it
synchronized (currentFeedView) {
currentFeedView = defaultFeedView;
if (currentFeedView == DefaultFeedView.STORY) {
enableStoryMode();
} else {
enableTextMode();
}
}
updateOverlayText();
}
} catch (IllegalStateException ise) {
// sometimes the pager is already shutting down by the time the callback finishes
@ -225,24 +214,22 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (pager == null) return false;
int currentItem = pager.getCurrentItem();
Story story = readingAdapter.getStory(currentItem);
if (story == null) return false;
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.menu_reading_original) {
if (story != null) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(story.permalink));
startActivity(i);
}
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(story.permalink));
startActivity(i);
return true;
} else if (item.getItemId() == R.id.menu_reading_sharenewsblur) {
if (story != null) {
DialogFragment newFragment = ShareDialogFragment.newInstance(getReadingFragment(), story, getReadingFragment().previouslySavedShareText);
newFragment.show(getSupportFragmentManager(), "dialog");
}
DialogFragment newFragment = ShareDialogFragment.newInstance(getReadingFragment(), story, getReadingFragment().previouslySavedShareText);
newFragment.show(getSupportFragmentManager(), "dialog");
return true;
} else if (item.getItemId() == R.id.menu_shared) {
FeedUtils.shareStory(story, this);
@ -305,6 +292,8 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
markStoryRead(story);
}
checkStoryCount(position);
updateOverlayText();
}
// interface ScrollChangeListener
@ -387,7 +376,7 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
*/
private void checkStoryCount(int position) {
// if the pager is at or near the number of stories loaded, check for more unless we know we are at the end of the list
if (((position + 2) >= stories.getCount()) && !noMoreApiPages && !requestedPage && !stopLoading) {
if (((position + AppConstants.READING_STORY_PRELOAD) >= stories.getCount()) && !noMoreApiPages && !requestedPage && !stopLoading) {
currentApiPage += 1;
requestedPage = true;
enableMainProgress(true);
@ -399,6 +388,10 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
enableProgressCircle(overlayProgressRight, enabled);
}
public void enableLeftProgressCircle(boolean enabled) {
enableProgressCircle(overlayProgressLeft, enabled);
}
private void enableProgressCircle(final ProgressBar view, final boolean enabled) {
runOnUiThread(new Runnable() {
public void run() {
@ -440,9 +433,10 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
// NB: this callback is for the text size slider
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
PrefsUtils.setTextSize(this, (float) progress / AppConstants.FONT_SIZE_INCREMENT_FACTOR);
float size = AppConstants.READING_FONT_SIZE[progress];
PrefsUtils.setTextSize(this, size);
Intent data = new Intent(ReadingItemFragment.TEXT_SIZE_CHANGED);
data.putExtra(ReadingItemFragment.TEXT_SIZE_VALUE, (float) progress / AppConstants.FONT_SIZE_INCREMENT_FACTOR);
data.putExtra(ReadingItemFragment.TEXT_SIZE_VALUE, size);
sendBroadcast(data);
}
@ -587,51 +581,21 @@ public abstract class Reading extends NbFragmentActivity implements OnPageChange
}
public void overlayText(View v) {
synchronized (currentFeedView) {
// if we were already in text mode, switch back to story mode
if (currentFeedView == DefaultFeedView.TEXT) {
enableStoryMode();
} else {
enableTextMode();
}
}
}
private void enableTextMode() {
final Story story = readingAdapter.getStory(pager.getCurrentItem());
if (story != null) {
new AsyncTask<Void, Void, StoryTextResponse>() {
@Override
protected void onPreExecute() {
enableProgressCircle(overlayProgressLeft, true);
}
@Override
protected StoryTextResponse doInBackground(Void... arg) {
return apiManager.getStoryText(story.feedId, story.id);
}
@Override
protected void onPostExecute(StoryTextResponse result) {
ReadingItemFragment item = getReadingFragment();
if ((item != null) && (result != null) && (result.originalText != null)) {
item.showCustomContentInWebview(result.originalText);
}
enableProgressCircle(overlayProgressLeft, false);
}
}.execute();
}
this.overlayText.setBackgroundResource(R.drawable.selector_overlay_bg_story);
this.overlayText.setText(R.string.overlay_story);
this.currentFeedView = DefaultFeedView.TEXT;
}
private void enableStoryMode() {
ReadingItemFragment item = getReadingFragment();
if (item != null) item.showStoryContentInWebview();
if (item == null) return;
item.switchSelectedFeedView();
updateOverlayText();
}
this.overlayText.setBackgroundResource(R.drawable.selector_overlay_bg_text);
this.overlayText.setText(R.string.overlay_text);
this.currentFeedView = DefaultFeedView.STORY;
private void updateOverlayText() {
ReadingItemFragment item = getReadingFragment();
if (item.getSelectedFeedView() == DefaultFeedView.STORY) {
this.overlayText.setBackgroundResource(R.drawable.selector_overlay_bg_text);
this.overlayText.setText(R.string.overlay_text);
} else {
this.overlayText.setBackgroundResource(R.drawable.selector_overlay_bg_story);
this.overlayText.setText(R.string.overlay_story);
}
}
private ReadingItemFragment getReadingFragment() {

View file

@ -4,18 +4,19 @@ import android.database.Cursor;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.util.Log;
import android.view.ViewGroup;
import com.newsblur.domain.Story;
import com.newsblur.fragment.LoadingFragment;
import com.newsblur.util.DefaultFeedView;
public abstract class ReadingAdapter extends FragmentStatePagerAdapter {
protected Cursor stories;
protected DefaultFeedView defaultFeedView;
public ReadingAdapter(FragmentManager fm) {
public ReadingAdapter(FragmentManager fm, DefaultFeedView defaultFeedView) {
super(fm);
this.defaultFeedView = defaultFeedView;
}
@Override

View file

@ -1,6 +1,5 @@
package com.newsblur.activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.content.CursorLoader;
@ -10,7 +9,6 @@ import com.newsblur.R;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.StoryOrder;
@ -21,7 +19,7 @@ public class SavedStoriesReading extends Reading {
super.onCreate(savedInstanceBundle);
setTitle(getResources().getString(R.string.saved_stories_title));
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), defaultFeedView);
getSupportLoaderManager().initLoader(0, null, this);
}
@ -34,7 +32,7 @@ public class SavedStoriesReading extends Reading {
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
return new CursorLoader(this, FeedProvider.STARRED_STORIES_URI, null, null, null, DatabaseConstants.getStorySortOrder(StoryOrder.NEWEST));
return new CursorLoader(this, FeedProvider.STARRED_STORIES_URI, null, null, null, DatabaseConstants.STARRED_STORY_ORDER);
}
@Override

View file

@ -1,6 +1,5 @@
package com.newsblur.activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
@ -11,11 +10,8 @@ import com.newsblur.database.DatabaseConstants;
import com.newsblur.database.FeedProvider;
import com.newsblur.database.MixedFeedsReadingAdapter;
import com.newsblur.domain.SocialFeed;
import com.newsblur.service.SyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefConstants;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StoryOrder;
public class SocialFeedReading extends Reading {
@ -31,7 +27,7 @@ public class SocialFeedReading extends Reading {
setTitle(getIntent().getStringExtra(EXTRA_USERNAME));
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver());
readingAdapter = new MixedFeedsReadingAdapter(getSupportFragmentManager(), getContentResolver(), defaultFeedView);
getSupportLoaderManager().initLoader(0, null, this);
}

View file

@ -109,6 +109,7 @@ public class BlurDatabase extends SQLiteOpenHelper {
DatabaseConstants.STORY_PERMALINK + TEXT + ", " +
DatabaseConstants.STORY_READ + INTEGER + ", " +
DatabaseConstants.STORY_STARRED + INTEGER + ", " +
DatabaseConstants.STORY_STARRED_DATE + INTEGER + ", " +
DatabaseConstants.STORY_TITLE + TEXT;
private final String STORY_SQL = "CREATE TABLE " + DatabaseConstants.STORY_TABLE + " (" + STORY_TABLES_COLS + ")";

View file

@ -86,6 +86,7 @@ public class DatabaseConstants {
public static final String STORY_PERMALINK = "permalink";
public static final String STORY_READ = "read";
public static final String STORY_STARRED = "starred";
public static final String STORY_STARRED_DATE = "starred_date";
public static final String STORY_SHARE_COUNT = "share_count";
public static final String STORY_SHARED_USER_IDS = "shared_user_ids";
public static final String STORY_FRIEND_USER_IDS = "comment_user_ids";
@ -171,13 +172,15 @@ public class DatabaseConstants {
public static final String[] STORY_COLUMNS = {
STORY_AUTHORS, STORY_COMMENT_COUNT, STORY_CONTENT, STORY_DATE, STORY_SHARED_DATE, STORY_SHORTDATE, STORY_LONGDATE, STORY_TABLE + "." + STORY_FEED_ID, STORY_TABLE + "." + STORY_ID, STORY_INTELLIGENCE_AUTHORS, STORY_INTELLIGENCE_FEED, STORY_INTELLIGENCE_TAGS, STORY_INTELLIGENCE_TITLE,
STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_STARRED_DATE, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
};
public static final String[] STARRED_STORY_COLUMNS = {
STORY_AUTHORS, STORY_COMMENT_COUNT, STORY_CONTENT, STORY_DATE, STORY_SHARED_DATE, STORY_SHORTDATE, STORY_LONGDATE, STARRED_STORIES_TABLE + "." + STORY_FEED_ID, STARRED_STORIES_TABLE + "." + STORY_ID, STORY_INTELLIGENCE_AUTHORS, STORY_INTELLIGENCE_FEED, STORY_INTELLIGENCE_TAGS, STORY_INTELLIGENCE_TITLE,
STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_STARRED_DATE, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE, STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
};
public static final String STARRED_STORY_ORDER = STORY_STARRED_DATE + " ASC";
/**
* Selection args to filter stories.
*/

View file

@ -1,6 +1,5 @@
package com.newsblur.database;
import android.database.Cursor;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
@ -8,24 +7,24 @@ import com.newsblur.activity.ReadingAdapter;
import com.newsblur.domain.Classifier;
import com.newsblur.domain.Feed;
import com.newsblur.domain.Story;
import com.newsblur.fragment.LoadingFragment;
import com.newsblur.fragment.ReadingItemFragment;
import com.newsblur.util.DefaultFeedView;
public class FeedReadingAdapter extends ReadingAdapter {
private final Feed feed;
private Classifier classifier;
public FeedReadingAdapter(FragmentManager fm, Feed feed, Classifier classifier) {
super(fm);
public FeedReadingAdapter(FragmentManager fm, Feed feed, Classifier classifier, DefaultFeedView defaultFeedView) {
super(fm, defaultFeedView);
this.feed = feed;
this.classifier = classifier;
}
}
@Override
protected synchronized Fragment getReadingItemFragment(int position) {
stories.moveToPosition(position);
return ReadingItemFragment.newInstance(Story.fromCursor(stories), feed.title, feed.faviconColor, feed.faviconFade, feed.faviconBorder, feed.faviconText, feed.faviconUrl, classifier, false);
return ReadingItemFragment.newInstance(Story.fromCursor(stories), feed.title, feed.faviconColor, feed.faviconFade, feed.faviconBorder, feed.faviconText, feed.faviconUrl, classifier, false, defaultFeedView);
}
}

View file

@ -9,15 +9,15 @@ import android.support.v4.app.FragmentManager;
import com.newsblur.activity.ReadingAdapter;
import com.newsblur.domain.Classifier;
import com.newsblur.domain.Story;
import com.newsblur.fragment.LoadingFragment;
import com.newsblur.fragment.ReadingItemFragment;
import com.newsblur.util.DefaultFeedView;
public class MixedFeedsReadingAdapter extends ReadingAdapter {
private final ContentResolver resolver;
public MixedFeedsReadingAdapter(final FragmentManager fragmentManager, final ContentResolver resolver) {
super(fragmentManager);
public MixedFeedsReadingAdapter(final FragmentManager fragmentManager, final ContentResolver resolver, DefaultFeedView defaultFeedView) {
super(fragmentManager, defaultFeedView);
this.resolver = resolver;
}
@ -36,7 +36,7 @@ public class MixedFeedsReadingAdapter extends ReadingAdapter {
Cursor feedClassifierCursor = resolver.query(classifierUri, null, null, null, null);
Classifier classifier = Classifier.fromCursor(feedClassifierCursor);
return ReadingItemFragment.newInstance(story, feedTitle, feedFaviconColor, feedFaviconFade, feedFaviconBorder, feedFaviconText, feedFaviconUrl, classifier, true);
return ReadingItemFragment.newInstance(story, feedTitle, feedFaviconColor, feedFaviconFade, feedFaviconBorder, feedFaviconText, feedFaviconUrl, classifier, true, defaultFeedView);
}
}

View file

@ -44,6 +44,9 @@ public class Story implements Serializable {
@SerializedName("starred")
public boolean starred;
@SerializedName("starred_dated")
public Date starredDate;
@SerializedName("story_tags")
public String[] tags;
@ -114,6 +117,7 @@ public class Story implements Serializable {
values.put(DatabaseConstants.STORY_TAGS, TextUtils.join(",", tags));
values.put(DatabaseConstants.STORY_READ, read);
values.put(DatabaseConstants.STORY_STARRED, starred);
values.put(DatabaseConstants.STORY_STARRED_DATE, starredDate != null ? starredDate.getTime() : new Date().getTime());
values.put(DatabaseConstants.STORY_FEED_ID, feedId);
values.put(DatabaseConstants.STORY_HASH, storyHash);
return values;

View file

@ -61,6 +61,7 @@ public class AllSharedStoriesItemListFragment extends ItemListFragment implement
groupFrom = new String[] { DatabaseConstants.STORY_TITLE, DatabaseConstants.STORY_AUTHORS, DatabaseConstants.STORY_TITLE, DatabaseConstants.STORY_SHORTDATE, DatabaseConstants.STORY_INTELLIGENCE_AUTHORS, DatabaseConstants.FEED_TITLE };
groupTo = new int[] { R.id.row_item_title, R.id.row_item_author, R.id.row_item_title, R.id.row_item_date, R.id.row_item_sidebar, R.id.row_item_feedtitle };
// TODO: defer creation of the adapter until the loader's first callback so we don't leak this first cursor
Cursor cursor = contentResolver.query(FeedProvider.ALL_SHARED_STORIES_URI, null, DatabaseConstants.getStorySelectionFromState(currentState), null, DatabaseConstants.getStorySortOrder(storyOrder));
adapter = new MultipleFeedItemsAdapter(getActivity(), R.layout.row_socialitem, cursor, groupFrom, groupTo, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
adapter.setViewBinder(new SocialItemViewBinder(getActivity()));

View file

@ -68,6 +68,7 @@ public class FeedItemListFragment extends StoryItemListFragment implements Loade
itemList.setEmptyView(v.findViewById(R.id.empty_view));
ContentResolver contentResolver = getActivity().getContentResolver();
// TODO: defer creation of the adapter until the loader's first callback so we don't leak this first stories cursor
Uri storiesUri = FeedProvider.FEED_STORIES_URI.buildUpon().appendPath(feedId).build();
Cursor storiesCursor = contentResolver.query(storiesUri, null, DatabaseConstants.getStorySelectionFromState(currentState), null, DatabaseConstants.getStorySortOrder(storyOrder));
Uri feedUri = FeedProvider.FEEDS_URI.buildUpon().appendPath(feedId).build();
@ -85,6 +86,7 @@ public class FeedItemListFragment extends StoryItemListFragment implements Loade
feedCursor.moveToFirst();
Feed feed = Feed.fromCursor(feedCursor);
feedCursor.close();
String[] groupFrom = new String[] { DatabaseConstants.STORY_TITLE, DatabaseConstants.STORY_AUTHORS, DatabaseConstants.STORY_READ, DatabaseConstants.STORY_SHORTDATE, DatabaseConstants.STORY_INTELLIGENCE_AUTHORS };
int[] groupTo = new int[] { R.id.row_item_title, R.id.row_item_author, R.id.row_item_title, R.id.row_item_date, R.id.row_item_sidebar };

View file

@ -31,13 +31,16 @@ public abstract class ItemListFragment extends Fragment implements OnScrollListe
public void resetPagination() {
this.currentPage = 0;
// also re-enable the loading indicator, since this means the story list was reset
firstSyncDone = false;
setEmptyListView(R.string.empty_list_view_loading);
}
public void syncDone() {
this.firstSyncDone = true;
}
private void finishLoadingScreen() {
private void setEmptyListView(int rid) {
View v = this.getView();
if (v == null) return; // we might have beat construction?
@ -48,13 +51,13 @@ public abstract class ItemListFragment extends Fragment implements OnScrollListe
}
TextView emptyView = (TextView) itemList.getEmptyView();
emptyView.setText(R.string.empty_list_view_no_stories);
emptyView.setText(rid);
}
@Override
public synchronized void onScroll(AbsListView view, int firstVisible, int visibleCount, int totalCount) {
// load an extra page worth of stories past the viewport
if (totalCount != 0 && (firstVisible + visibleCount + visibleCount - 1 >= totalCount) && !requestedPage) {
if (totalCount != 0 && (firstVisible + (visibleCount*2) >= totalCount) && !requestedPage) {
currentPage += 1;
requestedPage = true;
triggerRefresh(currentPage);
@ -80,7 +83,7 @@ public abstract class ItemListFragment extends Fragment implements OnScrollListe
// iff a sync has finished and a cursor load has finished, it is safe to remove the loading message
if (this.firstSyncDone) {
finishLoadingScreen();
setEmptyListView(R.string.empty_list_view_no_stories);
}
}
}

View file

@ -9,11 +9,13 @@ import android.content.IntentFilter;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@ -31,7 +33,9 @@ import com.newsblur.domain.Story;
import com.newsblur.domain.UserDetails;
import com.newsblur.network.APIManager;
import com.newsblur.network.SetupCommentSectionTask;
import com.newsblur.network.domain.StoryTextResponse;
import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.ImageLoader;
import com.newsblur.util.PrefsUtils;
@ -63,10 +67,12 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
public String previouslySavedShareText;
private ImageView feedIcon;
private Reading activity;
private DefaultFeedView selectedFeedView;
private String originalText;
private final Object WEBVIEW_CONTENT_MUTEX = new Object();
public static ReadingItemFragment newInstance(Story story, String feedTitle, String feedFaviconColor, String feedFaviconFade, String feedFaviconBorder, String faviconText, String faviconUrl, Classifier classifier, boolean displayFeedDetails) {
public static ReadingItemFragment newInstance(Story story, String feedTitle, String feedFaviconColor, String feedFaviconFade, String feedFaviconBorder, String faviconText, String faviconUrl, Classifier classifier, boolean displayFeedDetails, DefaultFeedView defaultFeedView) {
ReadingItemFragment readingFragment = new ReadingItemFragment();
Bundle args = new Bundle();
@ -79,6 +85,7 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
args.putString("faviconUrl", faviconUrl);
args.putBoolean("displayFeedDetails", displayFeedDetails);
args.putSerializable("classifier", classifier);
args.putSerializable("defaultFeedView", defaultFeedView);
readingFragment.setArguments(args);
return readingFragment;
@ -115,9 +122,37 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
classifier = (Classifier) getArguments().getSerializable("classifier");
selectedFeedView = (DefaultFeedView)getArguments().getSerializable("defaultFeedView");
receiver = new TextSizeReceiver();
getActivity().registerReceiver(receiver, new IntentFilter(TEXT_SIZE_CHANGED));
}
private void loadOriginalText() {
if (story != null) {
new AsyncTask<Void, Void, StoryTextResponse>() {
@Override
protected void onPreExecute() {
((Reading)getActivity()).enableLeftProgressCircle(true);
}
@Override
protected StoryTextResponse doInBackground(Void... arg) {
return apiManager.getStoryText(story.feedId, story.id);
}
@Override
protected void onPostExecute(StoryTextResponse result) {
if ((result != null) && (result.originalText != null)) {
ReadingItemFragment.this.originalText = result.originalText;
showTextContentInWebview();
}
if (getActivity() != null) {
((Reading)getActivity()).enableLeftProgressCircle(false);
}
}
}.execute();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putSerializable("story", story);
@ -149,6 +184,12 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
web = (NewsblurWebview) view.findViewById(R.id.reading_webview);
if (selectedFeedView == DefaultFeedView.TEXT) {
loadOriginalText();
} else {
showStoryContentInWebview();
}
setupItemMetadata();
setupShareButton();
setupSaveButton();
@ -191,6 +232,9 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
public void updateStory(Story story) {
if (story != null ) {
this.story = story;
if (selectedFeedView == DefaultFeedView.TEXT && originalText == null) {
loadOriginalText();
}
}
}
@ -317,6 +361,23 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
}
public void switchSelectedFeedView() {
synchronized (selectedFeedView) {
// if we were already in text mode, switch back to story mode
if (selectedFeedView == DefaultFeedView.TEXT) {
showStoryContentInWebview();
selectedFeedView = DefaultFeedView.STORY;
} else {
showTextContentInWebview();
selectedFeedView = DefaultFeedView.TEXT;
}
}
}
public DefaultFeedView getSelectedFeedView() {
return selectedFeedView;
}
/**
* Set the webview to show the default story content.
*/
@ -329,9 +390,13 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
/**
* Set the webview to show non-default content, tracking the change.
*/
public void showCustomContentInWebview(String content) {
synchronized (WEBVIEW_CONTENT_MUTEX) {
setupWebview(content);
public void showTextContentInWebview() {
if (originalText == null) {
loadOriginalText();
} else {
synchronized (WEBVIEW_CONTENT_MUTEX) {
setupWebview(originalText);
}
}
}
@ -344,9 +409,9 @@ public class ReadingItemFragment extends Fragment implements ClassifierDialogFra
float currentSize = PrefsUtils.getTextSize(getActivity());
StringBuilder builder = new StringBuilder();
builder.append("<html><head><meta name=\"viewport\" content=\"width=device-width; initial-scale=1; maximum-scale=1; minimum-scale=1; user-scalable=0; target-densityDpi=medium-dpi\" />");
builder.append("<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0\" />");
builder.append("<style style=\"text/css\">");
builder.append(String.format("body { font-size: %sem; } ", Float.toString(currentSize + AppConstants.FONT_SIZE_LOWER_BOUND)));
builder.append(String.format("body { font-size: %sem; } ", Float.toString(currentSize)));
builder.append("</style>");
builder.append("<link rel=\"stylesheet\" type=\"text/css\" href=\"reading.css\" /></head><body><div class=\"NB-story\">");
builder.append(storyText);

View file

@ -50,7 +50,7 @@ public class SavedStoriesItemListFragment extends ItemListFragment implements Lo
itemList.setEmptyView(v.findViewById(R.id.empty_view));
contentResolver = getActivity().getContentResolver();
Cursor cursor = contentResolver.query(FeedProvider.STARRED_STORIES_URI, null, null, null, DatabaseConstants.getStorySortOrder(StoryOrder.NEWEST));
Cursor cursor = contentResolver.query(FeedProvider.STARRED_STORIES_URI, null, null, null, DatabaseConstants.STARRED_STORY_ORDER);
String[] groupFrom = new String[] { DatabaseConstants.STORY_TITLE, DatabaseConstants.STORY_AUTHORS, DatabaseConstants.STORY_TITLE, DatabaseConstants.STORY_SHORTDATE, DatabaseConstants.STORY_INTELLIGENCE_AUTHORS, DatabaseConstants.FEED_TITLE };
int[] groupTo = new int[] { R.id.row_item_title, R.id.row_item_author, R.id.row_item_title, R.id.row_item_date, R.id.row_item_sidebar, R.id.row_item_feedtitle };
@ -103,7 +103,7 @@ public class SavedStoriesItemListFragment extends ItemListFragment implements Lo
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new CursorLoader(getActivity(), FeedProvider.STARRED_STORIES_URI, null, null, null, DatabaseConstants.getStorySortOrder(StoryOrder.NEWEST));
return new CursorLoader(getActivity(), FeedProvider.STARRED_STORIES_URI, null, null, null, DatabaseConstants.STARRED_STORY_ORDER);
}
@Override

View file

@ -41,8 +41,13 @@ public class TextSizeDialogFragment extends DialogFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
this.currentValue = getArguments().getFloat(CURRENT_SIZE);
View v = inflater.inflate(R.layout.textsize_slider_dialog, null);
int currentSizeIndex = 0;
for (int i=0; i<AppConstants.READING_FONT_SIZE.length; i++) {
if (currentValue >= AppConstants.READING_FONT_SIZE[i]) currentSizeIndex = i;
}
seekBar = (SeekBar) v.findViewById(R.id.textSizeSlider);
seekBar.setProgress((int) (currentValue * AppConstants.FONT_SIZE_INCREMENT_FACTOR));
seekBar.setProgress(currentSizeIndex);
getDialog().getWindow().setFlags(WindowManager.LayoutParams.FLAG_DITHER, WindowManager.LayoutParams.FLAG_DITHER);
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);

View file

@ -110,6 +110,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
Cursor userCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { id }, null);
UserProfile user = UserProfile.fromCursor(userCursor);
userCursor.close();
imageLoader.displayImage(user.photoUrl, favouriteImage, 10f);
favouriteImage.setTag(id);
@ -135,6 +136,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
if (story != null) {
Cursor userCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { comment.userId }, null);
UserProfile user = UserProfile.fromCursor(userCursor);
userCursor.close();
DialogFragment newFragment = ReplyDialogFragment.newInstance(story, comment.userId, user.username);
newFragment.show(manager, "dialog");
@ -171,15 +173,18 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
TextView replyUsername = (TextView) replyView.findViewById(R.id.reply_username);
replyUsername.setText(R.string.unknown_user);
}
replyCursor.close();
TextView replySharedDate = (TextView) replyView.findViewById(R.id.reply_shareddate);
replySharedDate.setText(reply.shortDate + " ago");
((LinearLayout) commentView.findViewById(R.id.comment_replies_container)).addView(replyView);
}
replies.close();
Cursor userCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { comment.userId }, null);
UserProfile commentUser = UserProfile.fromCursor(userCursor);
userCursor.close();
TextView commentUsername = (TextView) commentView.findViewById(R.id.comment_username);
commentUsername.setText(commentUser.username);
@ -209,6 +214,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
imageLoader.displayImage(sourceUser.photoUrl, sourceUserImage, 10f);
imageLoader.displayImage(userPhoto, usershareImage, 10f);
}
sourceUserCursor.close();
} else {
imageLoader.displayImage(userPhoto, commentImage, 10f);
}
@ -260,6 +266,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
ImageView image = ViewUtils.createSharebarImage(context, imageLoader, user.photoUrl, user.userId);
sharedGrid.addView(image);
}
userCursor.close();
}
}

View file

@ -16,8 +16,9 @@ public class AppConstants {
public static final int REGISTRATION_COMPLETED = 1;
public static final String FOLDER_PRE = "folder_collapsed";
public static final float FONT_SIZE_LOWER_BOUND = 0.7f;
public static final float FONT_SIZE_INCREMENT_FACTOR = 8;
// reading view font sizes, in em
public static final float[] READING_FONT_SIZE = {0.75f, 0.9f, 1.0f, 1.2f, 1.5f, 2.0f};
// the name to give the "root" folder in the local DB since the API does not assign it one.
// this name should be unique and such that it will sort to the beginning of a list, ideally.
@ -40,4 +41,7 @@ public class AppConstants {
// when generating a request for multiple feeds, limit the total number requested to prevent
// unworkably long URLs
public static final int MAX_FEED_LIST_SIZE = 250;
// when reading stories, how many stories worth of buffer to keep loaded ahead of the user
public static final int READING_STORY_PRELOAD = 5;
}

View file

@ -242,7 +242,7 @@ public class PrefsUtils {
public static float getTextSize(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return preferences.getFloat(PrefConstants.PREFERENCE_TEXT_SIZE, 0.5f);
return preferences.getFloat(PrefConstants.PREFERENCE_TEXT_SIZE, 1.0f);
}
public static void setTextSize(Context context, float size) {

View file

@ -59,8 +59,8 @@ public class NewsblurWebview extends WebView {
}
public void setTextSize(float textSize) {
Log.d("Reading", "Setting textsize to " + (AppConstants.FONT_SIZE_LOWER_BOUND + textSize));
String script = "javascript:document.body.style.fontSize='" + (AppConstants.FONT_SIZE_LOWER_BOUND + textSize) + "em';";
Log.d("Reading", "Setting textsize to " + textSize);
String script = "javascript:document.body.style.fontSize='" + textSize + "em';";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
evaluateJavascript(script, null);
} else {

4
config/requirements.txt Normal file → Executable file
View file

@ -23,7 +23,7 @@ kombu==2.5.7
mongoengine==0.8.2
nltk==2.0.4
oauth2==1.5.211
psutil==0.7.1
psutil>=1.2.1
pyes==0.19.1
pyelasticsearch==0.5
pyflakes==0.6.1
@ -39,3 +39,5 @@ requests==1.1.0
seacucumber==1.5
South==0.7.6
stripe==1.7.10
django-oauth-toolkit==0.5.0
django-cors-headers==0.12

23
fabfile.py vendored
View file

@ -100,6 +100,7 @@ def list_do():
def host(*names):
env.hosts = []
env.doname = ','.join(names)
hostnames = do(split=True)
for role, hosts in hostnames.items():
for host in hosts:
@ -278,7 +279,10 @@ def setup_task_image():
copy_task_settings()
setup_hosts()
config_pgbouncer()
pull()
pip()
deploy()
done()
# ==================
# = Setup - Common =
@ -286,7 +290,7 @@ def setup_task_image():
def done():
print "\n\n\n\n-----------------------------------------------------"
print "\n\n %s IS SUCCESSFULLY BOOTSTRAPPED" % env.host_string
print "\n\n %s IS SUCCESSFULLY BOOTSTRAPPED" % (env.get('doname') or env.host_string)
print "\n\n-----------------------------------------------------\n\n\n\n"
def setup_installs():
@ -408,9 +412,10 @@ def setup_psycopg():
sudo('easy_install -U psycopg2')
def setup_python():
# sudo('easy_install -U pip')
sudo('easy_install -U $(<%s)' %
os.path.join(env.NEWSBLUR_PATH, 'config/requirements.txt'))
sudo('easy_install -U pip')
# sudo('easy_install -U $(<%s)' %
# os.path.join(env.NEWSBLUR_PATH, 'config/requirements.txt'))
pip()
put('config/pystartup.py', '.pystartup')
# with cd(os.path.join(env.NEWSBLUR_PATH, 'vendor/cjson')):
@ -426,6 +431,12 @@ def setup_python():
with settings(warn_only=True):
sudo('chown -R ubuntu.ubuntu /home/ubuntu/.python-eggs')
def pip():
with cd(env.NEWSBLUR_PATH):
sudo('easy_install -U pip')
sudo('pip install --upgrade pip')
sudo('pip install -r requirements.txt')
# PIL - Only if python-imaging didn't install through apt-get, like on Mac OS X.
def setup_imaging():
sudo('easy_install --always-unzip pil')
@ -1038,6 +1049,8 @@ def setup_do(name, size=2, image=None):
images = dict((s.name, s.id) for s in doapi.images(show_all=False))
image_id = images[IMAGE_NAME]
name = do_name(name)
env.doname = name
print "Creating droplet: %s" % name
instance = doapi.create_droplet(name=name,
size_id=size_id,
image_id=image_id,
@ -1077,7 +1090,7 @@ def do_name(name):
hosts = do_roledefs(split=False)
hostnames = [host.name for host in hosts]
existing_hosts = [hostname for hostname in hostnames if name in hostname]
for i in range(1, 50):
for i in range(1, 100):
try_host = "%s%02d" % (name, i)
if try_host not in existing_hosts:
print " ---> %s hosts in %s (%s). %s is unused." % (len(existing_hosts), name,

View file

@ -1 +1 @@
../../django/django/contrib/admin/media/
/Users/sclay/projects/django/django/contrib/admin/static/admin

View file

@ -58,14 +58,19 @@
.NB-static-form input,
.NB-static-form select {
margin: 6px 0 2px;
width: 200px;
font-size: 14px;
padding: 2px;
border: 1px solid #606060;
-moz-box-shadow:2px 2px 0 #A0B998;
-webkit-box-shadow:2px 2px 0 #A0B998;
box-shadow:2px 2px 0 #A0B998;
margin: 6px 4px;
border: 1px solid rgba(0, 0, 0, .2);
border-radius: 1px;
-moz-box-shadow: inset 0 2px 2px rgba(50, 50, 50, 0.15);
box-shadow: inset 0 2px 2px rgba(50, 50, 50, 0.15);
width: 200px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.NB-static-form input.error,

View file

@ -4098,9 +4098,9 @@ form.opml_import_form input {
.NB-splash-info .NB-splash-title {
position: absolute;
bottom: -1px;
height: 55px;
width: 282px;
height: 54px;
right: 166px;
width: 312px;
z-index: 2;
}
@ -4108,8 +4108,8 @@ form.opml_import_form input {
top: 0px;
bottom: inherit;
right: 24px;
width: 312px;
height: 55px;
width: 282px;
height: 54px;
}
.NB-body-main .NB-splash-info.NB-splash-top .NB-splash-title {
display: none;
@ -4499,7 +4499,10 @@ form.opml_import_form input {
position: relative;
padding: 5px 8px 4px;
}
.NB-narrow .NB-intelligence-slider .NB-intelligence-label {
.NB-narrow .NB-intelligence-slider .NB-intelligence-slider-green .NB-intelligence-label {
display: none;
}
.NB-extra-narrow .NB-intelligence-slider .NB-intelligence-slider-yellow .NB-intelligence-label {
display: none;
}
.NB-intelligence-slider img {
@ -4509,9 +4512,13 @@ form.opml_import_form input {
float: left;
vertical-align: bottom;
}
.NB-narrow .NB-intelligence-slider img {
.NB-narrow .NB-intelligence-slider .NB-intelligence-slider-green img {
margin: 2px 8px 1px;
}
.NB-extra-narrow .NB-intelligence-slider .NB-intelligence-slider-yellow img {
margin: 2px 8px 1px;
}
/* ===================== */
/* = Add Feeds/Folders = */
/* ===================== */
@ -8636,6 +8643,9 @@ form.opml_import_form input {
color: #303030;
line-height: 18px;
}
.NB-static h3 {
line-height: 1.4em;
}
.NB-static .NB-splash-info {
opacity: .9;
@ -10230,6 +10240,56 @@ form.opml_import_form input {
margin-left: 200px;
}
/* =============== */
/* = OAuth Forms = */
/* =============== */
.NB-static-oauth h3 {
margin-top: 0;
text-align: center;
}
.NB-static-oauth .NB-static-form {
width: 500px;
margin: 24px auto;
}
.NB-static-login .NB-static-form {
width: 360px;
}
.NB-static-oauth .NB-static-form-label label {
width: 120px;
text-transform: uppercase;
font-size: 18px;
padding: 8px 0 0 0;
color: rgba(0, 0, 0, .6);
}
.NB-static-oauth .NB-static-form-input input {
margin-left: 0;
margin-right: 0;
height: 34px;
font-size: 22px;
}
.NB-static-oauth input[type=submit].NB-static-form-submit {
margin: 24px 0 0 0;
font-size: 26px;
font-size: 18px;
padding: 8px 12px;
}
.NB-static-login input[type=submit].NB-static-form-submit {
margin: 12px 0 0 120px;
}
.NB-static-oauth .NB-error {
font-size: 12px;
text-align: center;
color: #6A1000;
padding: 4px 0 0;
line-height: 14px;
font-weight: bold;
clear: both;
}
/* ======================== */
/* = Feed Options Popover = */
/* ======================== */

View file

@ -231,7 +231,9 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
var selected = this.starred_feeds.selected();
var pre_callback = function(data) {
self.starred_feeds.reset(data.starred_counts, {parse: true});
if (data.starred_counts) {
self.starred_feeds.reset(data.starred_counts, {parse: true});
}
if (selected) {
self.starred_feeds.get(selected).set('selected', true);
@ -253,7 +255,9 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
var selected = this.starred_feeds.selected();
var pre_callback = function(data) {
self.starred_feeds.reset(data.starred_counts, {parse: true});
if (data.starred_counts) {
self.starred_feeds.reset(data.starred_counts, {parse: true, update: true});
}
if (selected && self.starred_feeds.get(selected)) {
self.starred_feeds.get(selected).set('selected', true);
@ -262,11 +266,6 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
if (callback) callback(data);
};
var pre_callback = function(data) {
self.starred_feeds.reset(data.starred_counts, {parse: true, update: true});
if (callback) callback(data);
};
this.make_request('/reader/mark_story_as_unstarred', {
story_id: story_id
}, pre_callback);
@ -429,7 +428,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
self.user_profile.set(subscriptions.social_profile);
self.social_services = subscriptions.social_services;
if (selected) {
if (selected && self.feeds.get(selected)) {
self.feeds.get(selected).set('selected', true);
}
if (!_.isEqual(self.favicons, {})) {
@ -774,7 +773,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
_.each(data.feeds, _.bind(function(feed, feed_id) {
var existing_feed = this.feeds.get(feed_id);
if (!existing_feed) return;
if (!existing_feed) {
console.log(["Trying to refresh unsub feed", feed_id, feed]);
return;
}
var feed_id = feed.id || feed_id;
if (feed.id && feed_id != feed.id) {

View file

@ -33,6 +33,67 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
return score_name;
},
formatted_short_date: function() {
var timestamp = this.get('story_timestamp');
var dateformat = NEWSBLUR.assets.preference('dateformat');
var date = new Date(parseInt(timestamp, 10) * 1000);
var midnight_today = function() {
var midnight = new Date();
midnight.setHours(0);
midnight.setMinutes(0);
midnight.setSeconds(0);
return midnight;
};
var midnight_yesterday = function(midnight) {
return new Date(midnight - 60*60*24*1000);
};
var midnight = midnight_today();
var time = date.format(dateformat == "24" ? "H:i" : "g:ia");
if (date > midnight) {
return time;
} else if (date > midnight_yesterday(midnight)) {
return "Yesterday, " + time;
} else {
return date.format("d M Y, ") + time;
}
},
formatted_long_date: function() {
var timestamp = this.get('story_timestamp');
var dateformat = NEWSBLUR.assets.preference('dateformat');
var date = new Date(parseInt(timestamp, 10) * 1000);
var midnight_today = function() {
var midnight = new Date();
midnight.setHours(0);
midnight.setMinutes(0);
midnight.setSeconds(0);
return midnight;
};
var midnight_yesterday = function(midnight) {
return new Date(midnight - 60*60*24*1000);
};
var beginning_of_month = function() {
var month = new Date();
month.setHours(0);
month.setMinutes(0);
month.setSeconds(0);
month.setDate(1);
return month;
};
var midnight = midnight_today();
var time = date.format(dateformat == "24" ? "H:i" : "g:ia");
if (date > midnight) {
return "Today, " + date.format("F jS ") + time;
} else if (date > midnight_yesterday(midnight)) {
return "Yesterday, " + date.format("F jS ") + time;
} else if (date > beginning_of_month()) {
return date.format("l, F jS ") + time;
} else {
return date.format("l, F jS Y ") + time;
}
},
has_modifications: function() {
if (this.get('story_content').indexOf('<ins') != -1 ||
this.get('story_content').indexOf('<del') != -1) {

View file

@ -433,7 +433,8 @@
var feed_pane_size = state.size;
$('#NB-splash').css('left', feed_pane_size);
$pane.toggleClass("NB-narrow", this.layout.outerLayout.state.west.size < 220);
$pane.toggleClass("NB-narrow", this.layout.outerLayout.state.west.size < 240);
$pane.toggleClass("NB-extra-narrow", this.layout.outerLayout.state.west.size < 218);
this.flags.set_feed_pane_size = this.flags.set_feed_pane_size || _.debounce( _.bind(function() {
var feed_pane_size = this.layout.outerLayout.state.west.size;
this.model.preference('feed_pane_size', feed_pane_size);
@ -605,10 +606,10 @@
if (next_feed_id == this.active_feed) return;
if (NEWSBLUR.utils.is_feed_social(next_feed_id)) {
this.open_social_stories(next_feed_id, {force: true, $feed_link: $next_feed});
this.open_social_stories(next_feed_id, {force: true, $feed: $next_feed});
} else {
next_feed_id = parseInt(next_feed_id, 10);
this.open_feed(next_feed_id, {force: true, $feed_link: $next_feed});
this.open_feed(next_feed_id, {force: true, $feed: $next_feed});
}
}
}
@ -669,16 +670,19 @@
return this.show_next_folder(direction, $current_feed);
}
var $next_feed = this.get_next_feed(direction, $current_feed, {include_selected: true});
var $next_feed = this.get_next_feed(direction, $current_feed, {
include_selected: true,
feed_id: this.active_feed
});
var next_feed_id = $next_feed.data('id');
if (next_feed_id && next_feed_id == this.active_feed) {
this.show_next_feed(direction, $next_feed);
} else if (NEWSBLUR.utils.is_feed_social(next_feed_id)) {
this.open_social_stories(next_feed_id, {force: true, $feed_link: $next_feed});
this.open_social_stories(next_feed_id, {force: true, $feed: $next_feed});
} else {
next_feed_id = parseInt(next_feed_id, 10);
this.open_feed(next_feed_id, {force: true, $feed_link: $next_feed});
this.open_feed(next_feed_id, {force: true, $feed: $next_feed});
}
},
@ -693,7 +697,18 @@
options = options || {};
var self = this;
var $feed_list = this.$s.$feed_list.add(this.$s.$social_feeds);
var $current_feed = $current_feed || $('.selected', $feed_list);
if (!$current_feed) {
$current_feed = $('.selected', $feed_list);
}
if (options.feed_id && $current_feed && $current_feed.length) {
var current_feed = NEWSBLUR.assets.get_feed(options.feed_id);
if (current_feed) {
var selected_title_view = current_feed.get("selected_title_view");
if (selected_title_view) {
$current_feed = selected_title_view.$el;
}
}
}
var $next_feed,
scroll;
var $feeds = $('.feed:visible:not(.NB-empty)', $feed_list);
@ -1222,10 +1237,11 @@
var self = this;
var $story_titles = this.$s.$story_titles;
var feed = this.model.get_feed(feed_id) || options.feed;
var temp = feed && (feed.get('temp') || !feed.get('subscribed'));
var temp = feed && feed.get('temp') && !feed.get('subscribed');
if (!feed || (temp && !options.try_feed)) {
// Setup tryfeed views first, then come back here.
console.log(["Temp open feed", feed_id, feed, options, temp]);
options.feed = options.feed && options.feed.attributes;
return this.load_feed_in_tryfeed_view(feed_id, options);
}
@ -1242,6 +1258,14 @@
this.active_feed = feed.id;
this.next_feed = feed.id;
if (options.$feed) {
var selected_title_view = _.detect(feed.views, function(view) {
return view.el == options.$feed.get(0);
});
if (selected_title_view) {
feed.set("selected_title_view", selected_title_view, {silent: true});
}
}
feed.set('selected', true, options);
if (NEWSBLUR.app.story_unread_counter) {
NEWSBLUR.app.story_unread_counter.remove();

View file

@ -314,11 +314,21 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
if (feed_address.length) {
this.model.save_exception_change_feed_address(feed_id, feed_address, _.bind(function(data) {
console.log(["return to change address", data]);
if (data && data.new_feed_id) {
NEWSBLUR.reader.force_feed_refresh(feed_id, data.new_feed_id);
NEWSBLUR.assets.feeds.add(_.values(data.feeds));
var feed = NEWSBLUR.assets.get_feed(data.new_feed_id || feed_id);
var old_feed = NEWSBLUR.assets.get_feed(feed_id);
if (data.new_feed_id != feed_id && old_feed.get('selected')) {
old_feed.set('selected', false);
}
if (data && data.new_feed_id) {
NEWSBLUR.assets.load_feeds(function() {
var feed = NEWSBLUR.assets.get_feed(data.new_feed_id || feed_id);
console.log(["Loading feed", data.new_feed_id || feed_id, feed]);
NEWSBLUR.reader.open_feed(feed.id);
});
}
var feed = NEWSBLUR.assets.get_feed(data.new_feed_id || feed_id);
console.log(["feed address", feed, NEWSBLUR.assets.get_feed(feed_id)]);
if (!data || data.code < 0 || !data.new_feed_id) {
var error = data.message || "There was a problem fetching the feed from this URL.";
@ -348,10 +358,19 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
if (feed_link.length) {
this.model.save_exception_change_feed_link(feed_id, feed_link, _.bind(function(data) {
if (data.new_feed_id) {
NEWSBLUR.reader.force_feed_refresh(feed_id, data.new_feed_id);
var old_feed = NEWSBLUR.assets.get_feed(feed_id);
if (data.new_feed_id != feed_id && old_feed.get('selected')) {
old_feed.set('selected', false);
}
if (data && data.new_feed_id) {
NEWSBLUR.assets.load_feeds(function() {
var feed = NEWSBLUR.assets.get_feed(data.new_feed_id || feed_id);
console.log(["Loading feed", data.new_feed_id || feed_id, feed]);
NEWSBLUR.reader.open_feed(feed.id);
});
}
var feed = NEWSBLUR.assets.get_feed(data.new_feed_id) || NEWSBLUR.assets.get_feed(feed_id);
if (!data || data.code < 0 || !data.new_feed_id) {

View file

@ -145,6 +145,20 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
])
])
]),
$.make('div', { className: 'NB-preference-options' }, [
$.make('div', [
$.make('input', { id: 'NB-preference-dateformat-1', type: 'radio', name: 'dateformat', value: '12' }),
$.make('label', { 'for': 'NB-preference-dateformat-1' }, [
'Use 12-hour clock'
])
]),
$.make('div', [
$.make('input', { id: 'NB-preference-dateformat-2', type: 'radio', name: 'dateformat', value: '24' }),
$.make('label', { 'for': 'NB-preference-dateformat-2' }, [
'Use 24-hour clock'
])
])
]),
$.make('div', { className: 'NB-preference-label'}, [
'Timezone'
])
@ -945,6 +959,12 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
return false;
}
});
$('input[name=dateformat]', $modal).each(function() {
if ($(this).val() == ""+NEWSBLUR.Preferences.dateformat) {
$(this).attr('checked', true);
return false;
}
});
$('input[name=folder_counts]', $modal).each(function() {
if ($(this).val() == ""+NEWSBLUR.Preferences.folder_counts) {
$(this).attr('checked', true);

View file

@ -259,9 +259,12 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
NEWSBLUR.assets.social_feeds.selected() ||
NEWSBLUR.assets.starred_feeds.selected();
if (!model) return;
var feed_view = _.detect(model.views, _.bind(function(view) {
return !!view.$el.closest(this.$s.$feed_lists).length;
}, this));
var feed_view = model.get("selected_title_view");
if (!feed_view) {
feed_view = _.detect(model.views, _.bind(function(view) {
return !!view.$el.closest(this.$s.$feed_lists).length;
}, this));
}
if (!feed_view) return;
if (!$feed_lists.isScrollVisible(feed_view.$el)) {

View file

@ -168,10 +168,10 @@ NEWSBLUR.Views.FeedSelector = Backbone.View.extend({
var feed_id = this.$next_feed.data('id');
if (_.string.include(feed_id, 'social:')) {
NEWSBLUR.reader.open_social_stories(this.$next_feed.data('id'), {
$feed_link: this.$next_feed
$feed: this.$next_feed
});
} else {
NEWSBLUR.reader.open_feed(this.$next_feed.data('id'), this.$next_feed);
NEWSBLUR.reader.open_feed(this.$next_feed.data('id'), {$feed: this.$next_feed});
}
e.preventDefault();

View file

@ -54,7 +54,7 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
this.add_extra_classes();
if (!options.instant) this.flash_changes();
} else if (only_selected_changed) {
this.select_feed();
this.select_feed(options);
} else {
this.render();
if (!options.instant && counts_changed) this.flash_changes();
@ -200,7 +200,7 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
}
},
select_feed: function() {
select_feed: function(options) {
this.$el.toggleClass('selected', this.model.get('selected'));
this.$el.toggleClass('NB-selected', this.model.get('selected'));
@ -267,7 +267,8 @@ NEWSBLUR.Views.FeedTitleView = Backbone.View.extend({
} else if (this.model.is_starred()) {
NEWSBLUR.reader.open_starred_stories({
tag: this.model.tag_slug(),
model: this.model
model: this.model,
$feed: this.$el
});
} else {
NEWSBLUR.reader.open_feed(this.model.id, {$feed: this.$el});

View file

@ -156,16 +156,14 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
<div class="NB-feed-story-manage-icon"></div>\
<a class="NB-feed-story-title" href="<%= story.get("story_permalink") %>"><%= title %></a>\
</div>\
<% if (story.get("long_parsed_date")) { %>\
<div class="NB-feed-story-date">\
<% if (story.has_modifications()) { %>\
<div class="NB-feed-story-hide-changes" \
title="<%= NEWSBLUR.assets.preference("hide_story_changes") ? "Show" : "Hide" %> story modifications">\
</div>\
<% } %>\
<%= story.get("long_parsed_date") %>\
</div>\
<% } %>\
<div class="NB-feed-story-date">\
<% if (story.has_modifications()) { %>\
<div class="NB-feed-story-hide-changes" \
title="<%= NEWSBLUR.assets.preference("hide_story_changes") ? "Show" : "Hide" %> story modifications">\
</div>\
<% } %>\
<%= story.formatted_long_date() %>\
</div>\
<% if (story.get("story_authors")) { %>\
<div class="NB-feed-story-author-wrapper">\
<span class="NB-middot">&middot;</span>\

View file

@ -229,7 +229,7 @@ NEWSBLUR.Views.StorySaveView = Backbone.View.extend({
// Reset story content height to get an accurate height measurement.
$story_content.stop(true, true).css('height', 'auto');
$story_content.removeData('original_height');
this.resize({change_tag: true});
},

View file

@ -54,7 +54,7 @@ NEWSBLUR.Views.StoryTitleView = Backbone.View.extend({
<span class="NB-storytitles-title"><%= story.get("story_title") %></span>\
<span class="NB-storytitles-author"><%= story.get("story_authors") %></span>\
</a>\
<span class="story_date NB-hidden-fade"><%= story.get("short_parsed_date") %></span>\
<span class="story_date NB-hidden-fade"><%= story.formatted_short_date() %></span>\
<% if (story.get("comment_count_friends")) { %>\
<div class="NB-storytitles-shares">\
<% _.each(story.get("commented_by_friends"), function(user_id) { %>\

View file

@ -51,6 +51,7 @@ NEWSBLUR.Views.TextTabView = Backbone.View.extend({
}).render();
this.$el.html(this.story_detail.el);
this.$el.scrollTop(0);
this.story_detail.attach_handlers();
this.show_loading();
NEWSBLUR.assets.fetch_original_text(story.get('id'), story.get('story_feed_id'),
this.render, this.error);
@ -109,6 +110,7 @@ NEWSBLUR.Views.TextTabView = Backbone.View.extend({
var $content = this.$('.NB-feed-story-content');
$content.html(this.story.get('story_content'));
this.story_detail.attach_handlers();
},
append_premium_only_notification: function() {

82
media/js/vendor/date.format.js vendored Normal file
View file

@ -0,0 +1,82 @@
(function() {
// defining patterns
var replaceChars = {
shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
// Day
d: function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
D: function() { return replaceChars.shortDays[this.getDay()]; },
j: function() { return this.getDate(); },
l: function() { return replaceChars.longDays[this.getDay()]; },
N: function() { return this.getDay() + 1; },
S: function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); },
w: function() { return this.getDay(); },
z: function() { var d = new Date(this.getFullYear(),0,1); return Math.ceil((this - d) / 86400000); }, // Fixed now
// Week
W: function() { var d = new Date(this.getFullYear(), 0, 1); return Math.ceil((((this - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
// Month
F: function() { return replaceChars.longMonths[this.getMonth()]; },
m: function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); },
M: function() { return replaceChars.shortMonths[this.getMonth()]; },
n: function() { return this.getMonth() + 1; },
t: function() { var d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 0).getDate() }, // Fixed now, gets #days of date
// Year
L: function() { var year = this.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); }, // Fixed now
o: function() { var d = new Date(this.valueOf()); d.setDate(d.getDate() - ((this.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
Y: function() { return this.getFullYear(); },
y: function() { return ('' + this.getFullYear()).substr(2); },
// Time
a: function() { return this.getHours() < 12 ? 'am' : 'pm'; },
A: function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
B: function() { return Math.floor((((this.getUTCHours() + 1) % 24) + this.getUTCMinutes() / 60 + this.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
g: function() { return this.getHours() % 12 || 12; },
G: function() { return this.getHours(); },
h: function() { return ((this.getHours() % 12 || 12) < 10 ? '0' : '') + (this.getHours() % 12 || 12); },
H: function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
i: function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
s: function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
u: function() { var m = this.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ?
'0' : '')) + m; },
// Timezone
e: function() { return "Not Yet Supported"; },
I: function() {
var DST = null;
for (var i = 0; i < 12; ++i) {
var d = new Date(this.getFullYear(), i, 1);
var offset = d.getTimezoneOffset();
if (DST === null) DST = offset;
else if (offset < DST) { DST = offset; break; } else if (offset > DST) break;
}
return (this.getTimezoneOffset() == DST) | 0;
},
O: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; },
P: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
T: function() { var m = this.getMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;},
Z: function() { return -this.getTimezoneOffset() * 60; },
// Full Date/Time
c: function() { return this.format("Y-m-d\\TH:i:sP"); }, // Fixed now
r: function() { return this.toString(); },
U: function() { return this.getTime() / 1000; }
};
// Simulates PHP's date function
Date.prototype.format = function(format) {
var returnStr = '';
var replace = replaceChars;
for (var i = 0; i < format.length; i++) { var curChar = format.charAt(i); if (i - 1 >= 0 && format.charAt(i - 1) == "\\") {
returnStr += curChar;
}
else if (replace[curChar]) {
returnStr += replace[curChar].call(this);
} else if (curChar != "\\"){
returnStr += curChar;
}
}
return returnStr;
};
}).call(this);

View file

@ -1,104 +0,0 @@
/**
* Version: 1.0 Alpha-1
* Build Date: 13-Nov-2007
* Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved.
* License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
* Website: http://www.datejs.com/ or http://www.coolite.com/datejs/
*/
Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}};
Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
return-1;};Date.getDayNumberFromName=function(name){var n=Date.CultureInfo.dayNames,m=Date.CultureInfo.abbreviatedDayNames,o=Date.CultureInfo.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
return-1;};Date.isLeapYear=function(year){return(((year%4===0)&&(year%100!==0))||(year%400===0));};Date.getDaysInMonth=function(year,month){return[31,(Date.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};Date.getTimezoneOffset=function(s,dst){return(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST[s.toUpperCase()]:Date.CultureInfo.abbreviatedTimeZoneStandard[s.toUpperCase()];};Date.getTimezoneAbbreviation=function(offset,dst){var n=(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST:Date.CultureInfo.abbreviatedTimeZoneStandard,p;for(p in n){if(n[p]===offset){return p;}}
return null;};Date.prototype.clone=function(){return new Date(this.getTime());};Date.prototype.compareTo=function(date){if(isNaN(this)){throw new Error(this);}
if(date instanceof Date&&!isNaN(date)){return(this>date)?1:(this<date)?-1:0;}else{throw new TypeError(date);}};Date.prototype.equals=function(date){return(this.compareTo(date)===0);};Date.prototype.between=function(start,end){var t=this.getTime();return t>=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);}
if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);}
if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);}
if(x.hour||x.hours){this.addHours(x.hour||x.hours);}
if(x.month||x.months){this.addMonths(x.month||x.months);}
if(x.year||x.years){this.addYears(x.year||x.years);}
if(x.day||x.days){this.addDays(x.day||x.days);}
return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(value<min||value>max){throw new RangeError(value+" is not a valid value for "+name+".");}
return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;}
if(!x.second&&x.second!==0){x.second=-1;}
if(!x.minute&&x.minute!==0){x.minute=-1;}
if(!x.hour&&x.hour!==0){x.hour=-1;}
if(!x.day&&x.day!==0){x.day=-1;}
if(!x.month&&x.month!==0){x.month=-1;}
if(!x.year&&x.year!==0){x.year=-1;}
if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());}
if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());}
if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());}
if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());}
if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());}
if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());}
if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());}
if(x.timezone){this.setTimezone(x.timezone);}
if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);}
return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;}
var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}}
return w;};Date.prototype.isDST=function(){console.log('isDST');return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();};
Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i<dx.length;i++){$D[dx[i]]=$D[dx[i].substring(0,3)]=df(i);}
var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
return this.moveToMonth(n,this._orient);};};for(var j=0;j<mx.length;j++){$D[mx[j]]=$D[mx[j].substring(0,3)]=mf(j);}
var ef=function(j){return function(){if(j.substring(j.length-1)!="s"){j+="s";}
return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$D[de]=$D[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}}());Date.prototype.toJSONString=function(){return this.toString("yyyy-MM-ddThh:mm:ssZ");};Date.prototype.toShortDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortDatePattern);};Date.prototype.toLongDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.longDatePattern);};Date.prototype.toShortTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortTimePattern);};Date.prototype.toLongTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.longTimePattern);};Date.prototype.getOrdinal=function(){switch(this.getDate()){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};
(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
break;}
return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
rx.push(r[0]);s=r[1];}
return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){r=null;}
if(r){return r;}}
throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
rx.push(r[0]);s=r[1];}
return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
s=q[1];}
if(!r){throw new $P.Exception(s);}
if(q){throw new $P.Exception(q[1]);}
if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
if(!last&&q[1].length===0){last=true;}
if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
if(rx[1].length<best[1].length){best=rx;}
if(best[1].length===0){break;}}
if(best[0].length===0){return best;}
if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
best[1]=q[1];}
return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
return rx;};Date.Grammar={};Date.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=((s.length==3)?Date.getMonthNumberFromName(s):(Number(s)-1));};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<Date.CultureInfo.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];var now=new Date();this.year=now.getFullYear();this.month=now.getMonth();this.day=1;this.hour=0;this.minute=0;this.second=0;for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
this.hour=(this.meridian=="p"&&this.hour<13)?this.hour+12:this.hour;if(this.day>Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
if(this.now){return new Date();}
var today=Date.today();var method=null;var expression=!!(this.days!=null||this.orient||this.operator);if(expression){var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(this.weekday){this.unit="day";gap=(Date.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
if(this.month){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
if(!this.unit){this.unit="day";}
if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
if(this.unit=="week"){this.unit="day";this.value=this.value*7;}
this[this.unit+"s"]=this.value*orient;}
return today.add(this);}else{if(this.meridian&&this.hour){this.hour=(this.hour<13&&this.meridian=="p")?this.hour+12:this.hour;}
if(this.weekday&&!this.day){this.day=(today.addDays((Date.getDayNumberFromName(this.weekday)-today.getDay()))).getDate();}
if(this.month&&!this.day){this.day=1;}
return today.set(this);}}};var _=Date.Parsing.Operators,g=Date.Grammar,t=Date.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=Date.CultureInfo.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
fn=_C[keys]=_.any.apply(null,px);}
return fn;};g.ctoken2=function(key){return _.rtoken(Date.CultureInfo.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.mm,g.ss],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^(\+|\-)?\s*\d\d\d\d?/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^(\+|\-)\s*\d\d\d\d/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[Date.CultureInfo.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw Date.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["yyyy-MM-ddTHH:mm:ss","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
return g._start.call({},s);};}());Date._parse=Date.parse;Date.parse=function(s){var r=null;if(!s){return null;}
try{r=Date.Grammar.start.call({},s);}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};Date.getParseFunction=function(fx){var fn=Date.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};};Date.parseExact=function(s,fx){return Date.getParseFunction(fx)(s);};

View file

@ -8,7 +8,7 @@ import os
CURRENT_DIR = os.path.dirname(__file__)
NEWSBLUR_DIR = CURRENT_DIR
TEMPLATE_DIRS = (os.path.join(CURRENT_DIR, 'templates'),
os.path.join(CURRENT_DIR, 'vendor/zebra/templates'),)
os.path.join(CURRENT_DIR, 'vendor/zebra/templates'))
MEDIA_ROOT = os.path.join(CURRENT_DIR, 'media')
STATIC_ROOT = os.path.join(CURRENT_DIR, 'static')
UTILS_ROOT = os.path.join(CURRENT_DIR, 'utils')
@ -64,12 +64,13 @@ LANGUAGE_CODE = 'en-us'
SITE_ID = 1
USE_I18N = False
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/reader/login'
LOGIN_URL = '/account/login'
MEDIA_URL = '/media/'
STATIC_URL = '/media/'
STATIC_ROOT = '/media/'
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/admin/'
CIPHER_USERNAMES = False
DEBUG_ASSETS = DEBUG
HOMEPAGE_USERNAME = 'popular'
@ -100,6 +101,7 @@ MIDDLEWARE_CLASSES = (
'django.middleware.gzip.GZipMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'apps.profile.middleware.TimingMiddleware',
'apps.profile.middleware.LastSeenMiddleware',
@ -108,10 +110,27 @@ MIDDLEWARE_CLASSES = (
'subdomains.middleware.SubdomainMiddleware',
'apps.profile.middleware.SimpsonsMiddleware',
'apps.profile.middleware.ServerHostnameMiddleware',
'corsheaders.middleware.CorsMiddleware',
'oauth2_provider.middleware.OAuth2TokenMiddleware',
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
)
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
AUTHENTICATION_BACKENDS = (
'oauth2_provider.backends.OAuth2Backend',
'django.contrib.auth.backends.ModelBackend',
)
CORS_ORIGIN_ALLOW_ALL = True
OAUTH2_PROVIDER = {
'SCOPES': {
'read': 'View new unread stories, saved stories, and shared stories.',
'write': 'Create new saved stories, shared stories, and subscriptions.',
'ifttt': 'Pair your NewsBlur account with other IFTTT channels.',
},
'CLIENT_ID_GENERATOR_CLASS': 'oauth2_provider.generators.ClientIdGenerator',
'ACCESS_TOKEN_EXPIRE_SECONDS': 60*60*24*365*10 # 10 years
}
# ===========
# = Logging =
@ -264,6 +283,8 @@ INSTALLED_APPS = (
'vendor.paypal.standard.ipn',
'vendor.zebra',
'vendor.haystack',
'oauth2_provider',
'corsheaders',
)
# ==========
@ -521,9 +542,8 @@ DEBUG_TOOLBAR_CONFIG = {
if DEBUG:
TEMPLATE_LOADERS = (
('django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
),
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
else:
TEMPLATE_LOADERS = (

View file

@ -0,0 +1,45 @@
{% extends 'base.html' %}
{% load typogrify_tags utils_tags zebra_tags %}
{% block bodyclass %}NB-static NB-static-oauth NB-static-login{% endblock %}
{% block extra_head_js %}
<script type="text/javascript" charset="utf-8">
$(document).ready(function() {
$("input[name=username]").focus();
});
</script>
{% include_stylesheets "common" %}
{% endblock %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="NB-static-title">
Login
</div>
<div class="NB-static-form-wrapper" style="overflow:hidden">
<form method="post" class="NB-static-form" action="{% url 'django.contrib.auth.views.login' %}">
{% if form.errors %}
<p class="NB-error error">Your username and password didn't match.<br />Please try again.</p>
{% else %}{% if next %}
<p class="NB-error error">Please login to continue.</p>
{% endif %}{% endif %}
{% csrf_token %}
<div class="NB-static-form-label">{{ form.username.label_tag }}</div>
<div class="NB-static-form-input">{{ form.username }}</div>
<div class="NB-static-form-label">{{ form.password.label_tag }}</div>
<div class="NB-static-form-input">{{ form.password }}</div>
<input type="submit" value="login" class="NB-modal-submit-button NB-modal-submit-green NB-static-form-submit" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
</div>
{% endblock %}

View file

@ -59,6 +59,7 @@
'hide_story_changes' : 1,
'feed_view_single_story' : 0,
'animations' : true,
'dateformat' : "12",
'folder_counts' : false,
'send_emails' : {{ user_profile.send_emails|yesno:"true,false" }},
'email_cc' : true,

View file

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html lang="en">
<head>
@ -84,8 +85,7 @@
<img src="/media/img/logo_512.png" class="logo">
<h1>NewsBlur is in <span class="error404">maintenance mode</span></h1>
<div class="description">
<p>This will take about 2 minutes. About a week ago my primary Redis server became big enough to consume the entire machine, which is a one way ticket to getting killed automatically by the machine. The last four outages, three lasting less than 2 minutes and this morning's bigger outage, were all caused by this server toppling over.</p>
<p>I am performing the simple fix right now by moving it to a bigger machine. I am also performing the more complicated fix by concurrently writing data with a smaller footprint to another server. But this more complicated solution takes 14 days to run and won't complete until July 15th. I was hoping that I could avoid the simple fix and just wait until the 15th, but four outages is more than enough to convince me.</p>
<p>This will take about 5 minutes. This is one of those maintenance modes that you should be happy about, since I'm adding some new tables to the database. I can't say exactly what these tables are for (although if you care enough, <a href="http://github.com/samuelclay">check the source code on GitHub</a>). But if you're patient and can wait a few weeks, you'll be delighted by this mega-huge feature I've wanted to build for over two years now.</p>
<p>To pass the time, <a href="http://mlkshk.com/popular">check out what's popular on MLKSHK</a>.</p>
</div>
</div>

View file

@ -0,0 +1,19 @@
{% extends "oauth2_provider/base.html" %}
{% load i18n %}
{% load url from future %}
{% block content %}
<div class="block-center">
<h3 class="block-center-heading">{% trans "Are you sure to delete the application" %} {{ application.name }}?</h3>
<form method="post" action="{% url 'oauth2_provider:delete' application.pk %}">
{% csrf_token %}
<div class="control-group">
<div class="controls">
<a class="btn btn-large" href="{% url "oauth2_provider:list" %}">{% trans "Cancel" %}</a>
<input type="submit" class="btn btn-large btn-danger" name="allow" value="{% trans "Delete" %}"/>
</div>
</div>
</form>
</div>
{% endblock content %}

View file

@ -0,0 +1,42 @@
{% extends "oauth2_provider/base.html" %}
{% load i18n %}
{% load url from future %}
{% block content %}
<div class="block-center">
<h3 class="block-center-heading">{{ application.name }}</h3>
<ul class="unstyled">
<li>
<p><b>{% trans "Client id" %}</b></p>
<input class="input-block-level" type="text" value="{{ application.client_id }}" readonly>
</li>
<li>
<p><b>{% trans "Client secret" %}</b></p>
<input class="input-block-level" type="text" value="{{ application.client_secret }}" readonly>
</li>
<li>
<p><b>{% trans "Client type" %}</b></p>
<p>{{ application.client_type }}</p>
</li>
<li>
<p><b>{% trans "Authorization Grant Type" %}</b></p>
<p>{{ application.authorization_grant_type }}</p>
</li>
<li>
<p><b>{% trans "Redirect Uris" %}</b></p>
<textarea class="input-block-level" readonly>{{ application.redirect_uris }}</textarea>
</li>
</ul>
<div class="btn-toolbar">
<a class="btn" href="{% url "oauth2_provider:list" %}">{% trans "Go Back" %}</a>
<a class="btn btn-primary" href="{% url "oauth2_provider:update" application.id %}">{% trans "Edit" %}</a>
<a class="btn btn-danger" href="{% url "oauth2_provider:delete" application.id %}">{% trans "Delete" %}</a>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,43 @@
{% extends "oauth2_provider/base.html" %}
{% load i18n %}
{% load url from future %}
{% block content %}
<div class="block-center">
<form class="form-horizontal" method="post" action="{% block app-form-action-url %}{% url 'oauth2_provider:update' application.id %}{% endblock app-form-action-url %}">
<h3 class="block-center-heading">
{% block app-form-title %}
{% trans "Edit application" %} {{ application.name }}
{% endblock app-form-title %}
</h3>
{% csrf_token %}
{% for field in form %}
<div class="control-group {% if field.errors %}error{% endif %}">
<label class="control-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="controls">
{{ field }}
{% for error in field.errors %}
<span class="help-inline">{{ error }}</span>
{% endfor %}
</div>
</div>
{% endfor %}
<div class="control-group {% if form.non_field_errors %}error{% endif %}">
{% for error in form.non_field_errors %}
<span class="help-inline">{{ error }}</span>
{% endfor %}
</div>
<div class="control-group">
<div class="controls">
<a class="btn" href="{% block app-form-back-url %}{% url "oauth2_provider:detail" application.id %}{% endblock app-form-back-url %}">
{% trans "Go Back" %}
</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,20 @@
{% extends "oauth2_provider/base.html" %}
{% load i18n %}
{% load url from future %}
{% block content %}
<div class="block-center">
<h3 class="block-center-heading">{% trans "Your applications" %}</h3>
{% if applications %}
<ul>
{% for application in applications %}
<li><a href="{{ application.get_absolute_url }}">{{ application.name }}</a></li>
{% endfor %}
</ul>
<a class="btn btn-success" href="{% url "oauth2_provider:register" %}">New Application</a>
{% else %}
<p>{% trans "No applications defined" %}. <a href="{% url 'oauth2_provider:register' %}">{% trans "Click here" %}</a> {% trans "if you want to register a new one" %}</p>
{% endif %}
</div>
{% endblock content %}

View file

@ -0,0 +1,10 @@
{% extends "oauth2_provider/application_form.html" %}
{% load i18n %}
{% load url from future %}
{% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %}
{% block app-form-action-url %}{% url 'oauth2_provider:register' %}{% endblock app-form-action-url %}
{% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %}

View file

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block bodyclass %}NB-static NB-static-oauth{% endblock %}
{% load i18n %}
{% block content %}
<div class="NB-static-title">
Authorize {{ application.name }}
</div>
<div class="NB-static-form-wrapper block-center">
{% if not error %}
<form id="authorizationForm" method="post" class="NB-static-form">
<h3 class="block-center-heading">{{ application.name }} would like to access your NewsBlur account</h3>
{% csrf_token %}
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% endif %}
{% endfor %}
<p>{{ application.name }} is requesting these permissions:</p>
<ul>
{% for scope in scopes_descriptions %}
<li>{{ scope }}</li>
{% endfor %}
</ul>
{{ form.errors }}
{{ form.non_field_errors }}
<div class="control-group">
<div class="controls">
<input type="submit" class="NB-static-form-submit NB-modal-submit-button NB-modal-submit-grey" value="Deny" style="float: right"/>
<input type="submit" class="NB-static-form-submit NB-modal-submit-button NB-modal-submit-green" name="allow" value="Authorize" style="float: left" />
</div>
</div>
</form>
{% else %}
<h2>Error: {{ error.error }}</h2>
<p>{{ error.description }}</p>
{% endif %}
</div>
{% endblock %}

View file

@ -70,7 +70,9 @@
<div class="NB-module-header-signup">Sign up</div>
</div>
<div class="NB-login">
<form method="post" action="{% url "login" %}">
<form method="post" action="{% url "welcome-login" %}">
{% csrf_token %}
<div>
{{ login_form.username.label_tag }}
{{ login_form.username }}
@ -97,7 +99,9 @@
</div>
<div class="NB-signup">
<form method="post" action="{% url "signup" %}">
<form method="post" action="{% url "welcome-signup" %}">
{% csrf_token %}
<div>
{{ signup_form.username.label_tag }}
{{ signup_form.username }}

12
urls.py
View file

@ -3,6 +3,9 @@ from django.conf import settings
from apps.reader import views as reader_views
from apps.social import views as social_views
from apps.static import views as static_views
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
url(r'^$', reader_views.index, name='index'),
@ -32,6 +35,7 @@ urlpatterns = patterns('',
(r'^push/', include('apps.push.urls')),
(r'^categories/', include('apps.categories.urls')),
(r'^_haproxychk', static_views.haproxy_check),
url(r'^admin/', include(admin.site.urls)),
url(r'^about/?', static_views.about, name='about'),
url(r'^faq/?', static_views.faq, name='faq'),
url(r'^api/?', static_views.api, name='api'),
@ -46,6 +50,14 @@ urlpatterns = patterns('',
url(r'^android/?', static_views.android, name='android-static'),
url(r'^firefox/?', static_views.firefox, name='firefox'),
url(r'zebra/', include('zebra.urls', namespace="zebra", app_name='zebra')),
url(r'^account/login/?$',
'django.contrib.auth.views.login',
{'template_name': 'accounts/login.html'}, name='login'),
url(r'^account/logout/?$',
'django.contrib.auth.views.logout',
{'next_page': '/'}, name='logout'),
url(r'^account/ifttt/v1/', include('apps.oauth.urls')),
url(r'^account/', include('oauth2_provider.urls', namespace='oauth2_provider')),
)
if settings.DEBUG:

View file

@ -330,4 +330,4 @@ def htmldiff(old_html, new_html):
result = htmldiff_tokens(old_html_tokens, new_html_tokens)
result = ''.join(result).strip()
return fixup_ins_del_tags(result)
return fixup_ins_del_tags(result)

View file

@ -60,18 +60,24 @@ class required_params(object):
def view_wrapper(self, request, fn, *args, **kwargs):
if request.method != self.method and self.method != 'REQUEST':
return self.disallowed(method=True, status_code=405)
# Check if parameter is included
for param in self.params:
if not getattr(request, self.method).get(param):
if getattr(request, self.method).get(param) is None:
print " Unnamed parameter not found: %s" % param
return self.disallowed(param)
# Check if parameter is correct type
for param, param_type in self.named_params.items():
if not getattr(request, self.method).get(param):
if getattr(request, self.method).get(param) is None:
print " Typed parameter not found: %s" % param
return self.disallowed(param)
try:
if not param_type(getattr(request, self.method).get(param)):
if param_type(getattr(request, self.method).get(param)) is None:
print " Typed parameter wrong: %s" % param
return self.disallowed(param, param_type)
except (TypeError, ValueError):
except (TypeError, ValueError), e:
print " %s -> %s" % (param, e)
return self.disallowed(param, param_type)
return fn(request, *args, **kwargs)

View file

@ -5,7 +5,7 @@
"""
Tweepy Twitter API library
"""
__version__ = '2.0'
__version__ = '2.2'
__author__ = 'Joshua Roesslein'
__license__ = 'MIT'
@ -13,7 +13,7 @@ from tweepy.models import Status, User, DirectMessage, Friendship, SavedSearch,
from tweepy.error import TweepError
from tweepy.api import API
from tweepy.cache import Cache, MemoryCache, FileCache
from tweepy.auth import BasicAuthHandler, OAuthHandler
from tweepy.auth import OAuthHandler
from tweepy.streaming import Stream, StreamListener
from tweepy.cursor import Cursor

84
vendor/tweepy/api.py vendored
View file

@ -57,14 +57,6 @@ class API(object):
require_auth = True
)
"""/statuses/:id/retweeted_by.format"""
retweeted_by = bind_api(
path = '/statuses/{id}/retweeted_by.json',
payload_type = 'status', payload_list = True,
allowed_param = ['id', 'count', 'page'],
require_auth = True
)
"""/related_results/show/:id.format"""
related_results = bind_api(
path = '/related_results/show/{id}.json',
@ -73,14 +65,6 @@ class API(object):
require_auth = False
)
"""/statuses/:id/retweeted_by/ids.format"""
retweeted_by_ids = bind_api(
path = '/statuses/{id}/retweeted_by/ids.json',
payload_type = 'ids',
allowed_param = ['id', 'count', 'page'],
require_auth = True
)
""" statuses/retweets_of_me """
retweets_of_me = bind_api(
path = '/statuses/retweets_of_me.json',
@ -105,6 +89,22 @@ class API(object):
require_auth = True
)
""" statuses/update_with_media """
def update_with_media(self, filename, *args, **kwargs):
headers, post_data = API._pack_image(filename, 3072, form_field='media[]')
kwargs.update({'headers': headers, 'post_data': post_data})
return bind_api(
path='/statuses/update_with_media.json',
method = 'POST',
payload_type='status',
allowed_param = [
'status', 'possibly_sensitive', 'in_reply_to_status_id', 'lat', 'long',
'place_id', 'display_coordinates'
],
require_auth=True
)(self, *args, **kwargs)
""" statuses/destroy """
destroy_status = bind_api(
path = '/statuses/destroy/{id}.json',
@ -131,6 +131,12 @@ class API(object):
require_auth = True
)
retweeters = bind_api(
path = '/statuses/retweeters/ids.json',
payload_type = 'ids',
allowed_param = ['id', 'cursor', 'stringify_ids']
)
""" users/show """
get_user = bind_api(
path = '/users/show.json',
@ -310,7 +316,8 @@ class API(object):
followers = bind_api(
path = '/followers/list.json',
payload_type = 'user', payload_list = True,
allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
allowed_param = ['id', 'user_id', 'screen_name', 'cursor', 'count',
'skip_status', 'include_user_entities']
)
""" account/verify_credentials """
@ -376,6 +383,17 @@ class API(object):
require_auth = True
)(self, post_data=post_data, headers=headers)
""" account/update_profile_banner """
def update_profile_banner(self, filename, *args, **kargs):
headers, post_data = API._pack_image(filename, 700, form_field="banner")
bind_api(
path = '/account/update_profile_banner.json',
method = 'POST',
allowed_param = ['width', 'height', 'offset_left', 'offset_right'],
require_auth = True
)(self, post_data=post_data, headers=headers)
""" account/update_profile """
update_profile = bind_api(
path = '/account/update_profile.json',
@ -485,16 +503,6 @@ class API(object):
require_auth = True
)
""" help/test """
def test(self):
try:
bind_api(
path = '/help/test.json',
)(self)
except TweepError:
return False
return True
create_list = bind_api(
path = '/lists/create.json',
method = 'POST',
@ -543,7 +551,7 @@ class API(object):
list_timeline = bind_api(
path = '/lists/statuses.json',
payload_type = 'status', payload_list = True,
allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id', 'since_id', 'max_id', 'count']
allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id', 'since_id', 'max_id', 'count', 'include_rts']
)
get_list = bind_api(
@ -630,7 +638,7 @@ class API(object):
search = bind_api(
path = '/search/tweets.json',
payload_type = 'search_results',
allowed_param = ['q', 'lang', 'locale', 'since_id', 'geocode', 'show_user', 'max_id', 'since', 'until', 'result_type']
allowed_param = ['q', 'lang', 'locale', 'since_id', 'geocode', 'max_id', 'since', 'until', 'result_type', 'count', 'include_entities', 'from', 'to', 'source']
)
""" trends/daily """
@ -675,9 +683,23 @@ class API(object):
allowed_param = ['lat', 'long', 'name', 'contained_within']
)
""" help/languages.json """
supported_languages = bind_api(
path = '/help/languages.json',
payload_type = 'json',
require_auth = True
)
""" help/configuration """
configuration = bind_api(
path = '/help/configuration.json',
payload_type = 'json',
require_auth = True
)
""" Internal use only """
@staticmethod
def _pack_image(filename, max_size):
def _pack_image(filename, max_size, form_field="image"):
"""Pack image from file into multipart-formdata post body"""
# image must be less than 700kb in size
try:
@ -699,7 +721,7 @@ class API(object):
BOUNDARY = 'Tw3ePy'
body = []
body.append('--' + BOUNDARY)
body.append('Content-Disposition: form-data; name="image"; filename="%s"' % filename)
body.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (form_field, filename))
body.append('Content-Type: %s' % file_type)
body.append('')
body.append(fp.read())

23
vendor/tweepy/auth.py vendored
View file

@ -21,26 +21,19 @@ class AuthHandler(object):
raise NotImplementedError
class BasicAuthHandler(AuthHandler):
def __init__(self, username, password):
self.username = username
self._b64up = base64.b64encode('%s:%s' % (username, password))
def apply_auth(self, url, method, headers, parameters):
headers['Authorization'] = 'Basic %s' % self._b64up
def get_username(self):
return self.username
class OAuthHandler(AuthHandler):
"""OAuth authentication handler"""
OAUTH_HOST = 'api.twitter.com'
OAUTH_ROOT = '/oauth/'
def __init__(self, consumer_key, consumer_secret, callback=None, secure=False):
def __init__(self, consumer_key, consumer_secret, callback=None, secure=True):
if type(consumer_key) == unicode:
consumer_key = bytes(consumer_key)
if type(consumer_secret) == unicode:
consumer_secret = bytes(consumer_secret)
self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
self._sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1()
self.request_token = None
@ -49,7 +42,7 @@ class OAuthHandler(AuthHandler):
self.username = None
self.secure = secure
def _get_oauth_url(self, endpoint, secure=False):
def _get_oauth_url(self, endpoint, secure=True):
if self.secure or secure:
prefix = 'https://'
else:

View file

@ -104,6 +104,8 @@ def bind_api(**config):
self.path = self.path.replace(variable, value)
def execute(self):
self.api.cached_result = False
# Build the request URL
url = self.api_root + self.path
if len(self.parameters):
@ -123,6 +125,7 @@ def bind_api(**config):
else:
if isinstance(cache_result, Model):
cache_result._api = self.api
self.api.cached_result = True
return cache_result
# Continue attempting request until successful
@ -165,7 +168,7 @@ def bind_api(**config):
# If an error was returned, throw an exception
self.api.last_response = resp
if resp.status != 200:
if resp.status and not 200 <= resp.status < 300:
try:
error_msg = self.api.parser.parse_error(resp.read())
except Exception:

View file

@ -53,8 +53,9 @@ class CursorIterator(BaseIterator):
def __init__(self, method, args, kargs):
BaseIterator.__init__(self, method, args, kargs)
self.next_cursor = -1
self.prev_cursor = 0
start_cursor = kargs.pop('cursor', None)
self.next_cursor = start_cursor or -1
self.prev_cursor = start_cursor or 0
self.count = 0
def next(self):
@ -84,9 +85,13 @@ class IdIterator(BaseIterator):
BaseIterator.__init__(self, method, args, kargs)
self.max_id = kargs.get('max_id')
self.since_id = kargs.get('since_id')
self.count = 0
def next(self):
"""Fetch a set of items with IDs less than current set."""
if self.limit and self.limit == self.count:
raise StopIteration
# max_id is inclusive so decrement by one
# to avoid requesting duplicate items.
max_id = self.since_id - 1 if self.max_id else None
@ -95,16 +100,21 @@ class IdIterator(BaseIterator):
raise StopIteration
self.max_id = data.max_id
self.since_id = data.since_id
self.count += 1
return data
def prev(self):
"""Fetch a set of items with IDs greater than current set."""
if self.limit and self.limit == self.count:
raise StopIteration
since_id = self.max_id
data = self.method(since_id = since_id, *self.args, **self.kargs)
if len(data) == 0:
raise StopIteration
self.max_id = data.max_id
self.since_id = data.since_id
self.count += 1
return data
class PageIterator(BaseIterator):

View file

@ -3,8 +3,7 @@
# See LICENSE for details.
from tweepy.error import TweepError
from tweepy.utils import parse_datetime, parse_html_value, parse_a_href, \
parse_search_datetime, unescape_html
from tweepy.utils import parse_datetime, parse_html_value, parse_a_href
class ResultSet(list):
@ -67,7 +66,7 @@ class Status(Model):
status = cls(api)
for k, v in json.items():
if k == 'user':
user_model = getattr(api.parser.model_factory, 'user')
user_model = getattr(api.parser.model_factory, 'user') if api else User
user = user_model.parse(api, v)
setattr(status, 'author', user)
setattr(status, 'user', user) # DEPRECIATED
@ -160,7 +159,7 @@ class User(Model):
return self._api.lists_subscriptions(user=self.screen_name, *args, **kargs)
def lists(self, *args, **kargs):
return self._api.lists(user=self.screen_name, *args, **kargs)
return self._api.lists_all(user=self.screen_name, *args, **kargs)
def followers_ids(self, *args, **kargs):
return self._api.followers_ids(user_id=self.id, *args, **kargs)
@ -238,6 +237,8 @@ class SearchResults(ResultSet):
results.refresh_url = metadata.get('refresh_url')
results.completed_in = metadata.get('completed_in')
results.query = metadata.get('query')
results.count = metadata.get('count')
results.next_results = metadata.get('next_results')
for status in json['statuses']:
results.append(Status.parse(api, status))

View file

@ -2,10 +2,12 @@
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
import logging
import httplib
from socket import timeout
from threading import Thread
from time import sleep
import ssl
from tweepy.models import Status
from tweepy.api import API
@ -31,33 +33,59 @@ class StreamListener(object):
"""
pass
def on_data(self, data):
def on_data(self, raw_data):
"""Called when raw data is received from connection.
Override this method if you wish to manually handle
the stream data. Return False to stop stream and close connection.
"""
data = json.loads(raw_data)
if 'in_reply_to_status_id' in data:
status = Status.parse(self.api, json.loads(data))
status = Status.parse(self.api, data)
if self.on_status(status) is False:
return False
elif 'delete' in data:
delete = json.loads(data)['delete']['status']
delete = data['delete']['status']
if self.on_delete(delete['id'], delete['user_id']) is False:
return False
elif 'limit' in data:
if self.on_limit(json.loads(data)['limit']['track']) is False:
elif 'event' in data:
status = Status.parse(self.api, data)
if self.on_event(status) is False:
return False
elif 'direct_message' in data:
status = Status.parse(self.api, data)
if self.on_direct_message(status) is False:
return False
elif 'limit' in data:
if self.on_limit(data['limit']['track']) is False:
return False
elif 'disconnect' in data:
if self.on_disconnect(data['disconnect']) is False:
return False
else:
logging.error("Unknown message type: " + str(raw_data))
def on_status(self, status):
"""Called when a new status arrives"""
return
def on_exception(self, exception):
"""Called when an unhandled exception occurs."""
return
def on_delete(self, status_id, user_id):
"""Called when a delete notice arrives for a status"""
return
def on_event(self, status):
"""Called when a new event arrives"""
return
def on_direct_message(self, status):
"""Called when a new direct message arrives"""
return
def on_limit(self, track):
"""Called when a limitation notice arrvies"""
return
@ -70,6 +98,14 @@ class StreamListener(object):
"""Called when stream connection times out"""
return
def on_disconnect(self, notice):
"""Called when twitter sends a disconnect notice
Disconnect codes are listed here:
https://dev.twitter.com/docs/streaming-apis/messages#Disconnect_messages_disconnect
"""
return
class Stream(object):
@ -81,8 +117,12 @@ class Stream(object):
self.running = False
self.timeout = options.get("timeout", 300.0)
self.retry_count = options.get("retry_count")
self.retry_time = options.get("retry_time", 10.0)
self.snooze_time = options.get("snooze_time", 5.0)
# values according to https://dev.twitter.com/docs/streaming-apis/connecting#Reconnecting
self.retry_time_start = options.get("retry_time", 5.0)
self.retry_420_start = options.get("retry_420", 60.0)
self.retry_time_cap = options.get("retry_time_cap", 320.0)
self.snooze_time_step = options.get("snooze_time", 0.25)
self.snooze_time_cap = options.get("snooze_time_cap", 16)
self.buffer_size = options.get("buffer_size", 1500)
if options.get("secure", True):
self.scheme = "https"
@ -93,6 +133,8 @@ class Stream(object):
self.headers = options.get("headers") or {}
self.parameters = None
self.body = None
self.retry_time = self.retry_time_start
self.snooze_time = self.snooze_time_step
def _run(self):
# Authenticate
@ -108,30 +150,41 @@ class Stream(object):
break
try:
if self.scheme == "http":
conn = httplib.HTTPConnection(self.host)
conn = httplib.HTTPConnection(self.host, timeout=self.timeout)
else:
conn = httplib.HTTPSConnection(self.host)
conn = httplib.HTTPSConnection(self.host, timeout=self.timeout)
self.auth.apply_auth(url, 'POST', self.headers, self.parameters)
conn.connect()
conn.sock.settimeout(self.timeout)
conn.request('POST', self.url, self.body, headers=self.headers)
resp = conn.getresponse()
if resp.status != 200:
if self.listener.on_error(resp.status) is False:
break
error_counter += 1
if resp.status == 420:
self.retry_time = max(self.retry_420_start, self.retry_time)
sleep(self.retry_time)
self.retry_time = min(self.retry_time * 2, self.retry_time_cap)
else:
error_counter = 0
self.retry_time = self.retry_time_start
self.snooze_time = self.snooze_time_step
self.listener.on_connect()
self._read_loop(resp)
except timeout:
except (timeout, ssl.SSLError), exc:
# If it's not time out treat it like any other exception
if isinstance(exc, ssl.SSLError) and not (exc.args and 'timed out' in str(exc.args[0])):
exception = exc
break
if self.listener.on_timeout() == False:
break
if self.running is False:
break
conn.close()
sleep(self.snooze_time)
self.snooze_time = min(self.snooze_time + self.snooze_time_step,
self.snooze_time_cap)
except Exception, exception:
# any other exception is fatal, so kill loop
break
@ -142,6 +195,8 @@ class Stream(object):
conn.close()
if exception:
# call a handler first so that the exception can be logged.
self.listener.on_exception(exception)
raise
def _data(self, data):
@ -184,12 +239,26 @@ class Stream(object):
""" Called when the response has been closed by Twitter """
pass
def userstream(self, count=None, async=False, secure=True):
def userstream(self, stall_warnings=False, _with=None, replies=None,
track=None, locations=None, async=False, encoding='utf8'):
self.parameters = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
self.url = '/2/user.json?delimited=length'
self.url = '/%s/user.json?delimited=length' % STREAM_VERSION
self.host='userstream.twitter.com'
if stall_warnings:
self.parameters['stall_warnings'] = stall_warnings
if _with:
self.parameters['with'] = _with
if replies:
self.parameters['replies'] = replies
if locations and len(locations) > 0:
assert len(locations) % 4 == 0
self.parameters['locations'] = ','.join(['%.2f' % l for l in locations])
if track:
encoded_track = [s.encode(encoding) for s in track]
self.parameters['track'] = ','.join(encoded_track)
self.body = urlencode_noplus(self.parameters)
self._start(async)
def firehose(self, count=None, async=False):
@ -217,17 +286,19 @@ class Stream(object):
self.url += '&count=%s' % count
self._start(async)
def filter(self, follow=None, track=None, async=False, locations=None,
count = None, stall_warnings=False, languages=None):
def filter(self, follow=None, track=None, async=False, locations=None,
count=None, stall_warnings=False, languages=None, encoding='utf8'):
self.parameters = {}
self.headers['Content-type'] = "application/x-www-form-urlencoded"
if self.running:
raise TweepError('Stream object already connected!')
self.url = '/%s/statuses/filter.json?delimited=length' % STREAM_VERSION
if follow:
self.parameters['follow'] = ','.join(map(str, follow))
encoded_follow = [s.encode(encoding) for s in follow]
self.parameters['follow'] = ','.join(encoded_follow)
if track:
self.parameters['track'] = ','.join(map(str, track))
encoded_track = [s.encode(encoding) for s in track]
self.parameters['track'] = ','.join(encoded_track)
if locations and len(locations) > 0:
assert len(locations) % 4 == 0
self.parameters['locations'] = ','.join(['%.2f' % l for l in locations])

View file

@ -8,18 +8,11 @@ import htmlentitydefs
import re
import locale
from urllib import quote
from email.utils import parsedate
def parse_datetime(string):
# Set locale for date parsing
locale.setlocale(locale.LC_TIME, 'C')
# We must parse datetime this way to work in python 2.4
date = datetime(*(time.strptime(string, '%a %b %d %H:%M:%S +0000 %Y')[0:6]))
# Reset locale back to the default setting
locale.setlocale(locale.LC_TIME, '')
return date
return datetime(*(parsedate(string)[:6]))
def parse_html_value(html):
@ -34,41 +27,6 @@ def parse_a_href(atag):
return atag[start:end]
def parse_search_datetime(string):
# Set locale for date parsing
locale.setlocale(locale.LC_TIME, 'C')
# We must parse datetime this way to work in python 2.4
date = datetime(*(time.strptime(string, '%a, %d %b %Y %H:%M:%S +0000')[0:6]))
# Reset locale back to the default setting
locale.setlocale(locale.LC_TIME, '')
return date
def unescape_html(text):
"""Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)"""
def fixup(m):
text = m.group(0)
if text[:2] == "&#":
# character reference
try:
if text[:3] == "&#x":
return unichr(int(text[3:-1], 16))
else:
return unichr(int(text[2:-1]))
except ValueError:
pass
else:
# named entity
try:
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
except KeyError:
pass
return text # leave as is
return re.sub("&#?\w+;", fixup, text)
def convert_to_utf8_str(arg):
# written by Michael Norton (http://docondev.blogspot.com/)
if isinstance(arg, unicode):
@ -98,6 +56,5 @@ def list_to_csv(item_list):
return ','.join([str(i) for i in item_list])
def urlencode_noplus(query):
return '&'.join(['%s=%s' % (quote(str(k)), quote(str(v))) \
return '&'.join(['%s=%s' % (quote(str(k), ''), quote(str(v), '')) \
for k, v in query.iteritems()])