mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-09-18 21:50:56 +00:00
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:
commit
6094e28fac
83 changed files with 1642 additions and 500 deletions
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
}]}
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 + ")";
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
4
config/requirements.txt
Normal file → Executable 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
23
fabfile.py
vendored
|
@ -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,
|
||||
|
|
|
@ -1 +1 @@
|
|||
../../django/django/contrib/admin/media/
|
||||
/Users/sclay/projects/django/django/contrib/admin/static/admin
|
|
@ -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,
|
||||
|
|
|
@ -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 = */
|
||||
/* ======================== */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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">·</span>\
|
||||
|
|
|
@ -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});
|
||||
},
|
||||
|
||||
|
|
|
@ -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) { %>\
|
||||
|
|
|
@ -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
82
media/js/vendor/date.format.js
vendored
Normal 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);
|
104
media/js/vendor/jquery.date.js
vendored
104
media/js/vendor/jquery.date.js
vendored
|
@ -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);};
|
34
settings.py
34
settings.py
|
@ -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 = (
|
||||
|
|
45
templates/accounts/login.html
Normal file
45
templates/accounts/login.html
Normal 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 %}
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
19
templates/oauth2_provider/application_confirm_delete.html
Normal file
19
templates/oauth2_provider/application_confirm_delete.html
Normal 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 %}
|
42
templates/oauth2_provider/application_detail.html
Normal file
42
templates/oauth2_provider/application_detail.html
Normal 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 %}
|
43
templates/oauth2_provider/application_form.html
Normal file
43
templates/oauth2_provider/application_form.html
Normal 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 %}
|
20
templates/oauth2_provider/application_list.html
Normal file
20
templates/oauth2_provider/application_list.html
Normal 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 %}
|
10
templates/oauth2_provider/application_registration_form.html
Normal file
10
templates/oauth2_provider/application_registration_form.html
Normal 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 %}
|
47
templates/oauth2_provider/authorize.html
Normal file
47
templates/oauth2_provider/authorize.html
Normal 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 %}
|
|
@ -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
12
urls.py
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
4
vendor/tweepy/__init__.py
vendored
4
vendor/tweepy/__init__.py
vendored
|
@ -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
84
vendor/tweepy/api.py
vendored
|
@ -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
23
vendor/tweepy/auth.py
vendored
|
@ -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:
|
||||
|
|
5
vendor/tweepy/binder.py
vendored
5
vendor/tweepy/binder.py
vendored
|
@ -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:
|
||||
|
|
14
vendor/tweepy/cursor.py
vendored
14
vendor/tweepy/cursor.py
vendored
|
@ -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):
|
||||
|
|
9
vendor/tweepy/models.py
vendored
9
vendor/tweepy/models.py
vendored
|
@ -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))
|
||||
|
|
105
vendor/tweepy/streaming.py
vendored
105
vendor/tweepy/streaming.py
vendored
|
@ -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])
|
||||
|
|
49
vendor/tweepy/utils.py
vendored
49
vendor/tweepy/utils.py
vendored
|
@ -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()])
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue