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:
Samuel Clay 2013-01-09 12:54:26 -08:00
commit c1725399b2
94 changed files with 17649 additions and 495 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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'),
)

View file

@ -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,
}

View file

@ -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)

View 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']

View file

@ -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')

View file

@ -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()

View file

@ -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

View file

@ -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'),

View file

@ -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):

View 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
View file

@ -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')

View file

@ -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 =
# ===========

View file

@ -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;
}

View file

@ -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]];
}

View file

@ -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"]];

View file

@ -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;
}
}

View file

@ -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]];
}

View file

@ -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];
}
}

View file

@ -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];

View file

@ -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;

View file

@ -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>

View file

@ -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 @"";
}

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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');
});
},
// ===================

View file

@ -1,5 +1,6 @@
NEWSBLUR.ReaderAccount = function(options) {
var defaults = {
'width': 700,
'animate_email': false,
'change_password': false,
'onOpen': _.bind(function() {

View file

@ -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');
},

View file

@ -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));

View file

@ -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
View file

@ -0,0 +1,2 @@
node_modules/
npm-debug.log

4
node/node_modules/mkdirp/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,4 @@
language: node_js
node_js:
- 0.4
- 0.6

21
node/node_modules/mkdirp/LICENSE generated vendored Normal file
View 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
View file

@ -0,0 +1,61 @@
mkdirp
======
Like `mkdir -p`, but in node.js!
[![build status](https://secure.travis-ci.org/substack/node-mkdirp.png)](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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
node_modules

4
node_modules/qs/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,4 @@
language: node_js
node_js:
- 0.6
- 0.4

94
node_modules/qs/History.md generated vendored Normal file
View 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
View file

@ -0,0 +1,6 @@
test:
@./node_modules/.bin/mocha \
--ui bdd
.PHONY: test

58
node_modules/qs/Readme.md generated vendored Normal file
View 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 &lt;tj@vision-media.ca&gt;
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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

18
node_modules/qs/test/browser/index.html generated vendored Normal file
View 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

File diff suppressed because it is too large Load diff

163
node_modules/qs/test/browser/mocha.css generated vendored Normal file
View 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

File diff suppressed because it is too large Load diff

0
node_modules/qs/test/browser/qs.css generated vendored Normal file
View file

351
node_modules/qs/test/browser/qs.js generated vendored Normal file
View 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
View 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
View 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'))
})

View file

@ -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 =

View file

@ -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 %}",

View file

@ -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.

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View 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 %}

View 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 &nbsp; and log me in"></button>
</form>
</div>
</div>
{% endblock %}

View file

@ -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&amp;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 %}

View file

@ -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">

View file

@ -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? Its 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 youre 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.
Theres a way to fix this so it works like you would expect, but its pretty difficult to do. Weve 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 hasnt happened yet. If you really want to see it change, drop us a line, and well 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.” Youll 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 theres 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 dont 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 As work but not Author B or Author Cs, 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 dont much feel like training it. We want to eventually develop an automated way of detecting stories without you having to train NewsBlur, but thats 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&mdash;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 youve increased the likelihood that blog will snag other subscribers, and itll get bumped up NewsBlurs 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 were sorry about that. About half of these misbehaving errors arent 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. Its pretty great when this happens, but we know its not frequent enough. Were 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 thats gone wrong, the more able and likely we are to fix it. Dont be shy, drop us a line!
</div>
</li>
</ul>

View file

@ -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
View 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

View file

@ -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:

View file

@ -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')