mirror of
https://github.com/viq/NewsBlur.git
synced 2025-09-18 21:43:31 +00:00
Merge branch 'master' into circular
* master: (28 commits) Adding logging to photo uploading. Undoing some broken changes on reading. Adding avatar photo uploading. Moving bakc to Verdana for Windows font. Ugh, this has to be fixed somehow. Fixing deactivation of expired premiums. Forgot password flow. Holy crap, this took two years. Bumping up duplicate address length for feeds. Submitting v1.8 to iOS App Store. Fixing db firewall ports. Preparing original pages node server for launch. Adding font size choices to web preferences. Closing #66 with both integration of #68 and using the same fonts. Users can add an optional class if they want these other convenient fonts. Also using a few fonts as backups for non-Mac users. Fix default theming hook Add multiple targeted font stacks Fixing #75: shared stories should use story permalink, not story guid. Doh. Thanks @denubis! Categorizing preferences. Adding window title count back in. Adding email lookup to forgot password flow. Adding email lookup to forgot password flow. Removing unused original pages from S3 when stored on node server. Refactoring original page saving toa ccount for node server being down. ...
This commit is contained in:
commit
c1725399b2
94 changed files with 17649 additions and 495 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -55,3 +55,4 @@ media/android/NewsBlurTest/gen/
|
|||
# Local configuration file (sdk path, etc)
|
||||
media/android/NewsBlur/local.properties
|
||||
media/android/NewsBlurTest/local.properties
|
||||
originals
|
||||
|
|
|
@ -2,6 +2,7 @@ from django import forms
|
|||
from vendor.zebra.forms import StripePaymentForm
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
PLANS = [
|
||||
("newsblur-premium-12", mark_safe("$12 / year <span class='NB-small'>($1/month)</span>")),
|
||||
|
@ -59,3 +60,28 @@ class DeleteAccountForm(forms.Form):
|
|||
raise forms.ValidationError('Please type "DELETE" to confirm deletion.')
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
class ForgotPasswordForm(forms.Form):
|
||||
email = forms.CharField(widget=forms.TextInput(),
|
||||
label="Your email address",
|
||||
required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ForgotPasswordForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_email(self):
|
||||
if not self.cleaned_data['email']:
|
||||
raise forms.ValidationError('Please enter in an email address.')
|
||||
try:
|
||||
User.objects.get(email__iexact=self.cleaned_data['email'])
|
||||
except User.MultipleObjectsReturned:
|
||||
pass
|
||||
except User.DoesNotExist:
|
||||
raise forms.ValidationError('No user has that email address.')
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
class ForgotPasswordReturnForm(forms.Form):
|
||||
password = forms.CharField(widget=forms.PasswordInput(),
|
||||
label="Your new password",
|
||||
required=False)
|
||||
|
|
|
@ -9,20 +9,33 @@ class Command(BaseCommand):
|
|||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
username = options['username']
|
||||
username = options.get('username')
|
||||
email = options.get('email')
|
||||
user = None
|
||||
try:
|
||||
user = User.objects.get(username__icontains=username)
|
||||
except User.MultipleObjectsFound:
|
||||
user = User.objects.get(username__iexact=username)
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.get(email__icontains=username)
|
||||
except User.DoesNotExist:
|
||||
print " ---> No user/email found at: %s" % username
|
||||
|
||||
if username:
|
||||
try:
|
||||
user = User.objects.get(username__icontains=username)
|
||||
except User.MultipleObjectsReturned:
|
||||
user = User.objects.get(username__iexact=username)
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.get(email__iexact=username)
|
||||
except User.DoesNotExist:
|
||||
print " ---> No user found at: %s" % username
|
||||
elif email:
|
||||
try:
|
||||
user = User.objects.get(email__icontains=email)
|
||||
except User.MultipleObjectsReturned:
|
||||
user = User.objects.get(email__iexact=email)
|
||||
except User.MultipleObjectsReturned:
|
||||
users = User.objects.filter(email__iexact=email)
|
||||
user = users[0]
|
||||
except User.DoesNotExist:
|
||||
print " ---> No email found at: %s" % email
|
||||
|
||||
if user:
|
||||
email = options.get("email") or user.email
|
||||
user.profile.send_forgot_password_email(email)
|
||||
|
||||
else:
|
||||
print " ---> No user/email found at: %s/%s" % (username, email)
|
||||
|
||||
|
|
@ -309,9 +309,6 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
msg.attach_alternative(html, "text/html")
|
||||
msg.send(fail_silently=True)
|
||||
|
||||
user.set_password('')
|
||||
user.save()
|
||||
|
||||
logging.user(self.user, "~BB~FM~SBSending email for forgotten password: %s" % self.user.email)
|
||||
|
||||
def send_upload_opml_finished_email(self, feed_count):
|
||||
|
@ -360,7 +357,7 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
|
||||
def send_premium_expire_grace_period_email(self, force=False):
|
||||
if not self.user.email:
|
||||
logging.user(self.user, "~FM~SB~FRNot~FM sending premium expire grace for user: %s" % (self.user))
|
||||
logging.user(self.user, "~FM~SB~FRNot~FM~SN sending premium expire grace for user: %s" % (self.user))
|
||||
return
|
||||
|
||||
emails_sent = MSentEmail.objects.filter(receiver_user_id=self.user.pk,
|
||||
|
@ -368,7 +365,7 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
day_ago = datetime.datetime.now() - datetime.timedelta(days=360)
|
||||
for email in emails_sent:
|
||||
if email.date_sent > day_ago:
|
||||
logging.user(self.user, "~SK~FMNot sending premium expire grace email, already sent before.")
|
||||
logging.user(self.user, "~SN~FMNot sending premium expire grace email, already sent before.")
|
||||
return
|
||||
|
||||
self.premium_expire = datetime.datetime.now()
|
||||
|
@ -400,7 +397,7 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
day_ago = datetime.datetime.now() - datetime.timedelta(days=360)
|
||||
for email in emails_sent:
|
||||
if email.date_sent > day_ago:
|
||||
logging.user(self.user, "~SK~FMNot sending premium expire email, already sent before.")
|
||||
logging.user(self.user, "~FM~SBNot sending premium expire email, already sent before.")
|
||||
return
|
||||
|
||||
delta = datetime.datetime.now() - self.last_seen_on
|
||||
|
|
|
@ -35,4 +35,4 @@ class PremiumExpire(Task):
|
|||
logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count())
|
||||
for profile in expired_profiles:
|
||||
profile.send_premium_expire_email()
|
||||
profile.deactive_premium()
|
||||
profile.deactivate_premium()
|
||||
|
|
|
@ -16,4 +16,6 @@ urlpatterns = patterns('',
|
|||
url(r'^activities/?', views.load_activities, name='profile-activities'),
|
||||
url(r'^payment_history/?', views.payment_history, name='profile-payment-history'),
|
||||
url(r'^delete_account/?', views.delete_account, name='profile-delete-account'),
|
||||
url(r'^forgot_password_return/?', views.forgot_password_return, name='profile-forgot-password-return'),
|
||||
url(r'^forgot_password/?', views.forgot_password, name='profile-forgot-password'),
|
||||
)
|
||||
|
|
|
@ -14,6 +14,7 @@ from django.conf import settings
|
|||
from apps.profile.models import Profile, change_password, PaymentHistory
|
||||
from apps.reader.models import UserSubscription
|
||||
from apps.profile.forms import StripePlusPaymentForm, PLANS, DeleteAccountForm
|
||||
from apps.profile.forms import ForgotPasswordForm, ForgotPasswordReturnForm
|
||||
from apps.social.models import MSocialServices, MActivity, MSocialProfile
|
||||
from utils import json_functions as json
|
||||
from utils.user_functions import ajax_login_required
|
||||
|
@ -311,4 +312,47 @@ def delete_account(request):
|
|||
|
||||
return {
|
||||
'delete_form': form,
|
||||
}
|
||||
|
||||
|
||||
@render_to('profile/forgot_password.xhtml')
|
||||
def forgot_password(request):
|
||||
if request.method == 'POST':
|
||||
form = ForgotPasswordForm(request.POST)
|
||||
if form.is_valid():
|
||||
logging.user(request.user, "~BC~FRForgot password: ~SB%s" % request.POST['email'])
|
||||
try:
|
||||
user = User.objects.get(email__iexact=request.POST['email'])
|
||||
except User.MultipleObjectsReturned:
|
||||
user = User.objects.filter(email__iexact=request.POST['email'])[0]
|
||||
user.profile.send_forgot_password_email()
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
else:
|
||||
logging.user(request.user, "~BC~FRFailed forgot password: ~SB%s~SN" %
|
||||
request.POST['email'])
|
||||
else:
|
||||
logging.user(request.user, "~BC~FRAttempting to retrieve forgotton password.")
|
||||
form = ForgotPasswordForm()
|
||||
|
||||
return {
|
||||
'forgot_password_form': form,
|
||||
}
|
||||
|
||||
@login_required
|
||||
@render_to('profile/forgot_password_return.xhtml')
|
||||
def forgot_password_return(request):
|
||||
if request.method == 'POST':
|
||||
logging.user(request.user, "~BC~FRReseting ~SB%s~SN's password." %
|
||||
request.user.username)
|
||||
new_password = request.POST.get('password', '')
|
||||
request.user.set_password(new_password)
|
||||
request.user.save()
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
else:
|
||||
logging.user(request.user, "~BC~FRAttempting to reset ~SB%s~SN's password." %
|
||||
request.user.username)
|
||||
form = ForgotPasswordReturnForm()
|
||||
|
||||
return {
|
||||
'forgot_password_return_form': form,
|
||||
}
|
|
@ -2,6 +2,7 @@ import datetime
|
|||
import time
|
||||
import boto
|
||||
import redis
|
||||
import requests
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
@ -187,10 +188,11 @@ def autologin(request, username, secret):
|
|||
login_user(request, user)
|
||||
logging.user(user, "~FG~BB~SKAuto-Login. Next stop: %s~FW" % (next if next else 'Homepage',))
|
||||
|
||||
if next:
|
||||
if next and not next.startswith('/'):
|
||||
next = '?next=' + next
|
||||
|
||||
return HttpResponseRedirect(reverse('index') + next)
|
||||
return HttpResponseRedirect(reverse('index') + next)
|
||||
else:
|
||||
return HttpResponseRedirect(next)
|
||||
|
||||
@ratelimit(minutes=1, requests=24)
|
||||
@never_cache
|
||||
|
@ -231,7 +233,7 @@ def load_feeds(request):
|
|||
elif sub.active and sub.feed.active_subscribers <= 0:
|
||||
scheduled_feeds.append(sub.feed.pk)
|
||||
|
||||
if len(scheduled_feeds) > 0:
|
||||
if len(scheduled_feeds) > 0 and request.user.is_authenticated():
|
||||
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))
|
||||
|
@ -603,24 +605,38 @@ def load_feed_page(request, feed_id):
|
|||
|
||||
feed = Feed.get_by_id(feed_id)
|
||||
|
||||
if (feed and feed.has_page and
|
||||
not feed.has_page_exception and
|
||||
settings.BACKED_BY_AWS['pages_on_s3'] and
|
||||
feed.s3_page):
|
||||
if settings.PROXY_S3_PAGES:
|
||||
key = settings.S3_PAGES_BUCKET.get_key(feed.s3_pages_key)
|
||||
if key:
|
||||
compressed_data = key.get_contents_as_string()
|
||||
response = HttpResponse(compressed_data, mimetype="text/html; charset=utf-8")
|
||||
if feed and feed.has_page and not feed.has_page_exception:
|
||||
if settings.BACKED_BY_AWS.get('pages_on_node'):
|
||||
url = "http://%s/original_page/%s" % (
|
||||
settings.ORIGINAL_PAGE_SERVER,
|
||||
feed.pk,
|
||||
)
|
||||
page_response = requests.get(url)
|
||||
if page_response.status_code == 200:
|
||||
response = HttpResponse(page_response.content, mimetype="text/html; charset=utf-8")
|
||||
response['Content-Encoding'] = 'gzip'
|
||||
|
||||
logging.user(request, "~FYLoading original page, proxied: ~SB%s bytes" %
|
||||
(len(compressed_data)))
|
||||
response['Last-Modified'] = page_response.headers.get('Last-modified')
|
||||
response['Etag'] = page_response.headers.get('Etag')
|
||||
response['Content-Length'] = str(len(page_response.content))
|
||||
logging.user(request, "~FYLoading original page, proxied from node: ~SB%s bytes" %
|
||||
(len(page_response.content)))
|
||||
return response
|
||||
else:
|
||||
logging.user(request, "~FYLoading original page, non-proxied")
|
||||
return HttpResponseRedirect('//%s/%s' % (settings.S3_PAGES_BUCKET_NAME,
|
||||
feed.s3_pages_key))
|
||||
|
||||
if settings.BACKED_BY_AWS['pages_on_s3'] and feed.s3_page:
|
||||
if settings.PROXY_S3_PAGES:
|
||||
key = settings.S3_PAGES_BUCKET.get_key(feed.s3_pages_key)
|
||||
if key:
|
||||
compressed_data = key.get_contents_as_string()
|
||||
response = HttpResponse(compressed_data, mimetype="text/html; charset=utf-8")
|
||||
response['Content-Encoding'] = 'gzip'
|
||||
|
||||
logging.user(request, "~FYLoading original page, proxied: ~SB%s bytes" %
|
||||
(len(compressed_data)))
|
||||
return response
|
||||
else:
|
||||
logging.user(request, "~FYLoading original page, non-proxied")
|
||||
return HttpResponseRedirect('//%s/%s' % (settings.S3_PAGES_BUCKET_NAME,
|
||||
feed.s3_pages_key))
|
||||
|
||||
data = MFeedPage.get_data(feed_id=feed_id)
|
||||
|
||||
|
|
86
apps/rss_feeds/migrations/0062_dupe_feed_maxlen.py
Normal file
86
apps/rss_feeds/migrations/0062_dupe_feed_maxlen.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Changing field 'DuplicateFeed.duplicate_link'
|
||||
db.alter_column('rss_feeds_duplicatefeed', 'duplicate_link', self.gf('django.db.models.fields.CharField')(max_length=764, null=True))
|
||||
|
||||
# Changing field 'DuplicateFeed.duplicate_address'
|
||||
db.alter_column('rss_feeds_duplicatefeed', 'duplicate_address', self.gf('django.db.models.fields.CharField')(max_length=764))
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Changing field 'DuplicateFeed.duplicate_link'
|
||||
db.alter_column('rss_feeds_duplicatefeed', 'duplicate_link', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
|
||||
|
||||
# Changing field 'DuplicateFeed.duplicate_address'
|
||||
db.alter_column('rss_feeds_duplicatefeed', 'duplicate_address', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
models = {
|
||||
'rss_feeds.duplicatefeed': {
|
||||
'Meta': {'object_name': 'DuplicateFeed'},
|
||||
'duplicate_address': ('django.db.models.fields.CharField', [], {'max_length': '764', 'db_index': 'True'}),
|
||||
'duplicate_feed_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}),
|
||||
'duplicate_link': ('django.db.models.fields.CharField', [], {'max_length': '764', 'null': 'True', 'db_index': 'True'}),
|
||||
'feed': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'duplicate_addresses'", 'to': "orm['rss_feeds.Feed']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'rss_feeds.feed': {
|
||||
'Meta': {'ordering': "['feed_title']", 'object_name': 'Feed', 'db_table': "'feeds'"},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
|
||||
'active_premium_subscribers': ('django.db.models.fields.IntegerField', [], {'default': '-1', 'db_index': 'True'}),
|
||||
'active_subscribers': ('django.db.models.fields.IntegerField', [], {'default': '-1', 'db_index': 'True'}),
|
||||
'average_stories_per_month': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'branch_from_feed': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['rss_feeds.Feed']", 'null': 'True', 'blank': 'True'}),
|
||||
'creation': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'days_to_trim': ('django.db.models.fields.IntegerField', [], {'default': '90'}),
|
||||
'errors_since_good': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'etag': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'exception_code': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'favicon_color': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}),
|
||||
'favicon_not_found': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'feed_address': ('django.db.models.fields.URLField', [], {'max_length': '764', 'db_index': 'True'}),
|
||||
'feed_address_locked': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
|
||||
'feed_link': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '1000', 'null': 'True', 'blank': 'True'}),
|
||||
'feed_link_locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'feed_title': ('django.db.models.fields.CharField', [], {'default': "'[Untitled]'", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'fetched_once': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'has_feed_exception': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
|
||||
'has_page': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'has_page_exception': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
|
||||
'hash_address_and_link': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_push': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
|
||||
'known_good': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
|
||||
'last_load_time': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'last_modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
|
||||
'min_to_decay': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'next_scheduled_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
|
||||
'num_subscribers': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
|
||||
'premium_subscribers': ('django.db.models.fields.IntegerField', [], {'default': '-1'}),
|
||||
'queued_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
|
||||
's3_icon': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
|
||||
's3_page': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
|
||||
'stories_last_month': ('django.db.models.fields.IntegerField', [], {'default': '0'})
|
||||
},
|
||||
'rss_feeds.feeddata': {
|
||||
'Meta': {'object_name': 'FeedData'},
|
||||
'feed': ('utils.fields.AutoOneToOneField', [], {'related_name': "'data'", 'unique': 'True', 'to': "orm['rss_feeds.Feed']"}),
|
||||
'feed_classifier_counts': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'feed_tagline': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'popular_authors': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True', 'blank': 'True'}),
|
||||
'popular_tags': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
|
||||
'story_count_history': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['rss_feeds']
|
|
@ -1414,6 +1414,7 @@ class MStory(mongo.Document):
|
|||
story_author_name = mongo.StringField()
|
||||
story_permalink = mongo.StringField()
|
||||
story_guid = mongo.StringField()
|
||||
story_hash = mongo.StringField()
|
||||
story_tags = mongo.ListField(mongo.StringField(max_length=250))
|
||||
comment_count = mongo.IntField()
|
||||
comment_user_ids = mongo.ListField(mongo.IntField())
|
||||
|
@ -1432,6 +1433,10 @@ class MStory(mongo.Document):
|
|||
@property
|
||||
def guid_hash(self):
|
||||
return hashlib.sha1(self.story_guid).hexdigest()[:6]
|
||||
|
||||
@property
|
||||
def feed_guid_hash(self):
|
||||
return hashlib.sha1("%s:%s" % (self.story_feed_id, self.story_guid)).hexdigest()[:6]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
story_title_max = MStory._fields['story_title'].max_length
|
||||
|
@ -1449,6 +1454,7 @@ class MStory(mongo.Document):
|
|||
self.story_title = self.story_title[:story_title_max]
|
||||
if self.story_content_type and len(self.story_content_type) > story_content_type_max:
|
||||
self.story_content_type = self.story_content_type[:story_content_type_max]
|
||||
|
||||
super(MStory, self).save(*args, **kwargs)
|
||||
|
||||
self.sync_redis()
|
||||
|
@ -1713,8 +1719,8 @@ class MFeedPushHistory(mongo.Document):
|
|||
|
||||
|
||||
class DuplicateFeed(models.Model):
|
||||
duplicate_address = models.CharField(max_length=255, db_index=True)
|
||||
duplicate_link = models.CharField(max_length=255, null=True, db_index=True)
|
||||
duplicate_address = models.CharField(max_length=764, db_index=True)
|
||||
duplicate_link = models.CharField(max_length=764, null=True, db_index=True)
|
||||
duplicate_feed_id = models.CharField(max_length=255, null=True, db_index=True)
|
||||
feed = models.ForeignKey(Feed, related_name='duplicate_addresses')
|
||||
|
||||
|
|
|
@ -6,10 +6,9 @@ import feedparser
|
|||
import time
|
||||
import urllib2
|
||||
import httplib
|
||||
import gzip
|
||||
import StringIO
|
||||
from boto.s3.key import Key
|
||||
from django.conf import settings
|
||||
from django.utils.text import compress_string
|
||||
from utils import log as logging
|
||||
from apps.rss_feeds.models import MFeedPage
|
||||
from utils.feed_functions import timelimit, mail_feed_error_to_admin
|
||||
|
@ -174,35 +173,65 @@ class PageImporter(object):
|
|||
return ''.join(ret)
|
||||
|
||||
def save_page(self, html):
|
||||
if html and len(html) > 100:
|
||||
if settings.BACKED_BY_AWS.get('pages_on_s3'):
|
||||
k = Key(settings.S3_PAGES_BUCKET)
|
||||
k.key = self.feed.s3_pages_key
|
||||
k.set_metadata('Content-Encoding', 'gzip')
|
||||
k.set_metadata('Content-Type', 'text/html')
|
||||
k.set_metadata('Access-Control-Allow-Origin', '*')
|
||||
out = StringIO.StringIO()
|
||||
f = gzip.GzipFile(fileobj=out, mode='w')
|
||||
f.write(html)
|
||||
f.close()
|
||||
compressed_html = out.getvalue()
|
||||
k.set_contents_from_string(compressed_html)
|
||||
k.set_acl('public-read')
|
||||
|
||||
try:
|
||||
feed_page = MFeedPage.objects.get(feed_id=self.feed.pk)
|
||||
feed_page.delete()
|
||||
logging.debug(' --->> [%-30s] ~FYTransfering page data to S3...' % (self.feed))
|
||||
except MFeedPage.DoesNotExist:
|
||||
pass
|
||||
|
||||
self.feed.s3_page = True
|
||||
self.feed.save()
|
||||
else:
|
||||
try:
|
||||
feed_page = MFeedPage.objects.get(feed_id=self.feed.pk)
|
||||
feed_page.page_data = html
|
||||
feed_page.save()
|
||||
except MFeedPage.DoesNotExist:
|
||||
feed_page = MFeedPage.objects.create(feed_id=self.feed.pk, page_data=html)
|
||||
return feed_page
|
||||
saved = False
|
||||
|
||||
if not html or len(html) < 100:
|
||||
return
|
||||
|
||||
if settings.BACKED_BY_AWS.get('pages_on_node'):
|
||||
saved = self.save_page_node(html)
|
||||
if saved and self.feed.s3_page and settings.BACKED_BY_AWS.get('pages_on_s3'):
|
||||
self.delete_page_s3()
|
||||
|
||||
if settings.BACKED_BY_AWS.get('pages_on_s3') and not saved:
|
||||
saved = self.save_page_s3(html)
|
||||
|
||||
if not saved:
|
||||
try:
|
||||
feed_page = MFeedPage.objects.get(feed_id=self.feed.pk)
|
||||
feed_page.page_data = html
|
||||
feed_page.save()
|
||||
except MFeedPage.DoesNotExist:
|
||||
feed_page = MFeedPage.objects.create(feed_id=self.feed.pk, page_data=html)
|
||||
return feed_page
|
||||
|
||||
def save_page_node(self, html):
|
||||
url = "http://%s/original_page/%s" % (
|
||||
settings.ORIGINAL_PAGE_SERVER,
|
||||
self.feed.pk,
|
||||
)
|
||||
response = requests.post(url, files={
|
||||
'original_page': compress_string(html),
|
||||
})
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
|
||||
def save_page_s3(self, html):
|
||||
k = Key(settings.S3_PAGES_BUCKET)
|
||||
k.key = self.feed.s3_pages_key
|
||||
k.set_metadata('Content-Encoding', 'gzip')
|
||||
k.set_metadata('Content-Type', 'text/html')
|
||||
k.set_metadata('Access-Control-Allow-Origin', '*')
|
||||
k.set_contents_from_string(compress_string(html))
|
||||
k.set_acl('public-read')
|
||||
|
||||
try:
|
||||
feed_page = MFeedPage.objects.get(feed_id=self.feed.pk)
|
||||
feed_page.delete()
|
||||
logging.debug(' ---> [%-30s] ~FYTransfering page data to S3...' % (self.feed))
|
||||
except MFeedPage.DoesNotExist:
|
||||
pass
|
||||
|
||||
if not self.feed.s3_page:
|
||||
self.feed.s3_page = True
|
||||
self.feed.save()
|
||||
|
||||
return True
|
||||
|
||||
def delete_page_s3(self):
|
||||
k = Key(settings.S3_PAGES_BUCKET)
|
||||
k.key = self.feed.s3_pages_key
|
||||
k.delete()
|
||||
|
||||
self.feed.s3_page = False
|
||||
self.feed.save()
|
||||
|
|
|
@ -32,6 +32,7 @@ from utils import json_functions as json
|
|||
from utils.feed_functions import relative_timesince
|
||||
from utils.story_functions import truncate_chars, strip_tags, linkify, image_size
|
||||
from utils.scrubber import SelectiveScriptScrubber
|
||||
from utils import s3_utils
|
||||
|
||||
RECOMMENDATIONS_LIMIT = 5
|
||||
IGNORE_IMAGE_SOURCES = [
|
||||
|
@ -303,6 +304,8 @@ class MSocialProfile(mongo.Document):
|
|||
return photo_url + '?type=large'
|
||||
elif 'twimg' in photo_url:
|
||||
return photo_url.replace('_normal', '')
|
||||
elif '/avatars/' in photo_url:
|
||||
return photo_url.replace('thumbnail_', 'large_')
|
||||
return photo_url
|
||||
|
||||
@property
|
||||
|
@ -2050,6 +2053,22 @@ class MSocialServices(mongo.Document):
|
|||
def profile(cls, user_id):
|
||||
profile = cls.get_user(user_id=user_id)
|
||||
return profile.to_json()
|
||||
|
||||
def save_uploaded_photo(self, photo):
|
||||
photo_body = photo.read()
|
||||
filename = photo.name
|
||||
|
||||
s3 = s3_utils.S3Store()
|
||||
image_name = s3.save_profile_picture(self.user_id, filename, photo_body)
|
||||
if image_name:
|
||||
self.upload_picture_url = "https://s3.amazonaws.com/%s/avatars/%s/thumbnail_%s" % (
|
||||
settings.S3_AVATARS_BUCKET_NAME,
|
||||
self.user_id,
|
||||
image_name,
|
||||
)
|
||||
self.save()
|
||||
|
||||
return image_name and self.upload_picture_url
|
||||
|
||||
def twitter_api(self):
|
||||
twitter_consumer_key = settings.TWITTER_CONSUMER_KEY
|
||||
|
|
|
@ -10,6 +10,7 @@ urlpatterns = patterns('',
|
|||
url(r'^profile/?$', views.profile, name='profile'),
|
||||
url(r'^load_user_profile/?$', views.load_user_profile, name='load-user-profile'),
|
||||
url(r'^save_user_profile/?$', views.save_user_profile, name='save-user-profile'),
|
||||
url(r'^upload_avatar/?', views.upload_avatar, name='upload-avatar'),
|
||||
url(r'^save_blurblog_settings/?$', views.save_blurblog_settings, name='save-blurblog-settings'),
|
||||
url(r'^interactions/?$', views.load_interactions, name='social-interactions'),
|
||||
url(r'^activities/?$', views.load_activities, name='social-activities'),
|
||||
|
|
|
@ -503,7 +503,7 @@ def mark_story_as_shared(request):
|
|||
if not shared_story:
|
||||
story_db = {
|
||||
"story_guid": story.story_guid,
|
||||
"story_permalink": story.story_guid,
|
||||
"story_permalink": story.story_permalink,
|
||||
"story_title": story.story_title,
|
||||
"story_feed_id": story.story_feed_id,
|
||||
"story_content_z": story.story_content_z,
|
||||
|
@ -870,7 +870,27 @@ def save_user_profile(request):
|
|||
|
||||
return dict(code=1, user_profile=profile.to_json(include_follows=True))
|
||||
|
||||
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def upload_avatar(request):
|
||||
photo = request.FILES['photo']
|
||||
profile = MSocialProfile.get_user(request.user.pk)
|
||||
social_services = MSocialServices.objects.get(user_id=request.user.pk)
|
||||
|
||||
logging.user(request, "~FC~BM~SBUploading photo...")
|
||||
|
||||
image_url = social_services.save_uploaded_photo(photo)
|
||||
if image_url:
|
||||
profile = social_services.set_photo('upload')
|
||||
|
||||
return {
|
||||
"code": 1 if image_url else -1,
|
||||
"uploaded": image_url,
|
||||
"services": social_services,
|
||||
"user_profile": profile.to_json(include_follows=True),
|
||||
}
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def save_blurblog_settings(request):
|
||||
|
|
10
config/supervisor_node_original.conf
Normal file
10
config/supervisor_node_original.conf
Normal file
|
@ -0,0 +1,10 @@
|
|||
[program:node_original_page]
|
||||
command=node original_page.js
|
||||
directory=/srv/newsblur/node
|
||||
user=sclay
|
||||
autostart=true
|
||||
autorestart=true
|
||||
#redirect_stderr=True
|
||||
priority=991
|
||||
stopsignal=HUP
|
||||
stdout_logfile = /srv/newsblur/logs/original_page.log
|
42
fabfile.py
vendored
42
fabfile.py
vendored
|
@ -8,6 +8,7 @@ from fabric.contrib import django
|
|||
import os
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
|
||||
django.settings_module('settings')
|
||||
try:
|
||||
|
@ -603,7 +604,7 @@ def setup_node():
|
|||
sudo('add-apt-repository -y ppa:chris-lea/node.js')
|
||||
sudo('apt-get update')
|
||||
sudo('apt-get install -y nodejs')
|
||||
run('curl http://npmjs.org/install.sh | sudo sh')
|
||||
run('curl -L https://npmjs.org/install.sh | sudo sh')
|
||||
sudo('npm install -g supervisor')
|
||||
sudo('ufw allow 8888')
|
||||
|
||||
|
@ -641,23 +642,29 @@ def maintenance_off():
|
|||
# ==============
|
||||
|
||||
def setup_db_firewall():
|
||||
ports = [
|
||||
5432, # PostgreSQL
|
||||
27017, # MongoDB
|
||||
28017, # MongoDB web
|
||||
6379, # Redis
|
||||
11211, # Memcached
|
||||
3060, # Node original page server
|
||||
9200, # Elasticsearch
|
||||
]
|
||||
sudo('ufw default deny')
|
||||
sudo('ufw allow ssh')
|
||||
sudo('ufw allow 80')
|
||||
sudo('ufw allow from 199.15.248.0/21 to any port 5432 ') # PostgreSQL
|
||||
sudo('ufw allow from 199.15.248.0/21 to any port 27017') # MongoDB
|
||||
sudo('ufw allow from 199.15.248.0/21 to any port 28017') # MongoDB web
|
||||
sudo('ufw allow from 199.15.248.0/21 to any port 6379 ') # Redis
|
||||
sudo('ufw allow from 199.15.248.0/21 to any port 11211 ') # Memcached
|
||||
sudo('ufw allow from 199.15.248.0/21 to any port 9200 ') # Elasticsearch
|
||||
|
||||
sudo('ufw allow proto tcp from 199.15.248.0/21 to any port %s ' % ','.join(map(str, ports)))
|
||||
|
||||
# EC2
|
||||
sudo('ufw allow proto tcp from 54.242.38.48 to any port 5432,27017,6379,11211')
|
||||
sudo('ufw allow proto tcp from 184.72.214.147 to any port 5432,27017,6379,11211')
|
||||
sudo('ufw allow proto tcp from 107.20.103.16 to any port 5432,27017,6379,11211')
|
||||
sudo('ufw allow proto tcp from 50.17.12.16 to any port 5432,27017,6379,11211')
|
||||
sudo('ufw allow proto tcp from 184.73.2.61 to any port 5432,27017,6379,11211')
|
||||
sudo('ufw allow proto tcp from 54.242.34.138 to any port 5432,27017,6379,11211')
|
||||
for host in env.roledefs['ec2task']:
|
||||
ip = re.search('ec2-(\d+-\d+-\d+-\d+)', host).group(1).replace('-', '.')
|
||||
sudo('ufw allow proto tcp from %s to any port %s' % (
|
||||
ip,
|
||||
','.join(map(str, ports))
|
||||
))
|
||||
|
||||
sudo('ufw --force enable')
|
||||
|
||||
def setup_db_motd():
|
||||
|
@ -769,6 +776,15 @@ def setup_db_mdadm():
|
|||
sudo("echo '/dev/md0 /srv/db xfs rw,nobarrier,noatime,nodiratime,noauto 0 0' | sudo tee -a /etc/fstab")
|
||||
sudo("sudo update-initramfs -u -v -k `uname -r`")
|
||||
|
||||
def setup_original_page_server():
|
||||
setup_node()
|
||||
sudo('mkdir -p /srv/originals')
|
||||
sudo('chown sclay.sclay -R /srv/originals')
|
||||
put('config/supervisor_node_original.conf',
|
||||
'/etc/supervisor/conf.d/node_original.conf', use_sudo=True)
|
||||
sudo('supervisorctl reread')
|
||||
sudo('supervisorctl reload')
|
||||
|
||||
def setup_elasticsearch():
|
||||
ES_VERSION = "0.20.1"
|
||||
sudo('apt-get update')
|
||||
|
|
|
@ -89,10 +89,13 @@ REDIS = {
|
|||
ELASTICSEARCH_HOSTS = ['127.0.0.1:9200']
|
||||
|
||||
BACKED_BY_AWS = {
|
||||
'pages_on_node': False,
|
||||
'pages_on_s3': False,
|
||||
'icons_on_s3': False,
|
||||
}
|
||||
|
||||
ORIGINAL_PAGE_SERVER = "127.0.0.1:3060"
|
||||
|
||||
# ===========
|
||||
# = Logging =
|
||||
# ===========
|
||||
|
|
|
@ -51,17 +51,23 @@ a img {
|
|||
/* ==================== */
|
||||
|
||||
body {
|
||||
font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
|
||||
font-family: "Lucida Sans", "Lucida Grande", Verdana, "Helvetica Neue", Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body.NB-theme-sans-serif #story_pane {
|
||||
font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
|
||||
.NB-theme-sans-serif-alt1 #story_pane {
|
||||
font-family: "Helvetica Neue", "Helvetica", sans-serif;
|
||||
}
|
||||
|
||||
body.NB-theme-serif #story_pane .NB-feed-story-content {
|
||||
font-family: "Palatino Linotype", Georgia, "URW Palladio L", "Century Schoolbook L", serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
.NB-theme-sans-serif-alt2 #story_pane {
|
||||
font-family: "Open Sans", "Liberation Sans", sans-serif;
|
||||
}
|
||||
|
||||
.NB-theme-sans-serif-wide #story_pane {
|
||||
font-family: "DejaVu Sans", "Bitstream Vera Sans", "Verdana", "Tahoma", "Geneva", sans-serif;
|
||||
}
|
||||
|
||||
.NB-theme-serif #story_pane .NB-feed-story-content {
|
||||
font-family: "Palatino Linotype", "Palatino", "URW Palladio L", "Nimbus Roman No9 L", Georgia, serif;
|
||||
}
|
||||
|
||||
.chzn-drop {
|
||||
|
@ -1721,7 +1727,37 @@ background: transparent;
|
|||
width: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.NB-theme-sans-serif.NB-theme-size-xs #story_pane {
|
||||
font-size: 11px;
|
||||
}
|
||||
.NB-theme-sans-serif.NB-theme-size-s #story_pane {
|
||||
font-size: 12px;
|
||||
}
|
||||
.NB-theme-sans-serif.NB-theme-size-m #story_pane {
|
||||
font-size: 13px;
|
||||
}
|
||||
.NB-theme-sans-serif.NB-theme-size-l #story_pane {
|
||||
font-size: 14px;
|
||||
}
|
||||
.NB-theme-sans-serif.NB-theme-size-xl #story_pane {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.NB-theme-serif.NB-theme-size-xs #story_pane {
|
||||
font-size: 12px;
|
||||
}
|
||||
.NB-theme-serif.NB-theme-size-s #story_pane {
|
||||
font-size: 13px;
|
||||
}
|
||||
.NB-theme-serif.NB-theme-size-m #story_pane {
|
||||
font-size: 14px;
|
||||
}
|
||||
.NB-theme-serif.NB-theme-size-l #story_pane {
|
||||
font-size: 15px;
|
||||
}
|
||||
.NB-theme-serif.NB-theme-size-xl #story_pane {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#story_pane .wrapper {
|
||||
margin-left: 220px;
|
||||
|
@ -7161,7 +7197,7 @@ form.opml_import_form input {
|
|||
|
||||
.NB-modal-preferences .NB-preferences-scroll {
|
||||
overflow: auto;
|
||||
max-height: 500px;
|
||||
max-height: 600px;
|
||||
width: 100%;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
@ -7180,11 +7216,11 @@ form.opml_import_form input {
|
|||
|
||||
.NB-modal-preferences .NB-preference .NB-preference-options {
|
||||
float: right;
|
||||
width: 370px;
|
||||
width: 420px;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference .NB-preference-label {
|
||||
float: left;
|
||||
width: 150px;
|
||||
width: 176px;
|
||||
position: relative;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference .NB-preference-sublabel {
|
||||
|
@ -7239,6 +7275,27 @@ form.opml_import_form input {
|
|||
.NB-modal-preferences .NB-preference-story-styling .NB-preference-story-styling-serif {
|
||||
font-family: "Palatino Linotype", Georgia, "URW Palladio L", "Century Schoolbook L", serif;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference-story-size .NB-preference-options div {
|
||||
clear: both;
|
||||
overflow: hidden;
|
||||
margin: 1px 0 0 0;
|
||||
line-height: 18px;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-xs {
|
||||
font-size: 11px;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-s {
|
||||
font-size: 12px;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-m {
|
||||
font-size: 13px;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-l {
|
||||
font-size: 14px;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference-story-size .NB-preference-story-size-xl {
|
||||
font-size: 15px;
|
||||
}
|
||||
.NB-modal-preferences .NB-preference-window input {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
|
|
@ -92,9 +92,9 @@
|
|||
[options addObject:[deleteText uppercaseString]];
|
||||
|
||||
[options addObject:[@"Move to another folder" uppercaseString]];
|
||||
[options addObject:[@"Train this site" uppercaseString]];
|
||||
|
||||
if (!appDelegate.isRiverView) {
|
||||
[options addObject:[@"Train this site" uppercaseString]];
|
||||
[options addObject:[@"Insta-fetch stories" uppercaseString]];
|
||||
}
|
||||
|
||||
|
|
|
@ -888,7 +888,6 @@
|
|||
|
||||
|
||||
- (void)markFeedsReadWithAllStories:(BOOL)includeHidden {
|
||||
NSLog(@"mark feeds read: %d %d", appDelegate.isRiverView, includeHidden);
|
||||
if (appDelegate.isRiverView && [appDelegate.activeFolder isEqualToString:@"everything"]) {
|
||||
// Mark folder as read
|
||||
NSString *urlString = [NSString stringWithFormat:@"http://%@/reader/mark_all_as_read",
|
||||
|
@ -1193,7 +1192,6 @@
|
|||
}
|
||||
|
||||
- (void)changeActiveFeedDetailRow {
|
||||
NSLog(@"changeActiveFeedDetailRow in feed detail view");
|
||||
int rowIndex = [appDelegate locationOfActiveStory];
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:0];
|
||||
|
@ -1209,7 +1207,6 @@
|
|||
cellRect = [storyTitlesTable convertRect:cellRect toView:storyTitlesTable.superview];
|
||||
|
||||
BOOL completelyVisible = CGRectContainsRect(storyTitlesTable.frame, cellRect);
|
||||
|
||||
if (!completelyVisible) {
|
||||
[storyTitlesTable scrollToRowAtIndexPath:offsetIndexPath
|
||||
atScrollPosition:UITableViewScrollPositionTop
|
||||
|
@ -1224,9 +1221,7 @@
|
|||
// called when the user taps refresh button
|
||||
|
||||
- (void)instafetchFeed {
|
||||
NSLog(@"Instafetch");
|
||||
|
||||
NSString *urlString = [NSString
|
||||
NSString *urlString = [NSString
|
||||
stringWithFormat:@"http://%@/reader/refresh_feed/%@",
|
||||
NEWSBLUR_URL,
|
||||
[appDelegate.activeFeed objectForKey:@"id"]];
|
||||
|
|
|
@ -421,6 +421,7 @@
|
|||
self.storyNavigationController.view.frame = CGRectMake(NB_DEFAULT_MASTER_WIDTH + 1, 0, vb.size.width - NB_DEFAULT_MASTER_WIDTH - 1, vb.size.height);
|
||||
} completion:^(BOOL finished) {
|
||||
[self.feedDetailViewController checkScroll];
|
||||
[appDelegate.storyPageControl refreshPages];
|
||||
[appDelegate adjustStoryDetailWebView];
|
||||
[self.feedDetailViewController.storyTitlesTable reloadData];
|
||||
}];
|
||||
|
@ -450,7 +451,7 @@
|
|||
[self.masterNavigationController.view removeFromSuperview];
|
||||
self.storyNavigationController.view.frame = CGRectMake(0, 0, vb.size.width, storyTitlesYCoordinate);
|
||||
|
||||
self.storyTitlesStub.frame = CGRectMake(0, storyTitlesYCoordinate, vb.size.width, vb.size.height - storyTitlesYCoordinate);
|
||||
self.storyTitlesStub.frame = CGRectMake(0, storyTitlesYCoordinate, vb.size.width, vb.size.height - storyTitlesYCoordinate - 44 - 20);
|
||||
} completion:^(BOOL finished) {
|
||||
if ([[self.masterNavigationController viewControllers] containsObject:self.feedDetailViewController]) {
|
||||
[self.masterNavigationController popViewControllerAnimated:NO];
|
||||
|
@ -459,6 +460,7 @@
|
|||
self.feedDetailViewController.view.frame = CGRectMake(0, storyTitlesYCoordinate, vb.size.width, vb.size.height - storyTitlesYCoordinate);
|
||||
self.storyTitlesStub.hidden = YES;
|
||||
[self.feedDetailViewController checkScroll];
|
||||
[appDelegate.storyPageControl refreshPages];
|
||||
[appDelegate adjustStoryDetailWebView];
|
||||
[self.feedDetailViewController.storyTitlesTable reloadData];
|
||||
}];
|
||||
|
@ -472,7 +474,7 @@
|
|||
|
||||
// adding feedDetailViewController
|
||||
[self addChildViewController:self.feedDetailViewController];
|
||||
// [self.view addSubview:self.feedDetailViewController.view];
|
||||
[self.view addSubview:self.feedDetailViewController.view];
|
||||
[self.feedDetailViewController didMoveToParentViewController:self];
|
||||
|
||||
// adding storyDetailViewController
|
||||
|
@ -529,18 +531,6 @@
|
|||
// set center title
|
||||
UIView *titleLabel = [appDelegate makeFeedTitle:appDelegate.activeFeed];
|
||||
self.storyPageControl.navigationItem.titleView = titleLabel;
|
||||
|
||||
// // set right avatar title image
|
||||
// if (appDelegate.isSocialView) {
|
||||
// UIButton *titleImageButton = [appDelegate makeRightFeedTitle:appDelegate.activeFeed];
|
||||
// [titleImageButton addTarget:self action:@selector(showUserProfilePopover) forControlEvents:UIControlEventTouchUpInside];
|
||||
// UIBarButtonItem *titleImageBarButton = [[UIBarButtonItem alloc]
|
||||
// initWithCustomView:titleImageButton];
|
||||
// self.storyPageControl.navigationItem.rightBarButtonItem = titleImageBarButton;
|
||||
// } else {
|
||||
// self.storyPageControl.navigationItem.rightBarButtonItem = nil;
|
||||
// }
|
||||
|
||||
} else {
|
||||
// CASE: story titles on left
|
||||
self.storyPageControl.navigationItem.leftBarButtonItem = nil;
|
||||
|
@ -640,8 +630,7 @@
|
|||
CGRect vb = [self.view bounds];
|
||||
self.isSharingStory = YES;
|
||||
|
||||
NSLog(@"VB: %@", NSStringFromCGRect(self.view.bounds));
|
||||
// adding shareViewController
|
||||
// adding shareViewController
|
||||
[self addChildViewController:self.shareNavigationController];
|
||||
[self.view insertSubview:self.shareNavigationController.view
|
||||
aboveSubview:self.storyNavigationController.view];
|
||||
|
@ -707,13 +696,9 @@
|
|||
NSUserDefaults *userPreferences = [NSUserDefaults standardUserDefaults];
|
||||
|
||||
if (yCoordinate > 344 && yCoordinate <= (vb.size.height)) {
|
||||
|
||||
// save coordinate
|
||||
self.storyTitlesYCoordinate = yCoordinate;
|
||||
[userPreferences setInteger:yCoordinate forKey:@"storyTitlesYCoordinate"];
|
||||
[userPreferences synchronize];
|
||||
|
||||
// change frames
|
||||
|
||||
self.storyNavigationController.view.frame = CGRectMake(self.storyNavigationController.view.frame.origin.x,
|
||||
0,
|
||||
|
@ -733,7 +718,6 @@
|
|||
[self.feedDetailViewController checkScroll];
|
||||
}
|
||||
} else if (yCoordinate >= (vb.size.height)){
|
||||
// save coordinate
|
||||
[userPreferences setInteger:1004 forKey:@"storyTitlesYCoordinate"];
|
||||
[userPreferences synchronize];
|
||||
self.storyTitlesYCoordinate = 1004;
|
||||
|
@ -749,6 +733,7 @@
|
|||
0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-(void)keyboardWillShowOrHide:(NSNotification*)notification {
|
||||
|
@ -781,7 +766,6 @@
|
|||
shareViewFrame.origin.y = vb.size.height - NB_DEFAULT_SHARE_HEIGHT;
|
||||
} else {
|
||||
storyNavigationFrame.size.height = vb.size.height - NB_DEFAULT_SHARE_HEIGHT + 44;
|
||||
NSLog(@"storyNavigationFrame.size.height %f", storyNavigationFrame.size.height);
|
||||
shareViewFrame.origin.y = vb.size.height - NB_DEFAULT_SHARE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1165,7 +1165,7 @@ shouldStartLoadWithRequest:(NSURLRequest *)request
|
|||
|
||||
frame = CGRectMake(x, y, width, height);
|
||||
}
|
||||
NSLog(@"Open trainer: %@ (%d/%d/%d/%d)", NSStringFromCGRect(frame), x, y, width, height);
|
||||
// NSLog(@"Open trainer: %@ (%d/%d/%d/%d)", NSStringFromCGRect(frame), x, y, width, height);
|
||||
[appDelegate openTrainStory:[NSValue valueWithCGRect:frame]];
|
||||
}
|
||||
|
||||
|
|
|
@ -207,7 +207,6 @@
|
|||
}
|
||||
|
||||
- (void)resetPages {
|
||||
NSLog(@"resetPages");
|
||||
[currentPage clearStory];
|
||||
[nextPage clearStory];
|
||||
[previousPage clearStory];
|
||||
|
@ -219,7 +218,7 @@
|
|||
CGRect frame = self.scrollView.frame;
|
||||
self.scrollView.contentSize = frame.size;
|
||||
|
||||
NSLog(@"Pages are at: %f / %f / %f", previousPage.view.frame.origin.x, currentPage.view.frame.origin.x, nextPage.view.frame.origin.x);
|
||||
// NSLog(@"Pages are at: %f / %f / %f", previousPage.view.frame.origin.x, currentPage.view.frame.origin.x, nextPage.view.frame.origin.x);
|
||||
currentPage.view.frame = self.scrollView.frame;
|
||||
nextPage.view.frame = self.scrollView.frame;
|
||||
previousPage.view.frame = self.scrollView.frame;
|
||||
|
@ -231,7 +230,6 @@
|
|||
}
|
||||
|
||||
- (void)refreshPages {
|
||||
NSLog(@"refreshPages");
|
||||
[self resizeScrollView];
|
||||
[appDelegate adjustStoryDetailWebView];
|
||||
int pageIndex = currentPage.pageIndex;
|
||||
|
@ -253,7 +251,6 @@
|
|||
if (widthCount == 0) {
|
||||
widthCount = 1;
|
||||
}
|
||||
NSLog(@"resizeScrollView: %@", NSStringFromCGRect(self.scrollView.frame));
|
||||
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width
|
||||
* widthCount,
|
||||
self.scrollView.frame.size.height);
|
||||
|
@ -310,9 +307,8 @@
|
|||
pageController.view.frame = pageFrame;
|
||||
}
|
||||
|
||||
int wasIndex = pageController.pageIndex;
|
||||
pageController.pageIndex = newIndex;
|
||||
NSLog(@"Applied Index: Was %d, now %d (%d/%d/%d) [%d stories - %d]", wasIndex, newIndex, previousPage.pageIndex, currentPage.pageIndex, nextPage.pageIndex, [appDelegate.activeFeedStoryLocations count], outOfBounds);
|
||||
// NSLog(@"Applied Index: Was %d, now %d (%d/%d/%d) [%d stories - %d]", wasIndex, newIndex, previousPage.pageIndex, currentPage.pageIndex, nextPage.pageIndex, [appDelegate.activeFeedStoryLocations count], outOfBounds);
|
||||
|
||||
if (newIndex > 0 && newIndex >= [appDelegate.activeFeedStoryLocations count]) {
|
||||
pageController.pageIndex = -2;
|
||||
|
@ -320,7 +316,7 @@
|
|||
!self.appDelegate.feedDetailViewController.pageFinished &&
|
||||
!self.appDelegate.feedDetailViewController.pageFetching) {
|
||||
[self.appDelegate.feedDetailViewController fetchNextPage:^() {
|
||||
NSLog(@"Fetched next page, %d stories", [appDelegate.activeFeedStoryLocations count]);
|
||||
// NSLog(@"Fetched next page, %d stories", [appDelegate.activeFeedStoryLocations count]);
|
||||
[self applyNewIndex:newIndex pageController:pageController];
|
||||
}];
|
||||
} else if (!self.appDelegate.feedDetailViewController.pageFinished &&
|
||||
|
@ -342,7 +338,7 @@
|
|||
[pageController drawStory];
|
||||
} else {
|
||||
[pageController clearStory];
|
||||
NSLog(@"Skipping drawing %d (waiting for %d)", newIndex, self.scrollingToPage);
|
||||
// NSLog(@"Skipping drawing %d (waiting for %d)", newIndex, self.scrollingToPage);
|
||||
}
|
||||
} else if (outOfBounds) {
|
||||
[pageController clearStory];
|
||||
|
@ -367,34 +363,34 @@
|
|||
// NSLog(@"Did Scroll: %f = %d (%d/%d/%d)", fractionalPage, lowerNumber, previousPage.pageIndex, currentPage.pageIndex, nextPage.pageIndex);
|
||||
if (lowerNumber == currentPage.pageIndex) {
|
||||
if (upperNumber != nextPage.pageIndex) {
|
||||
NSLog(@"Next was %d, now %d (A)", nextPage.pageIndex, upperNumber);
|
||||
// NSLog(@"Next was %d, now %d (A)", nextPage.pageIndex, upperNumber);
|
||||
[self applyNewIndex:upperNumber pageController:nextPage];
|
||||
}
|
||||
if (previousNumber != previousPage.pageIndex) {
|
||||
NSLog(@"Prev was %d, now %d (A)", previousPage.pageIndex, previousNumber);
|
||||
// NSLog(@"Prev was %d, now %d (A)", previousPage.pageIndex, previousNumber);
|
||||
[self applyNewIndex:previousNumber pageController:previousPage];
|
||||
}
|
||||
} else if (upperNumber == currentPage.pageIndex) {
|
||||
// Going backwards
|
||||
if (lowerNumber != previousPage.pageIndex) {
|
||||
NSLog(@"Prev was %d, now %d (B)", previousPage.pageIndex, previousNumber);
|
||||
// NSLog(@"Prev was %d, now %d (B)", previousPage.pageIndex, previousNumber);
|
||||
[self applyNewIndex:lowerNumber pageController:previousPage];
|
||||
}
|
||||
} else {
|
||||
// Going forwards
|
||||
if (lowerNumber == nextPage.pageIndex) {
|
||||
NSLog(@"Prev was %d, now %d (C1)", previousPage.pageIndex, previousNumber);
|
||||
// NSLog(@"Prev was %d, now %d (C1)", previousPage.pageIndex, previousNumber);
|
||||
// [self applyNewIndex:upperNumber pageController:nextPage];
|
||||
// [self applyNewIndex:lowerNumber pageController:currentPage];
|
||||
[self applyNewIndex:previousNumber pageController:previousPage];
|
||||
} else if (upperNumber == nextPage.pageIndex) {
|
||||
NSLog(@"Prev was %d, now %d (C2)", previousPage.pageIndex, previousNumber);
|
||||
// NSLog(@"Prev was %d, now %d (C2)", previousPage.pageIndex, previousNumber);
|
||||
[self applyNewIndex:lowerNumber pageController:currentPage];
|
||||
[self applyNewIndex:previousNumber pageController:previousPage];
|
||||
} else {
|
||||
NSLog(@"Next was %d, now %d (C3)", nextPage.pageIndex, upperNumber);
|
||||
NSLog(@"Current was %d, now %d (C3)", currentPage.pageIndex, lowerNumber);
|
||||
NSLog(@"Prev was %d, now %d (C3)", previousPage.pageIndex, previousNumber);
|
||||
// NSLog(@"Next was %d, now %d (C3)", nextPage.pageIndex, upperNumber);
|
||||
// NSLog(@"Current was %d, now %d (C3)", currentPage.pageIndex, lowerNumber);
|
||||
// NSLog(@"Prev was %d, now %d (C3)", previousPage.pageIndex, previousNumber);
|
||||
[self applyNewIndex:lowerNumber pageController:currentPage];
|
||||
[self applyNewIndex:upperNumber pageController:nextPage];
|
||||
[self applyNewIndex:previousNumber pageController:previousPage];
|
||||
|
@ -448,7 +444,7 @@
|
|||
}
|
||||
|
||||
- (void)changePage:(NSInteger)pageIndex animated:(BOOL)animated {
|
||||
NSLog(@"changePage to %d (animated: %d)", pageIndex, animated);
|
||||
// NSLog(@"changePage to %d (animated: %d)", pageIndex, animated);
|
||||
// update the scroll view to the appropriate page
|
||||
[self resizeScrollView];
|
||||
|
||||
|
@ -487,14 +483,14 @@
|
|||
}
|
||||
|
||||
if (currentPage.pageIndex < nearestNumber) {
|
||||
NSLog(@"Swap next into current, current into previous: %d / %d", currentPage.pageIndex, nearestNumber);
|
||||
// NSLog(@"Swap next into current, current into previous: %d / %d", currentPage.pageIndex, nearestNumber);
|
||||
StoryDetailViewController *swapCurrentController = currentPage;
|
||||
StoryDetailViewController *swapPreviousController = previousPage;
|
||||
currentPage = nextPage;
|
||||
previousPage = swapCurrentController;
|
||||
nextPage = swapPreviousController;
|
||||
} else if (currentPage.pageIndex > nearestNumber) {
|
||||
NSLog(@"Swap previous into current: %d / %d", currentPage.pageIndex, nearestNumber);
|
||||
// NSLog(@"Swap previous into current: %d / %d", currentPage.pageIndex, nearestNumber);
|
||||
StoryDetailViewController *swapCurrentController = currentPage;
|
||||
StoryDetailViewController *swapNextController = nextPage;
|
||||
currentPage = previousPage;
|
||||
|
@ -502,7 +498,7 @@
|
|||
previousPage = swapNextController;
|
||||
}
|
||||
|
||||
NSLog(@"Set Story from scroll: %f = %d (%d/%d/%d)", fractionalPage, nearestNumber, previousPage.pageIndex, currentPage.pageIndex, nextPage.pageIndex);
|
||||
// NSLog(@"Set Story from scroll: %f = %d (%d/%d/%d)", fractionalPage, nearestNumber, previousPage.pageIndex, currentPage.pageIndex, nextPage.pageIndex);
|
||||
|
||||
nextPage.webView.scrollView.scrollsToTop = NO;
|
||||
previousPage.webView.scrollView.scrollsToTop = NO;
|
||||
|
@ -513,6 +509,7 @@
|
|||
self.scrollView.scrollsToTop = NO;
|
||||
|
||||
if (self.isDraggingScrollview || self.scrollingToPage == currentPage.pageIndex) {
|
||||
if (currentPage.pageIndex == -2) return;
|
||||
self.scrollingToPage = -1;
|
||||
int storyIndex = [appDelegate indexFromLocation:currentPage.pageIndex];
|
||||
appDelegate.activeStory = [appDelegate.activeFeedStories objectAtIndex:storyIndex];
|
||||
|
@ -544,15 +541,15 @@
|
|||
[appDelegate changeActiveFeedDetailRow];
|
||||
|
||||
if (self.currentPage.pageIndex != location) {
|
||||
NSLog(@"Updating Current: from %d to %d", currentPage.pageIndex, location);
|
||||
// NSLog(@"Updating Current: from %d to %d", currentPage.pageIndex, location);
|
||||
[self applyNewIndex:location pageController:self.currentPage];
|
||||
}
|
||||
if (self.nextPage.pageIndex != location+1) {
|
||||
NSLog(@"Updating Next: from %d to %d", nextPage.pageIndex, location+1);
|
||||
// NSLog(@"Updating Next: from %d to %d", nextPage.pageIndex, location+1);
|
||||
[self applyNewIndex:location+1 pageController:self.nextPage];
|
||||
}
|
||||
if (self.previousPage.pageIndex != location-1) {
|
||||
NSLog(@"Updating Previous: from %d to %d", previousPage.pageIndex, location-1);
|
||||
// NSLog(@"Updating Previous: from %d to %d", previousPage.pageIndex, location-1);
|
||||
[self applyNewIndex:location-1 pageController:self.previousPage];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -369,7 +369,9 @@
|
|||
NSString *titleTrainer = [NSString stringWithFormat:@"<div class=\"NB-trainer-section-inner\">"
|
||||
" <div class=\"NB-trainer-section-title\">Story Title</div>"
|
||||
" <div class=\"NB-trainer-section-body NB-title\">"
|
||||
" <div class=\"NB-title-trainer\">%@</div>"
|
||||
" <div class=\"NB-title-trainer\">"
|
||||
" <span>%@</span>"
|
||||
" </div>"
|
||||
" %@"
|
||||
" </div>"
|
||||
"</div>", storyTitle, titleClassifiers];
|
||||
|
|
|
@ -2463,8 +2463,8 @@
|
|||
HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/**";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PROVISIONING_PROFILE = "EE8BC292-FFF2-41A0-AE29-C4B39D6A2C5A";
|
||||
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "EE8BC292-FFF2-41A0-AE29-C4B39D6A2C5A";
|
||||
PROVISIONING_PROFILE = "548B341C-C438-40E2-943F-ACB87D42AED4";
|
||||
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "548B341C-C438-40E2-943F-ACB87D42AED4";
|
||||
RUN_CLANG_STATIC_ANALYZER = YES;
|
||||
SDKROOT = iphoneos;
|
||||
STRIP_INSTALLED_PRODUCT = NO;
|
||||
|
@ -2486,8 +2486,8 @@
|
|||
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
|
||||
OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1";
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PROVISIONING_PROFILE = "EE8BC292-FFF2-41A0-AE29-C4B39D6A2C5A";
|
||||
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "EE8BC292-FFF2-41A0-AE29-C4B39D6A2C5A";
|
||||
PROVISIONING_PROFILE = "548B341C-C438-40E2-943F-ACB87D42AED4";
|
||||
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "548B341C-C438-40E2-943F-ACB87D42AED4";
|
||||
SDKROOT = iphoneos;
|
||||
STRIP_INSTALLED_PRODUCT = NO;
|
||||
VALID_ARCHS = armv7;
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "Classes/StoryPageControl.m"
|
||||
timestampString = "377572385.895663"
|
||||
timestampString = "379293086.668773"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "301"
|
||||
endingLineNumber = "301"
|
||||
startingLineNumber = "298"
|
||||
endingLineNumber = "298"
|
||||
landmarkName = "-applyNewIndex:pageController:"
|
||||
landmarkType = "5">
|
||||
</FileBreakpoint>
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
- (NSString*)facebookLocalAppId {
|
||||
return @"";
|
||||
}
|
||||
// Read It Later - http://readitlaterlist.com/api/signup/
|
||||
// Pocket - http://getpocket.com/api/signup.php
|
||||
- (NSString*)readItLaterKey {
|
||||
return @"";
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
#define SHKFacebookLocalAppID @""
|
||||
#define SHKFacebookSessionProxyURL @""
|
||||
|
||||
// Read It Later - http://readitlaterlist.com/api/?shk
|
||||
// Pocket - http://getpocket.com/api/signup.php
|
||||
#define SHKReadItLaterKey @"c23d9HbTT2a8fma098AfIr9zQTgcF0l9"
|
||||
|
||||
// Twitter - http://dev.twitter.com/apps/new
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
+ (NSString *)sharerTitle
|
||||
{
|
||||
return @"Read It Later";
|
||||
return @"Pocket";
|
||||
}
|
||||
|
||||
+ (BOOL)canShareURL
|
||||
|
@ -72,7 +72,7 @@
|
|||
|
||||
+ (NSString *)authorizationFormCaption
|
||||
{
|
||||
return SHKLocalizedString(@"Create a free account at %@", @"Readitlaterlist.com");
|
||||
return SHKLocalizedString(@"Create a free account at %@", @"getpocket.com");
|
||||
}
|
||||
|
||||
- (void)authorizationFormValidate:(SHKFormController *)form
|
||||
|
|
|
@ -452,7 +452,6 @@ a.NB-show-profile {
|
|||
a.NB-show-profile {
|
||||
-webkit-text-size-adjust: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-highlight: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
@ -460,7 +459,6 @@ a.NB-show-profile {
|
|||
.NB-header,
|
||||
#NB-share-bar-wrapper {
|
||||
-webkit-user-select: none;
|
||||
-webkit-highlight: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -56,18 +56,25 @@
|
|||
padding: 0 12px 12px;
|
||||
}
|
||||
.NB-trainer-title .NB-title-trainer {
|
||||
padding: 18px 0 12px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
-webkit-user-select: text;
|
||||
-webkit-touch-callout: default;
|
||||
-webkit-highlight: auto;
|
||||
-webkit-tap-highlight-color: auto;
|
||||
}
|
||||
|
||||
.NB-trainer-title .NB-title-trainer span {
|
||||
display: block;
|
||||
padding: 18px 0 18px;
|
||||
min-height: 36px;
|
||||
}
|
||||
.NB-trainer-title .NB-title-trainer .NB-spacer {
|
||||
display: block;
|
||||
height: 1px;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: none;
|
||||
}
|
||||
.NB-title-info {
|
||||
text-transform: uppercase;
|
||||
color: #C0C0C0;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
NEWSBLUR.Modal = function(options) {
|
||||
var defaults = {};
|
||||
var defaults = {
|
||||
width: 600
|
||||
};
|
||||
|
||||
this.options = $.extend({}, defaults, options);
|
||||
this.options = _.extend({}, defaults, options);
|
||||
this.model = NEWSBLUR.assets;
|
||||
this.runner();
|
||||
this.flags = {};
|
||||
|
@ -15,8 +17,8 @@ NEWSBLUR.Modal.prototype = {
|
|||
var self = this;
|
||||
|
||||
this.$modal.modal({
|
||||
'minWidth': 600,
|
||||
'maxWidth': 600,
|
||||
'minWidth': this.options.width || 600,
|
||||
'maxWidth': this.options.width || 600,
|
||||
'overlayClose': true,
|
||||
'onOpen': function (dialog) {
|
||||
self.flags.open = true;
|
||||
|
|
|
@ -2264,6 +2264,13 @@
|
|||
$body.addClass('NB-theme-serif');
|
||||
}
|
||||
|
||||
$body.removeClass('NB-theme-size-xs')
|
||||
.removeClass('NB-theme-size-s')
|
||||
.removeClass('NB-theme-size-m')
|
||||
.removeClass('NB-theme-size-l')
|
||||
.removeClass('NB-theme-size-xl');
|
||||
$body.addClass('NB-theme-size-' + NEWSBLUR.Preferences['story_size']);
|
||||
|
||||
if (reset_stories) {
|
||||
this.show_story_titles_above_intelligence_level({'animate': true, 'follow': true});
|
||||
}
|
||||
|
@ -4603,7 +4610,9 @@
|
|||
$module.removeClass('NB-loading');
|
||||
$module.replaceWith(resp);
|
||||
self.load_javascript_elements_on_page();
|
||||
}, $.noop);
|
||||
}, function() {
|
||||
$module.removeClass('NB-loading');
|
||||
});
|
||||
},
|
||||
|
||||
// ===================
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
NEWSBLUR.ReaderAccount = function(options) {
|
||||
var defaults = {
|
||||
'width': 700,
|
||||
'animate_email': false,
|
||||
'change_password': false,
|
||||
'onOpen': _.bind(function() {
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
// - New window behavior
|
||||
|
||||
NEWSBLUR.ReaderPreferences = function(options) {
|
||||
var defaults = {};
|
||||
var defaults = {
|
||||
width: 700
|
||||
};
|
||||
|
||||
this.options = $.extend({}, defaults, options);
|
||||
this.model = NEWSBLUR.assets;
|
||||
|
@ -32,10 +34,15 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
var self = this;
|
||||
|
||||
this.$modal = $.make('div', { className: 'NB-modal-preferences NB-modal' }, [
|
||||
$.make('a', { href: '#preferences', className: 'NB-link-account-preferences NB-splash-link' }, 'Switch to Account'),
|
||||
$.make('div', { className: 'NB-modal-tabs' }, [
|
||||
$.make('div', { className: 'NB-modal-loading' }),
|
||||
$.make('div', { className: 'NB-modal-tab NB-active NB-modal-tab-general' }, 'General'),
|
||||
$.make('div', { className: 'NB-modal-tab NB-modal-tab-feeds' }, 'Feeds'),
|
||||
$.make('div', { className: 'NB-modal-tab NB-modal-tab-stories' }, 'Stories')
|
||||
]),
|
||||
$.make('h2', { className: 'NB-modal-title' }, 'Preferences'),
|
||||
$.make('form', { className: 'NB-preferences-form' }, [
|
||||
$.make('div', { className: 'NB-preferences-scroll' }, [
|
||||
$.make('div', { className: 'NB-tab NB-tab-general NB-active' }, [
|
||||
$.make('div', { className: 'NB-preference' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
|
@ -137,6 +144,138 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
'Timezone'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-ssl' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-ssl-1', type: 'radio', name: 'ssl', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-ssl-1' }, [
|
||||
'Use a standard connection'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-ssl-2', type: 'radio', name: 'ssl', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-ssl-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/lock.png' }),
|
||||
'Only use a secure https connection'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'SSL'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-showunreadcountsintitle' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-showunreadcountsintitle-1', type: 'checkbox', name: 'title_counts', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-showunreadcountsintitle-1' }, [
|
||||
'Show unread counts in the window title'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Window title'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-animations' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-animations-1', type: 'radio', name: 'animations', value: 'true' }),
|
||||
$.make('label', { 'for': 'NB-preference-animations-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/arrow_in.png' }),
|
||||
'Show all animations'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-animations-2', type: 'radio', name: 'animations', value: 'false' }),
|
||||
$.make('label', { 'for': 'NB-preference-animations-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/arrow_right.png' }),
|
||||
'Jump immediately with no animations'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Animations'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-feedorder' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-feedorder-1', type: 'radio', name: 'feed_order', value: 'ALPHABETICAL' }),
|
||||
$.make('label', { 'for': 'NB-preference-feedorder-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/pilcrow.png' }),
|
||||
'Alphabetical'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-feedorder-2', type: 'radio', name: 'feed_order', value: 'MOSTUSED' }),
|
||||
$.make('label', { 'for': 'NB-preference-feedorder-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/report_user.png' }),
|
||||
'Most used at top, then alphabetical'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Site sidebar order'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-folder-counts' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-folder-counts-1', type: 'radio', name: 'folder_counts', value: 'false' }),
|
||||
$.make('label', { 'for': 'NB-preference-folder-counts-1' }, [
|
||||
'Only show counts on collapsed folders'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-folder-counts-2', type: 'radio', name: 'folder_counts', value: 'true' }),
|
||||
$.make('label', { 'for': 'NB-preference-folder-counts-2' }, [
|
||||
'Always show unread counts on folders'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Folder unread counts'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-tooltips' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-tooltips-1', type: 'radio', name: 'show_tooltips', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-tooltips-1' }, [
|
||||
'Show tooltips'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-tooltips-2', type: 'radio', name: 'show_tooltips', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-tooltips-2' }, [
|
||||
'Don\'t bother showing tooltips'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label' }, [
|
||||
'Tooltips',
|
||||
$.make('div', { className: 'tipsy tipsy-n' }, [
|
||||
$.make('div', { className: 'tipsy-arrow' }),
|
||||
$.make('div', { className: 'tipsy-inner' }, 'Tooltips like this')
|
||||
]).css({
|
||||
'display': 'block',
|
||||
'top': 24,
|
||||
'left': -5
|
||||
})
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-opml' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('a', { className: 'NB-splash-link', href: NEWSBLUR.URLs['opml-export'] }, 'Download OPML')
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Backup your sites',
|
||||
$.make('div', { className: 'NB-preference-sublabel' }, 'Download this XML file as a backup')
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-tab NB-tab-feeds' }, [
|
||||
$.make('div', { className: 'NB-preference NB-preference-view' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
|
@ -179,6 +318,27 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
$.make('div', { className: 'NB-preference-sublabel' }, 'You can override this on a per-site and per-folder basis.')
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-singlestory' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-singlestory-1', type: 'radio', name: 'feed_view_single_story', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-singlestory-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/text_linespacing.png' }),
|
||||
'Show all stories'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-singlestory-2', type: 'radio', name: 'feed_view_single_story', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-singlestory-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/text_horizontalrule.png' }),
|
||||
'Show a single story at a time'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Feed view'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-story-pane-position' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
|
@ -207,90 +367,6 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
'Story titles pane'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-window' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-window-1', type: 'radio', name: 'new_window', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-window-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/application_view_gallery.png' }),
|
||||
'In this window'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-window-2', type: 'radio', name: 'new_window', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-window-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/application_side_expand.png' }),
|
||||
'In a new window'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Open links'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-hidereadfeeds' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-hidereadfeeds-1', type: 'radio', name: 'hide_read_feeds', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-hidereadfeeds-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/text_list_bullets.png' }),
|
||||
'Show everything'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-hidereadfeeds-2', type: 'radio', name: 'hide_read_feeds', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-hidereadfeeds-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/text_list_bullets_single.png' }),
|
||||
'Hide sites with no unread stories'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Site sidebar',
|
||||
$.make('div', { className: 'NB-preference-sublabel' }, this.make_site_sidebar_count())
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-feedorder' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-feedorder-1', type: 'radio', name: 'feed_order', value: 'ALPHABETICAL' }),
|
||||
$.make('label', { 'for': 'NB-preference-feedorder-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/pilcrow.png' }),
|
||||
'Alphabetical'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-feedorder-2', type: 'radio', name: 'feed_order', value: 'MOSTUSED' }),
|
||||
$.make('label', { 'for': 'NB-preference-feedorder-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/report_user.png' }),
|
||||
'Most used at top, then alphabetical'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Site sidebar order'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-ssl' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-ssl-1', type: 'radio', name: 'ssl', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-ssl-1' }, [
|
||||
'Use a standard connection'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-ssl-2', type: 'radio', name: 'ssl', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-ssl-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/lock.png' }),
|
||||
'Only use a secure https connection'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'SSL'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-openfeedaction' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
|
@ -340,149 +416,10 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Mark a story as read'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-hidestorychanges' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-hidestorychanges-1', type: 'radio', name: 'hide_story_changes', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-hidestorychanges-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/code_icon.png' }),
|
||||
'Show ',
|
||||
$.make('del', 'changes'),
|
||||
' ',
|
||||
$.make('ins', 'revisions'),
|
||||
' in stories'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-hidestorychanges-2', type: 'radio', name: 'hide_story_changes', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-hidestorychanges-2' }, [
|
||||
'Hide changes and only show the final story'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Story changes'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-singlestory' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-singlestory-1', type: 'radio', name: 'feed_view_single_story', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-singlestory-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/text_linespacing.png' }),
|
||||
'Show all stories'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-singlestory-2', type: 'radio', name: 'feed_view_single_story', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-singlestory-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/text_horizontalrule.png' }),
|
||||
'Show a single story at a time'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Feed view'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-animations' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-animations-1', type: 'radio', name: 'animations', value: 'true' }),
|
||||
$.make('label', { 'for': 'NB-preference-animations-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/arrow_in.png' }),
|
||||
'Show all animations'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-animations-2', type: 'radio', name: 'animations', value: 'false' }),
|
||||
$.make('label', { 'for': 'NB-preference-animations-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/arrow_right.png' }),
|
||||
'Jump immediately with no animations'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Animations'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-folder-counts' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-folder-counts-1', type: 'radio', name: 'folder_counts', value: 'false' }),
|
||||
$.make('label', { 'for': 'NB-preference-folder-counts-1' }, [
|
||||
'Only show counts on collapsed folders'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-folder-counts-2', type: 'radio', name: 'folder_counts', value: 'true' }),
|
||||
$.make('label', { 'for': 'NB-preference-folder-counts-2' }, [
|
||||
'Always show unread counts on folders'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Folder unread counts'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-story-styling' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-styling-1', type: 'radio', name: 'story_styling', value: 'sans-serif' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-styling-1', className: 'NB-preference-story-styling-sans-serif' }, 'Lucida Grande, sans serif')
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-styling-2', type: 'radio', name: 'story_styling', value: 'serif' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-styling-2', className: 'NB-preference-story-styling-serif' }, 'Georgia, serif')
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Feed view styling'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-public-comments' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-public-comments-1', type: 'radio', name: 'hide_public_comments', value: 'false' }),
|
||||
$.make('label', { 'for': 'NB-preference-public-comments-1' }, 'Show from both friends and the public')
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-public-comments-2', type: 'radio', name: 'hide_public_comments', value: 'true' }),
|
||||
$.make('label', { 'for': 'NB-preference-public-comments-2' }, 'Only show comments from friends')
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Show all comments'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-tooltips' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-tooltips-1', type: 'radio', name: 'show_tooltips', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-tooltips-1' }, [
|
||||
'Show tooltips'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-tooltips-2', type: 'radio', name: 'show_tooltips', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-tooltips-2' }, [
|
||||
'Don\'t bother showing tooltips'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label' }, [
|
||||
'Tooltips',
|
||||
$.make('div', { className: 'tipsy tipsy-n' }, [
|
||||
$.make('div', { className: 'tipsy-arrow' }),
|
||||
$.make('div', { className: 'tipsy-inner' }, 'Tooltips like this')
|
||||
]).css({
|
||||
'display': 'block',
|
||||
'top': 24,
|
||||
'left': -5
|
||||
})
|
||||
])
|
||||
]),
|
||||
])
|
||||
|
||||
]),
|
||||
$.make('div', { className: 'NB-tab NB-tab-stories' }, [
|
||||
$.make('div', { className: 'NB-preference NB-preference-story-share' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', { className: 'NB-preference-option', title: 'Twitter' }, [
|
||||
|
@ -538,15 +475,109 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
'Sharing services'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-opml' }, [
|
||||
$.make('div', { className: 'NB-preference NB-preference-window' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('a', { className: 'NB-splash-link', href: NEWSBLUR.URLs['opml-export'] }, 'Download OPML')
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-window-1', type: 'radio', name: 'new_window', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-window-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/application_view_gallery.png' }),
|
||||
'In this window'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-window-2', type: 'radio', name: 'new_window', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-window-2' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/icons/silk/application_side_expand.png' }),
|
||||
'In a new window'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Backup your sites',
|
||||
$.make('div', { className: 'NB-preference-sublabel' }, 'Download this XML file as a backup')
|
||||
'Open links'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-hidestorychanges' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-hidestorychanges-1', type: 'radio', name: 'hide_story_changes', value: 0 }),
|
||||
$.make('label', { 'for': 'NB-preference-hidestorychanges-1' }, [
|
||||
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL+'/img/reader/code_icon.png' }),
|
||||
'Show ',
|
||||
$.make('del', 'changes'),
|
||||
' ',
|
||||
$.make('ins', 'revisions'),
|
||||
' in stories'
|
||||
])
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-hidestorychanges-2', type: 'radio', name: 'hide_story_changes', value: 1 }),
|
||||
$.make('label', { 'for': 'NB-preference-hidestorychanges-2' }, [
|
||||
'Hide changes and only show the final story'
|
||||
])
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Story changes'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-story-styling' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-styling-1', type: 'radio', name: 'story_styling', value: 'sans-serif' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-styling-1', className: 'NB-preference-story-styling-sans-serif' }, 'Lucida Grande, sans serif')
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-styling-2', type: 'radio', name: 'story_styling', value: 'serif' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-styling-2', className: 'NB-preference-story-styling-serif' }, 'Georgia, serif')
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Feed view font family'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-story-size' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-size-1', type: 'radio', name: 'story_size', value: 'xs' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-size-1', className: 'NB-preference-story-size-xs' }, 'Extra small')
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-size-2', type: 'radio', name: 'story_size', value: 's' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-size-2', className: 'NB-preference-story-size-s' }, 'Small')
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-size-3', type: 'radio', name: 'story_size', value: 'm' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-size-3', className: 'NB-preference-story-size-m' }, 'Medium')
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-size-4', type: 'radio', name: 'story_size', value: 'l' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-size-4', className: 'NB-preference-story-size-l' }, 'Large')
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-story-size-5', type: 'radio', name: 'story_size', value: 'xl' }),
|
||||
$.make('label', { 'for': 'NB-preference-story-size-5', className: 'NB-preference-story-size-xl' }, 'Extra large')
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Feed view text size'
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference NB-preference-public-comments' }, [
|
||||
$.make('div', { className: 'NB-preference-options' }, [
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-public-comments-1', type: 'radio', name: 'hide_public_comments', value: 'false' }),
|
||||
$.make('label', { 'for': 'NB-preference-public-comments-1' }, 'Show from both friends and the public')
|
||||
]),
|
||||
$.make('div', [
|
||||
$.make('input', { id: 'NB-preference-public-comments-2', type: 'radio', name: 'hide_public_comments', value: 'true' }),
|
||||
$.make('label', { 'for': 'NB-preference-public-comments-2' }, 'Only show comments from friends')
|
||||
])
|
||||
]),
|
||||
$.make('div', { className: 'NB-preference-label'}, [
|
||||
'Show all comments'
|
||||
])
|
||||
])
|
||||
|
||||
]),
|
||||
$.make('div', { className: 'NB-modal-submit' }, [
|
||||
$.make('input', { type: 'submit', disabled: 'true', className: 'NB-modal-submit-green NB-disabled', value: 'Change what you like above...' }),
|
||||
|
@ -562,7 +593,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
},
|
||||
|
||||
resize_modal: function() {
|
||||
var $scroll = $('.NB-preferences-scroll', this.$modal);
|
||||
var $scroll = $('.NB-tab.NB-active', this.$modal);
|
||||
var $modal = this.$modal;
|
||||
var $modal_container = $modal.closest('.simplemodal-container');
|
||||
|
||||
|
@ -603,12 +634,6 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
return false;
|
||||
}
|
||||
});
|
||||
$('input[name=hide_read_feeds]', $modal).each(function() {
|
||||
if ($(this).val() == NEWSBLUR.Preferences.hide_read_feeds) {
|
||||
$(this).attr('checked', true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$('input[name=feed_order]', $modal).each(function() {
|
||||
if ($(this).val() == NEWSBLUR.Preferences.feed_order) {
|
||||
$(this).attr('checked', true);
|
||||
|
@ -621,6 +646,12 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
return false;
|
||||
}
|
||||
});
|
||||
$('input[name=title_counts]', $modal).each(function() {
|
||||
if (NEWSBLUR.Preferences.title_counts) {
|
||||
$(this).attr('checked', true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$('input[name=open_feed_action]', $modal).each(function() {
|
||||
if ($(this).val() == NEWSBLUR.Preferences.open_feed_action) {
|
||||
$(this).attr('checked', true);
|
||||
|
@ -669,6 +700,12 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
return false;
|
||||
}
|
||||
});
|
||||
$('input[name=story_size]', $modal).each(function() {
|
||||
if ($(this).val() == NEWSBLUR.Preferences.story_size) {
|
||||
$(this).attr('checked', true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$('input[name=hide_public_comments]', $modal).each(function() {
|
||||
if ($(this).val() == ""+NEWSBLUR.Preferences.hide_public_comments) {
|
||||
$(this).attr('checked', true);
|
||||
|
@ -742,6 +779,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
NEWSBLUR.reader.apply_story_styling(true);
|
||||
NEWSBLUR.reader.apply_tipsy_titles();
|
||||
NEWSBLUR.app.story_list.show_stories_preference_in_feed_view();
|
||||
NEWSBLUR.app.sidebar_header.count();
|
||||
if (self.original_preferences['feed_order'] != form['feed_order'] ||
|
||||
self.original_preferences['folder_counts'] != form['folder_counts']) {
|
||||
NEWSBLUR.app.feed_list.make_feeds();
|
||||
|
@ -757,23 +795,6 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
});
|
||||
},
|
||||
|
||||
make_site_sidebar_count: function() {
|
||||
var sites = _.keys(this.model.feeds).length;
|
||||
var unreads = _.select(this.model.feeds, function(f) {
|
||||
return f.ng || f.nt || f.ps;
|
||||
}).length;
|
||||
var message = [
|
||||
"Currently ",
|
||||
unreads,
|
||||
" out of ",
|
||||
sites,
|
||||
Inflector.pluralize(' site', sites),
|
||||
" would be shown."
|
||||
].join('');
|
||||
|
||||
return message;
|
||||
},
|
||||
|
||||
close_and_load_account: function() {
|
||||
this.close(function() {
|
||||
NEWSBLUR.reader.open_account_modal();
|
||||
|
@ -801,6 +822,19 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
handle_click: function(elem, e) {
|
||||
var self = this;
|
||||
|
||||
$.targetIs(e, { tagSelector: '.NB-modal-tab' }, function($t, $p) {
|
||||
e.preventDefault();
|
||||
var newtab;
|
||||
if ($t.hasClass('NB-modal-tab-general')) {
|
||||
newtab = 'general';
|
||||
} else if ($t.hasClass('NB-modal-tab-feeds')) {
|
||||
newtab = 'feeds';
|
||||
} else if ($t.hasClass('NB-modal-tab-stories')) {
|
||||
newtab = 'stories';
|
||||
}
|
||||
self.switch_tab(newtab);
|
||||
});
|
||||
|
||||
$.targetIs(e, { tagSelector: '.NB-add-url-submit' }, function($t, $p) {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -833,6 +867,19 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
|
|||
$('input[type=radio],input[type=checkbox],select', this.$modal).bind('change', _.bind(this.enable_save, this));
|
||||
},
|
||||
|
||||
switch_tab: function(newtab) {
|
||||
var $modal_tabs = $('.NB-modal-tab', this.$modal);
|
||||
var $tabs = $('.NB-tab', this.$modal);
|
||||
|
||||
$modal_tabs.removeClass('NB-active');
|
||||
$tabs.removeClass('NB-active');
|
||||
|
||||
$modal_tabs.filter('.NB-modal-tab-'+newtab).addClass('NB-active');
|
||||
$tabs.filter('.NB-tab-'+newtab).addClass('NB-active');
|
||||
|
||||
this.resize_modal();
|
||||
},
|
||||
|
||||
enable_save: function() {
|
||||
$('input[type=submit]', this.$modal).removeAttr('disabled').removeClass('NB-disabled').val('Save Preferences');
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
|
|||
this.fetch_user_profile();
|
||||
|
||||
this.$modal.bind('click', $.rescope(this.handle_click, this));
|
||||
this.$modal.bind('change', $.rescope(this.handle_change, this));
|
||||
this.handle_profile_counts();
|
||||
this.delegate_change();
|
||||
},
|
||||
|
@ -217,7 +218,9 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
|
|||
make_profile_photo_chooser: function() {
|
||||
var $profiles = $('.NB-friends-profilephoto', this.$modal).empty();
|
||||
|
||||
_.each(['nothing', 'twitter', 'facebook', 'gravatar'], _.bind(function(service) {
|
||||
$profiles.append($.make('div', { className: "NB-photo-upload-error NB-error" }));
|
||||
|
||||
_.each(['nothing', 'upload', 'twitter', 'facebook', 'gravatar'], _.bind(function(service) {
|
||||
var $profile = $.make('div', { className: 'NB-friends-profile-photo-group NB-friends-photo-'+service }, [
|
||||
$.make('div', { className: 'NB-friends-photo-title' }, [
|
||||
$.make('input', { type: 'radio', name: 'profile_photo_service', value: service, id: 'NB-profile-photo-service-'+service }),
|
||||
|
@ -233,8 +236,10 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
|
|||
])
|
||||
]),
|
||||
(service == 'upload' && $.make('div', { className: 'NB-photo-link' }, [
|
||||
$.make('a', { href: '#', className: 'NB-photo-upload-link NB-splash-link' }, 'Upload picture'),
|
||||
$.make('input', { type: 'file', name: 'photo' })
|
||||
$.make('form', { method: 'post', enctype: 'multipart/form-data', encoding: 'multipart/form-data' }, [
|
||||
$.make('a', { href: '#', className: 'NB-photo-upload-link NB-splash-link' }, 'upload picture'),
|
||||
$.make('input', { type: 'file', name: 'photo', id: "NB-photo-upload-file", className: 'NB-photo-upload-file' })
|
||||
])
|
||||
])),
|
||||
(service == 'gravatar' && $.make('div', { className: 'NB-gravatar-link' }, [
|
||||
$.make('a', { href: 'http://www.gravatar.com', className: 'NB-splash-link', target: '_blank' }, 'gravatar.com')
|
||||
|
@ -463,6 +468,15 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
|
|||
});
|
||||
},
|
||||
|
||||
handle_change: function(elem, e) {
|
||||
var self = this;
|
||||
$.targetIs(e, { tagSelector: '.NB-photo-upload-file' }, function($t, $p) {
|
||||
e.preventDefault();
|
||||
|
||||
self.handle_photo_upload();
|
||||
});
|
||||
},
|
||||
|
||||
handle_cancel: function() {
|
||||
var $cancel = $('.NB-modal-cancel', this.$modal);
|
||||
|
||||
|
@ -492,6 +506,65 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
|
|||
});
|
||||
},
|
||||
|
||||
|
||||
handle_photo_upload: function() {
|
||||
var self = this;
|
||||
var $loading = $('.NB-modal-loading', this.$modal);
|
||||
var $error = $('.NB-photo-upload-error', this.$modal);
|
||||
var $file = $('.NB-photo-upload-file', this.$modal);
|
||||
|
||||
$error.slideUp(300);
|
||||
$loading.addClass('NB-active');
|
||||
|
||||
var params = {
|
||||
url: NEWSBLUR.URLs['upload-avatar'],
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
success: _.bind(function(data, status) {
|
||||
if (data.code < 0) {
|
||||
this.error_uploading_photo();
|
||||
} else {
|
||||
$loading.removeClass('NB-active');
|
||||
console.log(["success uploading", data, status, this]);
|
||||
NEWSBLUR.assets.user_profile.set(data.user_profile);
|
||||
this.services = data.services;
|
||||
this.make_profile_section();
|
||||
this.make_profile_photo_chooser();
|
||||
}
|
||||
}, this),
|
||||
error: _.bind(this.error_uploading_photo, this),
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false
|
||||
};
|
||||
if (window.FormData) {
|
||||
var formData = new FormData($file.closest('form')[0]);
|
||||
params['data'] = formData;
|
||||
|
||||
$.ajax(params);
|
||||
} else {
|
||||
// IE9 has no FormData
|
||||
params['secureuri'] = false;
|
||||
params['fileElementId'] = 'NB-photo-upload-file';
|
||||
params['dataType'] = 'json';
|
||||
|
||||
$.ajaxFileUpload(params);
|
||||
}
|
||||
|
||||
$file.replaceWith($file.clone());
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
error_uploading_photo: function() {
|
||||
var $loading = $('.NB-modal-loading', this.$modal);
|
||||
var $error = $('.NB-photo-upload-error', this.$modal);
|
||||
|
||||
$loading.removeClass('NB-active');
|
||||
$error.text("There was a problem uploading your photo.");
|
||||
$error.slideDown(300);
|
||||
},
|
||||
|
||||
delegate_change: function() {
|
||||
$('.NB-tab-profile', this.$modal).delegate('input[type=radio],input[type=checkbox],select', 'change', _.bind(this.enable_save_profile, this));
|
||||
$('.NB-tab-profile', this.$modal).delegate('input[type=text]', 'keydown', _.bind(this.enable_save_profile, this));
|
||||
|
|
|
@ -65,6 +65,7 @@ NEWSBLUR.Views.SidebarHeader = Backbone.View.extend({
|
|||
this.feeds_count = this.count_feeds();
|
||||
|
||||
if (!NEWSBLUR.Globals.is_authenticated) return;
|
||||
if (!NEWSBLUR.assets.preference('title_counts')) return;
|
||||
|
||||
var counts = [];
|
||||
var unread_view = _.isNumber(this.options.unread_view) && this.options.unread_view || NEWSBLUR.assets.preference('unread_view');
|
||||
|
|
2
node/node_modules/mkdirp/.npmignore
generated
vendored
Normal file
2
node/node_modules/mkdirp/.npmignore
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
npm-debug.log
|
4
node/node_modules/mkdirp/.travis.yml
generated
vendored
Normal file
4
node/node_modules/mkdirp/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 0.4
|
||||
- 0.6
|
21
node/node_modules/mkdirp/LICENSE
generated
vendored
Normal file
21
node/node_modules/mkdirp/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
Copyright 2010 James Halliday (mail@substack.net)
|
||||
|
||||
This project is free software released under the MIT/X11 license:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
61
node/node_modules/mkdirp/README.markdown
generated
vendored
Normal file
61
node/node_modules/mkdirp/README.markdown
generated
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
mkdirp
|
||||
======
|
||||
|
||||
Like `mkdir -p`, but in node.js!
|
||||
|
||||
[](http://travis-ci.org/substack/node-mkdirp)
|
||||
|
||||
example
|
||||
=======
|
||||
|
||||
pow.js
|
||||
------
|
||||
var mkdirp = require('mkdirp');
|
||||
|
||||
mkdirp('/tmp/foo/bar/baz', function (err) {
|
||||
if (err) console.error(err)
|
||||
else console.log('pow!')
|
||||
});
|
||||
|
||||
Output
|
||||
pow!
|
||||
|
||||
And now /tmp/foo/bar/baz exists, huzzah!
|
||||
|
||||
methods
|
||||
=======
|
||||
|
||||
var mkdirp = require('mkdirp');
|
||||
|
||||
mkdirp(dir, mode, cb)
|
||||
---------------------
|
||||
|
||||
Create a new directory and any necessary subdirectories at `dir` with octal
|
||||
permission string `mode`.
|
||||
|
||||
If `mode` isn't specified, it defaults to `0777 & (~process.umask())`.
|
||||
|
||||
`cb(err, made)` fires with the error or the first directory `made`
|
||||
that had to be created, if any.
|
||||
|
||||
mkdirp.sync(dir, mode)
|
||||
----------------------
|
||||
|
||||
Synchronously create a new directory and any necessary subdirectories at `dir`
|
||||
with octal permission string `mode`.
|
||||
|
||||
If `mode` isn't specified, it defaults to `0777 & (~process.umask())`.
|
||||
|
||||
Returns the first directory that had to be created, if any.
|
||||
|
||||
install
|
||||
=======
|
||||
|
||||
With [npm](http://npmjs.org) do:
|
||||
|
||||
npm install mkdirp
|
||||
|
||||
license
|
||||
=======
|
||||
|
||||
MIT/X11
|
6
node/node_modules/mkdirp/examples/pow.js
generated
vendored
Normal file
6
node/node_modules/mkdirp/examples/pow.js
generated
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
var mkdirp = require('mkdirp');
|
||||
|
||||
mkdirp('/tmp/foo/bar/baz', function (err) {
|
||||
if (err) console.error(err)
|
||||
else console.log('pow!')
|
||||
});
|
82
node/node_modules/mkdirp/index.js
generated
vendored
Normal file
82
node/node_modules/mkdirp/index.js
generated
vendored
Normal file
|
@ -0,0 +1,82 @@
|
|||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP;
|
||||
|
||||
function mkdirP (p, mode, f, made) {
|
||||
if (typeof mode === 'function' || mode === undefined) {
|
||||
f = mode;
|
||||
mode = 0777 & (~process.umask());
|
||||
}
|
||||
if (!made) made = null;
|
||||
|
||||
var cb = f || function () {};
|
||||
if (typeof mode === 'string') mode = parseInt(mode, 8);
|
||||
p = path.resolve(p);
|
||||
|
||||
fs.mkdir(p, mode, function (er) {
|
||||
if (!er) {
|
||||
made = made || p;
|
||||
return cb(null, made);
|
||||
}
|
||||
switch (er.code) {
|
||||
case 'ENOENT':
|
||||
mkdirP(path.dirname(p), mode, function (er, made) {
|
||||
if (er) cb(er, made);
|
||||
else mkdirP(p, mode, cb, made);
|
||||
});
|
||||
break;
|
||||
|
||||
// In the case of any other error, just see if there's a dir
|
||||
// there already. If so, then hooray! If not, then something
|
||||
// is borked.
|
||||
default:
|
||||
fs.stat(p, function (er2, stat) {
|
||||
// if the stat fails, then that's super weird.
|
||||
// let the original error be the failure reason.
|
||||
if (er2 || !stat.isDirectory()) cb(er, made)
|
||||
else cb(null, made);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mkdirP.sync = function sync (p, mode, made) {
|
||||
if (mode === undefined) {
|
||||
mode = 0777 & (~process.umask());
|
||||
}
|
||||
if (!made) made = null;
|
||||
|
||||
if (typeof mode === 'string') mode = parseInt(mode, 8);
|
||||
p = path.resolve(p);
|
||||
|
||||
try {
|
||||
fs.mkdirSync(p, mode);
|
||||
made = made || p;
|
||||
}
|
||||
catch (err0) {
|
||||
switch (err0.code) {
|
||||
case 'ENOENT' :
|
||||
made = sync(path.dirname(p), mode, made);
|
||||
sync(p, mode, made);
|
||||
break;
|
||||
|
||||
// In the case of any other error, just see if there's a dir
|
||||
// there already. If so, then hooray! If not, then something
|
||||
// is borked.
|
||||
default:
|
||||
var stat;
|
||||
try {
|
||||
stat = fs.statSync(p);
|
||||
}
|
||||
catch (err1) {
|
||||
throw err0;
|
||||
}
|
||||
if (!stat.isDirectory()) throw err0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return made;
|
||||
};
|
40
node/node_modules/mkdirp/package.json
generated
vendored
Normal file
40
node/node_modules/mkdirp/package.json
generated
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "mkdirp",
|
||||
"description": "Recursively mkdir, like `mkdir -p`",
|
||||
"version": "0.3.4",
|
||||
"author": {
|
||||
"name": "James Halliday",
|
||||
"email": "mail@substack.net",
|
||||
"url": "http://substack.net"
|
||||
},
|
||||
"main": "./index",
|
||||
"keywords": [
|
||||
"mkdir",
|
||||
"directory"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/substack/node-mkdirp.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tap test/*.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "~0.2.4"
|
||||
},
|
||||
"license": "MIT/X11",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"_id": "mkdirp@0.3.4",
|
||||
"dependencies": {},
|
||||
"optionalDependencies": {},
|
||||
"_engineSupported": true,
|
||||
"_npmVersion": "1.1.12",
|
||||
"_nodeVersion": "v0.6.14",
|
||||
"_defaultsLoaded": true,
|
||||
"dist": {
|
||||
"shasum": "5673ccb5622bbdab381ce014eecea71cd14209af"
|
||||
},
|
||||
"_from": "mkdirp"
|
||||
}
|
38
node/node_modules/mkdirp/test/chmod.js
generated
vendored
Normal file
38
node/node_modules/mkdirp/test/chmod.js
generated
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
var mkdirp = require('../').mkdirp;
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
var ps = [ '', 'tmp' ];
|
||||
|
||||
for (var i = 0; i < 25; i++) {
|
||||
var dir = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
ps.push(dir);
|
||||
}
|
||||
|
||||
var file = ps.join('/');
|
||||
|
||||
test('chmod-pre', function (t) {
|
||||
var mode = 0744
|
||||
mkdirp(file, mode, function (er) {
|
||||
t.ifError(er, 'should not error');
|
||||
fs.stat(file, function (er, stat) {
|
||||
t.ifError(er, 'should exist');
|
||||
t.ok(stat && stat.isDirectory(), 'should be directory');
|
||||
t.equal(stat && stat.mode & 0777, mode, 'should be 0744');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('chmod', function (t) {
|
||||
var mode = 0755
|
||||
mkdirp(file, mode, function (er) {
|
||||
t.ifError(er, 'should not error');
|
||||
fs.stat(file, function (er, stat) {
|
||||
t.ifError(er, 'should exist');
|
||||
t.ok(stat && stat.isDirectory(), 'should be directory');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
37
node/node_modules/mkdirp/test/clobber.js
generated
vendored
Normal file
37
node/node_modules/mkdirp/test/clobber.js
generated
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
var mkdirp = require('../').mkdirp;
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
var ps = [ '', 'tmp' ];
|
||||
|
||||
for (var i = 0; i < 25; i++) {
|
||||
var dir = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
ps.push(dir);
|
||||
}
|
||||
|
||||
var file = ps.join('/');
|
||||
|
||||
// a file in the way
|
||||
var itw = ps.slice(0, 3).join('/');
|
||||
|
||||
|
||||
test('clobber-pre', function (t) {
|
||||
console.error("about to write to "+itw)
|
||||
fs.writeFileSync(itw, 'I AM IN THE WAY, THE TRUTH, AND THE LIGHT.');
|
||||
|
||||
fs.stat(itw, function (er, stat) {
|
||||
t.ifError(er)
|
||||
t.ok(stat && stat.isFile(), 'should be file')
|
||||
t.end()
|
||||
})
|
||||
})
|
||||
|
||||
test('clobber', function (t) {
|
||||
t.plan(2);
|
||||
mkdirp(file, 0755, function (err) {
|
||||
t.ok(err);
|
||||
t.equal(err.code, 'ENOTDIR');
|
||||
t.end();
|
||||
});
|
||||
});
|
28
node/node_modules/mkdirp/test/mkdirp.js
generated
vendored
Normal file
28
node/node_modules/mkdirp/test/mkdirp.js
generated
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('woo', function (t) {
|
||||
t.plan(2);
|
||||
var x = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var y = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var z = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
|
||||
var file = '/tmp/' + [x,y,z].join('/');
|
||||
|
||||
mkdirp(file, 0755, function (err) {
|
||||
if (err) t.fail(err);
|
||||
else path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
t.equal(stat.mode & 0777, 0755);
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
t.end();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
32
node/node_modules/mkdirp/test/perm.js
generated
vendored
Normal file
32
node/node_modules/mkdirp/test/perm.js
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('async perm', function (t) {
|
||||
t.plan(2);
|
||||
var file = '/tmp/' + (Math.random() * (1<<30)).toString(16);
|
||||
|
||||
mkdirp(file, 0755, function (err) {
|
||||
if (err) t.fail(err);
|
||||
else path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
t.equal(stat.mode & 0777, 0755);
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
t.end();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
test('async root perm', function (t) {
|
||||
mkdirp('/tmp', 0755, function (err) {
|
||||
if (err) t.fail(err);
|
||||
t.end();
|
||||
});
|
||||
t.end();
|
||||
});
|
39
node/node_modules/mkdirp/test/perm_sync.js
generated
vendored
Normal file
39
node/node_modules/mkdirp/test/perm_sync.js
generated
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('sync perm', function (t) {
|
||||
t.plan(2);
|
||||
var file = '/tmp/' + (Math.random() * (1<<30)).toString(16) + '.json';
|
||||
|
||||
mkdirp.sync(file, 0755);
|
||||
path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
t.equal(stat.mode & 0777, 0755);
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
t.end();
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
test('sync root perm', function (t) {
|
||||
t.plan(1);
|
||||
|
||||
var file = '/tmp';
|
||||
mkdirp.sync(file, 0755);
|
||||
path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
t.end();
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
41
node/node_modules/mkdirp/test/race.js
generated
vendored
Normal file
41
node/node_modules/mkdirp/test/race.js
generated
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
var mkdirp = require('../').mkdirp;
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('race', function (t) {
|
||||
t.plan(4);
|
||||
var ps = [ '', 'tmp' ];
|
||||
|
||||
for (var i = 0; i < 25; i++) {
|
||||
var dir = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
ps.push(dir);
|
||||
}
|
||||
var file = ps.join('/');
|
||||
|
||||
var res = 2;
|
||||
mk(file, function () {
|
||||
if (--res === 0) t.end();
|
||||
});
|
||||
|
||||
mk(file, function () {
|
||||
if (--res === 0) t.end();
|
||||
});
|
||||
|
||||
function mk (file, cb) {
|
||||
mkdirp(file, 0755, function (err) {
|
||||
if (err) t.fail(err);
|
||||
else path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
t.equal(stat.mode & 0777, 0755);
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
if (cb) cb();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
32
node/node_modules/mkdirp/test/rel.js
generated
vendored
Normal file
32
node/node_modules/mkdirp/test/rel.js
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('rel', function (t) {
|
||||
t.plan(2);
|
||||
var x = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var y = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var z = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
|
||||
var cwd = process.cwd();
|
||||
process.chdir('/tmp');
|
||||
|
||||
var file = [x,y,z].join('/');
|
||||
|
||||
mkdirp(file, 0755, function (err) {
|
||||
if (err) t.fail(err);
|
||||
else path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
process.chdir(cwd);
|
||||
t.equal(stat.mode & 0777, 0755);
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
t.end();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
25
node/node_modules/mkdirp/test/return.js
generated
vendored
Normal file
25
node/node_modules/mkdirp/test/return.js
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('return value', function (t) {
|
||||
t.plan(4);
|
||||
var x = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var y = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var z = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
|
||||
var file = '/tmp/' + [x,y,z].join('/');
|
||||
|
||||
// should return the first dir created.
|
||||
// By this point, it would be profoundly surprising if /tmp didn't
|
||||
// already exist, since every other test makes things in there.
|
||||
mkdirp(file, function (err, made) {
|
||||
t.ifError(err);
|
||||
t.equal(made, '/tmp/' + x);
|
||||
mkdirp(file, function (err, made) {
|
||||
t.ifError(err);
|
||||
t.equal(made, null);
|
||||
});
|
||||
});
|
||||
});
|
24
node/node_modules/mkdirp/test/return_sync.js
generated
vendored
Normal file
24
node/node_modules/mkdirp/test/return_sync.js
generated
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('return value', function (t) {
|
||||
t.plan(2);
|
||||
var x = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var y = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var z = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
|
||||
var file = '/tmp/' + [x,y,z].join('/');
|
||||
|
||||
// should return the first dir created.
|
||||
// By this point, it would be profoundly surprising if /tmp didn't
|
||||
// already exist, since every other test makes things in there.
|
||||
// Note that this will throw on failure, which will fail the test.
|
||||
var made = mkdirp.sync(file);
|
||||
t.equal(made, '/tmp/' + x);
|
||||
|
||||
// making the same file again should have no effect.
|
||||
made = mkdirp.sync(file);
|
||||
t.equal(made, null);
|
||||
});
|
18
node/node_modules/mkdirp/test/root.js
generated
vendored
Normal file
18
node/node_modules/mkdirp/test/root.js
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('root', function (t) {
|
||||
// '/' on unix, 'c:/' on windows.
|
||||
var file = path.resolve('/');
|
||||
|
||||
mkdirp(file, 0755, function (err) {
|
||||
if (err) throw err
|
||||
fs.stat(file, function (er, stat) {
|
||||
if (er) throw er
|
||||
t.ok(stat.isDirectory(), 'target is a directory');
|
||||
t.end();
|
||||
})
|
||||
});
|
||||
});
|
32
node/node_modules/mkdirp/test/sync.js
generated
vendored
Normal file
32
node/node_modules/mkdirp/test/sync.js
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('sync', function (t) {
|
||||
t.plan(2);
|
||||
var x = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var y = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var z = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
|
||||
var file = '/tmp/' + [x,y,z].join('/');
|
||||
|
||||
try {
|
||||
mkdirp.sync(file, 0755);
|
||||
} catch (err) {
|
||||
t.fail(err);
|
||||
return t.end();
|
||||
}
|
||||
|
||||
path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
t.equal(stat.mode & 0777, 0755);
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
t.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
28
node/node_modules/mkdirp/test/umask.js
generated
vendored
Normal file
28
node/node_modules/mkdirp/test/umask.js
generated
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('implicit mode from umask', function (t) {
|
||||
t.plan(2);
|
||||
var x = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var y = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var z = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
|
||||
var file = '/tmp/' + [x,y,z].join('/');
|
||||
|
||||
mkdirp(file, function (err) {
|
||||
if (err) t.fail(err);
|
||||
else path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
t.equal(stat.mode & 0777, 0777 & (~process.umask()));
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
t.end();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
32
node/node_modules/mkdirp/test/umask_sync.js
generated
vendored
Normal file
32
node/node_modules/mkdirp/test/umask_sync.js
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
var mkdirp = require('../');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var test = require('tap').test;
|
||||
|
||||
test('umask sync modes', function (t) {
|
||||
t.plan(2);
|
||||
var x = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var y = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
var z = Math.floor(Math.random() * Math.pow(16,4)).toString(16);
|
||||
|
||||
var file = '/tmp/' + [x,y,z].join('/');
|
||||
|
||||
try {
|
||||
mkdirp.sync(file);
|
||||
} catch (err) {
|
||||
t.fail(err);
|
||||
return t.end();
|
||||
}
|
||||
|
||||
path.exists(file, function (ex) {
|
||||
if (!ex) t.fail('file not created')
|
||||
else fs.stat(file, function (err, stat) {
|
||||
if (err) t.fail(err)
|
||||
else {
|
||||
t.equal(stat.mode & 0777, (0777 & (~process.umask())));
|
||||
t.ok(stat.isDirectory(), 'target not a directory');
|
||||
t.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
56
node/original_page.coffee
Normal file
56
node/original_page.coffee
Normal file
|
@ -0,0 +1,56 @@
|
|||
express = require 'express'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
mkdirp = require 'mkdirp'
|
||||
|
||||
DEV = process.env.NODE_ENV == 'development'
|
||||
|
||||
DB_PATH = if DEV then 'originals' else '/srv/originals'
|
||||
app = express.createServer()
|
||||
app.use express.bodyParser()
|
||||
|
||||
app.listen 3060
|
||||
|
||||
app.get /^\/original_page\/(\d+)\/?/, (req, res) =>
|
||||
feedId = parseInt(req.params, 10)
|
||||
etag = req.header('If-None-Match')
|
||||
lastModified = req.header('If-Modified-Since')
|
||||
feedIdDir = splitFeedId feedId
|
||||
filePath = "#{DB_PATH}/#{feedIdDir}.zhtml"
|
||||
|
||||
path.exists filePath, (exists, err) ->
|
||||
console.log " ---> Loading: #{feedId} (#{filePath}). " +
|
||||
"#{if exists then "" else "NOT FOUND"}"
|
||||
if not exists
|
||||
return res.send 404
|
||||
fs.stat filePath, (err, stats) ->
|
||||
if not err and etag and stats.mtime == etag
|
||||
return res.send 304
|
||||
if not err and lastModified and stats.mtime == lastModified
|
||||
return res.send 304
|
||||
|
||||
fs.readFile filePath, (err, content) ->
|
||||
res.header 'Etag', Date.parse(stats.mtime)
|
||||
res.send content
|
||||
|
||||
|
||||
app.post /^\/original_page\/(\d+)\/?/, (req, res) =>
|
||||
feedId = parseInt(req.params, 10)
|
||||
feedIdDir = splitFeedId feedId
|
||||
html = req.param "original_page"
|
||||
filePath = "#{DB_PATH}/#{feedIdDir}.zhtml"
|
||||
filePathDir = path.dirname filePath
|
||||
mkdirp filePathDir, (err) ->
|
||||
fs.rename req.files.original_page.path, filePath, (err) ->
|
||||
console.log err if err
|
||||
console.log " ---> Saving: #{feedId} (#{filePath})"
|
||||
res.send "OK"
|
||||
|
||||
|
||||
splitFeedId = (feedId) ->
|
||||
feedId += ''
|
||||
# x2 = if feedId.length > 1 then '.' + feedId[1] else ''
|
||||
rgx = /(\d+)(\d{3})/
|
||||
feedId = feedId.replace rgx, '$1' + '/' + '$2' while rgx.test(feedId)
|
||||
return feedId;
|
||||
|
79
node/original_page.js
Normal file
79
node/original_page.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
// Generated by CoffeeScript 1.4.0
|
||||
(function() {
|
||||
var DB_PATH, DEV, app, express, fs, mkdirp, path, splitFeedId,
|
||||
_this = this;
|
||||
|
||||
express = require('express');
|
||||
|
||||
path = require('path');
|
||||
|
||||
fs = require('fs');
|
||||
|
||||
mkdirp = require('mkdirp');
|
||||
|
||||
DEV = process.env.NODE_ENV === 'development';
|
||||
|
||||
DB_PATH = DEV ? 'originals' : '/srv/originals';
|
||||
|
||||
app = express.createServer();
|
||||
|
||||
app.use(express.bodyParser());
|
||||
|
||||
app.listen(3060);
|
||||
|
||||
app.get(/^\/original_page\/(\d+)\/?/, function(req, res) {
|
||||
var etag, feedId, feedIdDir, filePath, lastModified;
|
||||
feedId = parseInt(req.params, 10);
|
||||
etag = req.header('If-None-Match');
|
||||
lastModified = req.header('If-Modified-Since');
|
||||
feedIdDir = splitFeedId(feedId);
|
||||
filePath = "" + DB_PATH + "/" + feedIdDir + ".zhtml";
|
||||
return path.exists(filePath, function(exists, err) {
|
||||
console.log((" ---> Loading: " + feedId + " (" + filePath + "). ") + ("" + (exists ? "" : "NOT FOUND")));
|
||||
if (!exists) {
|
||||
return res.send(404);
|
||||
}
|
||||
return fs.stat(filePath, function(err, stats) {
|
||||
if (!err && etag && stats.mtime === etag) {
|
||||
return res.send(304);
|
||||
}
|
||||
if (!err && lastModified && stats.mtime === lastModified) {
|
||||
return res.send(304);
|
||||
}
|
||||
return fs.readFile(filePath, function(err, content) {
|
||||
res.header('Etag', Date.parse(stats.mtime));
|
||||
return res.send(content);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.post(/^\/original_page\/(\d+)\/?/, function(req, res) {
|
||||
var feedId, feedIdDir, filePath, filePathDir, html;
|
||||
feedId = parseInt(req.params, 10);
|
||||
feedIdDir = splitFeedId(feedId);
|
||||
html = req.param("original_page");
|
||||
filePath = "" + DB_PATH + "/" + feedIdDir + ".zhtml";
|
||||
filePathDir = path.dirname(filePath);
|
||||
return mkdirp(filePathDir, function(err) {
|
||||
return fs.rename(req.files.original_page.path, filePath, function(err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
console.log(" ---> Saving: " + feedId + " (" + filePath + ")");
|
||||
return res.send("OK");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
splitFeedId = function(feedId) {
|
||||
var rgx;
|
||||
feedId += '';
|
||||
rgx = /(\d+)(\d{3})/;
|
||||
while (rgx.test(feedId)) {
|
||||
feedId = feedId.replace(rgx, '$1' + '/' + '$2');
|
||||
}
|
||||
return feedId;
|
||||
};
|
||||
|
||||
}).call(this);
|
6
node_modules/qs/.gitmodules
generated
vendored
Normal file
6
node_modules/qs/.gitmodules
generated
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[submodule "support/expresso"]
|
||||
path = support/expresso
|
||||
url = git://github.com/visionmedia/expresso.git
|
||||
[submodule "support/should"]
|
||||
path = support/should
|
||||
url = git://github.com/visionmedia/should.js.git
|
1
node_modules/qs/.npmignore
generated
vendored
Normal file
1
node_modules/qs/.npmignore
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
4
node_modules/qs/.travis.yml
generated
vendored
Normal file
4
node_modules/qs/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 0.6
|
||||
- 0.4
|
94
node_modules/qs/History.md
generated
vendored
Normal file
94
node_modules/qs/History.md
generated
vendored
Normal file
|
@ -0,0 +1,94 @@
|
|||
|
||||
0.5.3 2012-12-09
|
||||
==================
|
||||
|
||||
* add info to component.json
|
||||
* remove regular client-side ./querystring.js, fix component.json support
|
||||
|
||||
0.5.2 / 2012-11-14
|
||||
==================
|
||||
|
||||
* fix uri encoding of non-plain object string values
|
||||
|
||||
0.5.1 / 2012-09-18
|
||||
==================
|
||||
|
||||
* fix encoded `=`. Closes #43
|
||||
|
||||
0.5.0 / 2012-05-04
|
||||
==================
|
||||
|
||||
* Added component support
|
||||
|
||||
0.4.2 / 2012-02-08
|
||||
==================
|
||||
|
||||
* Fixed: ensure objects are created when appropriate not arrays [aheckmann]
|
||||
|
||||
0.4.1 / 2012-01-26
|
||||
==================
|
||||
|
||||
* Fixed stringify()ing numbers. Closes #23
|
||||
|
||||
0.4.0 / 2011-11-21
|
||||
==================
|
||||
|
||||
* Allow parsing of an existing object (for `bodyParser()`) [jackyz]
|
||||
* Replaced expresso with mocha
|
||||
|
||||
0.3.2 / 2011-11-08
|
||||
==================
|
||||
|
||||
* Fixed global variable leak
|
||||
|
||||
0.3.1 / 2011-08-17
|
||||
==================
|
||||
|
||||
* Added `try/catch` around malformed uri components
|
||||
* Add test coverage for Array native method bleed-though
|
||||
|
||||
0.3.0 / 2011-07-19
|
||||
==================
|
||||
|
||||
* Allow `array[index]` and `object[property]` syntaxes [Aria Stewart]
|
||||
|
||||
0.2.0 / 2011-06-29
|
||||
==================
|
||||
|
||||
* Added `qs.stringify()` [Cory Forsyth]
|
||||
|
||||
0.1.0 / 2011-04-13
|
||||
==================
|
||||
|
||||
* Added jQuery-ish array support
|
||||
|
||||
0.0.7 / 2011-03-13
|
||||
==================
|
||||
|
||||
* Fixed; handle empty string and `== null` in `qs.parse()` [dmit]
|
||||
allows for convenient `qs.parse(url.parse(str).query)`
|
||||
|
||||
0.0.6 / 2011-02-14
|
||||
==================
|
||||
|
||||
* Fixed; support for implicit arrays
|
||||
|
||||
0.0.4 / 2011-02-09
|
||||
==================
|
||||
|
||||
* Fixed `+` as a space
|
||||
|
||||
0.0.3 / 2011-02-08
|
||||
==================
|
||||
|
||||
* Fixed case when right-hand value contains "]"
|
||||
|
||||
0.0.2 / 2011-02-07
|
||||
==================
|
||||
|
||||
* Fixed "=" presence in key
|
||||
|
||||
0.0.1 / 2011-02-07
|
||||
==================
|
||||
|
||||
* Initial release
|
6
node_modules/qs/Makefile
generated
vendored
Normal file
6
node_modules/qs/Makefile
generated
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
test:
|
||||
@./node_modules/.bin/mocha \
|
||||
--ui bdd
|
||||
|
||||
.PHONY: test
|
58
node_modules/qs/Readme.md
generated
vendored
Normal file
58
node_modules/qs/Readme.md
generated
vendored
Normal file
|
@ -0,0 +1,58 @@
|
|||
# node-querystring
|
||||
|
||||
query string parser for node and the browser supporting nesting, as it was removed from `0.3.x`, so this library provides the previous and commonly desired behaviour (and twice as fast). Used by [express](http://expressjs.com), [connect](http://senchalabs.github.com/connect) and others.
|
||||
|
||||
## Installation
|
||||
|
||||
$ npm install qs
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
var qs = require('qs');
|
||||
|
||||
qs.parse('user[name][first]=Tobi&user[email]=tobi@learnboost.com');
|
||||
// => { user: { name: { first: 'Tobi' }, email: 'tobi@learnboost.com' } }
|
||||
|
||||
qs.stringify({ user: { name: 'Tobi', email: 'tobi@learnboost.com' }})
|
||||
// => user[name]=Tobi&user[email]=tobi%40learnboost.com
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Install dev dependencies:
|
||||
|
||||
$ npm install -d
|
||||
|
||||
and execute:
|
||||
|
||||
$ make test
|
||||
|
||||
browser:
|
||||
|
||||
$ open test/browser/index.html
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2010 TJ Holowaychuk <tj@vision-media.ca>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
17
node_modules/qs/benchmark.js
generated
vendored
Normal file
17
node_modules/qs/benchmark.js
generated
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
var qs = require('./');
|
||||
|
||||
var times = 100000
|
||||
, start = new Date
|
||||
, n = times;
|
||||
|
||||
console.log('times: %d', times);
|
||||
|
||||
while (n--) qs.parse('foo=bar');
|
||||
console.log('simple: %dms', new Date - start);
|
||||
|
||||
var start = new Date
|
||||
, n = times;
|
||||
|
||||
while (n--) qs.parse('user[name][first]=tj&user[name][last]=holowaychuk');
|
||||
console.log('nested: %dms', new Date - start);
|
9
node_modules/qs/component.json
generated
vendored
Normal file
9
node_modules/qs/component.json
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "querystring",
|
||||
"repo": "visionmedia/node-querystring",
|
||||
"description": "query-string parser / stringifier with nesting support",
|
||||
"version": "0.5.3",
|
||||
"keywords": ["querystring", "query", "parser"],
|
||||
"scripts": ["index.js"],
|
||||
"license": "MIT"
|
||||
}
|
51
node_modules/qs/examples.js
generated
vendored
Normal file
51
node_modules/qs/examples.js
generated
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var qs = require('./');
|
||||
|
||||
var obj = qs.parse('foo');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('foo=bar=baz');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('users[]');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('name=tj&email=tj@vision-media.ca');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('users[]=tj&users[]=tobi&users[]=jane');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('user[name][first]=tj&user[name][last]=holowaychuk');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('users[][name][first]=tj&users[][name][last]=holowaychuk');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('a=a&a=b&a=c');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('user[tj]=tj&user[tj]=TJ');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('user[names]=tj&user[names]=TJ&user[names]=Tyler');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('user[name][first]=tj&user[name][first]=TJ');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('user[0]=tj&user[1]=TJ');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('user[0]=tj&user[]=TJ');
|
||||
console.log(obj)
|
||||
|
||||
var obj = qs.parse('user[0]=tj&user[foo]=TJ');
|
||||
console.log(obj)
|
||||
|
||||
var str = qs.stringify({ user: { name: 'Tobi', email: 'tobi@learnboost.com' }});
|
||||
console.log(str);
|
262
node_modules/qs/index.js
generated
vendored
Normal file
262
node_modules/qs/index.js
generated
vendored
Normal file
|
@ -0,0 +1,262 @@
|
|||
|
||||
/**
|
||||
* Object#toString() ref for stringify().
|
||||
*/
|
||||
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
/**
|
||||
* Cache non-integer test regexp.
|
||||
*/
|
||||
|
||||
var isint = /^[0-9]+$/;
|
||||
|
||||
function promote(parent, key) {
|
||||
if (parent[key].length == 0) return parent[key] = {};
|
||||
var t = {};
|
||||
for (var i in parent[key]) t[i] = parent[key][i];
|
||||
parent[key] = t;
|
||||
return t;
|
||||
}
|
||||
|
||||
function parse(parts, parent, key, val) {
|
||||
var part = parts.shift();
|
||||
// end
|
||||
if (!part) {
|
||||
if (Array.isArray(parent[key])) {
|
||||
parent[key].push(val);
|
||||
} else if ('object' == typeof parent[key]) {
|
||||
parent[key] = val;
|
||||
} else if ('undefined' == typeof parent[key]) {
|
||||
parent[key] = val;
|
||||
} else {
|
||||
parent[key] = [parent[key], val];
|
||||
}
|
||||
// array
|
||||
} else {
|
||||
var obj = parent[key] = parent[key] || [];
|
||||
if (']' == part) {
|
||||
if (Array.isArray(obj)) {
|
||||
if ('' != val) obj.push(val);
|
||||
} else if ('object' == typeof obj) {
|
||||
obj[Object.keys(obj).length] = val;
|
||||
} else {
|
||||
obj = parent[key] = [parent[key], val];
|
||||
}
|
||||
// prop
|
||||
} else if (~part.indexOf(']')) {
|
||||
part = part.substr(0, part.length - 1);
|
||||
if (!isint.test(part) && Array.isArray(obj)) obj = promote(parent, key);
|
||||
parse(parts, obj, part, val);
|
||||
// key
|
||||
} else {
|
||||
if (!isint.test(part) && Array.isArray(obj)) obj = promote(parent, key);
|
||||
parse(parts, obj, part, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge parent key/val pair.
|
||||
*/
|
||||
|
||||
function merge(parent, key, val){
|
||||
if (~key.indexOf(']')) {
|
||||
var parts = key.split('[')
|
||||
, len = parts.length
|
||||
, last = len - 1;
|
||||
parse(parts, parent, 'base', val);
|
||||
// optimize
|
||||
} else {
|
||||
if (!isint.test(key) && Array.isArray(parent.base)) {
|
||||
var t = {};
|
||||
for (var k in parent.base) t[k] = parent.base[k];
|
||||
parent.base = t;
|
||||
}
|
||||
set(parent.base, key, val);
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given obj.
|
||||
*/
|
||||
|
||||
function parseObject(obj){
|
||||
var ret = { base: {} };
|
||||
Object.keys(obj).forEach(function(name){
|
||||
merge(ret, name, obj[name]);
|
||||
});
|
||||
return ret.base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given str.
|
||||
*/
|
||||
|
||||
function parseString(str){
|
||||
return String(str)
|
||||
.split('&')
|
||||
.reduce(function(ret, pair){
|
||||
var eql = pair.indexOf('=')
|
||||
, brace = lastBraceInKey(pair)
|
||||
, key = pair.substr(0, brace || eql)
|
||||
, val = pair.substr(brace || eql, pair.length)
|
||||
, val = val.substr(val.indexOf('=') + 1, val.length);
|
||||
|
||||
// ?foo
|
||||
if ('' == key) key = pair, val = '';
|
||||
|
||||
return merge(ret, decode(key), decode(val));
|
||||
}, { base: {} }).base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given query `str` or `obj`, returning an object.
|
||||
*
|
||||
* @param {String} str | {Object} obj
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.parse = function(str){
|
||||
if (null == str || '' == str) return {};
|
||||
return 'object' == typeof str
|
||||
? parseObject(str)
|
||||
: parseString(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn the given `obj` into a query string
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
var stringify = exports.stringify = function(obj, prefix) {
|
||||
if (Array.isArray(obj)) {
|
||||
return stringifyArray(obj, prefix);
|
||||
} else if ('[object Object]' == toString.call(obj)) {
|
||||
return stringifyObject(obj, prefix);
|
||||
} else if ('string' == typeof obj) {
|
||||
return stringifyString(obj, prefix);
|
||||
} else {
|
||||
return prefix + '=' + encodeURIComponent(String(obj));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stringify the given `str`.
|
||||
*
|
||||
* @param {String} str
|
||||
* @param {String} prefix
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function stringifyString(str, prefix) {
|
||||
if (!prefix) throw new TypeError('stringify expects an object');
|
||||
return prefix + '=' + encodeURIComponent(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify the given `arr`.
|
||||
*
|
||||
* @param {Array} arr
|
||||
* @param {String} prefix
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function stringifyArray(arr, prefix) {
|
||||
var ret = [];
|
||||
if (!prefix) throw new TypeError('stringify expects an object');
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
ret.push(stringify(arr[i], prefix + '[' + i + ']'));
|
||||
}
|
||||
return ret.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify the given `obj`.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} prefix
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function stringifyObject(obj, prefix) {
|
||||
var ret = []
|
||||
, keys = Object.keys(obj)
|
||||
, key;
|
||||
|
||||
for (var i = 0, len = keys.length; i < len; ++i) {
|
||||
key = keys[i];
|
||||
ret.push(stringify(obj[key], prefix
|
||||
? prefix + '[' + encodeURIComponent(key) + ']'
|
||||
: encodeURIComponent(key)));
|
||||
}
|
||||
|
||||
return ret.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set `obj`'s `key` to `val` respecting
|
||||
* the weird and wonderful syntax of a qs,
|
||||
* where "foo=bar&foo=baz" becomes an array.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} key
|
||||
* @param {String} val
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function set(obj, key, val) {
|
||||
var v = obj[key];
|
||||
if (undefined === v) {
|
||||
obj[key] = val;
|
||||
} else if (Array.isArray(v)) {
|
||||
v.push(val);
|
||||
} else {
|
||||
obj[key] = [v, val];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate last brace in `str` within the key.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {Number}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function lastBraceInKey(str) {
|
||||
var len = str.length
|
||||
, brace
|
||||
, c;
|
||||
for (var i = 0; i < len; ++i) {
|
||||
c = str[i];
|
||||
if (']' == c) brace = false;
|
||||
if ('[' == c) brace = true;
|
||||
if ('=' == c && !brace) return i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode `str`.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function decode(str) {
|
||||
try {
|
||||
return decodeURIComponent(str.replace(/\+/g, ' '));
|
||||
} catch (err) {
|
||||
return str;
|
||||
}
|
||||
}
|
43
node_modules/qs/package.json
generated
vendored
Normal file
43
node_modules/qs/package.json
generated
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "qs",
|
||||
"description": "querystring parser",
|
||||
"version": "0.5.3",
|
||||
"keywords": [
|
||||
"query string",
|
||||
"parser",
|
||||
"component"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/visionmedia/node-querystring.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "*",
|
||||
"expect.js": "*"
|
||||
},
|
||||
"component": {
|
||||
"scripts": {
|
||||
"querystring": "querystring.js"
|
||||
}
|
||||
},
|
||||
"author": {
|
||||
"name": "TJ Holowaychuk",
|
||||
"email": "tj@vision-media.ca",
|
||||
"url": "http://tjholowaychuk.com"
|
||||
},
|
||||
"main": "index",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"_id": "qs@0.5.3",
|
||||
"dependencies": {},
|
||||
"optionalDependencies": {},
|
||||
"_engineSupported": true,
|
||||
"_npmVersion": "1.1.12",
|
||||
"_nodeVersion": "v0.6.14",
|
||||
"_defaultsLoaded": true,
|
||||
"dist": {
|
||||
"shasum": "c2cdb51268421f4fdd9350e2bf0594169efe1cba"
|
||||
},
|
||||
"_from": "qs"
|
||||
}
|
1202
node_modules/qs/test/browser/expect.js
generated
vendored
Normal file
1202
node_modules/qs/test/browser/expect.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
18
node_modules/qs/test/browser/index.html
generated
vendored
Normal file
18
node_modules/qs/test/browser/index.html
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Mocha</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<link rel="stylesheet" href="mocha.css" />
|
||||
<script src="jquery.js" type="text/javascript"></script>
|
||||
<script src="expect.js"></script>
|
||||
<script src="mocha.js"></script>
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script src="qs.js"></script>
|
||||
<script src="../parse.js"></script>
|
||||
<script src="../stringify.js"></script>
|
||||
<script>onload = mocha.run;</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
</body>
|
||||
</html>
|
8981
node_modules/qs/test/browser/jquery.js
generated
vendored
Normal file
8981
node_modules/qs/test/browser/jquery.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
163
node_modules/qs/test/browser/mocha.css
generated
vendored
Normal file
163
node_modules/qs/test/browser/mocha.css
generated
vendored
Normal file
|
@ -0,0 +1,163 @@
|
|||
|
||||
body {
|
||||
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
padding: 60px 50px;
|
||||
}
|
||||
|
||||
#mocha h1, h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mocha h1 {
|
||||
margin-top: 15px;
|
||||
font-size: 1em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
#mocha .suite .suite h1 {
|
||||
margin-top: 0;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
#mocha h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mocha .suite {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
#mocha .test {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
#mocha .test:hover h2::after {
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: -10px;
|
||||
content: '(view source)';
|
||||
font-size: 12px;
|
||||
font-family: arial;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#mocha .test.pending:hover h2::after {
|
||||
content: '(pending)';
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
#mocha .test.pass.medium .duration {
|
||||
background: #C09853;
|
||||
}
|
||||
|
||||
#mocha .test.pass.slow .duration {
|
||||
background: #B94A48;
|
||||
}
|
||||
|
||||
#mocha .test.pass::before {
|
||||
content: '✓';
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#mocha .test.pass .duration {
|
||||
font-size: 9px;
|
||||
margin-left: 5px;
|
||||
padding: 2px 5px;
|
||||
color: white;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
-ms-border-radius: 5px;
|
||||
-o-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mocha .test.pass.fast .duration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mocha .test.pending {
|
||||
color: #0b97c4;
|
||||
}
|
||||
|
||||
#mocha .test.pending::before {
|
||||
content: '◦';
|
||||
color: #0b97c4;
|
||||
}
|
||||
|
||||
#mocha .test.fail {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test.fail pre {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#mocha .test.fail::before {
|
||||
content: '✖';
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test pre.error {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test pre {
|
||||
display: inline-block;
|
||||
font: 12px/1.5 monaco, monospace;
|
||||
margin: 5px;
|
||||
padding: 15px;
|
||||
border: 1px solid #eee;
|
||||
border-bottom-color: #ddd;
|
||||
-webkit-border-radius: 3px;
|
||||
-webkit-box-shadow: 0 1px 3px #eee;
|
||||
}
|
||||
|
||||
#error {
|
||||
color: #c00;
|
||||
font-size: 1.5 em;
|
||||
font-weight: 100;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#stats {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
right: 10px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#stats .progress {
|
||||
float: right;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#stats em {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#stats li {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
list-style: none;
|
||||
padding-top: 11px;
|
||||
}
|
||||
|
||||
code .comment { color: #ddd }
|
||||
code .init { color: #2F6FAD }
|
||||
code .string { color: #5890AD }
|
||||
code .keyword { color: #8A6343 }
|
||||
code .number { color: #2F6FAD }
|
4201
node_modules/qs/test/browser/mocha.js
generated
vendored
Normal file
4201
node_modules/qs/test/browser/mocha.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
0
node_modules/qs/test/browser/qs.css
generated
vendored
Normal file
0
node_modules/qs/test/browser/qs.css
generated
vendored
Normal file
351
node_modules/qs/test/browser/qs.js
generated
vendored
Normal file
351
node_modules/qs/test/browser/qs.js
generated
vendored
Normal file
|
@ -0,0 +1,351 @@
|
|||
|
||||
/**
|
||||
* Require the given path.
|
||||
*
|
||||
* @param {String} path
|
||||
* @return {Object} exports
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function require(p, parent){
|
||||
var path = require.resolve(p)
|
||||
, mod = require.modules[path];
|
||||
if (!mod) throw new Error('failed to require "' + p + '" in ' + parent);
|
||||
if (!mod.exports) {
|
||||
mod.exports = {};
|
||||
mod.client = true;
|
||||
mod.call(mod.exports, mod, mod.exports, require.relative(path));
|
||||
}
|
||||
return mod.exports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered modules.
|
||||
*/
|
||||
|
||||
require.modules = {};
|
||||
|
||||
/**
|
||||
* Resolve `path`.
|
||||
*
|
||||
* @param {String} path
|
||||
* @return {Object} module
|
||||
* @api public
|
||||
*/
|
||||
|
||||
require.resolve = function(path){
|
||||
var orig = path
|
||||
, reg = path + '.js'
|
||||
, index = path + '/index.js';
|
||||
return require.modules[reg] && reg
|
||||
|| require.modules[index] && index
|
||||
|| orig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Register module at `path` with callback `fn`.
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Function} fn
|
||||
* @api public
|
||||
*/
|
||||
|
||||
require.register = function(path, fn){
|
||||
require.modules[path] = fn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines and executes anonymous module immediately, while preserving relative
|
||||
* paths.
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Function} require ref
|
||||
* @api public
|
||||
*/
|
||||
|
||||
require.exec = function (path, fn) {
|
||||
fn.call(window, require.relative(path));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a require function relative to the `parent` path.
|
||||
*
|
||||
* @param {String} parent
|
||||
* @return {Function}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
require.relative = function(parent) {
|
||||
return function(p){
|
||||
if ('.' != p[0]) return require(p);
|
||||
|
||||
var path = parent.split('/')
|
||||
, segs = p.split('/');
|
||||
path.pop();
|
||||
|
||||
for (var i = 0; i < segs.length; i++) {
|
||||
var seg = segs[i];
|
||||
if ('..' == seg) path.pop();
|
||||
else if ('.' != seg) path.push(seg);
|
||||
}
|
||||
|
||||
return require(path.join('/'), parent);
|
||||
};
|
||||
};
|
||||
// component qs: querystring
|
||||
require.register("querystring", function(module, exports, require){
|
||||
;(function(){
|
||||
|
||||
/**
|
||||
* Object#toString() ref for stringify().
|
||||
*/
|
||||
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
/**
|
||||
* Cache non-integer test regexp.
|
||||
*/
|
||||
|
||||
var isint = /^[0-9]+$/;
|
||||
|
||||
function promote(parent, key) {
|
||||
if (parent[key].length == 0) return parent[key] = {};
|
||||
var t = {};
|
||||
for (var i in parent[key]) t[i] = parent[key][i];
|
||||
parent[key] = t;
|
||||
return t;
|
||||
}
|
||||
|
||||
function parse(parts, parent, key, val) {
|
||||
var part = parts.shift();
|
||||
// end
|
||||
if (!part) {
|
||||
if (Array.isArray(parent[key])) {
|
||||
parent[key].push(val);
|
||||
} else if ('object' == typeof parent[key]) {
|
||||
parent[key] = val;
|
||||
} else if ('undefined' == typeof parent[key]) {
|
||||
parent[key] = val;
|
||||
} else {
|
||||
parent[key] = [parent[key], val];
|
||||
}
|
||||
// array
|
||||
} else {
|
||||
var obj = parent[key] = parent[key] || [];
|
||||
if (']' == part) {
|
||||
if (Array.isArray(obj)) {
|
||||
if ('' != val) obj.push(val);
|
||||
} else if ('object' == typeof obj) {
|
||||
obj[Object.keys(obj).length] = val;
|
||||
} else {
|
||||
obj = parent[key] = [parent[key], val];
|
||||
}
|
||||
// prop
|
||||
} else if (~part.indexOf(']')) {
|
||||
part = part.substr(0, part.length - 1);
|
||||
if (!isint.test(part) && Array.isArray(obj)) obj = promote(parent, key);
|
||||
parse(parts, obj, part, val);
|
||||
// key
|
||||
} else {
|
||||
if (!isint.test(part) && Array.isArray(obj)) obj = promote(parent, key);
|
||||
parse(parts, obj, part, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge parent key/val pair.
|
||||
*/
|
||||
|
||||
function merge(parent, key, val){
|
||||
if (~key.indexOf(']')) {
|
||||
var parts = key.split('[')
|
||||
, len = parts.length
|
||||
, last = len - 1;
|
||||
parse(parts, parent, 'base', val);
|
||||
// optimize
|
||||
} else {
|
||||
if (!isint.test(key) && Array.isArray(parent.base)) {
|
||||
var t = {};
|
||||
for (var k in parent.base) t[k] = parent.base[k];
|
||||
parent.base = t;
|
||||
}
|
||||
set(parent.base, key, val);
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given obj.
|
||||
*/
|
||||
|
||||
function parseObject(obj){
|
||||
var ret = { base: {} };
|
||||
Object.keys(obj).forEach(function(name){
|
||||
merge(ret, name, obj[name]);
|
||||
});
|
||||
return ret.base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given str.
|
||||
*/
|
||||
|
||||
function parseString(str){
|
||||
return String(str)
|
||||
.split('&')
|
||||
.reduce(function(ret, pair){
|
||||
try{
|
||||
pair = decodeURIComponent(pair.replace(/\+/g, ' '));
|
||||
} catch(e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
var eql = pair.indexOf('=')
|
||||
, brace = lastBraceInKey(pair)
|
||||
, key = pair.substr(0, brace || eql)
|
||||
, val = pair.substr(brace || eql, pair.length)
|
||||
, val = val.substr(val.indexOf('=') + 1, val.length);
|
||||
|
||||
// ?foo
|
||||
if ('' == key) key = pair, val = '';
|
||||
|
||||
return merge(ret, key, val);
|
||||
}, { base: {} }).base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given query `str` or `obj`, returning an object.
|
||||
*
|
||||
* @param {String} str | {Object} obj
|
||||
* @return {Object}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports.parse = function(str){
|
||||
if (null == str || '' == str) return {};
|
||||
return 'object' == typeof str
|
||||
? parseObject(str)
|
||||
: parseString(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn the given `obj` into a query string
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {String}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
var stringify = exports.stringify = function(obj, prefix) {
|
||||
if (Array.isArray(obj)) {
|
||||
return stringifyArray(obj, prefix);
|
||||
} else if ('[object Object]' == toString.call(obj)) {
|
||||
return stringifyObject(obj, prefix);
|
||||
} else if ('string' == typeof obj) {
|
||||
return stringifyString(obj, prefix);
|
||||
} else {
|
||||
return prefix + '=' + obj;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stringify the given `str`.
|
||||
*
|
||||
* @param {String} str
|
||||
* @param {String} prefix
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function stringifyString(str, prefix) {
|
||||
if (!prefix) throw new TypeError('stringify expects an object');
|
||||
return prefix + '=' + encodeURIComponent(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify the given `arr`.
|
||||
*
|
||||
* @param {Array} arr
|
||||
* @param {String} prefix
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function stringifyArray(arr, prefix) {
|
||||
var ret = [];
|
||||
if (!prefix) throw new TypeError('stringify expects an object');
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
ret.push(stringify(arr[i], prefix + '['+i+']'));
|
||||
}
|
||||
return ret.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify the given `obj`.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} prefix
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function stringifyObject(obj, prefix) {
|
||||
var ret = []
|
||||
, keys = Object.keys(obj)
|
||||
, key;
|
||||
|
||||
for (var i = 0, len = keys.length; i < len; ++i) {
|
||||
key = keys[i];
|
||||
ret.push(stringify(obj[key], prefix
|
||||
? prefix + '[' + encodeURIComponent(key) + ']'
|
||||
: encodeURIComponent(key)));
|
||||
}
|
||||
|
||||
return ret.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set `obj`'s `key` to `val` respecting
|
||||
* the weird and wonderful syntax of a qs,
|
||||
* where "foo=bar&foo=baz" becomes an array.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} key
|
||||
* @param {String} val
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function set(obj, key, val) {
|
||||
var v = obj[key];
|
||||
if (undefined === v) {
|
||||
obj[key] = val;
|
||||
} else if (Array.isArray(v)) {
|
||||
v.push(val);
|
||||
} else {
|
||||
obj[key] = [v, val];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate last brace in `str` within the key.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {Number}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function lastBraceInKey(str) {
|
||||
var len = str.length
|
||||
, brace
|
||||
, c;
|
||||
for (var i = 0; i < len; ++i) {
|
||||
c = str[i];
|
||||
if (']' == c) brace = false;
|
||||
if ('[' == c) brace = true;
|
||||
if ('=' == c && !brace) return i;
|
||||
}
|
||||
}
|
||||
})();
|
||||
});
|
147
node_modules/qs/test/parse.js
generated
vendored
Normal file
147
node_modules/qs/test/parse.js
generated
vendored
Normal file
|
@ -0,0 +1,147 @@
|
|||
|
||||
if (require.register) {
|
||||
var qs = require('querystring');
|
||||
} else {
|
||||
var qs = require('../')
|
||||
, expect = require('expect.js');
|
||||
}
|
||||
|
||||
describe('qs.parse()', function(){
|
||||
it('should support the basics', function(){
|
||||
expect(qs.parse('0=foo')).to.eql({ '0': 'foo' });
|
||||
|
||||
expect(qs.parse('foo=c++'))
|
||||
.to.eql({ foo: 'c ' });
|
||||
|
||||
expect(qs.parse('a[>=]=23'))
|
||||
.to.eql({ a: { '>=': '23' }});
|
||||
|
||||
expect(qs.parse('a[<=>]==23'))
|
||||
.to.eql({ a: { '<=>': '=23' }});
|
||||
|
||||
expect(qs.parse('a[==]=23'))
|
||||
.to.eql({ a: { '==': '23' }});
|
||||
|
||||
expect(qs.parse('foo'))
|
||||
.to.eql({ foo: '' });
|
||||
|
||||
expect(qs.parse('foo=bar'))
|
||||
.to.eql({ foo: 'bar' });
|
||||
|
||||
expect(qs.parse(' foo = bar = baz '))
|
||||
.to.eql({ ' foo ': ' bar = baz ' });
|
||||
|
||||
expect(qs.parse('foo=bar=baz'))
|
||||
.to.eql({ foo: 'bar=baz' });
|
||||
|
||||
expect(qs.parse('foo=bar&bar=baz'))
|
||||
.to.eql({ foo: 'bar', bar: 'baz' });
|
||||
|
||||
expect(qs.parse('foo=bar&baz'))
|
||||
.to.eql({ foo: 'bar', baz: '' });
|
||||
|
||||
expect(qs.parse('cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'))
|
||||
.to.eql({
|
||||
cht: 'p3'
|
||||
, chd: 't:60,40'
|
||||
, chs: '250x100'
|
||||
, chl: 'Hello|World'
|
||||
});
|
||||
})
|
||||
|
||||
it('should support encoded = signs', function(){
|
||||
expect(qs.parse('he%3Dllo=th%3Dere'))
|
||||
.to.eql({ 'he=llo': 'th=ere' });
|
||||
})
|
||||
|
||||
it('should support nesting', function(){
|
||||
expect(qs.parse('ops[>=]=25'))
|
||||
.to.eql({ ops: { '>=': '25' }});
|
||||
|
||||
expect(qs.parse('user[name]=tj'))
|
||||
.to.eql({ user: { name: 'tj' }});
|
||||
|
||||
expect(qs.parse('user[name][first]=tj&user[name][last]=holowaychuk'))
|
||||
.to.eql({ user: { name: { first: 'tj', last: 'holowaychuk' }}});
|
||||
})
|
||||
|
||||
it('should support array notation', function(){
|
||||
expect(qs.parse('images[]'))
|
||||
.to.eql({ images: [] });
|
||||
|
||||
expect(qs.parse('user[]=tj'))
|
||||
.to.eql({ user: ['tj'] });
|
||||
|
||||
expect(qs.parse('user[]=tj&user[]=tobi&user[]=jane'))
|
||||
.to.eql({ user: ['tj', 'tobi', 'jane'] });
|
||||
|
||||
expect(qs.parse('user[names][]=tj&user[names][]=tyler'))
|
||||
.to.eql({ user: { names: ['tj', 'tyler'] }});
|
||||
|
||||
expect(qs.parse('user[names][]=tj&user[names][]=tyler&user[email]=tj@vision-media.ca'))
|
||||
.to.eql({ user: { names: ['tj', 'tyler'], email: 'tj@vision-media.ca' }});
|
||||
|
||||
expect(qs.parse('items=a&items=b'))
|
||||
.to.eql({ items: ['a', 'b'] });
|
||||
|
||||
expect(qs.parse('user[names]=tj&user[names]=holowaychuk&user[names]=TJ'))
|
||||
.to.eql({ user: { names: ['tj', 'holowaychuk', 'TJ'] }});
|
||||
|
||||
expect(qs.parse('user[name][first]=tj&user[name][first]=TJ'))
|
||||
.to.eql({ user: { name: { first: ['tj', 'TJ'] }}});
|
||||
|
||||
var o = qs.parse('existing[fcbaebfecc][name][last]=tj')
|
||||
expect(o).to.eql({ existing: { 'fcbaebfecc': { name: { last: 'tj' }}}})
|
||||
expect(Array.isArray(o.existing)).to.equal(false);
|
||||
})
|
||||
|
||||
it('should support arrays with indexes', function(){
|
||||
expect(qs.parse('foo[0]=bar&foo[1]=baz')).to.eql({ foo: ['bar', 'baz'] });
|
||||
expect(qs.parse('foo[1]=bar&foo[0]=baz')).to.eql({ foo: ['baz', 'bar'] });
|
||||
expect(qs.parse('foo[base64]=RAWR')).to.eql({ foo: { base64: 'RAWR' }});
|
||||
expect(qs.parse('foo[64base]=RAWR')).to.eql({ foo: { '64base': 'RAWR' }});
|
||||
})
|
||||
|
||||
it('should expand to an array when dupliate keys are present', function(){
|
||||
expect(qs.parse('items=bar&items=baz&items=raz'))
|
||||
.to.eql({ items: ['bar', 'baz', 'raz'] });
|
||||
})
|
||||
|
||||
it('should support right-hand side brackets', function(){
|
||||
expect(qs.parse('pets=["tobi"]'))
|
||||
.to.eql({ pets: '["tobi"]' });
|
||||
|
||||
expect(qs.parse('operators=[">=", "<="]'))
|
||||
.to.eql({ operators: '[">=", "<="]' });
|
||||
|
||||
expect(qs.parse('op[>=]=[1,2,3]'))
|
||||
.to.eql({ op: { '>=': '[1,2,3]' }});
|
||||
|
||||
expect(qs.parse('op[>=]=[1,2,3]&op[=]=[[[[1]]]]'))
|
||||
.to.eql({ op: { '>=': '[1,2,3]', '=': '[[[[1]]]]' }});
|
||||
})
|
||||
|
||||
it('should support empty values', function(){
|
||||
expect(qs.parse('')).to.eql({});
|
||||
expect(qs.parse(undefined)).to.eql({});
|
||||
expect(qs.parse(null)).to.eql({});
|
||||
})
|
||||
|
||||
it('should transform arrays to objects', function(){
|
||||
expect(qs.parse('foo[0]=bar&foo[bad]=baz')).to.eql({ foo: { 0: "bar", bad: "baz" }});
|
||||
expect(qs.parse('foo[bad]=baz&foo[0]=bar')).to.eql({ foo: { 0: "bar", bad: "baz" }});
|
||||
})
|
||||
|
||||
it('should support malformed uri chars', function(){
|
||||
expect(qs.parse('{%:%}')).to.eql({ '{%:%}': '' });
|
||||
expect(qs.parse('foo=%:%}')).to.eql({ 'foo': '%:%}' });
|
||||
})
|
||||
|
||||
it('should support semi-parsed strings', function(){
|
||||
expect(qs.parse({ 'user[name]': 'tobi' }))
|
||||
.to.eql({ user: { name: 'tobi' }});
|
||||
|
||||
expect(qs.parse({ 'user[name]': 'tobi', 'user[email][main]': 'tobi@lb.com' }))
|
||||
.to.eql({ user: { name: 'tobi', email: { main: 'tobi@lb.com' } }});
|
||||
})
|
||||
})
|
79
node_modules/qs/test/stringify.js
generated
vendored
Normal file
79
node_modules/qs/test/stringify.js
generated
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
|
||||
if (require.register) {
|
||||
var qs = require('querystring');
|
||||
} else {
|
||||
var qs = require('../')
|
||||
, expect = require('expect.js');
|
||||
}
|
||||
|
||||
var date = new Date(0);
|
||||
|
||||
var str_identities = {
|
||||
'basics': [
|
||||
{ str: 'foo=bar', obj: {'foo' : 'bar'}},
|
||||
{ str: 'foo=%22bar%22', obj: {'foo' : '\"bar\"'}},
|
||||
{ str: 'foo=', obj: {'foo': ''}},
|
||||
{ str: 'foo=1&bar=2', obj: {'foo' : '1', 'bar' : '2'}},
|
||||
{ str: 'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F', obj: {'my weird field': "q1!2\"'w$5&7/z8)?"}},
|
||||
{ str: 'foo%3Dbaz=bar', obj: {'foo=baz': 'bar'}},
|
||||
{ str: 'foo=bar&bar=baz', obj: {foo: 'bar', bar: 'baz'}}
|
||||
],
|
||||
'escaping': [
|
||||
{ str: 'foo=foo%20bar', obj: {foo: 'foo bar'}},
|
||||
{ str: 'cht=p3&chd=t%3A60%2C40&chs=250x100&chl=Hello%7CWorld', obj: {
|
||||
cht: 'p3'
|
||||
, chd: 't:60,40'
|
||||
, chs: '250x100'
|
||||
, chl: 'Hello|World'
|
||||
}}
|
||||
],
|
||||
'nested': [
|
||||
{ str: 'foo[0]=bar&foo[1]=quux', obj: {'foo' : ['bar', 'quux']}},
|
||||
{ str: 'foo[0]=bar', obj: {foo: ['bar']}},
|
||||
{ str: 'foo[0]=1&foo[1]=2', obj: {'foo' : ['1', '2']}},
|
||||
{ str: 'foo=bar&baz[0]=1&baz[1]=2&baz[2]=3', obj: {'foo' : 'bar', 'baz' : ['1', '2', '3']}},
|
||||
{ str: 'foo[0]=bar&baz[0]=1&baz[1]=2&baz[2]=3', obj: {'foo' : ['bar'], 'baz' : ['1', '2', '3']}},
|
||||
{ str: 'x[y][z]=1', obj: {'x' : {'y' : {'z' : '1'}}}},
|
||||
{ str: 'x[y][z][0]=1', obj: {'x' : {'y' : {'z' : ['1']}}}},
|
||||
{ str: 'x[y][z]=2', obj: {'x' : {'y' : {'z' : '2'}}}},
|
||||
{ str: 'x[y][z][0]=1&x[y][z][1]=2', obj: {'x' : {'y' : {'z' : ['1', '2']}}}},
|
||||
{ str: 'x[y][0][z]=1', obj: {'x' : {'y' : [{'z' : '1'}]}}},
|
||||
{ str: 'x[y][0][z][0]=1', obj: {'x' : {'y' : [{'z' : ['1']}]}}},
|
||||
{ str: 'x[y][0][z]=1&x[y][0][w]=2', obj: {'x' : {'y' : [{'z' : '1', 'w' : '2'}]}}},
|
||||
{ str: 'x[y][0][v][w]=1', obj: {'x' : {'y' : [{'v' : {'w' : '1'}}]}}},
|
||||
{ str: 'x[y][0][z]=1&x[y][0][v][w]=2', obj: {'x' : {'y' : [{'z' : '1', 'v' : {'w' : '2'}}]}}},
|
||||
{ str: 'x[y][0][z]=1&x[y][1][z]=2', obj: {'x' : {'y' : [{'z' : '1'}, {'z' : '2'}]}}},
|
||||
{ str: 'x[y][0][z]=1&x[y][0][w]=a&x[y][1][z]=2&x[y][1][w]=3', obj: {'x' : {'y' : [{'z' : '1', 'w' : 'a'}, {'z' : '2', 'w' : '3'}]}}},
|
||||
{ str: 'user[name][first]=tj&user[name][last]=holowaychuk', obj: { user: { name: { first: 'tj', last: 'holowaychuk' }}}}
|
||||
],
|
||||
'errors': [
|
||||
{ obj: 'foo=bar', message: 'stringify expects an object' },
|
||||
{ obj: ['foo', 'bar'], message: 'stringify expects an object' }
|
||||
],
|
||||
'numbers': [
|
||||
{ str: 'limit[0]=1&limit[1]=2&limit[2]=3', obj: { limit: [1, 2, '3'] }},
|
||||
{ str: 'limit=1', obj: { limit: 1 }}
|
||||
],
|
||||
'others': [
|
||||
{ str: 'at=' + encodeURIComponent(date), obj: { at: date } }
|
||||
]
|
||||
};
|
||||
|
||||
function test(type) {
|
||||
return function(){
|
||||
var str, obj;
|
||||
for (var i = 0; i < str_identities[type].length; i++) {
|
||||
str = str_identities[type][i].str;
|
||||
obj = str_identities[type][i].obj;
|
||||
expect(qs.stringify(obj)).to.eql(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('qs.stringify()', function(){
|
||||
it('should support the basics', test('basics'))
|
||||
it('should support escapes', test('escaping'))
|
||||
it('should support nesting', test('nested'))
|
||||
it('should support numbers', test('numbers'))
|
||||
it('should support others', test('others'))
|
||||
})
|
|
@ -441,6 +441,8 @@ TWITTER_CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
|
|||
# = AWS Backing =
|
||||
# ===============
|
||||
|
||||
ORIGINAL_PAGE_SERVER = "db01.newsblur.com:3060"
|
||||
|
||||
BACKED_BY_AWS = {
|
||||
'pages_on_s3': False,
|
||||
'icons_on_s3': False,
|
||||
|
@ -450,6 +452,7 @@ PROXY_S3_PAGES = True
|
|||
S3_BACKUP_BUCKET = 'newsblur_backups'
|
||||
S3_PAGES_BUCKET_NAME = 'pages.newsblur.com'
|
||||
S3_ICONS_BUCKET_NAME = 'icons.newsblur.com'
|
||||
S3_AVATARS_BUCKET_NAME = 'avatars.newsblur.com'
|
||||
|
||||
# ==================
|
||||
# = Configurations =
|
||||
|
|
|
@ -59,8 +59,10 @@
|
|||
'view_settings' : {},
|
||||
'collapsed_folders' : [],
|
||||
'story_styling' : 'sans-serif',
|
||||
'story_size' : 's',
|
||||
'hide_public_comments' : false,
|
||||
'timezone' : "{{ user_profile.timezone }}",
|
||||
'title_counts' : true,
|
||||
'story_share_twitter' : true,
|
||||
'story_share_facebook' : true,
|
||||
'story_share_readitlater' : false,
|
||||
|
@ -70,6 +72,7 @@
|
|||
};
|
||||
NEWSBLUR.URLs = {
|
||||
'google-reader-authorize' : "{% url google-reader-authorize %}",
|
||||
'upload-avatar' : "{% url upload-avatar %}",
|
||||
'opml-upload' : "{% url opml-upload %}",
|
||||
'opml-export' : "{% url opml-export %}",
|
||||
'domain' : "{% current_domain %}",
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
{% extends "mail/email_base.txt" %}
|
||||
|
||||
{% load utils_tags %}
|
||||
|
||||
{% block body %}Forgot your password? No problem.
|
||||
|
||||
You can change your password by visiting this link:
|
||||
|
||||
http://www.newsblur.com{{ user.profile.autologin_url }}?next=password
|
||||
http://{% current_domain %}{{ user.profile.autologin_url }}?next=/profile/forgot_password_return
|
||||
|
||||
You will be auto-logged into your account and presented with a form to change your password.
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
{% extends "mail/email_base.xhtml" %}
|
||||
|
||||
{% load utils_tags %}
|
||||
|
||||
{% block body %}
|
||||
<p style="font-size: 37px; color:#555555; margin-top: 18px;margin-bottom: 10px;padding-top:6px;">Forgot your password? No problem.</p>
|
||||
<p style="line-height: 20px;">You can change your password by visiting this link:</p>
|
||||
<p style="line-height: 20px;"><a href="http://www.newsblur.com{{ user.profile.autologin_url }}?next=password">http://www.newsblur.com{{ user.profile.autologin_url }}?next=password</a></p>
|
||||
<p style="line-height: 20px;"><a href="http://{% current_domain %}{{ user.profile.autologin_url }}?next=/profile/forgot_password_return">http://{% current_domain %}{{ user.profile.autologin_url }}?next=/profile/forgot_password_return</a></p>
|
||||
<p style="line-height: 20px;">You will be auto-logged into your account and presented with a form to change your password.</p>
|
||||
<p style="line-height: 20px;">Hope everything on NewsBlur is working well for you. Let me know if you see any issues. I rely on folks like you who know how to email me to be my eyes and ears on the site.</p>
|
||||
{% if not user.profile.is_premium %}
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
|
||||
{% block body %}
|
||||
<p style="font-size: 37px; color:#555555; margin-top: 18px;margin-bottom: 10px;padding-top:6px;">Welcome to NewsBlur, {{ user.username }}.</p>
|
||||
<p style="line-height: 20px;">OK, firstly, thank you for trying out NewsBlur. We've worked hard to make a great app, so congratulations on discovering a handcrafted experience.</p>
|
||||
<p style="line-height: 20px;">Here are some easy ways to have a great time on NewsBlur:</p>
|
||||
<p style="line-height: 20px;">Thanks for trying out NewsBlur! We hope we can make your daily reading more personal, sociable, and pleasurable.</p>
|
||||
<p style="line-height: 20px;">Here are some ways to make NewsBlur work for you:</p>
|
||||
<p style="line-height: 20px;">
|
||||
<ul style="list-style: none;">
|
||||
<li style="line-height:22px;"><img src="http://{% current_domain %}/media/img/icons/silk/rainbow.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> <a href="http://{% current_domain %}{{ user.profile.autologin_url }}?next=friends" style="text-decoration:none">Follow friends from Twitter, Facebook, and NewsBlur</a>.</li>
|
||||
<li style="line-height:22px;"><img src="http://{% current_domain %}/media/img/reader/popular_thumb.jpg?1" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Visit the popular blurblog: <a href="http://popular.newsblur.com" style="text-decoration:none">The People Have Spoken</a>.</li>
|
||||
<li style="line-height:22px;"><img src="http://{% current_domain %}/media/img/reader/hamburger_l.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> <a href="http://{% current_domain %}{{ user.profile.autologin_url }}?next=chooser" style="text-decoration:none">Upgrade to a premium account for only $12/year</a>.</li>
|
||||
<li style="line-height:22px;"><img src="http://{% current_domain %}/media/img/reader/popular_thumb.jpg?1" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Visit <a href="http://popular.newsblur.com" style="text-decoration:none">The People Have Spoken</a>, our blog of the most popular and talked-about articles posted by NewsBlur readers.</li>
|
||||
<li style="line-height:22px;"><img src="http://{% current_domain %}/media/img/reader/hamburger_l.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> <a href="http://{% current_domain %}{{ user.profile.autologin_url }}?next=chooser" style="text-decoration:none">Upgrade to a premium account for only $12/year, and get access to an unlimited number of RSS feeds.</a>.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p style="line-height: 20px;">Spend a few days trying out NewsBlur. We hope you love it.</p>
|
||||
<p style="line-height: 20px;">We made NewsBlur because we wanted a better way to read the news. We hope you'll love it, too.</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block resources_header %}There are a couple resources you can use if you end up loving NewsBlur:{% endblock resources_header %}
|
||||
{% block resources_header %}If you enjoy using NewsBlur, these resources might be of interest to you:{% endblock resources_header %}
|
|
@ -74,7 +74,7 @@
|
|||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<div id="container">
|
||||
<img src="../media/img/logo_512.png" class="logo">
|
||||
<img src="/media/img/logo_512.png" class="logo">
|
||||
<h1>NewsBlur is in <span class="error404">maintenance mode</span></h1>
|
||||
<div class="description">
|
||||
<p>Doing a quick 60 second upgrade to the database. Be back in a jiffy.</p>
|
||||
|
|
37
templates/profile/forgot_password.xhtml
Normal file
37
templates/profile/forgot_password.xhtml
Normal file
|
@ -0,0 +1,37 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load typogrify_tags utils_tags %}
|
||||
|
||||
{% block bodyclass %}NB-static{% endblock %}
|
||||
{% block extra_head_js %}
|
||||
{% include_stylesheets "common" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="NB-static-form-wrapper">
|
||||
<div class="NB-delete-form NB-static-form">
|
||||
|
||||
<h2>Forgot your password? No problem!</h2>
|
||||
|
||||
<form action="" method="POST">{% csrf_token %}
|
||||
<div class="NB-fields">
|
||||
{{ forgot_password_form.email.label_tag }}
|
||||
{{ forgot_password_form.email }}
|
||||
</div>
|
||||
|
||||
{% if forgot_password_form.errors %}
|
||||
<div class="NB-errors">
|
||||
{% for field, error in forgot_password_form.errors.items %}
|
||||
{{ error|safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<input type="submit" class="submit-button NB-modal-submit-button NB-modal-submit-green" value="Email me the password change form"></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
37
templates/profile/forgot_password_return.xhtml
Normal file
37
templates/profile/forgot_password_return.xhtml
Normal file
|
@ -0,0 +1,37 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load typogrify_tags utils_tags %}
|
||||
|
||||
{% block bodyclass %}NB-static{% endblock %}
|
||||
{% block extra_head_js %}
|
||||
{% include_stylesheets "common" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="NB-static-form-wrapper">
|
||||
<div class="NB-delete-form NB-static-form">
|
||||
|
||||
<h2>You're almost done, just set a password.</h2>
|
||||
|
||||
<form action="" method="POST">{% csrf_token %}
|
||||
<div class="NB-fields">
|
||||
{{ forgot_password_return_form.password.label_tag }}
|
||||
{{ forgot_password_return_form.password }}
|
||||
</div>
|
||||
|
||||
{% if forgot_password_return_form.errors %}
|
||||
<div class="NB-errors">
|
||||
{% for field, error in forgot_password_return_form.errors.items %}
|
||||
{{ error|safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<input type="submit" class="submit-button NB-modal-submit-button NB-modal-submit-green" value="Change my password and log me in"></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -78,7 +78,7 @@
|
|||
<div>
|
||||
<div class="NB-signup-optional">
|
||||
{% if login_form.errors and login_form.errors|length %}
|
||||
<a href="mailto:password@newsblur.com?subject=Forgot%20Password%20on%20NewsBlur&body=Hello!%20My%20username%20is:%20" class="NB-splash-link">Forgot?</a>
|
||||
<a href="{% url profile-forgot-password %}" class="NB-splash-link">Forgot?</a>
|
||||
{% else %}
|
||||
Optional
|
||||
{% endif %}
|
||||
|
|
|
@ -13,15 +13,35 @@
|
|||
<div class="NB-static-title">
|
||||
About NewsBlur
|
||||
</div>
|
||||
|
||||
|
||||
<div class="NB-module">
|
||||
<h5 class="NB-module-title"><span class="NB-module-title-prefix">What:</span>The story behind NewsBlur</h5>
|
||||
<h5 class="NB-module-title"><span class="NB-module-title-prefix">What Is NewsBlur?</h5>
|
||||
<div class="NB-module-content">
|
||||
<ul class="NB-about-what">
|
||||
<li>NewsBlur is a social news reader built for people who want to enjoy reading the news. NewsBlur is a great place to read the best stories from your friends and favorite blogs.</li>
|
||||
<li>In New York City during the Summer of 2009, Samuel Clay wanted a better way to read the news. So he built the first version of NewsBlur almost entirely underground on the A train.</li>
|
||||
<li>In mid-2010, NewsBlur launched to the public and to favorable reviews. In October 2010 premium accounts launched, paying for NewsBlur's increasing server costs.</li>
|
||||
<li>After entering Y Combinator during the Summer of 2012, Blurblogs are launched as a new way to share and talk about the news between friends. Samuel and Shiloh celebrate with champagne and biscuits.
|
||||
<li>NewsBlur is a social news reader that's built to simplify the process of reading news and make it easier to share your favorite stories with your friends.
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="NB-module">
|
||||
<h5 class="NB-module-title"><span class="NB-module-title-prefix">Why:</span>What makes NewsBlur better</h5>
|
||||
<div class="NB-module-content">
|
||||
<ul class="NB-about-why">
|
||||
<li><b>News reading</b>: With first-class iOS, Android, and web apps, NewsBlur is an easy and organized way to read the news wherever you are.</li>
|
||||
<li><b>Training</b>: By using NewsBlur's training filters, you can hide stories you don't want to see and highlight the stories that interest you. Teaching NewsBlur your preferences (or lack thereof) for certain blogs, authors, and topics cuts down on the noise and connects you with the news that interests you most.</li>
|
||||
<li><b>Social</b>: Sharing and talking about the news is not only fun, but allows you to break out of your routine and embrace the serendipity of your friends' tastes.</li>
|
||||
<li><b>Blurblogs</b>: Even if your friends aren't NewsBlur users, they can keep up with what you're reading through a public blog of all the stories you've shared, including your comments.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="NB-module">
|
||||
<h5 class="NB-module-title"><span class="NB-module-title-prefix">When:</span>The story behind NewsBlur</h5>
|
||||
<div class="NB-module-content">
|
||||
<ul class="NB-about-what">
|
||||
<li>In New York City during the summer of 2009, Samuel Clay wanted a better way to read the news. So he built the first version of NewsBlur almost entirely underground on the A train.</li>
|
||||
<li>In mid-2010, NewsBlur launched to the public and to favorable reviews. In October 2010, premium accounts launched, paying for NewsBlur's increasing server costs.</li>
|
||||
<li>After Samuel enters Y Combinator in the summer of 2012, he's able to launch lots of new features, like Blurblogs. Samuel and his dog Shiloh celebrate with champagne and biscuits.
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -47,17 +67,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="NB-module">
|
||||
<h5 class="NB-module-title"><span class="NB-module-title-prefix">Why:</span>What makes NewsBlur better</h5>
|
||||
<div class="NB-module-content">
|
||||
<ul class="NB-about-why">
|
||||
<li><b>News reading</b>: With first-class iOS, Android, and web apps, NewsBlur is an easy and organized way to read the news wherever you are. </li>
|
||||
<li><b>Training</b>: By using NewsBlur's training filters, you can hide stories you don't want to see while highlighting the stories you want to focus on.</li>
|
||||
<li><b>Social</b>: Sharing and talking about the news is not only fun, but allows you to break out of your filter bubble and embrace the serendipity of your friend's tastes.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="NB-module">
|
||||
<h5 class="NB-module-title"><span class="NB-module-title-prefix">How:</span>Server-side technologies</h5>
|
||||
<div class="NB-module-content">
|
||||
|
|
|
@ -19,18 +19,21 @@
|
|||
What is the difference between the three views: Original, Feed, and Story?
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
Original view is the original site. Feed view is the RSS feed from the site. And Story view is the original site for one story at a time. Original view is the blog site, whereas Story view is an individual blog post. It's all personal preference, really.
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
You can double-click a story to temporarily open it up in the Story view. The next story you open will transport you back to whichever view you were on before. Double-clicking a Feed will open up the feed in a new tab.
|
||||
NewsBlur defaults to Feed view, which is the plain ol’ RSS feed from a given site. (You might recognize this look from Google Reader.) But we know a lot of people enjoy reading in the original design and typeface of a given site, which is why we also offer Original view (which shows the entire original site) and Story view (which shows each individual blog post from the original site, one story at a time). That sound you just heard? It’s a thousand web designers sighing with pleasure.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="NB-faq-question">
|
||||
How do I view a story in Story View?
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
To temporarily open up a story in Story view, just double-click. The next story you open will revert back to the view you were using before. Double-clicking a Feed will open up the feed in a new tab.
|
||||
<li>
|
||||
<div class="NB-faq-question">
|
||||
Am I actually at the original site? Can NewsBlur see what I see?
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
In order to show you the original site, NewsBlur takes a snapshot of the page. You may have noticed that if you are logged into the original site, you are not logged into NewsBlur's snapshot of the page. This is because NewsBlur fetched the site for you.
|
||||
Well, not exactly. In order to show you a site in Original or Story view, NewsBlur takes a snapshot of the page when you switch over. So if you log into the real site, it might not look exactly the same. We do this because it helps everything load more quickly, and no one likes waiting.
|
||||
</div>
|
||||
</li>
|
||||
<li class="last">
|
||||
|
@ -38,10 +41,10 @@
|
|||
Why doesn't NewsBlur follow me when I click on links on the page?
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
When you click on a link, you are technically leaving NewsBlur, although only for a portion of the page in an iframe. In order to track what you're reading, you need to read NewsBlur's snapshot of the page, or switch to the Feed view.
|
||||
</div>
|
||||
Since NewsBlur runs as a webpage, clicking on a link means you’re technically leaving our site, although only for a portion of the page. In order to track what you're reading, you need to read NewsBlur's snapshot of the page, or switch to the Feed view.
|
||||
</div>
|
||||
<div class="NB-faq-answer last">
|
||||
This may change one day. There is a way to fix this behavior so it works like you would expect. It is not easy to do, however. One day.
|
||||
There’s a way to fix this so it works like you would expect, but it’s pretty difficult to do. We’ve got a lot of other big priorities that come first, and we also like to have some time for things like eating dinner and watching television and playing with random dogs we pass walking down the street, so it hasn’t happened yet. If you really want to see it change, drop us a line, and we’ll consider it if the response is big enough.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -67,19 +70,24 @@
|
|||
How does NewsBlur know whether I like or dislike a story?
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
When you like or dislike a story, you mark a facet of that story by checking a tag, author, part of the title, or entire publisher. When these facets are found in future stories, the stories are then weighted with your preferences. It is a very simple, explicit process where you tell NewsBlur what you like and don't like.
|
||||
Our favorite thing about NewsBlur is that you can teach it your preferences every time you read a story. When you like or dislike something you read, you can click the button at the bottom that says “Train This Story.” You’ll be presented with a whole list of characteristics about the story, including the author, the title, any tags, the blog from which it comes, and the person who shared it. Everything defaults to yellow, which is neutral—you neither like or dislike it. But if there’s an author on a blog you particularly like, or a particular category you want to know more about, you can use the thumbs-up button to mark it green. The same goes for authors, categories, and even whole blogs that don’t really strike your fancy.
|
||||
</div>
|
||||
<li>
|
||||
<div class="NB-faq-question">
|
||||
What's the point of training your feed?
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
The idea is that by explicitly telling NewsBlur what your story preferences are, there is increased likelihood that you will like what the intelligence slider does for you.
|
||||
</div>
|
||||
<div class="NB-faq-answer last">
|
||||
Currently, there is not an automated way of detecting stories you like or dislike without having to train NewsBlur. This implicit, automatic intelligence will come in the near-term future, but it will require an evolution to the interface that has not been easy to figure out how to make in a simple, clear, and effective manner. Soon.
|
||||
The more you train NewsBlur, the more able it is to dish up stories that suit your interests. So if you read a blog that covers both politics and sports, but you only like politics, or a blog where you love Author A’s work but not Author B or Author C’s, NewsBlur will only give you stories about politics and stories by Author A. That means less stuff to sort through, and more time playing with random dogs you pass while walking down the street.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="NB-faq-question">
|
||||
Why should I invest the time to train my feed?
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
We know that training is time-consuming, which is why NewsBlur is still a good RSS reader even if you don’t much feel like training it. We want to eventually develop an automated way of detecting stories without you having to train NewsBlur, but that’s still in the future. In the meantime, consider the time you put into training it now as an investment in your interest and attention at a later date.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="NB-module">
|
||||
<h5 class="NB-module-title">Information for Publishers</h5>
|
||||
<div class="NB-module-content">
|
||||
|
@ -125,23 +133,28 @@
|
|||
<h5 class="NB-module-title">Something's Wrong</h5>
|
||||
<div class="NB-module-content">
|
||||
<ul class="NB-about-client">
|
||||
<li>
|
||||
<li>
|
||||
<div class="NB-faq-question">
|
||||
Help! All of the stories are several days old and new stories are not showing up.
|
||||
Help! All of my stories are several days old and new stories are not showing up.
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
Sites that only have a single subscriber tend to get updated much less often than popular sites. Additionally, the frequency that a site publishes stories (once per month or several per day) has an impact on how often the site is refreshed.
|
||||
Congratulations on your esoteric taste in blogs—and condolences as well. The way NewsBlur works requires that sites get updated on a regular basis, so we try to serve the most popular and frequently updated sites first. Sites that only have a single subscriber tend to get updated much less often, in comparison to those that have many; the same goes for those that update a few times a month instead of several times a day.
|
||||
<li>
|
||||
<div class="NB-faq-question">
|
||||
Why can't NewsBlur show me all the stories I want, when I want them?
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="NB-faq-answer">
|
||||
Totally unfair, we know. Our best recommendation is to post some of your favorite older stuff from that obscure site you love on your Blurblog. If more people see that stuff, then you’ve increased the likelihood that blog will snag other subscribers, and it’ll get bumped up NewsBlur’s list of sites to refresh more often.
|
||||
</div>
|
||||
<li>
|
||||
<div class="NB-faq-question">
|
||||
Help! A bunch of my sites are misbehaving and they work in Google Reader.
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
This is a known issue that is being addressed in a number of ways. About half of these misbehaving errors are errors that you really do need to address (like 404 Not Found errors). The other half are various edge cases, parser errors, uncaught exceptions, and bad code on NewsBlur's part.
|
||||
Yeah, we know, and we’re sorry about that. About half of these misbehaving errors aren’t on us (like 404 Not Found errors), but the other half are various edge cases, parser errors, uncaught exceptions, and bad code on NewsBlur's part. We do our best to find and root out everything we can, but there are only so many hours in a day.
|
||||
</div>
|
||||
<div class="NB-faq-answer">
|
||||
But because this problem is so severe, various measures are taken every few weeks that fix a huge swath of misbheaving sites at once. You might find that this happens and it's quite nice when it does.
|
||||
On the upside, the severity of this problem means that we take measures every few weeks that fix a huge swath of misbehaving sites at once. It’s pretty great when this happens, but we know it’s not frequent enough. We’re working on it, we promise.
|
||||
</div>
|
||||
</li>
|
||||
<li class="last">
|
||||
|
@ -149,7 +162,7 @@
|
|||
Help! I have an issue and it's not mentioned here.
|
||||
</div>
|
||||
<div class="NB-faq-answer last">
|
||||
Please, please, please e-mail <a href="mailto:samuel@newsblur.com">samuel@newsblur.com</a>. If you have an issue it is entirely possible that other people do, too.
|
||||
Please, please, please e-mail <a href="mailto:samuel@newsblur.com">samuel@newsblur.com</a>. If you have an issue it is entirely possible that other people do, too, and the more we know about something that’s gone wrong, the more able and likely we are to fix it. Don’t be shy, drop us a line!
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -381,7 +381,7 @@ class Dispatcher:
|
|||
mail_feed_error_to_admin(feed, e, local_vars=locals())
|
||||
if (not settings.DEBUG and hasattr(settings, 'RAVEN_CLIENT') and
|
||||
settings.RAVEN_CLIENT):
|
||||
settings.RAVEN_CLIENT.captureException(e)
|
||||
settings.RAVEN_CLIENT.captureException()
|
||||
|
||||
if not feed_code:
|
||||
if ret_feed == FEED_OK:
|
||||
|
@ -424,7 +424,7 @@ class Dispatcher:
|
|||
fetched_feed = None
|
||||
page_data = None
|
||||
mail_feed_error_to_admin(feed, e, local_vars=locals())
|
||||
settings.RAVEN_CLIENT.captureException(e)
|
||||
settings.RAVEN_CLIENT.captureException()
|
||||
|
||||
feed = self.refresh_feed(feed.pk)
|
||||
logging.debug(u' ---> [%-30s] ~FYFetching icon: %s' % (feed.title[:30], feed.feed_link))
|
||||
|
@ -442,7 +442,7 @@ class Dispatcher:
|
|||
logging.debug('[%d] ! -------------------------' % (feed_id,))
|
||||
# feed.save_feed_history(560, "Icon Error", tb)
|
||||
mail_feed_error_to_admin(feed, e, local_vars=locals())
|
||||
settings.RAVEN_CLIENT.captureException(e)
|
||||
settings.RAVEN_CLIENT.captureException()
|
||||
else:
|
||||
logging.debug(u' ---> [%-30s] ~FBSkipping page fetch: (%s on %s stories) %s' % (feed.title[:30], self.feed_trans[ret_feed], feed.stories_last_month, '' if feed.has_page else ' [HAS NO PAGE]'))
|
||||
|
||||
|
|
70
utils/image_functions.py
Normal file
70
utils/image_functions.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
"""Operations for images through the PIL."""
|
||||
|
||||
import Image
|
||||
import ImageOps as PILOps
|
||||
from ExifTags import TAGS
|
||||
from StringIO import StringIO
|
||||
|
||||
PROFILE_PICTURE_SIZES = {
|
||||
'fullsize': (256, 256),
|
||||
'thumbnail': (64, 64)
|
||||
}
|
||||
|
||||
class ImageOps:
|
||||
"""Module that holds all image operations. Since there's no state,
|
||||
everything is a classmethod."""
|
||||
|
||||
@classmethod
|
||||
def resize_image(cls, image_body, size, fit_to_size=False):
|
||||
"""Takes a raw image (in image_body) and resizes it to fit given
|
||||
dimensions. Returns a file-like object in the form of a StringIO.
|
||||
This must happen in this function because PIL is transforming the
|
||||
original as it works."""
|
||||
|
||||
image_file = StringIO(image_body)
|
||||
try:
|
||||
image = Image.open(image_file)
|
||||
except IOError:
|
||||
# Invalid image file
|
||||
return False
|
||||
|
||||
# Get the image format early, as we lose it after perform a `thumbnail` or `fit`.
|
||||
format = image.format
|
||||
|
||||
# Check for rotation
|
||||
image = cls.adjust_image_orientation(image)
|
||||
|
||||
if not fit_to_size:
|
||||
image.thumbnail(PROFILE_PICTURE_SIZES[size], Image.ANTIALIAS)
|
||||
else:
|
||||
image = PILOps.fit(image, PROFILE_PICTURE_SIZES[size],
|
||||
method=Image.ANTIALIAS,
|
||||
centering=(0.5, 0.5))
|
||||
|
||||
output = StringIO()
|
||||
if format.lower() == 'jpg':
|
||||
format = 'jpeg'
|
||||
image.save(output, format=format, quality=95)
|
||||
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def adjust_image_orientation(cls, image):
|
||||
"""Since the iPhone will store an image on its side but with EXIF
|
||||
data stating that it should be rotated, we need to find that
|
||||
EXIF data and correctly rotate the image before storage."""
|
||||
|
||||
if hasattr(image, '_getexif'):
|
||||
exif = image._getexif()
|
||||
if exif:
|
||||
for tag, value in exif.items():
|
||||
decoded = TAGS.get(tag, tag)
|
||||
if decoded == 'Orientation':
|
||||
if value == 6:
|
||||
image = image.rotate(-90)
|
||||
if value == 8:
|
||||
image = image.rotate(90)
|
||||
if value == 3:
|
||||
image = image.rotate(180)
|
||||
break
|
||||
return image
|
|
@ -16,13 +16,17 @@ import sys
|
|||
|
||||
def main():
|
||||
op = optparse.OptionParser()
|
||||
op.add_option("-i", "--identity", dest="identity")
|
||||
options, args = op.parse_args()
|
||||
streams = list()
|
||||
for arg in args:
|
||||
if re.match(r"^(.+@)?[a-zA-Z0-9.-]+:.+", arg):
|
||||
# this is a remote location
|
||||
hostname, path = arg.split(":", 1)
|
||||
s = subprocess.Popen(["ssh", hostname, "tail -f " + path], stdout=subprocess.PIPE)
|
||||
if options.identity:
|
||||
s = subprocess.Popen(["ssh", "-i", options.identity, hostname, "tail -f " + path], stdout=subprocess.PIPE)
|
||||
else:
|
||||
s = subprocess.Popen(["ssh", hostname, "tail -f " + path], stdout=subprocess.PIPE)
|
||||
s.name = arg
|
||||
streams.append(s)
|
||||
else:
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
from boto.s3.connection import S3Connection
|
||||
from boto.s3.key import Key
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import mimetypes
|
||||
from boto.s3.connection import S3Connection
|
||||
from boto.s3.key import Key
|
||||
from utils.image_functions import ImageOps
|
||||
|
||||
if '/home/sclay/newsblur' not in ' '.join(sys.path):
|
||||
sys.path.append("/home/sclay/newsblur")
|
||||
|
@ -60,3 +63,62 @@ if __name__ == '__main__':
|
|||
delete_all_backups()
|
||||
else:
|
||||
print 'Usage: %s <get/set/list/delete> <backup_filename>' % (sys.argv[0])
|
||||
|
||||
|
||||
class S3Store:
|
||||
|
||||
def __init__(self, bucket_name=settings.S3_AVATARS_BUCKET_NAME):
|
||||
self.s3 = S3Connection(ACCESS_KEY, SECRET)
|
||||
self.bucket = self.create_bucket(bucket_name)
|
||||
|
||||
def create_bucket(self, bucket_name):
|
||||
return self.s3.create_bucket(bucket_name)
|
||||
|
||||
def save_profile_picture(self, user_id, filename, image_body):
|
||||
mimetype, extension = self._extract_mimetype(filename)
|
||||
if not mimetype or not extension:
|
||||
return
|
||||
|
||||
image_name = 'profile_%s.%s' % (int(time.time()), extension)
|
||||
|
||||
image = ImageOps.resize_image(image_body, 'fullsize', fit_to_size=False)
|
||||
if image:
|
||||
key = 'avatars/%s/large_%s' % (user_id, image_name)
|
||||
self._save_object(key, image, mimetype=mimetype)
|
||||
|
||||
image = ImageOps.resize_image(image_body, 'thumbnail', fit_to_size=True)
|
||||
if image:
|
||||
key = 'avatars/%s/thumbnail_%s' % (user_id, image_name)
|
||||
self._save_object(key, image, mimetype=mimetype)
|
||||
|
||||
return image and image_name
|
||||
|
||||
def _extract_mimetype(self, filename):
|
||||
mimetype = mimetypes.guess_type(filename)[0]
|
||||
extension = None
|
||||
|
||||
if mimetype == 'image/jpeg':
|
||||
extension = 'jpg'
|
||||
elif mimetype == 'image/png':
|
||||
extension = 'png'
|
||||
elif mimetype == 'image/gif':
|
||||
extension = 'gif'
|
||||
|
||||
return mimetype, extension
|
||||
|
||||
def _make_key(self):
|
||||
return Key(bucket=self.bucket)
|
||||
|
||||
def _save_object(self, key, file_object, mimetype=None):
|
||||
k = self._make_key()
|
||||
k.key = key
|
||||
file_object.seek(0)
|
||||
|
||||
if mimetype:
|
||||
k.set_contents_from_file(file_object, headers={
|
||||
'Content-Type': mimetype,
|
||||
})
|
||||
else:
|
||||
k.set_contents_from_file(file_object)
|
||||
k.set_acl('public-read')
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue