Merge branch 'master' of github.com:samuelclay/NewsBlur

* 'master' of github.com:samuelclay/NewsBlur: (32 commits)
  Fixing insta-fetching.
  Attempting to correctly save duplicate feeds.
  Adding tons of logging (especially user-facing) to feed exception handling. Much much better feed address and link fetch history.
  New enterprise ios build includes full training.
  Changing intelligence training on social feeds to apply to original feed as well.
  Using a popover for training on the iPad. Also adding the train button to the bottom of every story.
  Finishing training on iPad. Works in river too. Just need to test social and it's shipping.
  Fixing title selectin bug when the title was too short to register the selection loupe.
  Training feeds and stories in feeds. Needs testing on ipad, in river, and on social.
  Finish look and feel of ios training for both story and feed. Just need to send requests, redraw intelligence, recount and reclassify stories, and update scores. All already written to support inline training.
  A few more log color tweaks.
  Adding clean stories task. Updating log colors.
  Reducing queue size of push feeds when overloaded.
  Showing tags, authors, title, and publisher, but only for stories. Needs to be hooked up to send to the server, redraw, and handle site training and popular tags.
  Showing maintenance mode message. One day ... put in a live chat.
  Preventing the double tasking fo feeds.
  Preventing the double tasking fo feeds.
  Migration to increase max length of feed address.
  Cleaning up task feeds logging.
  Stubbing in new modal trainer for ios.
  ...
This commit is contained in:
Samuel Clay 2013-01-02 09:58:07 -08:00
commit 9e067ebb37
155 changed files with 26569 additions and 25753 deletions

View file

@ -584,6 +584,7 @@ class MUserStory(mongo.Document):
story_date = mongo.DateTimeField()
story = mongo.ReferenceField(MStory, dbref=True)
found_story = mongo.GenericReferenceField()
shared = mongo.BooleanField()
meta = {
'collection': 'userstories',

View file

@ -3,7 +3,7 @@ from celery.task import Task
from utils import log as logging
from django.contrib.auth.models import User
from django.conf import settings
from apps.reader.models import UserSubscription
from apps.reader.models import UserSubscription, MUserStory
from apps.social.models import MSocialSubscription
@ -46,4 +46,21 @@ class CleanAnalytics(Task):
})
settings.MONGOANALYTICSDB.nbanalytics.page_loads.remove({
"date": {"$lt": day_ago},
})
})
class CleanStories(Task):
name = 'clean-stories'
def run(self, **kwargs):
days_ago = (datetime.datetime.utcnow() -
datetime.timedelta(days=settings.DAYS_OF_UNREAD*5))
old_stories = MUserStory.objects.filter(read_date__lte=days_ago)
logging.debug(" ---> Cleaning stories from %s days ago... %s/%s read stories" % (
settings.DAYS_OF_UNREAD*5,
MUserStory.objects.count(),
old_stories.count()
))
for s, story in enumerate(old_stories):
if (s+1) % 1000 == 0:
logging.debug(" ---> %s stories removed..." % (s+1))
story.delete()

View file

@ -248,7 +248,10 @@ def load_feeds(request):
categories = None
if not user_subs:
categories = MCategory.serialize()
logging.user(request, "~FB~SBLoading ~FY%s~FB/~FM%s~FB feeds/socials%s" % (
len(feeds.keys()), len(social_feeds), '. ~FCUpdating counts.' if update_counts else ''))
data = {
'feeds': feeds.values() if version == 2 else feeds,
'social_feeds': social_feeds,
@ -338,8 +341,8 @@ def load_feeds_flat(request):
if not user_subs:
categories = MCategory.serialize()
logging.user(request, "~FBLoading ~SB%s~SN/~SB%s~SN feeds/socials ~FMflat~FB. %s" % (
len(feeds.keys()), len(social_feeds), '~SBUpdating counts.' if update_counts else ''))
logging.user(request, "~FB~SBLoading ~FY%s~FB/~FM%s~FB feeds/socials ~FMflat~FB%s" % (
len(feeds.keys()), len(social_feeds), '. ~FCUpdating counts.' if update_counts else ''))
data = {
"flat_folders": flat_folders,

View file

@ -0,0 +1,80 @@
# -*- 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 'Feed.feed_address'
db.alter_column('feeds', 'feed_address', self.gf('django.db.models.fields.URLField')(max_length=764))
def backwards(self, orm):
# Changing field 'Feed.feed_address'
db.alter_column('feeds', 'feed_address', self.gf('django.db.models.fields.URLField')(max_length=255))
models = {
'rss_feeds.duplicatefeed': {
'Meta': {'object_name': 'DuplicateFeed'},
'duplicate_address': ('django.db.models.fields.CharField', [], {'max_length': '255', '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': '255', '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

@ -19,6 +19,7 @@ from django.core.urlresolvers import reverse
from django.contrib.sites.models import Site
from mongoengine.queryset import OperationError, Q
from mongoengine.base import ValidationError
from vendor.timezones.utilities import localtime_for_timezone
from apps.rss_feeds.tasks import UpdateFeeds, PushFeeds
from apps.search.models import SearchStarredStory
from utils import json_functions as json
@ -36,7 +37,7 @@ ENTRY_NEW, ENTRY_UPDATED, ENTRY_SAME, ENTRY_ERR = range(4)
class Feed(models.Model):
feed_address = models.URLField(max_length=255, db_index=True)
feed_address = models.URLField(max_length=764, db_index=True)
feed_address_locked = models.NullBooleanField(default=False, blank=True, null=True)
feed_link = models.URLField(max_length=1000, default="", blank=True, null=True)
feed_link_locked = models.BooleanField(default=False)
@ -181,19 +182,30 @@ class Feed(models.Model):
try:
super(Feed, self).save(*args, **kwargs)
return self
except IntegrityError:
logging.debug(" ---> ~FRFeed save collision, checking dupe...")
except IntegrityError, e:
logging.debug(" ---> ~FRFeed save collision (%s), checking dupe..." % e)
duplicate_feed = Feed.objects.filter(feed_address=self.feed_address,
feed_link=self.feed_link)
if duplicate_feed:
if self.pk != duplicate_feed[0].pk:
merge_feeds(self.pk, duplicate_feed[0].pk)
return self
if not duplicate_feed:
# Feed has been deleted. Just ignore it.
logging.debug("%s: %s" % (self.feed_address, duplicate_feed))
logging.debug(' ***> [%-30s] Feed deleted (%s).' % (unicode(self)[:30], self.pk))
return
if self.pk != duplicate_feed[0].pk:
logging.debug(" ---> ~FRFound different feed (%s), merging..." % duplicate_feed[0])
feed = Feed.get_by_id(merge_feeds(duplicate_feed[0].pk, self.pk))
return feed
else:
duplicate_feed = Feed.objects.filter(
hash_address_and_link=self.hash_address_and_link)
if self.pk != duplicate_feed[0].pk:
feed = Feed.get_by_id(merge_feeds(duplicate_feed[0].pk, self.pk))
return feed
return duplicate_feed[0]
return self
# Feed has been deleted. Just ignore it.
logging.debug("%s: %s" % (self.feed_address, duplicate_feed))
logging.debug(' ***> [%-30s] Feed deleted (%s).' % (unicode(self)[:30], self.pk))
return
def sync_redis(self):
return MStory.sync_all_redis(self.pk)
@ -213,7 +225,7 @@ class Feed(models.Model):
@classmethod
def merge_feeds(cls, *args, **kwargs):
merge_feeds(*args, **kwargs)
return merge_feeds(*args, **kwargs)
@property
def favicon_fetching(self):
@ -278,17 +290,18 @@ class Feed(models.Model):
return feed
@classmethod
def task_feeds(cls, feeds, queue_size=12):
def task_feeds(cls, feeds, queue_size=12, verbose=True):
if isinstance(feeds, Feed):
logging.debug(" ---> Tasking feed: %s" % feeds)
if verbose:
logging.debug(" ---> Tasking feed: %s" % feeds)
feeds = [feeds]
else:
elif verbose:
logging.debug(" ---> Tasking %s feeds..." % len(feeds))
feed_queue = []
for f in feeds:
f.queued_date = datetime.datetime.utcnow()
f.set_next_scheduled_update()
f.set_next_scheduled_update(verbose=False)
for feed_queue in (feeds[pos:pos + queue_size] for pos in xrange(0, len(feeds), queue_size)):
feed_ids = [feed.pk for feed in feed_queue]
@ -739,6 +752,12 @@ class Feed(models.Model):
def add_update_stories(self, stories, existing_stories, verbose=False):
ret_values = dict(new=0, updated=0, same=0, error=0)
if settings.DEBUG:
logging.debug(" ---> [%-30s] ~FBChecking ~SB%s~SN new/updated against ~SB%s~SN stories" % (
self.title[:30],
len(stories),
len(existing_stories)))
for story in stories:
if not story.get('title'):
continue
@ -750,6 +769,9 @@ class Feed(models.Model):
existing_story, story_has_changed = self._exists_story(story, story_content, existing_stories)
if existing_story is None:
if settings.DEBUG and False:
logging.debug(' ---> New story in feed (%s - %s): %s' % (self.feed_title, story.get('title'), len(story_content)))
s = MStory(story_feed_id = self.pk,
story_date = story.get('published'),
story_title = story.get('title'),
@ -764,12 +786,10 @@ class Feed(models.Model):
ret_values['new'] += 1
except (IntegrityError, OperationError):
ret_values['error'] += 1
if verbose:
if settings.DEBUG:
logging.info(' ---> [%-30s] ~SN~FRIntegrityError on new story: %s' % (self.feed_title[:30], story.get('guid')[:30]))
elif existing_story and story_has_changed:
# update story
# logging.debug('- Updated story in feed (%s - %s): %s / %s' % (self.feed_title, story.get('title'), len(existing_story.story_content), len(story_content)))
original_content = None
try:
if existing_story and existing_story.id:
@ -803,11 +823,11 @@ class Feed(models.Model):
# logging.debug('\tExisting title / New: : \n\t\t- %s\n\t\t- %s' % (existing_story.story_title, story.get('title')))
if existing_story.story_guid != story.get('guid'):
self.update_read_stories_with_new_guid(existing_story.story_guid, story.get('guid'))
if settings.DEBUG and False:
logging.debug('- Updated story in feed (%s - %s): %s / %s' % (self.feed_title, story.get('title'), len(story_content_diff), len(story_content)))
existing_story.story_feed = self.pk
# Do not allow publishers to change the story date once a story is published.
# Leads to incorrect unread story counts.
# existing_story.story_date = story.get('published')
existing_story.story_title = story.get('title')
existing_story.story_content = story_content_diff
existing_story.story_latest_content = story_content
@ -816,6 +836,10 @@ class Feed(models.Model):
existing_story.story_permalink = story_link
existing_story.story_guid = story.get('guid')
existing_story.story_tags = story_tags
# Do not allow publishers to change the story date once a story is published.
# Leads to incorrect unread story counts.
# existing_story.story_date = story.get('published') # No, don't
try:
existing_story.save()
ret_values['updated'] += 1
@ -1052,68 +1076,68 @@ class Feed(models.Model):
def _exists_story(self, story=None, story_content=None, existing_stories=None):
story_in_system = None
story_has_changed = False
story_pub_date = story.get('published')
story_published_now = story.get('published_now', False)
story_link = self.get_permalink(story)
start_date = story_pub_date - datetime.timedelta(hours=8)
end_date = story_pub_date + datetime.timedelta(hours=8)
# story_pub_date = story.get('published')
# story_published_now = story.get('published_now', False)
# start_date = story_pub_date - datetime.timedelta(hours=8)
# end_date = story_pub_date + datetime.timedelta(hours=8)
for existing_story in existing_stories:
content_ratio = 0
existing_story_pub_date = existing_story.story_date
# existing_story_pub_date = existing_story.story_date
# print 'Story pub date: %s %s' % (story_published_now, story_pub_date)
if (story_published_now or
(existing_story_pub_date > start_date and existing_story_pub_date < end_date)):
if 'story_latest_content_z' in existing_story:
existing_story_content = unicode(zlib.decompress(existing_story.story_latest_content_z))
elif 'story_latest_content' in existing_story:
existing_story_content = existing_story.story_latest_content
elif 'story_content_z' in existing_story:
existing_story_content = unicode(zlib.decompress(existing_story.story_content_z))
elif 'story_content' in existing_story:
existing_story_content = existing_story.story_content
else:
existing_story_content = u''
if 'story_latest_content_z' in existing_story:
existing_story_content = unicode(zlib.decompress(existing_story.story_latest_content_z))
elif 'story_latest_content' in existing_story:
existing_story_content = existing_story.story_latest_content
elif 'story_content_z' in existing_story:
existing_story_content = unicode(zlib.decompress(existing_story.story_content_z))
elif 'story_content' in existing_story:
existing_story_content = existing_story.story_content
else:
existing_story_content = u''
if isinstance(existing_story.id, unicode):
existing_story.story_guid = existing_story.id
if story.get('guid') and story.get('guid') == existing_story.story_guid:
story_in_system = existing_story
if isinstance(existing_story.id, unicode):
existing_story.story_guid = existing_story.id
if story.get('guid') and story.get('guid') == existing_story.story_guid:
story_in_system = existing_story
# Title distance + content distance, checking if story changed
story_title_difference = abs(levenshtein_distance(story.get('title'),
existing_story.story_title))
seq = difflib.SequenceMatcher(None, story_content, existing_story_content)
if (seq
and story_content
and existing_story_content
and seq.real_quick_ratio() > .9
and seq.quick_ratio() > .95):
content_ratio = seq.ratio()
# Title distance + content distance, checking if story changed
story_title_difference = abs(levenshtein_distance(story.get('title'),
existing_story.story_title))
seq = difflib.SequenceMatcher(None, story_content, existing_story_content)
if (seq
and story_content
and existing_story_content
and seq.real_quick_ratio() > .9
and seq.quick_ratio() > .95):
content_ratio = seq.ratio()
if story_title_difference > 0 and content_ratio > .98:
story_in_system = existing_story
if story_title_difference > 0 or content_ratio < 1.0:
# print "Title difference - %s/%s (%s): %s" % (story.get('title'), existing_story.story_title, story_title_difference, content_ratio)
story_has_changed = True
break
# More restrictive content distance, still no story match
if not story_in_system and content_ratio > .98:
# print "Content difference - %s/%s (%s): %s" % (story.get('title'), existing_story.story_title, story_title_difference, content_ratio)
story_in_system = existing_story
if story_title_difference > 0 and content_ratio > .98:
story_in_system = existing_story
if story_title_difference > 0 or content_ratio < 1.0:
# print "Title difference - %s/%s (%s): %s" % (story.get('title'), existing_story.story_title, story_title_difference, content_ratio)
story_has_changed = True
break
if story_in_system and not story_has_changed:
if story_content != existing_story_content:
story_has_changed = True
if story_link != existing_story.story_permalink:
story_has_changed = True
break
# More restrictive content distance, still no story match
if not story_in_system and content_ratio > .98:
# print "Content difference - %s/%s (%s): %s" % (story.get('title'), existing_story.story_title, story_title_difference, content_ratio)
story_in_system = existing_story
story_has_changed = True
break
if story_in_system and not story_has_changed:
if story_content != existing_story_content:
story_has_changed = True
if story_link != existing_story.story_permalink:
story_has_changed = True
# if story_pub_date != existing_story.story_date:
# story_has_changed = True
break
# if story_has_changed or not story_in_system:
@ -1180,12 +1204,15 @@ class Feed(models.Model):
return total, random_factor*2
def set_next_scheduled_update(self):
def set_next_scheduled_update(self, verbose=True):
total, random_factor = self.get_next_scheduled_update(force=True, verbose=False)
if self.errors_since_good:
total = total * self.errors_since_good
logging.debug(' ---> [%-30s] ~FBScheduling feed fetch geometrically: ~SB%s errors. Time: %s min' % (unicode(self)[:30], self.errors_since_good, total))
if verbose:
logging.debug(' ---> [%-30s] ~FBScheduling feed fetch geometrically: '
'~SB%s errors. Time: %s min' % (
unicode(self)[:30], self.errors_since_good, total))
next_scheduled_update = datetime.datetime.utcnow() + datetime.timedelta(
minutes = total + random_factor)
@ -1212,13 +1239,17 @@ class Feed(models.Model):
self.save()
def queue_pushed_feed_xml(self, xml):
logging.debug(' ---> [%-30s] [%s] ~FBQueuing pushed stories...' % (unicode(self)[:30], self.pk))
self.queued_date = datetime.datetime.utcnow()
self.set_next_scheduled_update()
PushFeeds.apply_async(args=(self.pk, xml), queue='push_feeds')
r = redis.Redis(connection_pool=settings.REDIS_POOL)
queue_size = r.llen("push_feeds")
if queue_size > 1000:
self.schedule_feed_fetch_immediately()
else:
logging.debug(' ---> [%-30s] [%s] ~FBQueuing pushed stories...' % (unicode(self)[:30], self.pk))
self.queued_date = datetime.datetime.utcnow()
self.set_next_scheduled_update()
PushFeeds.apply_async(args=(self.pk, xml), queue='push_feeds')
# def calculate_collocations_story_content(self,
# collocation_measures=TrigramAssocMeasures,
# collocation_finder=TrigramCollocationFinder):
@ -1544,13 +1575,14 @@ class MFeedFetchHistory(mongo.Document):
super(MFeedFetchHistory, self).save(*args, **kwargs)
@classmethod
def feed_history(cls, feed_id):
def feed_history(cls, feed_id, timezone=None):
fetches = cls.objects(feed_id=feed_id).order_by('-fetch_date')[:5]
fetch_history = []
for fetch in fetches:
history = {}
history['message'] = fetch.message
history['fetch_date'] = fetch.fetch_date.strftime("%Y-%m-%d %H:%M:%S")
history['fetch_date'] = localtime_for_timezone(fetch.fetch_date,
timezone).strftime("%Y-%m-%d %H:%M:%S")
history['status_code'] = fetch.status_code
history['exception'] = fetch.exception
fetch_history.append(history)
@ -1577,13 +1609,14 @@ class MPageFetchHistory(mongo.Document):
super(MPageFetchHistory, self).save(*args, **kwargs)
@classmethod
def feed_history(cls, feed_id):
def feed_history(cls, feed_id, timezone=None):
fetches = cls.objects(feed_id=feed_id).order_by('-fetch_date')[:5]
fetch_history = []
for fetch in fetches:
history = {}
history['message'] = fetch.message
history['fetch_date'] = fetch.fetch_date.strftime("%Y-%m-%d %H:%M:%S")
history['fetch_date'] = localtime_for_timezone(fetch.fetch_date,
timezone).strftime("%Y-%m-%d %H:%M:%S")
history['status_code'] = fetch.status_code
history['exception'] = fetch.exception
fetch_history.append(history)
@ -1602,12 +1635,13 @@ class MFeedPushHistory(mongo.Document):
}
@classmethod
def feed_history(cls, feed_id):
def feed_history(cls, feed_id, timezone=None):
pushes = cls.objects(feed_id=feed_id).order_by('-push_date')[:5]
push_history = []
for push in pushes:
history = {}
history['push_date'] = push.push_date.strftime("%Y-%m-%d %H:%M:%S")
history['push_date'] = localtime_for_timezone(push.push_date,
timezone).strftime("%Y-%m-%d %H:%M:%S")
push_history.append(history)
return push_history
@ -1635,13 +1669,13 @@ def merge_feeds(original_feed_id, duplicate_feed_id, force=False):
if original_feed_id == duplicate_feed_id:
logging.info(" ***> Merging the same feed. Ignoring...")
return
return original_feed_id
try:
original_feed = Feed.objects.get(pk=original_feed_id)
duplicate_feed = Feed.objects.get(pk=duplicate_feed_id)
except Feed.DoesNotExist:
logging.info(" ***> Already deleted feed: %s" % duplicate_feed_id)
return
return original_feed_id
if original_feed.num_subscribers < duplicate_feed.num_subscribers and not force:
original_feed, duplicate_feed = duplicate_feed, original_feed
@ -1696,12 +1730,15 @@ def merge_feeds(original_feed_id, duplicate_feed_id, force=False):
logging.debug(" ***> Duplicate feed is the same as original feed. Panic!")
logging.debug(' ---> Deleted duplicate feed: %s/%s' % (duplicate_feed, duplicate_feed_id))
original_feed.count_subscribers()
original_feed.save()
logging.debug(' ---> Now original subscribers: %s' %
(original_feed.num_subscribers))
MSharedStory.switch_feed(original_feed_id, duplicate_feed_id)
return original_feed_id
def rewrite_folders(folders, original_feed, duplicate_feed):
new_folders = []

View file

@ -38,8 +38,6 @@ class PageImporter(object):
@property
def headers(self):
s = requests.session()
s.config['keep_alive'] = False
return {
'User-Agent': 'NewsBlur Page Fetcher (%s subscriber%s) - %s '
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
@ -80,7 +78,8 @@ class PageImporter(object):
response = requests.get(feed_link, headers=self.headers)
except requests.exceptions.TooManyRedirects:
response = requests.get(feed_link)
except AttributeError:
except AttributeError, e:
logging.debug(' ***> [%-30s] Page fetch failed using requests: %s' % (self.feed, e))
self.save_no_page()
return
try:
@ -130,7 +129,7 @@ class PageImporter(object):
return html
def save_no_page(self):
logging.debug(' --->> [%-30s] ~FYNo original page: %s' % (self.feed, self.feed.feed_link))
logging.debug(' ---> [%-30s] ~FYNo original page: %s' % (self.feed, self.feed.feed_link))
self.feed.has_page = False
self.feed.save()
self.feed.save_page_history(404, "Feed has no original page.")

View file

@ -22,26 +22,36 @@ class TaskFeeds(Task):
).exclude(
active_subscribers=0
).order_by('?')
Feed.task_feeds(feeds)
active_count = feeds.count()
# Mistakenly inactive feeds
day = now - datetime.timedelta(days=1)
feeds = Feed.objects.filter(
inactive_feeds = Feed.objects.filter(
last_update__lte=day,
queued_date__lte=day,
min_to_decay__lte=60*24,
active_subscribers__gte=1
).order_by('?')[:20]
if feeds: Feed.task_feeds(feeds)
inactive_count = inactive_feeds.count()
week = now - datetime.timedelta(days=7)
feeds = Feed.objects.filter(
old_feeds = Feed.objects.filter(
last_update__lte=week,
queued_date__lte=day,
active_subscribers__gte=1
).order_by('?')[:20]
if feeds: Feed.task_feeds(feeds)
old_count = old_feeds.count()
logging.debug(" ---> ~FBTasking ~SB~FC%s~SN~FB/~FC%s~FB/~FC%s~SN~FB feeds..." % (
active_count,
inactive_count,
old_count,
))
Feed.task_feeds(feeds, verbose=False)
if inactive_feeds: Feed.task_feeds(inactive_feeds, verbose=False)
if old_feeds: Feed.task_feeds(old_feeds, verbose=False)
class UpdateFeeds(Task):
name = 'update-feeds'

View file

@ -169,9 +169,10 @@ def load_feed_statistics(request, feed_id):
stats['classifier_counts'] = json.decode(feed.data.feed_classifier_counts)
# Fetch histories
stats['feed_fetch_history'] = MFeedFetchHistory.feed_history(feed_id)
stats['page_fetch_history'] = MPageFetchHistory.feed_history(feed_id)
stats['feed_push_history'] = MFeedPushHistory.feed_history(feed_id)
timezone = request.user.profile.timezone
stats['feed_fetch_history'] = MFeedFetchHistory.feed_history(feed_id, timezone=timezone)
stats['page_fetch_history'] = MPageFetchHistory.feed_history(feed_id, timezone=timezone)
stats['feed_push_history'] = MFeedPushHistory.feed_history(feed_id, timezone=timezone)
logging.user(request, "~FBStatistics: ~SB%s ~FG(%s/%s/%s subs)" % (feed, feed.num_subscribers, feed.active_subscribers, feed.premium_subscribers,))
@ -181,10 +182,11 @@ def load_feed_statistics(request, feed_id):
def load_feed_settings(request, feed_id):
stats = dict()
feed = get_object_or_404(Feed, pk=feed_id)
timezone = request.user.profile.timezone
stats['duplicate_addresses'] = feed.duplicate_addresses.all()
stats['feed_fetch_history'] = MFeedFetchHistory.feed_history(feed_id)
stats['page_fetch_history'] = MPageFetchHistory.feed_history(feed_id)
stats['feed_fetch_history'] = MFeedFetchHistory.feed_history(feed_id, timezone=timezone)
stats['page_fetch_history'] = MPageFetchHistory.feed_history(feed_id, timezone=timezone)
return stats
@ -236,6 +238,7 @@ def exception_change_feed_address(request):
feed = get_object_or_404(Feed, pk=feed_id)
original_feed = feed
feed_address = request.POST['feed_address']
timezone = request.user.profile.timezone
code = -1
if feed.has_page_exception or feed.has_feed_exception:
@ -260,6 +263,7 @@ def exception_change_feed_address(request):
# Branch good feed
logging.user(request, "~FRBranching feed by address: ~SB%s~SN to ~SB%s" % (feed.feed_address, feed_address))
feed, _ = Feed.objects.get_or_create(feed_address=feed_address, feed_link=feed.feed_link)
code = 1
if feed.pk != original_feed.pk:
try:
feed.branch_from_feed = original_feed.branch_from_feed or original_feed
@ -267,7 +271,6 @@ def exception_change_feed_address(request):
feed.branch_from_feed = original_feed
feed.feed_address_locked = True
feed.save()
code = 1
feed = feed.update()
feed = Feed.get_by_id(feed.pk)
@ -279,7 +282,11 @@ def exception_change_feed_address(request):
usersub = usersubs[0]
usersub.switch_feed(feed, original_feed)
else:
return {'code': -1}
return {
'code': -1,
'feed_fetch_history': MFeedFetchHistory.feed_history(feed_id, timezone=timezone),
'page_fetch_history': MPageFetchHistory.feed_history(feed_id, timezone=timezone),
}
usersub.calculate_feed_scores(silent=False)
@ -292,11 +299,13 @@ def exception_change_feed_address(request):
if feed and feed.has_feed_exception:
code = -1
return {
'code': code,
'feeds': feeds,
'new_feed_id': usersub.feed_id,
'feed_fetch_history': MFeedFetchHistory.feed_history(feed_id, timezone=timezone),
'page_fetch_history': MPageFetchHistory.feed_history(feed_id, timezone=timezone),
}
@ajax_login_required
@ -306,6 +315,7 @@ def exception_change_feed_link(request):
feed = get_object_or_404(Feed, pk=feed_id)
original_feed = feed
feed_link = request.POST['feed_link']
timezone = request.user.profile.timezone
code = -1
if feed.has_page_exception or feed.has_feed_exception:
@ -332,6 +342,7 @@ def exception_change_feed_link(request):
# Branch good feed
logging.user(request, "~FRBranching feed by link: ~SB%s~SN to ~SB%s" % (feed.feed_link, feed_link))
feed, _ = Feed.objects.get_or_create(feed_address=feed.feed_address, feed_link=feed_link)
code = 1
if feed.pk != original_feed.pk:
try:
feed.branch_from_feed = original_feed.branch_from_feed or original_feed
@ -339,7 +350,6 @@ def exception_change_feed_link(request):
feed.branch_from_feed = original_feed
feed.feed_link_locked = True
feed.save()
code = 1
feed = feed.update()
feed = Feed.get_by_id(feed.pk)
@ -352,7 +362,11 @@ def exception_change_feed_link(request):
usersub = usersubs[0]
usersub.switch_feed(feed, original_feed)
else:
return {'code': -1}
return {
'code': -1,
'feed_fetch_history': MFeedFetchHistory.feed_history(feed_id, timezone=timezone),
'page_fetch_history': MPageFetchHistory.feed_history(feed_id, timezone=timezone),
}
usersub.calculate_feed_scores(silent=False)
@ -369,6 +383,8 @@ def exception_change_feed_link(request):
'code': code,
'feeds': feeds,
'new_feed_id': usersub.feed_id,
'feed_fetch_history': MFeedFetchHistory.feed_history(feed_id, timezone=timezone),
'page_fetch_history': MPageFetchHistory.feed_history(feed_id, timezone=timezone),
}
@login_required

View file

@ -36,7 +36,6 @@ from vendor.timezones.utilities import localtime_for_timezone
@json.json_view
def load_social_stories(request, user_id, username=None):
start = time.time()
user = get_user(request)
social_user_id = int(user_id)
social_user = get_object_or_404(User, pk=social_user_id)
@ -74,8 +73,6 @@ def load_social_stories(request, user_id, username=None):
if not stories:
return dict(stories=[], message=message)
checkpoint1 = time.time()
stories, user_profiles = MSharedStory.stories_with_comments_and_profiles(stories, user.pk, check_all=True)
story_feed_ids = list(set(s['story_feed_id'] for s in stories))
@ -99,8 +96,6 @@ def load_social_stories(request, user_id, username=None):
classifier_titles = classifier_titles + list(MClassifierTitle.objects(user_id=user.pk, feed_id__in=story_feed_ids))
classifier_tags = classifier_tags + list(MClassifierTag.objects(user_id=user.pk, feed_id__in=story_feed_ids))
checkpoint2 = time.time()
story_ids = [story['id'] for story in stories]
userstories_db = MUserStory.objects(user_id=user.pk,
feed_id__in=story_feed_ids,
@ -168,10 +163,8 @@ def load_social_stories(request, user_id, username=None):
socialsub.needs_unread_recalc = True
socialsub.save()
diff1 = checkpoint1-start
diff2 = checkpoint2-start
logging.user(request, "~FYLoading ~FMshared stories~FY: ~SB%s%s ~SN(~SB%.4ss/%.4ss~SN)" % (
social_profile.title[:22], ('~SN/p%s' % page) if page > 1 else '', diff1, diff2))
logging.user(request, "~FYLoading ~FMshared stories~FY: ~SB%s%s" % (
social_profile.title[:22], ('~SN/p%s' % page) if page > 1 else ''))
return {
"stories": stories,
@ -316,7 +309,7 @@ def load_river_blurblog(request):
diff = time.time() - start
timediff = round(float(diff), 2)
logging.user(request, "~FYLoading ~FCriver blurblogs stories~FY: ~SBp%s~SN (%s/%s "
logging.user(request, "~FYLoading ~FCriver ~FMblurblogs~FC stories~FY: ~SBp%s~SN (%s/%s "
"stories, ~SN%s/%s/%s feeds)" %
(page, len(stories), len(mstories), len(story_feed_ids),
len(social_user_ids), len(original_user_ids)))
@ -331,7 +324,6 @@ def load_river_blurblog(request):
}
def load_social_page(request, user_id, username=None, **kwargs):
start = time.time()
user = request.user
social_user_id = int(user_id)
social_user = get_object_or_404(User, pk=social_user_id)
@ -365,8 +357,6 @@ def load_social_page(request, user_id, username=None, **kwargs):
has_next_page = True
stories = stories[:-1]
checkpoint1 = time.time()
if not stories:
params = {
"user": user,
@ -394,8 +384,6 @@ def load_social_page(request, user_id, username=None, **kwargs):
stories, profiles = MSharedStory.stories_with_comments_and_profiles(stories, social_user.pk,
check_all=True)
checkpoint2 = time.time()
if user.is_authenticated():
for story in stories:
if user.pk in story['share_user_ids']:
@ -446,12 +434,8 @@ def load_social_page(request, user_id, username=None, **kwargs):
'active_story' : active_story,
}
diff1 = checkpoint1-start
diff2 = checkpoint2-start
timediff = time.time()-start
logging.user(request, "~FYLoading ~FMsocial page~FY: ~SB%s%s ~SN(%.4s seconds, ~SB%.4s/%.4s~SN)" % (
social_profile.title[:22], ('~SN/p%s' % page) if page > 1 else '', timediff,
diff1, diff2))
logging.user(request, "~FYLoading ~FMsocial ~SBpage~SN~FY: ~SB%s%s" % (
social_profile.title[:22], ('~SN/p%s' % page) if page > 1 else ''))
if format == 'html':
template = 'social/social_stories.xhtml'
else:
@ -1202,7 +1186,7 @@ def shared_stories_rss_feed(request, user_id, username):
logging.user(request, "~FBGenerating ~SB%s~SN's RSS feed: ~FM%s" % (
user.username,
request.META['HTTP_USER_AGENT'][:100]
request.META['HTTP_USER_AGENT'][:24]
))
return HttpResponse(rss.writeString('utf-8'))

View file

@ -7,6 +7,7 @@ from apps.social.models import MSharedStory
from apps.profile.models import Profile
from utils import json_functions as json
from utils import db_functions
from utils import log as logging
class MStatistics(mongo.Document):
key = mongo.StringField(unique=True)
@ -318,7 +319,7 @@ class MAnalyticsPageLoad(mongo.Document):
try:
delete_old_history()
except TimeoutError:
print "Timed out on deleting old history. Shit."
logging.debug("~SK~SB~BR~FWTimed out on deleting old page load history. Shit.")
class MAnalyticsFetcher(mongo.Document):
@ -383,5 +384,5 @@ class MAnalyticsFetcher(mongo.Document):
try:
delete_old_history()
except TimeoutError:
print "Timed out on deleting old history. Shit."
logging.debug("~SK~SB~BR~FWTimed out on deleting old fetch history. Shit.")

View file

@ -1,7 +1,7 @@
from celery.task import Task
from apps.statistics.models import MStatistics
from apps.statistics.models import MFeedback
from utils import log as logging
# from utils import log as logging
@ -9,7 +9,7 @@ class CollectStats(Task):
name = 'collect-stats'
def run(self, **kwargs):
logging.debug(" ---> ~FMCollecting stats...")
# logging.debug(" ---> ~FBCollecting stats...")
MStatistics.collect_statistics()
@ -17,5 +17,5 @@ class CollectFeedback(Task):
name = 'collect-feedback'
def run(self, **kwargs):
logging.debug(" ---> ~FMCollecting feedback...")
MFeedback.collect_feedback()
# logging.debug(" ---> ~FBCollecting feedback...")
MFeedback.collect_feedback()

View file

@ -7,7 +7,7 @@
199.15.249.101 db01 db01.newsblur.com
199.15.252.50 db02 db02.newsblur.com
# 199.15.253.226 db03 db03.newsblur.com
199.15.253.90 db03 db03.newsblur.com
199.15.249.98 db04 db04.newsblur.com
199.15.249.99 db05 db05.newsblur.com
199.15.249.101 db07 db07.newsblur.com

View file

@ -1,3 +1,9 @@
# Configfile for Munin master
dbdir /var/lib/munin
htmldir /var/www/munin
logdir /var/log/munin
rundir /var/run/munin
includedir /etc/munin/munin-conf.d
[newsblur.com]

View file

@ -31,7 +31,9 @@ def getServerStatus():
name = "locked"
def doData():
print name + ".value " + str( 100 * getServerStatus()["globalLock"]["ratio"] )
ratio = (float(getServerStatus()["globalLock"]["lockTime"]) /
getServerStatus()["globalLock"]["totalTime"])
print name + ".value " + str( 100 * ratio )
def doConfig():

View file

@ -81,6 +81,13 @@ server {
location /munin/ {
alias /var/cache/munin/www/;
}
location ^~ /cgi-bin/munin-cgi-graph/ {
fastcgi_split_path_info ^(/cgi-bin/munin-cgi-graph)(.*);
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass unix:/var/run/munin/fcgi-graph.sock;
include fastcgi_params;
}
location ^~ /rss_feeds/icon/ {
proxy_set_header X-Real-IP $remote_addr;

View file

@ -0,0 +1,119 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: spawn-fcgi-munin-graph
# Required-Start: $all
# Required-Stop: $all
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: starts FastCGI for Munin-Graph
### END INIT INFO
# --------------------------------------------------------------
# Munin-CGI-Graph Spawn-FCGI Startscript by Julien Schmidt
# eMail: munin-trac at julienschmidt.com
# www: http://www.julienschmidt.com
# --------------------------------------------------------------
# Install:
# 1. Copy this file to /etc/init.d
# 2. Edit the variables below
# 3. run "update-rc.d spawn-fcgi-munin-graph defaults"
# --------------------------------------------------------------
# Last Update: 13. November 2012
#
# Please change the following variables:
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
NAME=spawn-fcgi-munin-graph
PID_FILE=/var/run/munin/fcgi-graph.pid
SOCK_FILE=/var/run/munin/fcgi-graph.sock
DAEMON=/usr/bin/spawn-fcgi
DAEMON_OPTS="-s $SOCK_FILE -U www-data -u www-data -g www-data /usr/lib/cgi-bin/munin-cgi-graph -P $PID_FILE"
# --------------------------------------------------------------
# No edits necessary beyond this line
# --------------------------------------------------------------
if [ ! -x $DAEMON ]; then
echo "File not found or is not executable: $DAEMON!"
exit 0
fi
status() {
if [ ! -r $PID_FILE ]; then
return 1
fi
FCGI_PID=`cat $PID_FILE`
if [ -z "${FCGI_PID}" ]; then
return 1
fi
FCGI_RUNNING=`ps -p ${FCGI_PID} | grep ${FCGI_PID}`
if [ -z "${FCGI_RUNNING}" ]; then
return 1
fi
return 0
}
start() {
if status; then
echo "FCGI is already running!"
exit 1
else
$DAEMON $DAEMON_OPTS
fi
}
stop () {
if ! status; then
echo "No PID-file at $PID_FILE found or PID not valid. Maybe not running"
exit 1
fi
# Kill process
kill -9 `cat $PID_FILE`
# Remove PID-file
rm -f $PID_FILE
# Remove Sock-File
rm -f $SOCK_FILE
}
case "$1" in
start)
echo "Starting $NAME: "
start
echo "... DONE"
;;
stop)
echo "Stopping $NAME: "
stop
echo "... DONE"
;;
force-reload|restart)
echo "Stopping $NAME: "
stop
echo "Starting $NAME: "
start
echo "... DONE"
;;
status)
if status; then
echo "FCGI is RUNNING"
else
echo "FCGI is NOT RUNNING"
fi
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
exit 0

View file

@ -1,6 +1,6 @@
[program:node_favicons]
command=node favicons.js
directory=/srv/newsblur/node
command=node node/favicons.js
directory=/srv/newsblur
user=sclay
autostart=true
autorestart=true

View file

@ -1,6 +1,6 @@
[program:node_unread]
command=node unread_counts.js
directory=/srv/newsblur/node
command=node node/unread_counts.js
directory=/srv/newsblur
user=sclay
autostart=true
autorestart=true
@ -8,3 +8,4 @@ autorestart=true
priority=991
stopsignal=HUP
stdout_logfile = /srv/newsblur/logs/unread_counts.log
environment = NODE_ENV=production

View file

@ -0,0 +1,11 @@
[program:node_unread_ssl]
command=node node/unread_counts.js
directory=/srv/newsblur
user=sclay
autostart=true
autorestart=true
#redirect_stderr=True
priority=991
stopsignal=HUP
stdout_logfile = /srv/newsblur/logs/unread_counts.log
environment = NODE_ENV=production,NODE_SSL=on

55
fabfile.py vendored
View file

@ -42,7 +42,7 @@ env.roledefs ={
],
'db': ['db01.newsblur.com',
'db02.newsblur.com',
# 'db03.newsblur.com',
'db03.newsblur.com',
'db04.newsblur.com',
'db05.newsblur.com',
],
@ -150,16 +150,22 @@ def deploy_code(copy_assets=False, full=False):
run('rm -fr static/*')
if copy_assets:
transfer_assets()
with settings(warn_only=True):
run('pkill -c gunicorn')
# run('kill -HUP `cat logs/gunicorn.pid`')
# with settings(warn_only=True):
# run('pkill -c gunicorn')
# # run('kill -HUP `cat logs/gunicorn.pid`')
sudo('supervisorctl reload')
run('curl -s http://%s > /dev/null' % env.host)
run('curl -s http://%s/api/add_site_load_script/ABCDEF > /dev/null' % env.host)
sudo('supervisorctl restart celery')
@parallel
def kill():
sudo('supervisorctl reload')
run('pkill -c gunicorn')
def deploy_node():
with cd(env.NEWSBLUR_PATH):
run('sudo supervisorctl restart node_unread')
run('sudo supervisorctl restart node_unread_ssl')
run('sudo supervisorctl restart node_favicons')
def gunicorn_restart():
@ -301,6 +307,7 @@ def setup_common():
setup_logrotate()
setup_nginx()
configure_nginx()
setup_munin()
def setup_app():
setup_common()
@ -326,7 +333,7 @@ def setup_db():
# setup_memcached()
# setup_postgres(standby=False)
setup_mongo()
setup_gunicorn(supervisor=False)
# setup_gunicorn(supervisor=False)
# setup_redis()
setup_db_munin()
@ -351,13 +358,12 @@ def setup_task():
def setup_installs():
sudo('apt-get -y update')
sudo('apt-get -y upgrade')
sudo('apt-get -y install build-essential gcc scons libreadline-dev sysstat iotop git zsh python-dev locate python-software-properties libpcre3-dev libncurses5-dev libdbd-pg-perl libssl-dev make pgbouncer python-psycopg2 libmemcache0 python-memcache libyaml-0-2 python-yaml python-numpy python-scipy python-imaging munin munin-node munin-plugins-extra curl monit')
sudo('apt-get -y install build-essential gcc scons libreadline-dev sysstat iotop git zsh python-dev locate python-software-properties libpcre3-dev libncurses5-dev libdbd-pg-perl libssl-dev make pgbouncer python-psycopg2 libmemcache0 python-memcache libyaml-0-2 python-yaml python-numpy python-scipy python-imaging curl monit')
# sudo('add-apt-repository ppa:pitti/postgresql')
sudo('apt-get -y update')
sudo('apt-get -y install postgresql-client')
sudo('mkdir -p /var/run/postgresql')
sudo('chown postgres.postgres /var/run/postgresql')
put('config/munin.conf', '/etc/munin/munin.conf', use_sudo=True)
with settings(warn_only=True):
run('git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh')
run('curl -O http://peak.telecommunity.com/dist/ez_setup.py')
@ -422,7 +428,7 @@ def setup_psycopg():
def setup_python():
# sudo('easy_install -U pip')
sudo('easy_install -U fabric django==1.3.1 readline pyflakes iconv celery django-celery django-celery-with-redis django-compress South django-extensions pymongo==2.2.0 stripe BeautifulSoup pyyaml nltk lxml oauth2 pytz boto seacucumber django_ses mongoengine redis requests django-subdomains psutil python-gflags cssutils raven pyes')
sudo('easy_install -U fabric django==1.3.1 readline chardet pyflakes iconv celery django-celery django-celery-with-redis django-compress South django-extensions pymongo==2.2.0 stripe BeautifulSoup pyyaml nltk lxml oauth2 pytz boto seacucumber django_ses mongoengine redis requests django-subdomains psutil python-gflags cssutils raven pyes')
put('config/pystartup.py', '.pystartup')
# with cd(os.path.join(env.NEWSBLUR_PATH, 'vendor/cjson')):
@ -495,7 +501,7 @@ def setup_pymongo_repo():
sudo('rm -fr /usr/local/lib/python2.7/dist-packages/pymongo*')
sudo('rm -fr /usr/local/lib/python2.7/dist-packages/bson*')
sudo('rm -fr /usr/local/lib/python2.7/dist-packages/gridgs*')
sudo('ln -s %s /usr/local/lib/python2.7/dist-packages/' %
sudo('ln -fs %s /usr/local/lib/python2.7/dist-packages/' %
os.path.join(env.VENDOR_PATH, 'pymongo/{pymongo,bson,gridfs}'))
def setup_forked_mongoengine():
@ -560,10 +566,11 @@ def setup_baremetal():
def setup_app_firewall():
sudo('ufw default deny')
sudo('ufw allow ssh')
sudo('ufw allow 80')
sudo('ufw allow 8888')
sudo('ufw allow 443')
sudo('ufw allow ssh') # ssh
sudo('ufw allow 80') # http
sudo('ufw allow 8888') # socket.io
sudo('ufw allow 8889') # socket.io ssl
sudo('ufw allow 443') # https
sudo('ufw --force enable')
def setup_app_motd():
@ -603,6 +610,7 @@ def setup_node():
def configure_node():
sudo('rm -fr /etc/supervisor/conf.d/node.conf')
put('config/supervisor_node_unread.conf', '/etc/supervisor/conf.d/node_unread.conf', use_sudo=True)
put('config/supervisor_node_unread_ssl.conf', '/etc/supervisor/conf.d/node_unread_ssl.conf', use_sudo=True)
put('config/supervisor_node_favicons.conf', '/etc/supervisor/conf.d/node_favicons.conf', use_sudo=True)
sudo('supervisorctl reload')
@ -614,12 +622,15 @@ def copy_certificates():
run('mkdir -p %s/config/certificates/' % env.NEWSBLUR_PATH)
put('config/certificates/comodo/newsblur.com.crt', '%s/config/certificates/' % env.NEWSBLUR_PATH)
put('config/certificates/comodo/newsblur.com.key', '%s/config/certificates/' % env.NEWSBLUR_PATH)
put('config/certificates/comodo/EssentialSSLCA_2.crt', '%s/config/certificates/intermediate.crt' % env.NEWSBLUR_PATH)
@parallel
def maintenance_on():
put('templates/maintenance_off.html', '%s/templates/maintenance_off.html' % env.NEWSBLUR_PATH)
with cd(env.NEWSBLUR_PATH):
run('mv templates/maintenance_off.html templates/maintenance_on.html')
@parallel
def maintenance_off():
with cd(env.NEWSBLUR_PATH):
run('mv templates/maintenance_on.html templates/maintenance_off.html')
@ -717,6 +728,16 @@ def setup_redis():
sudo('/etc/init.d/redis stop')
sudo('/etc/init.d/redis start')
def setup_munin():
sudo('apt-get update')
sudo('apt-get install -y munin munin-node munin-plugins-extra spawn-fcgi')
put('config/munin.conf', '/etc/munin/munin.conf', use_sudo=True)
put('config/spawn_fcgi_munin_graph.conf', '/etc/init.d/spawn_fcgi_munin_graph', use_sudo=True)
sudo('chmod u+x /etc/init.d/spawn_fcgi_munin_graph')
sudo('/etc/init.d/spawn_fcgi_munin_graph start')
sudo('update-rc.d spawn_fcgi_munin_graph defaults')
def setup_db_munin():
sudo('cp -frs %s/config/munin/mongo* /etc/munin/plugins/' % env.NEWSBLUR_PATH)
sudo('cp -frs %s/config/munin/pg_* /etc/munin/plugins/' % env.NEWSBLUR_PATH)
@ -725,6 +746,7 @@ def setup_db_munin():
run('git clone git://github.com/samuel/python-munin.git')
with cd(os.path.join(env.VENDOR_PATH, 'python-munin')):
run('sudo python setup.py install')
sudo('/etc/init.d/munin-node restart')
def enable_celerybeat():
with cd(env.NEWSBLUR_PATH):
@ -773,6 +795,9 @@ def setup_task_motd():
def enable_celery_supervisor():
put('config/supervisor_celeryd.conf', '/etc/supervisor/conf.d/celeryd.conf', use_sudo=True)
sudo('supervisorctl reread')
sudo('supervisorctl update')
def copy_task_settings():
with settings(warn_only=True):

View file

@ -432,7 +432,7 @@
}
.NB-modal .NB-fieldset {
background-color: #F0F0FF;
background-color: #EFF3F6;
border-bottom: 1px solid #D0D0D9;
margin: 16px 0 0;
overflow: hidden;

View file

@ -1993,7 +1993,7 @@ background: transparent;
font-size: 9px;
padding: 0px 4px 1px;
margin: 0 2px 2px;
background-color: #DBDBDB;
background-color: #D8DEE2;
color: #9D9A95;
text-shadow: 0 1px 0 #E9E9E9;
border-radius: 4px;
@ -2039,7 +2039,7 @@ background: transparent;
}
#story_pane .NB-feed-story-tag.NB-score-now-0:hover {
/* Grey, active */
background-color: #DBDBDB;
background-color: #D8DEE2;
color: #9D9A95;
text-shadow: 0 1px 0 #E9E9E9;
opacity: 1;
@ -4163,7 +4163,7 @@ form.opml_import_form input {
padding: 0 30px 0 26px;
font-size: 12px;
text-transform: uppercase;
background-color: #F5CD09;
background-color: #D8DEE2;
position: relative;
border: 1px solid transparent;
}
@ -4183,13 +4183,13 @@ form.opml_import_form input {
}
.NB-classifiers .NB-classifier label b {
color: #957D09;
color: rgba(0,0,0,.4);
text-shadow: none;
font-weight: normal;
}
.NB-classifiers .NB-classifier label span {
text-shadow: 1px 1px 0 #F4E576;
text-shadow: 1px 1px 0 rgba(255,255,255,0.5);
}
.NB-classifiers .NB-classifier.NB-classifier-facet-disabled {
@ -4264,7 +4264,7 @@ form.opml_import_form input {
.NB-classifiers .NB-classifier.NB-classifier-like label span,
.NB-classifiers .NB-classifier.NB-classifier-hover-like label span {
color: white;
text-shadow: 1px 1px 0 #254E18;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
}
.NB-classifiers .NB-classifier.NB-classifier-like .NB-classifier-icon-dislike,
.NB-classifiers .NB-classifier.NB-classifier-hover-like .NB-classifier-icon-dislike {
@ -4320,7 +4320,7 @@ form.opml_import_form input {
.NB-classifiers .NB-classifier.NB-classifier-dislike label span,
.NB-classifiers .NB-classifier.NB-classifier-hover-dislike label span {
color: white;
text-shadow: 1px 1px 0 #7F1012;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
}
/* =================== */
/* = Mouse Indicator = */
@ -5607,7 +5607,7 @@ form.opml_import_form input {
.NB-modal-statistics .NB-statistics-stat .NB-statistics-fetches-half {
float: left;
text-align: center;
margin-right: 18px;
margin: 0 18px 6px 0;
}
.NB-modal-statistics .NB-statistics-stat .NB-statistics-fetches-half:last-child {
margin-right: 0;
@ -5629,36 +5629,36 @@ form.opml_import_form input {
margin: 6px 8px 0;
}
.NB-modal-statistics .NB-statistics-history-fetch {
.NB-history-fetch {
overflow: hidden;
clear: both;
font-size: 10px;
margin: 2px 0 0 8px;
text-align: left;
}
.NB-modal-statistics .NB-statistics-history-fetch.NB-ok {
.NB-history-fetch.NB-ok {
color: #135500;
}
.NB-modal-statistics .NB-statistics-history-fetch.NB-errorcode {
.NB-history-fetch.NB-errorcode {
color: #6A1000;
}
.NB-modal-statistics .NB-statistics-history-fetch .NB-statistics-history-fetch-code {
.NB-history-fetch .NB-history-fetch-code {
display: inline;
font-weight: normal;
}
.NB-modal-statistics .NB-statistics-history-fetch .NB-statistics-history-fetch-date {
.NB-history-fetch .NB-history-fetch-date {
float: left;
padding-right: 4px;
}
.NB-modal-statistics .NB-statistics-history-fetch .NB-statistics-history-fetch-message {
.NB-history-fetch .NB-history-fetch-message {
padding-right: 4px;
margin-left: 110px;
font-weight: bold;
}
.NB-modal-statistics .NB-statistics-history-fetch .NB-statistics-history-fetch-exception {
.NB-history-fetch .NB-history-fetch-exception {
display: none;
}
.NB-modal-statistics .NB-statistics-history-empty {
.NB-history-empty {
color: #C0C0C0;
font-size: 10px;
padding: 4px 12px;

View file

@ -92,6 +92,7 @@
[options addObject:[deleteText uppercaseString]];
[options addObject:[@"Move to another folder" uppercaseString]];
[options addObject:[@"Train this site" uppercaseString]];
if (!appDelegate.isRiverView) {
[options addObject:[@"Insta-fetch stories" uppercaseString]];
@ -139,6 +140,8 @@
} else if (indexPath.row == 1) {
cell.imageView.image = [UIImage imageNamed:@"arrow_branch"];
} else if (indexPath.row == 2) {
cell.imageView.image = [UIImage imageNamed:@"bricks"];
} else if (indexPath.row == 3) {
cell.imageView.image = [UIImage imageNamed:@"car"];
}
@ -164,6 +167,8 @@
} else if (indexPath.row == 1) {
[appDelegate.feedDetailViewController openMoveView];
} else if (indexPath.row == 2) {
[appDelegate.feedDetailViewController openTrainSite];
} else if (indexPath.row == 3) {
[appDelegate.feedDetailViewController instafetchFeed];
}

View file

@ -74,6 +74,7 @@
- (void)deleteSite;
- (void)deleteFolder;
- (void)openMoveView;
- (void)openTrainSite;
- (void)showUserProfile;
- (void)changeActiveFeedDetailRow;
- (void)instafetchFeed;

View file

@ -222,6 +222,8 @@
[self.storyTitlesTable reloadData];
[storyTitlesTable scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
appDelegate.activeClassifiers = [NSMutableDictionary dictionary];
appDelegate.activePopularAuthors = [NSArray array];
appDelegate.activePopularTags = [NSArray array];
if (appDelegate.isRiverView) {
[self fetchRiverPage:1 withCallback:nil];
@ -400,7 +402,8 @@
options:kNilOptions
error:&error];
id feedId = [results objectForKey:@"feed_id"];
NSString *feedIdStr = [NSString stringWithFormat:@"%@",feedId];
if (!(appDelegate.isRiverView || appDelegate.isSocialView || appDelegate.isSocialRiverView)
&& request.tag != [feedId intValue]) {
return;
@ -422,9 +425,10 @@
[appDelegate.activeClassifiers setObject:[newClassifiers objectForKey:key] forKey:key];
}
} else {
NSString *feedIdStr = [NSString stringWithFormat:@"%@",feedId];
[appDelegate.activeClassifiers setObject:newClassifiers forKey:feedIdStr];
}
appDelegate.activePopularAuthors = [results objectForKey:@"feed_authors"];
appDelegate.activePopularTags = [results objectForKey:@"feed_tags"];
NSArray *newStories = [results objectForKey:@"stories"];
NSMutableArray *confirmedNewStories = [[NSMutableArray alloc] init];
@ -1081,7 +1085,7 @@
if ([self.popoverController respondsToSelector:@selector(setContainerViewProperties:)]) {
[self.popoverController setContainerViewProperties:[self improvedContainerViewProperties]];
}
[self.popoverController setPopoverContentSize:CGSizeMake(260, appDelegate.isRiverView ? 38 * 3 : 38 * 5)];
[self.popoverController setPopoverContentSize:CGSizeMake(260, appDelegate.isRiverView ? 38 * 4 : 38 * 6)];
[self.popoverController presentPopoverFromBarButtonItem:self.settingsButton
permittedArrowDirections:UIPopoverArrowDirectionDown
animated:YES];
@ -1178,6 +1182,10 @@
[appDelegate showMoveSite];
}
- (void)openTrainSite {
[appDelegate openTrainSite];
}
- (void)showUserProfile {
appDelegate.activeUserProfileId = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed objectForKey:@"user_id"]];
appDelegate.activeUserProfileName = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed objectForKey:@"username"]];

View file

@ -127,7 +127,7 @@
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 6;
return 7;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
@ -136,9 +136,9 @@
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIndentifier];
if (indexPath.row == 4) {
if (indexPath.row == 5) {
return [self makeFontSelectionTableCell];
} else if (indexPath.row == 5) {
} else if (indexPath.row == 6) {
return [self makeFontSizeTableCell];
}
@ -168,6 +168,9 @@
cell.textLabel.text = [@"Send to..." uppercaseString];
cell.imageView.image = [UIImage imageNamed:@"email"];
} else if (indexPath.row == 3) {
cell.textLabel.text = [@"Train this story" uppercaseString];
cell.imageView.image = [UIImage imageNamed:@"bricks"];
} else if (indexPath.row == 4) {
cell.textLabel.text = [@"Share this story" uppercaseString];
cell.imageView.image = [UIImage imageNamed:@"rainbow"];
}
@ -180,7 +183,7 @@
}
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row >= 4) {
if (indexPath.row >= 5) {
return nil;
}
return indexPath;
@ -206,6 +209,8 @@
} else if (indexPath.row == 2) {
[appDelegate.storyPageControl openSendToDialog];
} else if (indexPath.row == 3) {
[appDelegate openTrainStory:appDelegate.storyPageControl.fontSettingsButton];
} else if (indexPath.row == 4) {
[appDelegate.storyPageControl.currentPage openShareDialog];
}

View file

@ -35,6 +35,7 @@
- (void)showFeedMenuPopover:(id)sender;
- (void)showFeedDetailMenuPopover:(id)sender;
- (void)showFontSettingsPopover:(id)sender;
- (void)showTrainingPopover:(id)sender;
- (void)showSitePopover:(id)sender;
- (void)hidePopover;
@end

View file

@ -20,6 +20,7 @@
#import "FeedDetailMenuViewController.h"
#import "FontSettingsViewController.h"
#import "AddSiteViewController.h"
#import "TrainerViewController.h"
#define NB_DEFAULT_MASTER_WIDTH 270
#define NB_DEFAULT_STORY_TITLE_HEIGHT 1004
@ -279,7 +280,7 @@
popoverController.delegate = self;
[popoverController setPopoverContentSize:CGSizeMake(260, appDelegate.isRiverView ? 38*3 : 38*5)];
[popoverController setPopoverContentSize:CGSizeMake(260, appDelegate.isRiverView ? 38*4 : 38*6)];
[popoverController presentPopoverFromBarButtonItem:sender
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
@ -298,12 +299,40 @@
popoverController.delegate = self;
[popoverController setPopoverContentSize:CGSizeMake(240, 228)];
// UIBarButtonItem *settingsButton = [[UIBarButtonItem alloc]
[popoverController setPopoverContentSize:CGSizeMake(240, 38*7)];
// UIBarButtonItem *settingsButton = [[UIBarButtonItem alloc]
// initWithCustomView:sender];
[popoverController presentPopoverFromBarButtonItem:sender
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
[popoverController presentPopoverFromBarButtonItem:sender
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}
- (void)showTrainingPopover:(id)sender {
if (popoverController.isPopoverVisible) {
[popoverController dismissPopoverAnimated:NO];
// popoverController = nil;
// return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, .001 * NSEC_PER_SEC),
dispatch_get_current_queue(), ^{
popoverController = [[UIPopoverController alloc]
initWithContentViewController:appDelegate.trainerViewController];
popoverController.delegate = self;
[popoverController setPopoverContentSize:CGSizeMake(420, 382)];
if ([sender class] == [UIBarButtonItem class]) {
[popoverController presentPopoverFromBarButtonItem:sender
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:NO];
} else {
CGRect frame = [sender CGRectValue];
[popoverController presentPopoverFromRect:frame
inView:self.storyPageControl.view
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}
});
}
- (void)hidePopover {

View file

@ -35,6 +35,7 @@
@class LoginViewController;
@class AddSiteViewController;
@class MoveSiteViewController;
@class TrainerViewController;
@class OriginalStoryViewController;
@class UserProfileViewController;
@class NBContainerViewController;
@ -71,6 +72,7 @@
AddSiteViewController *addSiteViewController;
FindSitesViewController *findSitesViewController;
MoveSiteViewController *moveSiteViewController;
TrainerViewController *trainerViewController;
OriginalStoryViewController *originalStoryViewController;
UserProfileViewController *userProfileViewController;
@ -89,6 +91,8 @@
NSString *tryFeedStoryId;
NSDictionary * activeFeed;
NSMutableDictionary * activeClassifiers;
NSArray * activePopularTags;
NSArray * activePopularAuthors;
NSString * activeFolder;
NSDictionary * activeComment;
NSString * activeShareType;
@ -146,6 +150,7 @@
@property (nonatomic) IBOutlet AddSiteViewController *addSiteViewController;
@property (nonatomic) IBOutlet FindSitesViewController *findSitesViewController;
@property (nonatomic) IBOutlet MoveSiteViewController *moveSiteViewController;
@property (nonatomic) IBOutlet TrainerViewController *trainerViewController;
@property (nonatomic) IBOutlet OriginalStoryViewController *originalStoryViewController;
@property (nonatomic) IBOutlet ShareViewController *shareViewController;
@property (nonatomic) IBOutlet FontSettingsViewController *fontSettingsViewController;
@ -172,6 +177,8 @@
@property (nonatomic, readwrite) BOOL inStoryDetail;
@property (readwrite) NSDictionary * activeFeed;
@property (strong, readwrite) NSMutableDictionary * activeClassifiers;
@property (strong, readwrite) NSArray * activePopularTags;
@property (strong, readwrite) NSArray * activePopularAuthors;
@property (readwrite) NSString * activeFolder;
@property (readwrite) NSDictionary * activeComment;
@property (readwrite) NSString * activeShareType;
@ -220,6 +227,8 @@
- (void)showAddSiteModal:(id)sender;
- (void)showMoveSite;
- (void)openTrainSite;
- (void)openTrainStory:(id)sender;
- (void)loadFeedDetailView;
- (void)loadTryFeedDetailView:(NSString *)feedId withStory:(NSString *)contentId isSocial:(BOOL)social withUser:(NSDictionary *)user showFindingStory:(BOOL)showHUD;
- (void)loadRiverFeedDetailView;
@ -280,10 +289,18 @@
+ (int)computeStoryScore:(NSDictionary *)intelligence;
- (NSString *)extractFolderName:(NSString *)folderName;
- (NSString *)extractParentFolderName:(NSString *)folderName;
- (NSDictionary *)getFeed:(NSString *)feedId;
+ (UIView *)makeGradientView:(CGRect)rect startColor:(NSString *)start endColor:(NSString *)end;
- (UIView *)makeFeedTitleGradient:(NSDictionary *)feed withRect:(CGRect)rect;
- (UIView *)makeFeedTitle:(NSDictionary *)feed;
- (UIButton *)makeRightFeedTitle:(NSDictionary *)feed;
- (void)toggleAuthorClassifier:(NSString *)author feedId:(NSString *)feedId;
- (void)toggleTagClassifier:(NSString *)tag feedId:(NSString *)feedId;
- (void)toggleTitleClassifier:(NSString *)title feedId:(NSString *)feedId score:(int)score;
- (void)toggleFeedClassifier:(NSString *)feedId;
@end
@interface UnreadCounts : NSObject {

View file

@ -21,6 +21,7 @@
#import "AddSiteViewController.h"
#import "FindSitesViewController.h"
#import "MoveSiteViewController.h"
#import "TrainerViewController.h"
#import "OriginalStoryViewController.h"
#import "ShareViewController.h"
#import "UserProfileViewController.h"
@ -64,6 +65,7 @@
@synthesize addSiteViewController;
@synthesize findSitesViewController;
@synthesize moveSiteViewController;
@synthesize trainerViewController;
@synthesize originalStoryViewController;
@synthesize userProfileViewController;
@ -93,6 +95,8 @@
@synthesize activeFeed;
@synthesize activeClassifiers;
@synthesize activePopularTags;
@synthesize activePopularAuthors;
@synthesize activeFolder;
@synthesize activeFolderFeeds;
@synthesize activeFeedStories;
@ -178,11 +182,11 @@
}
[splashView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
splashView.alpha = 1.0;
[window addSubview:splashView];
[window bringSubviewToFront:splashView];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:.5];
[UIView setAnimationDuration:.6];
[UIView setAnimationTransition:UIViewAnimationTransitionNone forView:window cache:YES];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(startupAnimationDone:finished:context:)];
@ -404,6 +408,31 @@
}
}
- (void)openTrainSite {
UINavigationController *navController = self.navigationController;
trainerViewController.feedTrainer = YES;
trainerViewController.storyTrainer = NO;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
// trainerViewController.modalPresentationStyle=UIModalPresentationFormSheet;
// [navController presentModalViewController:trainerViewController animated:YES];
[self.masterContainerViewController showTrainingPopover:self.feedDetailViewController.settingsButton];
} else {
[navController presentModalViewController:trainerViewController animated:YES];
}
}
- (void)openTrainStory:(id)sender {
UINavigationController *navController = self.navigationController;
trainerViewController.feedTrainer = NO;
trainerViewController.storyTrainer = YES;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
[self.masterContainerViewController showTrainingPopover:sender];
} else {
[navController presentModalViewController:trainerViewController animated:YES];
}
}
- (void)reloadFeedsView:(BOOL)showLoader {
[feedsViewController fetchFeedList:showLoader];
[loginViewController dismissModalViewControllerAnimated:NO];
@ -634,6 +663,7 @@
for (NSString *author in [classifiers objectForKey:@"authors"]) {
if ([[intelligence objectForKey:@"author"] intValue] <= 0 &&
[[story objectForKey:@"story_authors"] class] != [NSNull class] &&
[[story objectForKey:@"story_authors"] containsString:author]) {
int score = [[[classifiers objectForKey:@"authors"] objectForKey:author] intValue];
[intelligence setObject:[NSNumber numberWithInt:score] forKey:@"author"];
@ -642,6 +672,7 @@
for (NSString *tag in [classifiers objectForKey:@"tags"]) {
if ([[intelligence objectForKey:@"tags"] intValue] <= 0 &&
[[story objectForKey:@"story_tags"] class] != [NSNull class] &&
[[story objectForKey:@"story_tags"] containsObject:tag]) {
int score = [[[classifiers objectForKey:@"tags"] objectForKey:tag] intValue];
[intelligence setObject:[NSNumber numberWithInt:score] forKey:@"tags"];
@ -650,7 +681,7 @@
for (NSString *feed in [classifiers objectForKey:@"feeds"]) {
if ([[intelligence objectForKey:@"feed"] intValue] <= 0 &&
[[story objectForKey:@"story_feed_id"] isEqualToString:feed]) {
[storyFeedId isEqualToString:feed]) {
int score = [[[classifiers objectForKey:@"feeds"] objectForKey:feed] intValue];
[intelligence setObject:[NSNumber numberWithInt:score] forKey:@"feed"];
}
@ -1479,6 +1510,21 @@
return folderName;
}
- (NSDictionary *)getFeed:(NSString *)feedId {
NSDictionary *feed;
if (self.isSocialView || self.isSocialRiverView) {
feed = [self.dictActiveFeeds objectForKey:feedId];
// this is to catch when a user is already subscribed
if (!feed) {
feed = [self.dictFeeds objectForKey:feedId];
}
} else {
feed = [self.dictFeeds objectForKey:feedId];
}
return feed;
}
#pragma mark -
#pragma mark Feed Templates
@ -1640,6 +1686,181 @@
return titleImageButton;
}
#pragma mark -
#pragma mark Classifiers
- (void)toggleAuthorClassifier:(NSString *)author feedId:(NSString *)feedId {
int authorScore = [[[[self.activeClassifiers objectForKey:feedId]
objectForKey:@"authors"]
objectForKey:author] intValue];
if (authorScore > 0) {
authorScore = -1;
} else if (authorScore < 0) {
authorScore = 0;
} else {
authorScore = 1;
}
NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId]
mutableCopy];
NSMutableDictionary *authors = [[feedClassifiers objectForKey:@"authors"] mutableCopy];
[authors setObject:[NSNumber numberWithInt:authorScore] forKey:author];
[feedClassifiers setObject:authors forKey:@"authors"];
[self.activeClassifiers setObject:feedClassifiers forKey:feedId];
[self.storyPageControl refreshHeaders];
[self.trainerViewController refresh];
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:author
forKey:authorScore >= 1 ? @"like_author" :
authorScore <= -1 ? @"dislike_author" :
@"remove_like_author"];
[request setPostValue:feedId forKey:@"feed_id"];
[request setCompletionBlock:^{
[self.feedsViewController refreshFeedList:feedId];
}];
[request setDidFailSelector:@selector(requestFailed:)];
[request setDelegate:self];
[request startAsynchronous];
[self recalculateIntelligenceScores:feedId];
[self.feedDetailViewController.storyTitlesTable reloadData];
}
- (void)toggleTagClassifier:(NSString *)tag feedId:(NSString *)feedId {
NSLog(@"toggleTagClassifier: %@", tag);
int tagScore = [[[[self.activeClassifiers objectForKey:feedId]
objectForKey:@"tags"]
objectForKey:tag] intValue];
if (tagScore > 0) {
tagScore = -1;
} else if (tagScore < 0) {
tagScore = 0;
} else {
tagScore = 1;
}
NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId]
mutableCopy];
NSMutableDictionary *tags = [[feedClassifiers objectForKey:@"tags"] mutableCopy];
[tags setObject:[NSNumber numberWithInt:tagScore] forKey:tag];
[feedClassifiers setObject:tags forKey:@"tags"];
[self.activeClassifiers setObject:feedClassifiers forKey:feedId];
[self.storyPageControl refreshHeaders];
[self.trainerViewController refresh];
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:tag
forKey:tagScore >= 1 ? @"like_tag" :
tagScore <= -1 ? @"dislike_tag" :
@"remove_like_tag"];
[request setPostValue:feedId forKey:@"feed_id"];
[request setCompletionBlock:^{
[self.feedsViewController refreshFeedList:feedId];
}];
[request setDidFailSelector:@selector(requestFailed:)];
[request setDelegate:self];
[request startAsynchronous];
[self recalculateIntelligenceScores:feedId];
[self.feedDetailViewController.storyTitlesTable reloadData];
}
- (void)toggleTitleClassifier:(NSString *)title feedId:(NSString *)feedId score:(int)score {
NSLog(@"toggle Title: %@ (%@) / %d", title, feedId, score);
int titleScore = [[[[self.activeClassifiers objectForKey:feedId]
objectForKey:@"titles"]
objectForKey:title] intValue];
if (score) {
titleScore = score;
} else {
if (titleScore > 0) {
titleScore = -1;
} else if (titleScore < 0) {
titleScore = 0;
} else {
titleScore = 1;
}
}
NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId]
mutableCopy];
NSMutableDictionary *titles = [[feedClassifiers objectForKey:@"titles"] mutableCopy];
[titles setObject:[NSNumber numberWithInt:titleScore] forKey:title];
[feedClassifiers setObject:titles forKey:@"titles"];
[self.activeClassifiers setObject:feedClassifiers forKey:feedId];
[self.storyPageControl refreshHeaders];
[self.trainerViewController refresh];
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:title
forKey:titleScore >= 1 ? @"like_title" :
titleScore <= -1 ? @"dislike_title" :
@"remove_like_title"];
[request setPostValue:feedId forKey:@"feed_id"];
[request setCompletionBlock:^{
[self.feedsViewController refreshFeedList:feedId];
}];
[request setDidFailSelector:@selector(requestFailed:)];
[request setDelegate:self];
[request startAsynchronous];
[self recalculateIntelligenceScores:feedId];
[self.feedDetailViewController.storyTitlesTable reloadData];
}
- (void)toggleFeedClassifier:(NSString *)feedId {
int feedScore = [[[[self.activeClassifiers objectForKey:feedId]
objectForKey:@"feeds"]
objectForKey:feedId] intValue];
if (feedScore > 0) {
feedScore = -1;
} else if (feedScore < 0) {
feedScore = 0;
} else {
feedScore = 1;
}
NSMutableDictionary *feedClassifiers = [[self.activeClassifiers objectForKey:feedId]
mutableCopy];
NSMutableDictionary *feeds = [[feedClassifiers objectForKey:@"feeds"] mutableCopy];
[feeds setObject:[NSNumber numberWithInt:feedScore] forKey:feedId];
[feedClassifiers setObject:feeds forKey:@"feeds"];
[self.activeClassifiers setObject:feedClassifiers forKey:feedId];
[self.storyPageControl refreshHeaders];
[self.trainerViewController refresh];
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:feedId
forKey:feedScore >= 1 ? @"like_feed" :
feedScore <= -1 ? @"dislike_feed" :
@"remove_like_feed"];
[request setPostValue:feedId forKey:@"feed_id"];
[request setCompletionBlock:^{
[self.feedsViewController refreshFeedList:feedId];
}];
[request setDidFailSelector:@selector(requestFailed:)];
[request setDelegate:self];
[request startAsynchronous];
[self recalculateIntelligenceScores:feedId];
[self.feedDetailViewController.storyTitlesTable reloadData];
}
@end

View file

@ -287,8 +287,12 @@ static const CGFloat kFolderTitleHeight = 28;
if ([request responseStatusCode] == 403) {
return [appDelegate showLogin];
} else if ([request responseStatusCode] == 404 ||
[request responseStatusCode] == 429 ||
[request responseStatusCode] >= 500) {
[pull finishedLoading];
if ([request responseStatusCode] == 429) {
return [self informError:@"Slow down. You're rate-limited."];
}
return [self informError:@"The server barfed!"];
}

View file

@ -2,7 +2,7 @@
<archive type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="8.00">
<data>
<int key="IBDocument.SystemTarget">1536</int>
<string key="IBDocument.SystemVersion">12C3006</string>
<string key="IBDocument.SystemVersion">12C2037</string>
<string key="IBDocument.InterfaceBuilderVersion">2840</string>
<string key="IBDocument.AppKitVersion">1187.34</string>
<string key="IBDocument.HIToolboxVersion">625.00</string>
@ -41,7 +41,6 @@
<int key="NSvFlags">303</int>
<string key="NSFrame">{{86, 15}, {532, 75}}</string>
<reference key="NSSuperview" ref="766721923"/>
<reference key="NSNextKeyView"/>
<string key="NSReuseIdentifierKey">_NS:9</string>
<object class="NSColor" key="IBUIBackgroundColor">
<int key="NSColorSpace">1</int>

View file

@ -53,6 +53,7 @@
- (void)refreshComments:(NSString *)replyId;
- (void)openShareDialog;
- (void)openTrainingDialog:(int)x yCoordinate:(int)y width:(int)width height:(int)height;
- (void)finishLikeComment:(ASIHTTPRequest *)request;
- (void)subscribeToBlurblog;
- (void)finishSubscribeToBlurblog:(ASIHTTPRequest *)request;
@ -66,9 +67,6 @@
- (NSString *)getAvatars:(NSString *)key;
- (NSDictionary *)getUser:(int)user_id;
- (void)toggleAuthorClassifier:(NSString *)author;
- (void)toggleTagClassifier:(NSString *)tag;
- (void)finishTrain:(ASIHTTPRequest *)request;
- (void)refreshHeader;

View file

@ -69,6 +69,10 @@
self.webView.scalesPageToFit = YES;
self.webView.multipleTouchEnabled = NO;
[self.webView.scrollView setDelaysContentTouches:NO];
[self.webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
self.pageIndex = -2;
}
@ -160,16 +164,22 @@
contentWidth];
footerString = [NSString stringWithFormat:@
"<script src=\"zepto.js\"></script>"
"<script src=\"storyDetailView.js\"></script>"];
"<script src=\"storyDetailView.js\"></script>"
"<script src=\"fastTouch.js\"></script>"];
sharingHtmlString = [NSString stringWithFormat:@
"<div class='NB-share-header'></div>"
"<div class='NB-share-wrapper'><div class='NB-share-inner-wrapper'>"
"<div id=\"NB-share-button-id\" class='NB-share-button NB-button'>"
"<a href=\"http://ios.newsblur.com/share\"><div>"
"Share this story <span class=\"NB-share-icon\"></span>"
"</div></a>"
"</div>"
" <div id=\"NB-share-button-id\" class='NB-share-button NB-button'>"
" <a href=\"http://ios.newsblur.com/share\"><div>"
" <span class=\"NB-icon\"></span> Share this story"
" </div></a>"
" </div>"
" <div id=\"NB-share-button-id\" class='NB-share-button NB-train-button NB-button'>"
" <a href=\"http://ios.newsblur.com/train\"><div>"
" <span class=\"NB-icon\"></span> Train this story"
" </div></a>"
" </div>"
"</div></div>"];
NSString *storyHeader = [self getHeader];
@ -209,20 +219,10 @@
[webView loadHTMLString:htmlString baseURL:baseURL];
NSDictionary *feed;
NSString *feedIdStr = [NSString stringWithFormat:@"%@",
[self.activeStory
objectForKey:@"story_feed_id"]];
if (appDelegate.isSocialView || appDelegate.isSocialRiverView) {
feed = [appDelegate.dictActiveFeeds objectForKey:feedIdStr];
// this is to catch when a user is already subscribed
if (!feed) {
feed = [appDelegate.dictFeeds objectForKey:feedIdStr];
}
} else {
feed = [appDelegate.dictFeeds objectForKey:feedIdStr];
}
NSDictionary *feed = [appDelegate getFeed:feedIdStr];
self.feedTitleGradient = [appDelegate
makeFeedTitleGradient:feed
@ -830,6 +830,8 @@ shouldStartLoadWithRequest:(NSURLRequest *)request
NSURL *url = [request URL];
NSArray *urlComponents = [url pathComponents];
NSString *action = @"";
NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory
objectForKey:@"story_feed_id"]];
if ([urlComponents count] > 1) {
action = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:1]];
}
@ -901,13 +903,19 @@ shouldStartLoadWithRequest:(NSURLRequest *)request
} else if ([action isEqualToString:@"share"]) {
[self openShareDialog];
return NO;
} else if ([action isEqualToString:@"train"]) {
[self openTrainingDialog:[[urlComponents objectAtIndex:2] intValue]
yCoordinate:[[urlComponents objectAtIndex:3] intValue]
width:[[urlComponents objectAtIndex:4] intValue]
height:[[urlComponents objectAtIndex:5] intValue]];
return NO;
} else if ([action isEqualToString:@"classify-author"]) {
NSString *author = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
[self toggleAuthorClassifier:author];
[self.appDelegate toggleAuthorClassifier:author feedId:feedId];
return NO;
} else if ([action isEqualToString:@"classify-tag"]) {
NSString *tag = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
[self toggleTagClassifier:tag];
[self.appDelegate toggleTagClassifier:tag feedId:feedId];
return NO;
} else if ([action isEqualToString:@"show-profile"]) {
appDelegate.activeUserProfileId = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
@ -1141,6 +1149,26 @@ shouldStartLoadWithRequest:(NSURLRequest *)request
}
}
- (void)openTrainingDialog:(int)x yCoordinate:(int)y width:(int)width height:(int)height {
CGRect frame = CGRectZero;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
// only adjust for the bar if user is scrolling
if (appDelegate.isRiverView || appDelegate.isSocialView) {
if (self.webView.scrollView.contentOffset.y == -20) {
y = y + 20;
}
} else {
if (self.webView.scrollView.contentOffset.y == -9) {
y = y + 9;
}
}
frame = CGRectMake(x, y, width, height);
}
NSLog(@"Open trainer: %@ (%d/%d/%d/%d)", NSStringFromCGRect(frame), x, y, width, height);
[appDelegate openTrainStory:[NSValue valueWithCGRect:frame]];
}
# pragma mark
# pragma mark Subscribing to blurblog
@ -1278,95 +1306,6 @@ shouldStartLoadWithRequest:(NSURLRequest *)request
// self.webView.hidden = NO;
}
#pragma mark -
#pragma mark Classifiers
- (void)toggleAuthorClassifier:(NSString *)author {
NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory
objectForKey:@"story_feed_id"]];
int authorScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
objectForKey:@"authors"]
objectForKey:author] intValue];
if (authorScore > 0) {
authorScore = -1;
} else if (authorScore < 0) {
authorScore = 0;
} else {
authorScore = 1;
}
NSMutableDictionary *feedClassifiers = [[appDelegate.activeClassifiers objectForKey:feedId]
mutableCopy];
NSMutableDictionary *authors = [[feedClassifiers objectForKey:@"authors"] mutableCopy];
[authors setObject:[NSNumber numberWithInt:authorScore] forKey:author];
[feedClassifiers setObject:authors forKey:@"authors"];
[appDelegate.activeClassifiers setObject:feedClassifiers forKey:feedId];
[appDelegate.storyPageControl refreshHeaders];
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:author
forKey:authorScore >= 1 ? @"like_author" :
authorScore <= -1 ? @"dislike_author" :
@"remove_like_author"];
[request setPostValue:feedId forKey:@"feed_id"];
[request setDidFinishSelector:@selector(finishTrain:)];
[request setDidFailSelector:@selector(requestFailed:)];
[request setDelegate:self];
[request startAsynchronous];
[appDelegate recalculateIntelligenceScores:feedId];
[appDelegate.feedDetailViewController.storyTitlesTable reloadData];
}
- (void)toggleTagClassifier:(NSString *)tag {
NSLog(@"toggleTagClassifier: %@", tag);
NSString *feedId = [NSString stringWithFormat:@"%@", [self.activeStory
objectForKey:@"story_feed_id"]];
int tagScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
objectForKey:@"tags"]
objectForKey:tag] intValue];
if (tagScore > 0) {
tagScore = -1;
} else if (tagScore < 0) {
tagScore = 0;
} else {
tagScore = 1;
}
NSMutableDictionary *feedClassifiers = [[appDelegate.activeClassifiers objectForKey:feedId]
mutableCopy];
NSMutableDictionary *tags = [[feedClassifiers objectForKey:@"tags"] mutableCopy];
[tags setObject:[NSNumber numberWithInt:tagScore] forKey:tag];
[feedClassifiers setObject:tags forKey:@"tags"];
[appDelegate.activeClassifiers setObject:feedClassifiers forKey:feedId];
[appDelegate.storyPageControl refreshHeaders];
NSString *urlString = [NSString stringWithFormat:@"http://%@/classifier/save",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:tag
forKey:tagScore >= 1 ? @"like_tag" :
tagScore <= -1 ? @"dislike_tag" :
@"remove_like_tag"];
[request setPostValue:feedId forKey:@"feed_id"];
[request setDidFinishSelector:@selector(finishTrain:)];
[request setDidFailSelector:@selector(requestFailed:)];
[request setDelegate:self];
[request startAsynchronous];
[appDelegate recalculateIntelligenceScores:feedId];
[appDelegate.feedDetailViewController.storyTitlesTable reloadData];
}
- (void)finishTrain:(ASIHTTPRequest *)request {
id feedId = [self.activeStory objectForKey:@"story_feed_id"];
[appDelegate.feedsViewController refreshFeedList:feedId];
}
- (void)refreshHeader {
NSString *headerString = [[[self getHeader] stringByReplacingOccurrencesOfString:@"\'" withString:@"\\'"]
stringByReplacingOccurrencesOfString:@"\n" withString:@" "];

View file

@ -842,7 +842,7 @@
if ([self.popoverController respondsToSelector:@selector(setContainerViewProperties:)]) {
[self.popoverController setContainerViewProperties:[self improvedContainerViewProperties]];
}
[self.popoverController setPopoverContentSize:CGSizeMake(240, 226)];
[self.popoverController setPopoverContentSize:CGSizeMake(240, 38*7-2)];
[self.popoverController presentPopoverFromBarButtonItem:self.fontSettingsButton
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];

View file

@ -0,0 +1,54 @@
//
// TrainerViewController.h
// NewsBlur
//
// Created by Samuel Clay on 12/24/12.
// Copyright (c) 2012 NewsBlur. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "BaseViewController.h"
#import "NewsBlurAppDelegate.h"
@interface TrainerWebView : UIWebView {}
- (void)focusTitle:(id)sender;
- (void)hideTitle:(id)sender;
@end
@interface TrainerViewController : BaseViewController
<UIWebViewDelegate> {
NewsBlurAppDelegate *appDelegate;
IBOutlet UIBarButtonItem * closeButton;
TrainerWebView *webView;
IBOutlet UINavigationBar *navBar;
BOOL feedTrainer;
BOOL storyTrainer;
}
@property (nonatomic) IBOutlet NewsBlurAppDelegate *appDelegate;
@property (nonatomic) IBOutlet UIBarButtonItem *closeButton;
@property (nonatomic) IBOutlet TrainerWebView *webView;
@property (nonatomic) IBOutlet UINavigationBar *navBar;
@property (nonatomic, assign) BOOL feedTrainer;
@property (nonatomic, assign) BOOL storyTrainer;
- (void)refresh;
- (NSString *)makeTrainerHTML;
- (NSString *)makeTrainerSections;
- (NSString *)makeStoryAuthor;
- (NSString *)makeFeedAuthors;
- (NSString *)makeStoryTags;
- (NSString *)makeFeedTags;
- (NSString *)makePublisher;
- (NSString *)makeTitle;
- (NSString *)makeClassifier:(NSString *)classifierName withType:(NSString *)classifierType score:(int)score;
- (IBAction)doCloseDialog:(id)sender;
@end

View file

@ -0,0 +1,480 @@
//
// TrainerViewController.m
// NewsBlur
//
// Created by Samuel Clay on 12/24/12.
// Copyright (c) 2012 NewsBlur. All rights reserved.
//
#import "TrainerViewController.h"
#import "NBContainerViewController.h"
#import "StringHelper.h"
@implementation TrainerViewController
@synthesize closeButton;
@synthesize webView;
@synthesize navBar;
@synthesize appDelegate;
@synthesize feedTrainer;
@synthesize storyTrainer;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
navBar.tintColor = UIColorFromRGB(0x183353);
[self hideGradientBackground:webView];
[self.webView.scrollView setDelaysContentTouches:YES];
[self.webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
}
- (void) hideGradientBackground:(UIView*)theView
{
for (UIView * subview in theView.subviews)
{
if ([subview isKindOfClass:[UIImageView class]])
subview.hidden = YES;
[self hideGradientBackground:subview];
}
}
- (void)viewWillAppear:(BOOL)animated {
[[UIMenuController sharedMenuController]
setMenuItems:[NSArray arrayWithObjects:
[[UIMenuItem alloc] initWithTitle:@"👎 Hide" action:@selector(hideTitle:)],
[[UIMenuItem alloc] initWithTitle:@"👍 Focus" action:@selector(focusTitle:)],
nil]];
UILabel *titleLabel = (UILabel *)[appDelegate makeFeedTitle:appDelegate.activeFeed];
titleLabel.shadowColor = UIColorFromRGB(0x306070);
navBar.topItem.titleView = titleLabel;
NSString *path = [[NSBundle mainBundle] bundlePath];
NSURL *baseURL = [NSURL fileURLWithPath:path];
[self.webView loadHTMLString:[self makeTrainerHTML] baseURL:baseURL];
}
- (void)refresh {
if (self.view.hidden || self.view.superview == nil) {
NSLog(@"Trainer hidden, ignoring redraw.");
return;
}
NSString *headerString = [[[self makeTrainerSections]
stringByReplacingOccurrencesOfString:@"\'" withString:@"\\'"]
stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
NSString *jsString = [NSString stringWithFormat:@"document.getElementById('NB-trainer').innerHTML = '%@';",
headerString];
[self.webView stringByEvaluatingJavaScriptFromString:jsString];
[self.webView stringByEvaluatingJavaScriptFromString:@"attachFastClick({skipEvent: true});"];
}
- (void)viewDidAppear:(BOOL)animated {
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.webView loadHTMLString:@"about:blank" baseURL:nil];
[[UIMenuController sharedMenuController] setMenuItems:nil];
}
#pragma mark -
#pragma mark Story layout
- (NSString *)makeTrainerHTML {
NSString *trainerSections = [self makeTrainerSections];
int contentWidth = self.view.frame.size.width;
NSString *contentWidthClass;
if (contentWidth > 700) {
contentWidthClass = @"NB-ipad-wide";
} else if (contentWidth > 480) {
contentWidthClass = @"NB-ipad-narrow";
} else {
contentWidthClass = @"NB-iphone";
}
// set up layout values based on iPad/iPhone
NSString *headerString = [NSString stringWithFormat:@
"<link rel=\"stylesheet\" type=\"text/css\" href=\"trainer.css\" >"
"<meta name=\"viewport\" id=\"viewport\" content=\"width=%i, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"/>",
contentWidth];
NSString *footerString = [NSString stringWithFormat:@
"<script src=\"zepto.js\"></script>"
"<script src=\"trainer.js\"></script>"
"<script src=\"fastTouch.js\"></script>"];
NSString *htmlString = [NSString stringWithFormat:@
"<html>"
"<head>%@</head>" // header string
"<body id=\"trainer\" class=\"%@\">"
"<div class=\"NB-trainer\" id=\"NB-trainer\">%@</div>"
"%@" // footer
"</body>"
"</html>",
headerString,
contentWidthClass,
trainerSections,
footerString
];
// NSLog(@"\n\n\n\nhtmlString:\n\n\n%@\n\n\n", htmlString);
return htmlString;
}
- (NSString *)makeTrainerSections {
NSString *storyAuthor = self.feedTrainer ? [self makeFeedAuthors] : [self makeStoryAuthor];
NSString *storyTags = self.feedTrainer ? [self makeFeedTags] : [self makeStoryTags];
NSString *storyTitle = self.feedTrainer ? @"" : [self makeTitle];
NSString *storyPublisher = [self makePublisher];
NSString *htmlString = [NSString stringWithFormat:@
"<div class=\"NB-trainer-inner\">"
" <div class=\"NB-trainer-title NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-author NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-tags NB-trainer-section\">%@</div>"
" <div class=\"NB-trainer-publisher NB-trainer-section\">%@</div>"
"</div>",
storyTitle,
storyAuthor,
storyTags,
storyPublisher];
return htmlString;
}
- (NSString *)makeStoryAuthor {
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
objectForKey:@"story_feed_id"]];
NSString *storyAuthor = @"";
if ([[appDelegate.activeStory objectForKey:@"story_authors"] class] != [NSNull class] &&
[[appDelegate.activeStory objectForKey:@"story_authors"] length]) {
NSString *author = [NSString stringWithFormat:@"%@",
[appDelegate.activeStory objectForKey:@"story_authors"]];
if (author && [author class] != [NSNull class]) {
int authorScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
objectForKey:@"authors"]
objectForKey:author] intValue];
storyAuthor = [NSString stringWithFormat:@"<div class=\"NB-trainer-section-inner\">"
" <div class=\"NB-trainer-section-title\">Story Authors</div>"
" <div class=\"NB-trainer-section-body\">"
" <a href=\"http://ios.newsblur.com/classify-author/%@\" "
" class=\"NB-story-author %@\">%@</a>"
" </div>"
"</div>",
author,
authorScore > 0 ? @"NB-story-author-positive" : authorScore < 0 ? @"NB-story-author-negative" : @"",
[self makeClassifier:author withType:@"Author" score:authorScore]];
}
}
return storyAuthor;
}
- (NSString *)makeFeedAuthors {
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed objectForKey:@"id"]];
NSString *feedAuthors = @"";
NSArray *authorArray = appDelegate.activePopularAuthors;
if ([authorArray count] > 0) {
NSMutableArray *authorStrings = [NSMutableArray array];
for (NSArray *authorObj in authorArray) {
NSString *author = [authorObj objectAtIndex:0];
int authorCount = [[authorObj objectAtIndex:1] intValue];
int authorScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
objectForKey:@"authors"]
objectForKey:author] intValue];
NSString *authorHtml = [NSString stringWithFormat:@"<div class=\"NB-classifier-container\">"
" <a href=\"http://ios.newsblur.com/classify-author/%@\" "
" class=\"NB-story-author %@\">%@</a>"
" <span class=\"NB-classifier-count\">&times;&nbsp; %d</span>"
"</div>",
author,
authorScore > 0 ? @"NB-story-author-positive" : authorScore < 0 ? @"NB-story-author-negative" : @"",
[self makeClassifier:author withType:@"author" score:authorScore],
authorCount];
[authorStrings addObject:authorHtml];
}
feedAuthors = [NSString
stringWithFormat:@"<div class=\"NB-trainer-section-inner\">"
" <div class=\"NB-trainer-section-title\">Authors</div>"
" <div class=\"NB-trainer-section-body\">"
" <div class=\"NB-story-authors\">"
" %@"
" </div>"
" </div>"
"</div>",
[authorStrings componentsJoinedByString:@""]];
}
return feedAuthors;
}
- (NSString *)makeStoryTags {
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
objectForKey:@"story_feed_id"]];
NSString *storyTags = @"";
if ([appDelegate.activeStory objectForKey:@"story_tags"]) {
NSArray *tagArray = [appDelegate.activeStory objectForKey:@"story_tags"];
if ([tagArray count] > 0) {
NSMutableArray *tagStrings = [NSMutableArray array];
for (NSString *tag in tagArray) {
int tagScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
objectForKey:@"tags"]
objectForKey:tag] intValue];
NSString *tagHtml = [NSString stringWithFormat:@"<div class=\"NB-classifier-container\">"
" <a href=\"http://ios.newsblur.com/classify-tag/%@\" "
" class=\"NB-story-tag %@\">%@</a>"
"</div>",
tag,
tagScore > 0 ? @"NB-story-tag-positive" : tagScore < 0 ? @"NB-story-tag-negative" : @"",
[self makeClassifier:tag withType:@"Tag" score:tagScore]];
[tagStrings addObject:tagHtml];
}
storyTags = [NSString
stringWithFormat:@"<div class=\"NB-trainer-section-inner\">"
" <div class=\"NB-trainer-section-title\">Story Tags</div>"
" <div class=\"NB-trainer-section-body\">"
" <div class=\"NB-story-tags\">"
" %@"
" </div>"
" </div>"
"</div>",
[tagStrings componentsJoinedByString:@""]];
}
}
return storyTags;
}
- (NSString *)makeFeedTags {
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed objectForKey:@"id"]];
NSString *feedTags = @"";
NSArray *tagArray = appDelegate.activePopularTags;
if ([tagArray count] > 0) {
NSMutableArray *tagStrings = [NSMutableArray array];
for (NSArray *tagObj in tagArray) {
NSString *tag = [tagObj objectAtIndex:0];
int tagCount = [[tagObj objectAtIndex:1] intValue];
int tagScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
objectForKey:@"tags"]
objectForKey:tag] intValue];
NSString *tagHtml = [NSString stringWithFormat:@"<div class=\"NB-classifier-container\">"
" <a href=\"http://ios.newsblur.com/classify-tag/%@\" "
" class=\"NB-story-tag %@\">%@</a>"
" <span class=\"NB-classifier-count\">&times;&nbsp; %d</span>"
"</div>",
tag,
tagScore > 0 ? @"NB-story-tag-positive" : tagScore < 0 ? @"NB-story-tag-negative" : @"",
[self makeClassifier:tag withType:@"Tag" score:tagScore],
tagCount];
[tagStrings addObject:tagHtml];
}
feedTags = [NSString
stringWithFormat:@"<div class=\"NB-trainer-section-inner\">"
" <div class=\"NB-trainer-section-title\">Story Tags</div>"
" <div class=\"NB-trainer-section-body\">"
" <div class=\"NB-story-tags\">"
" %@"
" </div>"
" </div>"
"</div>",
[tagStrings componentsJoinedByString:@""]];
}
return feedTags;
}
- (NSString *)makePublisher {
NSString *feedId;
NSString *feedTitle;
if (self.feedTrainer) {
feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed objectForKey:@"id"]];
feedTitle = [appDelegate.activeFeed objectForKey:@"feed_title"];
} else {
feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
objectForKey:@"story_feed_id"]];
NSDictionary *feed = [appDelegate getFeed:feedId];
feedTitle = [feed objectForKey:@"feed_title"];
}
int publisherScore = [[[[appDelegate.activeClassifiers objectForKey:feedId]
objectForKey:@"feeds"] objectForKey:feedId] intValue];
NSString *storyPublisher = [NSString stringWithFormat:@"<div class=\"NB-trainer-section-inner\">"
" <div class=\"NB-trainer-section-title\">Publisher</div>"
" <div class=\"NB-trainer-section-body\">"
" <div class=\"NB-classifier-container\">"
" <a href=\"http://ios.newsblur.com/classify-feed/%@\" "
" class=\"NB-story-publisher NB-story-publisher-%@\">%@</a>"
" </div>"
" </div>"
"</div",
feedId,
publisherScore > 0 ? @"positive" : publisherScore < 0 ? @"negative" : @"",
[self makeClassifier:feedTitle withType:@"publisher" score:publisherScore]];
return storyPublisher;
}
- (NSString *)makeTitle {
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
objectForKey:@"story_feed_id"]];
NSString *storyTitle = [appDelegate.activeStory objectForKey:@"story_title"];
if (!storyTitle) {
return @"";
}
NSMutableDictionary *classifiers = [[appDelegate.activeClassifiers objectForKey:feedId]
objectForKey:@"titles"];
NSMutableArray *titleStrings = [NSMutableArray array];
for (NSString *title in classifiers) {
if ([storyTitle containsString:title]) {
int titleScore = [[classifiers objectForKey:title] intValue];
NSString *titleClassifier = [NSString stringWithFormat:@
"<div class=\"NB-classifier-container\">"
" <a href=\"http://ios.newsblur.com/classify-title/%@\" "
" class=\"NB-story-title NB-story-title-%@\">%@</a>"
"</div>",
title,
titleScore > 0 ? @"positive" : titleScore < 0 ? @"negative" : @"",
[self makeClassifier:title withType:@"title" score:titleScore]];
[titleStrings addObject:titleClassifier];
}
}
NSString *titleClassifiers;
if ([titleStrings count]) {
titleClassifiers = [titleStrings componentsJoinedByString:@""];
} else {
titleClassifiers = @"<div class=\"NB-title-info\">Tap and hold the title</div>";
}
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>"
"</div>", storyTitle, titleClassifiers];
return titleTrainer;
}
- (NSString *)makeClassifier:(NSString *)classifierName withType:(NSString *)classifierType score:(int)score {
NSString *classifier = [NSString stringWithFormat:@"<span class=\"NB-classifier NB-classifier-%@ NB-classifier-%@\">"
"<div class=\"NB-classifier-icon-like\"></div>"
"<div class=\"NB-classifier-icon-dislike\">"
" <div class=\"NB-classifier-icon-dislike-inner\"></div>"
"</div>"
"<label><b>%@: </b><span>%@</span></label>"
"</span>",
classifierType,
score > 0 ? @"like" : score < 0 ? @"dislike" : @"",
classifierType,
classifierName];
return classifier;
}
#pragma mark -
#pragma mark Actions
- (IBAction)doCloseDialog:(id)sender {
[appDelegate.masterContainerViewController hidePopover];
[appDelegate.trainerViewController dismissModalViewControllerAnimated:YES];
}
- (void)changeTitle:(id)sender score:(int)score {
NSString *feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
objectForKey:@"story_feed_id"]];
NSString *selectedTitle = [self.webView
stringByEvaluatingJavaScriptFromString:@"window.getSelection().toString()"];
[self.appDelegate toggleTitleClassifier:selectedTitle feedId:feedId score:score];
}
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];
NSArray *urlComponents = [url pathComponents];
NSString *action = @"";
NSString *feedId;
if (appDelegate.isSocialView || appDelegate.isSocialRiverView) {
feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeStory
objectForKey:@"story_feed_id"]];
} else {
feedId = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed
objectForKey:@"id"]];
}
if ([urlComponents count] > 1) {
action = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:1]];
}
NSLog(@"Tapped url: %@", url);
if ([[url host] isEqualToString: @"ios.newsblur.com"]){
if ([action isEqualToString:@"classify-author"]) {
NSString *author = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
[self.appDelegate toggleAuthorClassifier:author feedId:feedId];
return NO;
} else if ([action isEqualToString:@"classify-tag"]) {
NSString *tag = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
[self.appDelegate toggleTagClassifier:tag feedId:feedId];
return NO;
} else if ([action isEqualToString:@"classify-title"]) {
NSString *title = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
[self.appDelegate toggleTitleClassifier:title feedId:feedId score:0];
return NO;
} else if ([action isEqualToString:@"classify-feed"]) {
NSString *feedId = [NSString stringWithFormat:@"%@", [urlComponents objectAtIndex:2]];
[self.appDelegate toggleFeedClassifier:feedId];
return NO;
}
}
return YES;
}
@end
@implementation TrainerWebView
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == @selector(focusTitle:) || action == @selector(hideTitle:)) {
return YES;
} else {
return NO;
}
}
- (void)focusTitle:(id)sender {
NewsBlurAppDelegate *appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
[appDelegate.trainerViewController changeTitle:sender score:1];
}
- (void)hideTitle:(id)sender {
NewsBlurAppDelegate *appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
[appDelegate.trainerViewController changeTitle:sender score:-1];
}
@end

Binary file not shown.

View file

@ -51,9 +51,6 @@
436ACA8D15BF1088004E01CC /* NBContainerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 436ACA8C15BF1088004E01CC /* NBContainerViewController.m */; };
43763AD1158F90B100B3DBE2 /* FontSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43763ACF158F90B100B3DBE2 /* FontSettingsViewController.m */; };
43763AD2158F90B100B3DBE2 /* FontSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 43763AD0158F90B100B3DBE2 /* FontSettingsViewController.xib */; };
437AA8C015929DFA005463F5 /* storyDetailView.css in Resources */ = {isa = PBXBuildFile; fileRef = 437AA8BF15929DFA005463F5 /* storyDetailView.css */; };
437AA8C115929FFF005463F5 /* zepto.js in Resources */ = {isa = PBXBuildFile; fileRef = 437AA8B815929D51005463F5 /* zepto.js */; };
437AA8C215929FFF005463F5 /* storyDetailView.js in Resources */ = {isa = PBXBuildFile; fileRef = 437AA8BD15929DA7005463F5 /* storyDetailView.js */; };
437AA8CA159394E2005463F5 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 437AA8C8159394E2005463F5 /* ShareViewController.m */; };
437AA8CB159394E2005463F5 /* ShareViewController~ipad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 437AA8C9159394E2005463F5 /* ShareViewController~ipad.xib */; };
437F974C15ACA0ED0007136B /* DashboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 437F974A15ACA0ED0007136B /* DashboardViewController.m */; };
@ -264,7 +261,6 @@
43E8382115BC73EB000553BE /* FirstTimeUserAddNewsBlurViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 43E8381A15BC73EB000553BE /* FirstTimeUserAddNewsBlurViewController.xib */; };
43E8382215BC73EB000553BE /* FirstTimeUserAddSitesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 43E8381C15BC73EB000553BE /* FirstTimeUserAddSitesViewController.m */; };
43E8382315BC73EB000553BE /* FirstTimeUserAddSitesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 43E8381D15BC73EB000553BE /* FirstTimeUserAddSitesViewController.xib */; };
43E8856015951F930032022E /* reader.css in Resources */ = {isa = PBXBuildFile; fileRef = 43E8855F15951F930032022E /* reader.css */; };
43F44B1C159D8DBC00F48F8A /* FeedTableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F44B1B159D8D7200F48F8A /* FeedTableCell.m */; };
43F6A79915B0C57F0092EE91 /* flag_orange.png in Resources */ = {isa = PBXBuildFile; fileRef = 43F6A79815B0C57F0092EE91 /* flag_orange.png */; };
43F6A79D15B0CDC60092EE91 /* ActivityCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 43F6A79C15B0CDC60092EE91 /* ActivityCell.m */; };
@ -372,6 +368,17 @@
FF5F3A87162B834E008DBE3E /* bin_closed.png in Resources */ = {isa = PBXBuildFile; fileRef = FF5F3A86162B834E008DBE3E /* bin_closed.png */; };
FF5F3A89162B8377008DBE3E /* arrow_branch.png in Resources */ = {isa = PBXBuildFile; fileRef = FF5F3A88162B8377008DBE3E /* arrow_branch.png */; };
FF5F3A8B162B8390008DBE3E /* car.png in Resources */ = {isa = PBXBuildFile; fileRef = FF5F3A8A162B8390008DBE3E /* car.png */; };
FF67D3B2168924C40057A7DA /* TrainerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FF67D3B1168924C40057A7DA /* TrainerViewController.m */; };
FF67D3B51689746B0057A7DA /* bricks.png in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3B41689746B0057A7DA /* bricks.png */; };
FF67D3B7168977690057A7DA /* TrainerViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3B6168977690057A7DA /* TrainerViewController.xib */; };
FF67D3B916897AD80057A7DA /* TrainerViewController~ipad.xib in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3B816897AD80057A7DA /* TrainerViewController~ipad.xib */; };
FF67D3BB168A70630057A7DA /* trainer.css in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3BA168A70630057A7DA /* trainer.css */; };
FF67D3C0168A708D0057A7DA /* reader.css in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3BC168A708D0057A7DA /* reader.css */; };
FF67D3C1168A708D0057A7DA /* storyDetailView.css in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3BD168A708D0057A7DA /* storyDetailView.css */; };
FF67D3CC168A73380057A7DA /* storyDetailView.js in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3BE168A708D0057A7DA /* storyDetailView.js */; };
FF67D3CD168A73380057A7DA /* zepto.js in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3BF168A708D0057A7DA /* zepto.js */; };
FF67D3CE168A73380057A7DA /* trainer.js in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3C4168A71870057A7DA /* trainer.js */; };
FF67D3CF168A73380057A7DA /* fastTouch.js in Resources */ = {isa = PBXBuildFile; fileRef = FF67D3C6168A71B30057A7DA /* fastTouch.js */; };
FF6A233216448E0700E15989 /* StoryPageControl.m in Sources */ = {isa = PBXBuildFile; fileRef = FF6A233116448E0700E15989 /* StoryPageControl.m */; };
FF6A23391644957800E15989 /* StoryPageControl.xib in Resources */ = {isa = PBXBuildFile; fileRef = FF6A23361644903900E15989 /* StoryPageControl.xib */; };
FF793E1B13F1A9F700F282D2 /* ASIDataCompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = FF793E1813F1A9F700F282D2 /* ASIDataCompressor.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; };
@ -439,9 +446,6 @@
43763ACE158F90B100B3DBE2 /* FontSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FontSettingsViewController.h; sourceTree = "<group>"; };
43763ACF158F90B100B3DBE2 /* FontSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FontSettingsViewController.m; sourceTree = "<group>"; };
43763AD0158F90B100B3DBE2 /* FontSettingsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = FontSettingsViewController.xib; path = ../Classes/FontSettingsViewController.xib; sourceTree = "<group>"; };
437AA8B815929D51005463F5 /* zepto.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = zepto.js; path = ../zepto.js; sourceTree = "<group>"; };
437AA8BD15929DA7005463F5 /* storyDetailView.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = storyDetailView.js; path = ../storyDetailView.js; sourceTree = "<group>"; };
437AA8BF15929DFA005463F5 /* storyDetailView.css */ = {isa = PBXFileReference; explicitFileType = text.css; fileEncoding = 4; name = storyDetailView.css; path = ../storyDetailView.css; sourceTree = "<group>"; };
437AA8C7159394E2005463F5 /* ShareViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareViewController.h; sourceTree = "<group>"; };
437AA8C8159394E2005463F5 /* ShareViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = "<group>"; };
437AA8C9159394E2005463F5 /* ShareViewController~ipad.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = "ShareViewController~ipad.xib"; path = "Classes/ShareViewController~ipad.xib"; sourceTree = "<group>"; };
@ -700,7 +704,6 @@
43E8381B15BC73EB000553BE /* FirstTimeUserAddSitesViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FirstTimeUserAddSitesViewController.h; sourceTree = "<group>"; };
43E8381C15BC73EB000553BE /* FirstTimeUserAddSitesViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FirstTimeUserAddSitesViewController.m; sourceTree = "<group>"; };
43E8381D15BC73EB000553BE /* FirstTimeUserAddSitesViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FirstTimeUserAddSitesViewController.xib; sourceTree = "<group>"; };
43E8855F15951F930032022E /* reader.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = reader.css; path = ../reader.css; sourceTree = "<group>"; };
43F44B1A159D8D7200F48F8A /* FeedTableCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FeedTableCell.h; sourceTree = "<group>"; };
43F44B1B159D8D7200F48F8A /* FeedTableCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FeedTableCell.m; sourceTree = "<group>"; };
43F6A79815B0C57F0092EE91 /* flag_orange.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = flag_orange.png; sourceTree = "<group>"; };
@ -903,6 +906,18 @@
FF5F3A86162B834E008DBE3E /* bin_closed.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bin_closed.png; sourceTree = "<group>"; };
FF5F3A88162B8377008DBE3E /* arrow_branch.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = arrow_branch.png; sourceTree = "<group>"; };
FF5F3A8A162B8390008DBE3E /* car.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = car.png; sourceTree = "<group>"; };
FF67D3B0168924C40057A7DA /* TrainerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TrainerViewController.h; sourceTree = "<group>"; };
FF67D3B1168924C40057A7DA /* TrainerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TrainerViewController.m; sourceTree = "<group>"; };
FF67D3B41689746B0057A7DA /* bricks.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bricks.png; sourceTree = "<group>"; };
FF67D3B6168977690057A7DA /* TrainerViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TrainerViewController.xib; sourceTree = "<group>"; };
FF67D3B816897AD80057A7DA /* TrainerViewController~ipad.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = "TrainerViewController~ipad.xib"; path = "Resources-iPad/Classes/TrainerViewController~ipad.xib"; sourceTree = "<group>"; };
FF67D3BA168A70630057A7DA /* trainer.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = trainer.css; path = ../static/trainer.css; sourceTree = "<group>"; };
FF67D3BC168A708D0057A7DA /* reader.css */ = {isa = PBXFileReference; explicitFileType = text.css; fileEncoding = 4; name = reader.css; path = ../static/reader.css; sourceTree = "<group>"; };
FF67D3BD168A708D0057A7DA /* storyDetailView.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = storyDetailView.css; path = ../static/storyDetailView.css; sourceTree = "<group>"; };
FF67D3BE168A708D0057A7DA /* storyDetailView.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = storyDetailView.js; path = ../static/storyDetailView.js; sourceTree = "<group>"; };
FF67D3BF168A708D0057A7DA /* zepto.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = zepto.js; path = ../static/zepto.js; sourceTree = "<group>"; };
FF67D3C4168A71870057A7DA /* trainer.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = trainer.js; path = ../static/trainer.js; sourceTree = "<group>"; };
FF67D3C6168A71B30057A7DA /* fastTouch.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = fastTouch.js; path = ../static/fastTouch.js; sourceTree = "<group>"; };
FF6A233016448E0700E15989 /* StoryPageControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StoryPageControl.h; sourceTree = "<group>"; };
FF6A233116448E0700E15989 /* StoryPageControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StoryPageControl.m; sourceTree = "<group>"; };
FF6A23361644903900E15989 /* StoryPageControl.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = StoryPageControl.xib; path = ../Classes/StoryPageControl.xib; sourceTree = "<group>"; };
@ -1181,6 +1196,8 @@
43763ACF158F90B100B3DBE2 /* FontSettingsViewController.m */,
78095EC6128F30B500230C8E /* OriginalStoryViewController.h */,
78095EC7128F30B500230C8E /* OriginalStoryViewController.m */,
FF67D3B0168924C40057A7DA /* TrainerViewController.h */,
FF67D3B1168924C40057A7DA /* TrainerViewController.m */,
);
name = Story;
sourceTree = "<group>";
@ -1367,8 +1384,9 @@
431B857715A132BE00DCE497 /* css */ = {
isa = PBXGroup;
children = (
437AA8BF15929DFA005463F5 /* storyDetailView.css */,
43E8855F15951F930032022E /* reader.css */,
FF67D3BC168A708D0057A7DA /* reader.css */,
FF67D3BD168A708D0057A7DA /* storyDetailView.css */,
FF67D3BA168A70630057A7DA /* trainer.css */,
);
name = css;
sourceTree = "<group>";
@ -1376,8 +1394,10 @@
431B857815A132C500DCE497 /* js */ = {
isa = PBXGroup;
children = (
437AA8B815929D51005463F5 /* zepto.js */,
437AA8BD15929DA7005463F5 /* storyDetailView.js */,
FF67D3BE168A708D0057A7DA /* storyDetailView.js */,
FF67D3BF168A708D0057A7DA /* zepto.js */,
FF67D3C6168A71B30057A7DA /* fastTouch.js */,
FF67D3C4168A71870057A7DA /* trainer.js */,
);
name = js;
sourceTree = "<group>";
@ -1385,6 +1405,7 @@
433323B5158901A40025064D /* Images */ = {
isa = PBXGroup;
children = (
FF67D3B41689746B0057A7DA /* bricks.png */,
432EBD1415D1A7800000729D /* user_on.png */,
432EBD1515D1A7800000729D /* user_on@2x.png */,
432EBD0C15D1A2B00000729D /* fountain_pen_on.png */,
@ -1479,6 +1500,7 @@
43081E2115AFE84200B24D7A /* ShareViewController.xib */,
43B8F021156603170008733D /* StoryDetailViewController.xib */,
FF6A23361644903900E15989 /* StoryPageControl.xib */,
FF67D3B6168977690057A7DA /* TrainerViewController.xib */,
43A4BAEA15C893E300F3B8D4 /* FriendsListViewController.xib */,
);
path = "Resources-iPhone";
@ -1493,6 +1515,7 @@
43D045241565BC150085F811 /* LoginViewController~ipad.xib */,
43D0451F1565BC150085F811 /* MainWindow~ipad.xib */,
43D045281565BC150085F811 /* MoveSiteViewController~ipad.xib */,
FF67D3B816897AD80057A7DA /* TrainerViewController~ipad.xib */,
43D045251565BC150085F811 /* OriginalStoryViewController~ipad.xib */,
43D045271565BC150085F811 /* AddSiteAutocompleteCell~ipad.xib */,
43D045221565BC150085F811 /* StoryDetailViewController~ipad.xib */,
@ -1962,11 +1985,13 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FF67D3CC168A73380057A7DA /* storyDetailView.js in Resources */,
FF67D3CD168A73380057A7DA /* zepto.js in Resources */,
FF67D3CE168A73380057A7DA /* trainer.js in Resources */,
FF67D3CF168A73380057A7DA /* fastTouch.js in Resources */,
FF2D8C341487EFF000057B80 /* SHKSharers.plist in Resources */,
FF2D8C3B1487EFF000057B80 /* ShareKit.bundle in Resources */,
FF2D8C8E1487F11500057B80 /* FBDialog.bundle in Resources */,
437AA8C115929FFF005463F5 /* zepto.js in Resources */,
437AA8C215929FFF005463F5 /* storyDetailView.js in Resources */,
43B8F022156603180008733D /* AddSiteAutocompleteCell.xib in Resources */,
43B8F023156603180008733D /* AddSiteViewController.xib in Resources */,
43B8F025156603180008733D /* FeedDetailViewController.xib in Resources */,
@ -1997,9 +2022,7 @@
43C95C4B158BF8810086C69B /* google_reader_selected_background.png in Resources */,
43763AD2158F90B100B3DBE2 /* FontSettingsViewController.xib in Resources */,
439DAB211590DA350019B0EB /* FeedsMenuViewController.xib in Resources */,
437AA8C015929DFA005463F5 /* storyDetailView.css in Resources */,
437AA8CB159394E2005463F5 /* ShareViewController~ipad.xib in Resources */,
43E8856015951F930032022E /* reader.css in Resources */,
437F974D15ACA0ED0007136B /* DashboardViewController.xib in Resources */,
43081E2215AFE84200B24D7A /* ShareViewController.xib in Resources */,
43A4C44215B00A26008787B5 /* 06-arrow-south.png in Resources */,
@ -2191,6 +2214,12 @@
FF4130AB162F3C2F00DDB6A7 /* archive_white.png in Resources */,
FF6A23391644957800E15989 /* StoryPageControl.xib in Resources */,
FFAD0BE71649EC1800EE33D0 /* StoryPageControl~ipad.xib in Resources */,
FF67D3B51689746B0057A7DA /* bricks.png in Resources */,
FF67D3B7168977690057A7DA /* TrainerViewController.xib in Resources */,
FF67D3B916897AD80057A7DA /* TrainerViewController~ipad.xib in Resources */,
FF67D3BB168A70630057A7DA /* trainer.css in Resources */,
FF67D3C1168A708D0057A7DA /* storyDetailView.css in Resources */,
FF67D3C0168A708D0057A7DA /* reader.css in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2341,6 +2370,7 @@
FFDE35EA162799B90034BFDE /* FeedDetailMenuViewController.m in Sources */,
FF4130A3162E10CF00DDB6A7 /* MenuTableViewCell.m in Sources */,
FF6A233216448E0700E15989 /* StoryPageControl.m in Sources */,
FF67D3B2168924C40057A7DA /* TrainerViewController.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -16,6 +16,19 @@
landmarkName = "-applyNewIndex:pageController:"
landmarkType = "5">
</FileBreakpoint>
<FileBreakpoint
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Classes/NewsBlurAppDelegate.m"
timestampString = "378407777.087073"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "683"
endingLineNumber = "683"
landmarkName = "-recalculateIntelligenceScores:"
landmarkType = "5">
</FileBreakpoint>
</FileBreakpoints>
<SymbolicBreakpoints>
<SymbolicBreakpoint

View file

@ -1,22 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<archive type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1296</int>
<string key="IBDocument.SystemVersion">11E53</string>
<string key="IBDocument.InterfaceBuilderVersion">2182</string>
<string key="IBDocument.AppKitVersion">1138.47</string>
<string key="IBDocument.HIToolboxVersion">569.00</string>
<int key="IBDocument.SystemTarget">1536</int>
<string key="IBDocument.SystemVersion">12C2037</string>
<string key="IBDocument.InterfaceBuilderVersion">2840</string>
<string key="IBDocument.AppKitVersion">1187.34</string>
<string key="IBDocument.HIToolboxVersion">625.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string key="NS.object.0">1181</string>
<string key="NS.object.0">1926</string>
</object>
<object class="NSArray" key="IBDocument.IntegratedClassDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>IBUIWebView</string>
<string>IBProxyObject</string>
<string>IBUIBarButtonItem</string>
<string>IBUIToolbar</string>
<string>IBUIView</string>
<string>IBProxyObject</string>
<string>IBUIWebView</string>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -46,7 +46,6 @@
<int key="NSvFlags">274</int>
<string key="NSFrameSize">{768, 960}</string>
<reference key="NSSuperview" ref="191373211"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="623759027"/>
<bool key="IBUIMultipleTouchEnabled">YES</bool>
<string key="targetRuntimeIdentifier">IBIPadFramework</string>
@ -59,7 +58,6 @@
<int key="NSvFlags">266</int>
<string key="NSFrame">{{0, 960}, {768, 44}}</string>
<reference key="NSSuperview" ref="191373211"/>
<reference key="NSWindow"/>
<bool key="IBUIOpaque">NO</bool>
<bool key="IBUIClearsContextBeforeDrawing">NO</bool>
<string key="targetRuntimeIdentifier">IBIPadFramework</string>
@ -113,7 +111,6 @@
</object>
<string key="NSFrame">{{0, 20}, {768, 1004}}</string>
<reference key="NSSuperview"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="812726678"/>
<object class="NSColor" key="IBUIBackgroundColor">
<int key="NSColorSpace">3</int>
@ -1448,7 +1445,7 @@
<string key="IBDocument.TargetRuntimeIdentifier">IBIPadFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string>
<real value="1296" key="NS.object.0"/>
<real value="1536" key="NS.object.0"/>
</object>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3</string>
@ -1469,6 +1466,6 @@
<string>{16, 24}</string>
</object>
</object>
<string key="IBCocoaTouchPluginVersion">1181</string>
<string key="IBCocoaTouchPluginVersion">1926</string>
</data>
</archive>

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

View file

@ -2,7 +2,7 @@
<archive type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1536</int>
<string key="IBDocument.SystemVersion">12C60</string>
<string key="IBDocument.SystemVersion">12C2037</string>
<string key="IBDocument.InterfaceBuilderVersion">2840</string>
<string key="IBDocument.AppKitVersion">1187.34</string>
<string key="IBDocument.HIToolboxVersion">625.00</string>
@ -201,6 +201,18 @@
<string key="targetRuntimeIdentifier">IBIPadFramework</string>
<bool key="IBUIHorizontal">NO</bool>
</object>
<object class="IBUIViewController" id="1050382266">
<string key="IBUINibName">TrainerViewController~ipad</string>
<object class="IBUISimulatedStatusBarMetrics" key="IBUISimulatedStatusBarMetrics">
<int key="IBUIStatusBarStyle">2</int>
</object>
<object class="IBUISimulatedOrientationMetrics" key="IBUISimulatedOrientationMetrics">
<int key="IBUIInterfaceOrientation">1</int>
<int key="interfaceOrientation">1</int>
</object>
<string key="targetRuntimeIdentifier">IBIPadFramework</string>
<bool key="IBUIHorizontal">NO</bool>
</object>
<object class="IBUIViewController" id="943309135">
<string key="IBUINibName">NewsBlurViewController</string>
<object class="IBUISimulatedNavigationBarMetrics" key="IBUISimulatedTopBarMetrics">
@ -315,6 +327,7 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBUIViewController" id="813520712">
<object class="IBUINavigationItem" key="IBUINavigationItem" id="165811166">
<reference key="IBUINavigationBar"/>
<string key="IBUITitle">Title</string>
<string key="targetRuntimeIdentifier">IBIPadFramework</string>
</object>
@ -528,6 +541,14 @@
</object>
<int key="connectionID">285</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">trainerViewController</string>
<reference key="source" ref="664661524"/>
<reference key="destination" ref="1050382266"/>
</object>
<int key="connectionID">289</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">appDelegate</string>
@ -696,6 +717,14 @@
</object>
<int key="connectionID">283</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">appDelegate</string>
<reference key="source" ref="1050382266"/>
<reference key="destination" ref="664661524"/>
</object>
<int key="connectionID">288</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
@ -870,6 +899,12 @@
<reference key="object" ref="396232691"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">287</int>
<reference key="object" ref="1050382266"/>
<reference key="parent" ref="0"/>
<string key="objectName">Trainer View Controller</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
@ -932,6 +967,9 @@
<string>282.CustomClassName</string>
<string>282.IBLastUsedUIStatusBarStylesToTargetRuntimesMap</string>
<string>282.IBPluginDependency</string>
<string>287.CustomClassName</string>
<string>287.IBLastUsedUIStatusBarStylesToTargetRuntimesMap</string>
<string>287.IBPluginDependency</string>
<string>3.CustomClassName</string>
<string>3.IBPluginDependency</string>
<string>51.CustomClassName</string>
@ -1023,6 +1061,12 @@
<integer value="0" key="NS.object.0"/>
</object>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string>TrainerViewController</string>
<object class="NSMutableDictionary">
<string key="NS.key.0">IBCocoaTouchFramework</string>
<integer value="0" key="NS.object.0"/>
</object>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string>NewsBlurAppDelegate</string>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string>FeedDetailViewController</string>
@ -1051,7 +1095,7 @@
<reference key="dict.values" ref="0"/>
</object>
<nil key="sourceID"/>
<int key="maxID">286</int>
<int key="maxID">289</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
@ -2580,6 +2624,7 @@
<string>shareViewController</string>
<string>storyDetailViewController</string>
<string>storyPageControl</string>
<string>trainerViewController</string>
<string>userProfileViewController</string>
<string>window</string>
</object>
@ -2608,6 +2653,7 @@
<string>ShareViewController</string>
<string>StoryDetailViewController</string>
<string>StoryPageControl</string>
<string>TrainerViewController</string>
<string>UserProfileViewController</string>
<string>UIWindow</string>
</object>
@ -2639,6 +2685,7 @@
<string>shareViewController</string>
<string>storyDetailViewController</string>
<string>storyPageControl</string>
<string>trainerViewController</string>
<string>userProfileViewController</string>
<string>window</string>
</object>
@ -2736,6 +2783,10 @@
<string key="name">storyPageControl</string>
<string key="candidateClassName">StoryPageControl</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">trainerViewController</string>
<string key="candidateClassName">TrainerViewController</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">userProfileViewController</string>
<string key="candidateClassName">UserProfileViewController</string>
@ -3203,7 +3254,6 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>doNextStory</string>
<string>doNextUnreadStory</string>
<string>doPreviousStory</string>
<string>showOriginalSubview:</string>
@ -3217,14 +3267,12 @@
<string>id</string>
<string>id</string>
<string>id</string>
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>doNextStory</string>
<string>doNextUnreadStory</string>
<string>doPreviousStory</string>
<string>showOriginalSubview:</string>
@ -3233,10 +3281,6 @@
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">doNextStory</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">doNextUnreadStory</string>
<string key="candidateClassName">id</string>
@ -3268,7 +3312,6 @@
<string>bottomPlaceholderToolbar</string>
<string>buttonAction</string>
<string>buttonNext</string>
<string>buttonNextStory</string>
<string>buttonPrevious</string>
<string>fontSettingsButton</string>
<string>originalStoryButton</string>
@ -3289,7 +3332,6 @@
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIPageControl</string>
<string>UIProgressView</string>
<string>UIView</string>
@ -3307,7 +3349,6 @@
<string>bottomPlaceholderToolbar</string>
<string>buttonAction</string>
<string>buttonNext</string>
<string>buttonNextStory</string>
<string>buttonPrevious</string>
<string>fontSettingsButton</string>
<string>originalStoryButton</string>
@ -3340,10 +3381,6 @@
<string key="name">buttonNext</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">buttonNextStory</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">buttonPrevious</string>
<string key="candidateClassName">UIBarButtonItem</string>
@ -3387,6 +3424,64 @@
<string key="minorKey">./Classes/StoryPageControl.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">TrainerViewController</string>
<string key="superclassName">BaseViewController</string>
<object class="NSMutableDictionary" key="actions">
<string key="NS.key.0">doCloseDialog:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">doCloseDialog:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">doCloseDialog:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>appDelegate</string>
<string>closeButton</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NewsBlurAppDelegate</string>
<string>UIBarButtonItem</string>
<string>UIWebView</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>appDelegate</string>
<string>closeButton</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">appDelegate</string>
<string key="candidateClassName">NewsBlurAppDelegate</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">closeButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webView</string>
<string key="candidateClassName">UIWebView</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/TrainerViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">UserProfileViewController</string>
<string key="superclassName">UIViewController</string>

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1536</int>
<string key="IBDocument.SystemVersion">12C60</string>
<string key="IBDocument.SystemVersion">12C2037</string>
<string key="IBDocument.InterfaceBuilderVersion">2840</string>
<string key="IBDocument.AppKitVersion">1187.34</string>
<string key="IBDocument.HIToolboxVersion">625.00</string>
@ -154,6 +154,16 @@
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
<bool key="IBUIHorizontal">NO</bool>
</object>
<object class="IBUIViewController" id="24323153">
<string key="IBUINibName">TrainerViewController</string>
<object class="IBUISimulatedStatusBarMetrics" key="IBUISimulatedStatusBarMetrics"/>
<object class="IBUISimulatedOrientationMetrics" key="IBUISimulatedOrientationMetrics">
<int key="IBUIInterfaceOrientation">1</int>
<int key="interfaceOrientation">1</int>
</object>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
<bool key="IBUIHorizontal">NO</bool>
</object>
<object class="IBUIViewController" id="538190729">
<string key="IBUINibName">OriginalStoryViewController</string>
<object class="IBUISimulatedStatusBarMetrics" key="IBUISimulatedStatusBarMetrics"/>
@ -480,6 +490,14 @@
</object>
<int key="connectionID">173</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">trainerViewController</string>
<reference key="source" ref="664661524"/>
<reference key="destination" ref="24323153"/>
</object>
<int key="connectionID">176</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">appDelegate</string>
@ -624,6 +642,14 @@
</object>
<int key="connectionID">171</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">appDelegate</string>
<reference key="source" ref="24323153"/>
<reference key="destination" ref="664661524"/>
</object>
<int key="connectionID">175</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
@ -788,6 +814,11 @@
<reference key="object" ref="384264276"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">174</int>
<reference key="object" ref="24323153"/>
<reference key="parent" ref="0"/>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
@ -833,6 +864,8 @@
<string>165.IBPluginDependency</string>
<string>170.CustomClassName</string>
<string>170.IBPluginDependency</string>
<string>174.CustomClassName</string>
<string>174.IBPluginDependency</string>
<string>3.CustomClassName</string>
<string>3.IBPluginDependency</string>
<string>39.IBPluginDependency</string>
@ -886,6 +919,8 @@
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string>StoryPageControl</string>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string>TrainerViewController</string>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string>NewsBlurAppDelegate</string>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
@ -911,7 +946,7 @@
<reference key="dict.values" ref="0"/>
</object>
<nil key="sourceID"/>
<int key="maxID">173</int>
<int key="maxID">176</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
@ -2440,6 +2475,7 @@
<string>shareViewController</string>
<string>storyDetailViewController</string>
<string>storyPageControl</string>
<string>trainerViewController</string>
<string>userProfileViewController</string>
<string>window</string>
</object>
@ -2468,6 +2504,7 @@
<string>ShareViewController</string>
<string>StoryDetailViewController</string>
<string>StoryPageControl</string>
<string>TrainerViewController</string>
<string>UserProfileViewController</string>
<string>UIWindow</string>
</object>
@ -2499,6 +2536,7 @@
<string>shareViewController</string>
<string>storyDetailViewController</string>
<string>storyPageControl</string>
<string>trainerViewController</string>
<string>userProfileViewController</string>
<string>window</string>
</object>
@ -2596,6 +2634,10 @@
<string key="name">storyPageControl</string>
<string key="candidateClassName">StoryPageControl</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">trainerViewController</string>
<string key="candidateClassName">TrainerViewController</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">userProfileViewController</string>
<string key="candidateClassName">UserProfileViewController</string>
@ -2998,11 +3040,71 @@
<object class="IBPartialClassDescription">
<string key="className">StoryDetailViewController</string>
<string key="superclassName">BaseViewController</string>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>appDelegate</string>
<string>feedTitleGradient</string>
<string>innerView</string>
<string>noStorySelectedLabel</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NewsBlurAppDelegate</string>
<string>UIView</string>
<string>UIView</string>
<string>UILabel</string>
<string>UIWebView</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>appDelegate</string>
<string>feedTitleGradient</string>
<string>innerView</string>
<string>noStorySelectedLabel</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">appDelegate</string>
<string key="candidateClassName">NewsBlurAppDelegate</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">feedTitleGradient</string>
<string key="candidateClassName">UIView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">innerView</string>
<string key="candidateClassName">UIView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">noStorySelectedLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webView</string>
<string key="candidateClassName">UIWebView</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/StoryDetailViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">StoryPageControl</string>
<string key="superclassName">BaseViewController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>doNextStory</string>
<string>doNextUnreadStory</string>
<string>doPreviousStory</string>
<string>showOriginalSubview:</string>
@ -3016,14 +3118,12 @@
<string>id</string>
<string>id</string>
<string>id</string>
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>doNextStory</string>
<string>doNextUnreadStory</string>
<string>doPreviousStory</string>
<string>showOriginalSubview:</string>
@ -3032,10 +3132,6 @@
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">doNextStory</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">doNextUnreadStory</string>
<string key="candidateClassName">id</string>
@ -3067,18 +3163,15 @@
<string>bottomPlaceholderToolbar</string>
<string>buttonAction</string>
<string>buttonNext</string>
<string>buttonNextStory</string>
<string>buttonPrevious</string>
<string>feedTitleGradient</string>
<string>fontSettingsButton</string>
<string>innerView</string>
<string>noStorySelectedLabel</string>
<string>originalStoryButton</string>
<string>pageControl</string>
<string>progressView</string>
<string>progressViewContainer</string>
<string>scrollView</string>
<string>subscribeButton</string>
<string>toolbar</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -3089,16 +3182,13 @@
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIView</string>
<string>UIBarButtonItem</string>
<string>UIView</string>
<string>UILabel</string>
<string>UIBarButtonItem</string>
<string>UIPageControl</string>
<string>UIProgressView</string>
<string>UIView</string>
<string>UIScrollView</string>
<string>UIBarButtonItem</string>
<string>UIToolbar</string>
<string>UIWebView</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
@ -3110,18 +3200,15 @@
<string>bottomPlaceholderToolbar</string>
<string>buttonAction</string>
<string>buttonNext</string>
<string>buttonNextStory</string>
<string>buttonPrevious</string>
<string>feedTitleGradient</string>
<string>fontSettingsButton</string>
<string>innerView</string>
<string>noStorySelectedLabel</string>
<string>originalStoryButton</string>
<string>pageControl</string>
<string>progressView</string>
<string>progressViewContainer</string>
<string>scrollView</string>
<string>subscribeButton</string>
<string>toolbar</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -3145,34 +3232,22 @@
<string key="name">buttonNext</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">buttonNextStory</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">buttonPrevious</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">feedTitleGradient</string>
<string key="candidateClassName">UIView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">fontSettingsButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">innerView</string>
<string key="candidateClassName">UIView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">noStorySelectedLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">originalStoryButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">pageControl</string>
<string key="candidateClassName">UIPageControl</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">progressView</string>
<string key="candidateClassName">UIProgressView</string>
@ -3181,6 +3256,10 @@
<string key="name">progressViewContainer</string>
<string key="candidateClassName">UIView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">scrollView</string>
<string key="candidateClassName">UIScrollView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">subscribeButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
@ -3189,6 +3268,60 @@
<string key="name">toolbar</string>
<string key="candidateClassName">UIToolbar</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/StoryPageControl.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">TrainerViewController</string>
<string key="superclassName">BaseViewController</string>
<object class="NSMutableDictionary" key="actions">
<string key="NS.key.0">doCloseDialog:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">doCloseDialog:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">doCloseDialog:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>appDelegate</string>
<string>closeButton</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NewsBlurAppDelegate</string>
<string>UIBarButtonItem</string>
<string>UIWebView</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>appDelegate</string>
<string>closeButton</string>
<string>webView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">appDelegate</string>
<string key="candidateClassName">NewsBlurAppDelegate</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">closeButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">webView</string>
<string key="candidateClassName">UIWebView</string>
@ -3197,65 +3330,7 @@
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/StoryDetailViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">StoryPageControl</string>
<string key="superclassName">UIViewController</string>
<object class="NSMutableDictionary" key="actions">
<string key="NS.key.0">changePage:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">changePage:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">changePage:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>appDelegate</string>
<string>pageControl</string>
<string>scrollView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NewsBlurAppDelegate</string>
<string>UIPageControl</string>
<string>UIScrollView</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>appDelegate</string>
<string>pageControl</string>
<string>scrollView</string>
</object>
<object class="NSArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">appDelegate</string>
<string key="candidateClassName">NewsBlurAppDelegate</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">pageControl</string>
<string key="candidateClassName">UIPageControl</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">scrollView</string>
<string key="candidateClassName">UIScrollView</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/StoryPageControl.h</string>
<string key="minorKey">./Classes/TrainerViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,95 @@
$('.NB-story img').each(function () {
setImage(this);
});
$('.NB-story img').bind('load', function () {
setImage(this);
});
$('a.NB-show-profile').live('click', function () {
var offset = $('img', this).offset();
console.log(offset);
var url = $(this).attr('href') + "/" + offset.left + "/" + (offset.top - window.pageYOffset) + "/" + offset.width + "/" + offset.height;
window.location = url;
return false;
});
$('.NB-button').live('touchstart', function () {
$(this).addClass('selected');
});
$('.NB-button').live('touchend', function (e) {
$(this).removeClass('selected');
});
function setImage(img) {
var $img = $(img);
var width = $(img).width();
var height = $(img).height();
if ($img.attr('src').indexOf('feedburner') != - 1) {
$img.attr('class', 'NB-feedburner');
} else if (width > 300 && height > 50) {
$img.attr('class', 'NB-large-image');
if ($img.parent().attr('href')) {
$img.parent().addClass('NB-contains-image')
}
} else if (width > 30 && height > 30) {
$img.attr('class', 'NB-medium-image');
if ($img.parent().attr('href')) {
$img.parent().addClass('NB-contains-image')
}
} else {
$img.attr('class', 'NB-small-image');
}
}
function slideToComment(commentId, highlight) {
setTimeout(function(){
var commentString = 'NB-user-comment-' + commentId;
var shareString = 'NB-user-share-bar-' + commentId;
//Get comment
var $comment = $('#' + commentString);
if ($comment.length) {
$.scroll($comment.offset().top - 32, 1000, 'ease-in-out');
} else {
var $shareBar = $('#' + shareString);
if ($shareBar.length) {
$.scroll($shareBar.offset().top - 32, 1000, 'ease-in-out');
} else {
var $shareButton =$("#NB-share-button-id");
$.scroll($shareButton.offset().top - 32, 1000, 'ease-in-out');
}
}
if (highlight) {
if ($comment.length) {
setTimeout(function(){
$comment.addClass('NB-highlighted');
setTimeout(function(){
$comment.removeClass('NB-highlighted');
}, 2000);
}, 1000);
} else if ($shareBar.length) {
setTimeout(function(){
$(".NB-story-comments-shares-teaser").addClass('NB-highlighted');
setTimeout(function(){
$(".NB-story-comments-shares-teaser").removeClass('NB-highlighted');
}, 2000);
}, 1000);
}
}
}, 500);
}
function findPos(obj) {
var curtop = 0;
if (obj.offsetParent) {
do {
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return [curtop];
}
}

View file

@ -0,0 +1,581 @@
/**
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
*
* @version 0.4.6
* @codingstandard ftlabs-jsv2
* @copyright The Financial Times Limited [All Rights Reserved]
* @license MIT License (see LICENSE.txt)
*/
/*jslint browser:true, node:true*/
/*global define, Event, Node*/
/**
* Instantiate fast-clicking listeners on the specificed layer.
*
* @constructor
* @param {Element} layer The layer to listen on
*/
function FastClick(layer) {
'use strict';
var oldOnClick, self = this;
/**
* Whether a click is currently being tracked.
*
* @type boolean
*/
this.trackingClick = false;
/**
* Timestamp for when when click tracking started.
*
* @type number
*/
this.trackingClickStart = 0;
/**
* The element being tracked for a click.
*
* @type EventTarget
*/
this.targetElement = null;
/**
* X-coordinate of touch start event.
*
* @type number
*/
this.touchStartX = 0;
/**
* Y-coordinate of touch start event.
*
* @type number
*/
this.touchStartY = 0;
/**
* ID of the last touch, retrieved from Touch.identifier.
*
* @type number
*/
this.lastTouchIdentifier = 0;
/**
* The FastClick layer.
*
* @type Element
*/
this.layer = layer;
if (!layer || !layer.nodeType) {
throw new TypeError('Layer must be a document node');
}
/** @type function() */
this.onClick = function() { FastClick.prototype.onClick.apply(self, arguments); };
/** @type function() */
this.onTouchStart = function() { FastClick.prototype.onTouchStart.apply(self, arguments); };
/** @type function() */
this.onTouchMove = function() { FastClick.prototype.onTouchMove.apply(self, arguments); };
/** @type function() */
this.onTouchEnd = function() { FastClick.prototype.onTouchEnd.apply(self, arguments); };
/** @type function() */
this.onTouchCancel = function() { FastClick.prototype.onTouchCancel.apply(self, arguments); };
// Devices that don't support touch don't need FastClick
if (typeof window.ontouchstart === 'undefined') {
return;
}
// Set up event handlers as required
layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchmove', this.onTouchMove, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
// layer when they are cancelled.
if (!Event.prototype.stopImmediatePropagation) {
layer.removeEventListener = function(type, callback, capture) {
var rmv = Node.prototype.removeEventListener;
if (type === 'click') {
rmv.call(layer, type, callback.hijacked || callback, capture);
} else {
rmv.call(layer, type, callback, capture);
}
};
layer.addEventListener = function(type, callback, capture) {
var adv = Node.prototype.addEventListener;
if (type === 'click') {
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
callback(event);
}
}), capture);
} else {
adv.call(layer, type, callback, capture);
}
};
}
// If a handler is already declared in the element's onclick attribute, it will be fired before
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
// adding it as listener.
if (typeof layer.onclick === 'function') {
// Android browser on at least 3.2 requires a new reference to the function in layer.onclick
// - the old one won't work if passed to addEventListener directly.
oldOnClick = layer.onclick;
layer.addEventListener('click', function(event) {
oldOnClick(event);
}, false);
layer.onclick = null;
}
}
/**
* Android requires an exception for labels.
*
* @type boolean
*/
FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
/**
* iOS requires an exception for alert confirm dialogs.
*
* @type boolean
*/
FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
/**
* iOS 4 requires an exception for select elements.
*
* @type boolean
*/
FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
/**
* Determine whether a given element requires a native click.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element needs a native click
*/
FastClick.prototype.needsClick = function(target) {
'use strict';
switch (target.nodeName.toLowerCase()) {
case 'label':
case 'video':
return true;
default:
return (/\bneedsclick\b/).test(target.className);
}
};
/**
* Determine whether a given element requires a call to focus to simulate click into element.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
*/
FastClick.prototype.needsFocus = function(target) {
'use strict';
switch (target.nodeName.toLowerCase()) {
case 'textarea':
case 'select':
return true;
case 'input':
switch (target.type) {
case 'button':
case 'checkbox':
case 'file':
case 'image':
case 'radio':
case 'submit':
return false;
}
return true;
default:
return (/\bneedsfocus\b/).test(target.className);
}
};
/**
* Send a click event to the specified element.
*
* @param {EventTarget|Element} targetElement
* @param {Event} event
*/
FastClick.prototype.sendClick = function(targetElement, event) {
'use strict';
var clickEvent, touch;
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
if (document.activeElement && document.activeElement !== targetElement) {
document.activeElement.blur();
}
touch = event.changedTouches[0];
// Synthesise a click event, with an extra attribute so it can be tracked
clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
clickEvent.forwardedTouchEvent = true;
targetElement.dispatchEvent(clickEvent);
};
/**
* On touch start, record the position and scroll offset.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchStart = function(event) {
'use strict';
var touch = event.targetTouches[0];
this.trackingClick = true;
this.trackingClickStart = event.timeStamp;
this.targetElement = event.target;
this.theTarget = $(this.targetElement).closest('a').get(0);
this.theTarget.className += ' pressed';
this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;
this.startClickTime = new Date;
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < 200) {
event.preventDefault();
}
return true;
};
/**
* Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.touchHasMoved = function(event) {
'use strict';
var touch = event.targetTouches[0];
if (Math.abs(touch.pageX - this.touchStartX) > 10 || Math.abs(touch.pageY - this.touchStartY) > 10) {
return true;
}
return false;
};
/**
* Update the last position.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchMove = function(event) {
'use strict';
if (!this.trackingClick) {
return true;
}
// If the touch has moved, cancel the click tracking
if (this.targetElement !== event.target || this.touchHasMoved(event)) {
this.trackingClick = false;
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
this.targetElement = null;
}
return true;
};
/**
* Attempt to find the labelled control for the given label element.
*
* @param {EventTarget|HTMLLabelElement} labelElement
* @returns {Element|null}
*/
FastClick.prototype.findControl = function(labelElement) {
'use strict';
// Fast path for newer browsers supporting the HTML5 control attribute
if (labelElement.control !== undefined) {
return labelElement.control;
}
// All browsers under test that support touch events also support the HTML5 htmlFor attribute
if (labelElement.htmlFor) {
return document.getElementById(labelElement.htmlFor);
}
// If no for attribute exists, attempt to retrieve the first labellable descendant element
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
};
/**
* On touch end, determine whether to send a click event at once.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchEnd = function(event) {
'use strict';
var forElement, trackingClickStart, targetElement = this.targetElement, touch = event.changedTouches[0];
if (!this.trackingClick) {
return true;
}
// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
// with the same identifier as the touch event that previously triggered the click that triggered the alert.
if (this.deviceIsIOS) {
if (touch.identifier === this.lastTouchIdentifier) {
event.preventDefault();
return false;
}
this.lastTouchIdentifier = touch.identifier;
}
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < 200) {
this.cancelNextClick = true;
return true;
}
if ((new Date - this.startClickTime) > 115) {
return false;
}
this.lastClickTime = event.timeStamp;
trackingClickStart = this.trackingClickStart;
this.trackingClick = false;
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
this.trackingClickStart = 0;
if (targetElement.nodeName.toLowerCase() === 'label') {
forElement = this.findControl(targetElement);
if (forElement) {
targetElement.focus();
if (this.deviceIsAndroid) {
return false;
}
if (!this.needsClick(forElement)) {
event.preventDefault();
this.sendClick(forElement, event);
}
return false;
}
} else if (this.needsFocus(targetElement)) {
// If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
if ((event.timeStamp - trackingClickStart) > 100) {
this.targetElement = null;
return true;
}
targetElement.focus();
// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
if (!this.deviceIsIOS4 || targetElement.tagName.toLowerCase() !== 'select') {
this.targetElement = null;
event.preventDefault();
}
return false;
}
// Prevent the actual click from going though - unless the target node is marked as requiring
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
if (!this.needsClick(targetElement)) {
event.preventDefault();
this.sendClick(targetElement, event);
}
return false;
};
/**
* On touch cancel, stop tracking the click.
*
* @returns {void}
*/
FastClick.prototype.onTouchCancel = function() {
'use strict';
this.theTarget.className = this.theTarget.className.replace(/ ?pressed/gi, '');
this.trackingClick = false;
this.targetElement = null;
};
/**
* On actual clicks, determine whether this is a touch-generated click, a click action occurring
* naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
* an actual click which should be permitted.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onClick = function(event) {
'use strict';
var oldTargetElement;
// If a target element was never set (because a touch event was never fired) allow the click
if (!this.targetElement) {
return true;
}
if (event.forwardedTouchEvent) {
return true;
}
oldTargetElement = this.targetElement;
this.targetElement = null;
// It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
if (this.trackingClick) {
this.trackingClick = false;
return true;
}
// Programmatically generated events targeting a specific element should be permitted
if (!event.cancelable) {
return true;
}
// Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
if (event.target.type === 'submit' && event.detail === 0) {
return true;
}
// Derive and check the target element to see whether the click needs to be permitted;
// unless explicitly enabled, prevent non-touch click events from triggering actions,
// to prevent ghost/doubleclicks.
if (!this.needsClick(oldTargetElement) || this.cancelNextClick) {
this.cancelNextClick = false;
// Prevent any user-added listeners declared on FastClick element from being fired.
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
} else {
// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
event.propagationStopped = true;
}
// Cancel the event
event.stopPropagation();
event.preventDefault();
return false;
}
// If clicks are permitted, return true for the action to go through.
return true;
};
/**
* Remove all FastClick's event listeners.
*
* @returns {void}
*/
FastClick.prototype.destroy = function() {
'use strict';
var layer = this.layer;
layer.removeEventListener('click', this.onClick, true);
layer.removeEventListener('touchstart', this.onTouchStart, false);
layer.removeEventListener('touchmove', this.onTouchMove, false);
layer.removeEventListener('touchend', this.onTouchEnd, false);
layer.removeEventListener('touchcancel', this.onTouchCancel, false);
};
if (typeof define !== 'undefined' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
'use strict';
return FastClick;
});
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = function(layer) {
'use strict';
return new FastClick(layer);
};
module.exports.FastClick = FastClick;
}
function attachFastClick(options) {
options = options || {};
var avatars = document.getElementsByClassName("NB-show-profile");
Array.prototype.slice.call(avatars, 0).forEach(function(avatar) {
new FastClick(avatar, options);
});
var tags = document.getElementsByClassName("NB-story-tag");
Array.prototype.slice.call(tags, 0).forEach(function(tag) {
new FastClick(tag, options);
});
var authors = document.getElementsByClassName("NB-story-author");
Array.prototype.slice.call(authors, 0).forEach(function(author) {
new FastClick(author, options);
});
var publishers = document.getElementsByClassName("NB-story-publisher");
Array.prototype.slice.call(publishers, 0).forEach(function(publisher) {
new FastClick(publisher, options);
});
var titles = document.getElementsByClassName("NB-story-title");
Array.prototype.slice.call(titles, 0).forEach(function(title) {
new FastClick(title, options);
});
var author = document.getElementById("NB-story-author");
if (author) {
new FastClick(author, options);
}
}
Zepto(function($) {
attachFastClick();
});

309
media/ios/static/reader.css Normal file
View file

@ -0,0 +1,309 @@
.NB-right {
float: right;
}
#story_pane .NB-story-comment {
border-top: 1px solid #A6A6A6;
background:#f5f9fb;
position: relative;
padding: 0 12px 2px 64px;
line-height: 20px;
overflow: hidden;
min-height: 72px;
box-shadow: inset 1px 1px 1px rgba(255, 255, 255, 1);shar
}
.NB-story-share-profiles-comments-friends,
.NB-story-share-profiles-comments-public {
float: left;
}
.NB-story-share-profiles-shares-friends,
.NB-story-share-profiles-shares-public {
float: right;
}
#story_pane .NB-story-comment .NB-user-avatar {
position: absolute;
left: 6px;
top: 8px;
}
#story_pane .NB-story-comment .NB-user-avatar.NB-story-comment-reshare {
top: 22px;
left: 6px;
z-index: 1;
}
#story_pane .NB-story-comment .NB-user-avatar img {
border-radius: 6px;
margin: 0;
width: 48px;
height: 48px;
}
#story_pane .NB-story-comment .NB-user-avatar.NB-story-comment-reshare img {
height: 36px;
width: 36px;
}
#story_pane .NB-story-comment .NB-story-comment-author-container {
padding: 8px 0 0;
}
#story_pane .NB-story-comment .NB-story-comment-reshares {
position: absolute;
top: 0;
left: 8px;
z-index: 0;
}
#story_pane .NB-story-comment .NB-story-comment-reshares .NB-user-avatar {
top: 8px;
left: 26px;
}
#story_pane .NB-story-comment .NB-story-comment-username {
float: left;
font-size: 11px;
color: #1D4BA6;
font-weight: bold;
margin: 0 4px 0 0;
text-shadow: 0 -1px 0 #F0F0F0;
cursor: pointer;
line-height: 17px;
}
#story_pane .NB-story-comment .NB-story-comment-date {
text-transform: uppercase;
font-size: 10px;
color: #9D9D9D;
font-weight: bold;
float: right;
line-height: 17px;
margin: 0 0 0;
}
#story_pane .NB-story-comment .NB-story-comment-likes {
float: right;
margin: 0;
overflow: hidden;
}
#story_pane .NB-story-comment .NB-story-comment-likes-icon {
float: right;
background: transparent url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAGmSURBVCiRbZG/axRBHMXfzO7tbvYua+IFJQa0sRBs1IiQFIEIgvgDEUSs7CSFCJLaA1MJYjrFI5VgmkBiCDYWNgZJlaB/wgknxOT0Lnt7s7Mz852xOgzxXvne98OXxwMG6PXm3PSbzblbgzJ21Kit3mcT4ye34NyI0AeT87PvxeGcHwVGj0d34lJyJQ6Gz1myDwZ+eLpxOapGp8Y95lVPVM4sDUdjF8kSOmK3sZ81HhpGP4XMfi3e3eqx+tcnzyM/ngVjlzjjlcgvIy1aUEaiHIwg110oynNjzXep0h1OVjeMVTNJWK34Xoh2vous6ECoA7R6TRhSiEvJEFk1Rc40+OOZ+rtU7l9r9ZopA1BoAecsHIBcZ/B4CZ18L8uK9r3a9Y+LHADmry5/TmXrmabCDQUJFEkYKhD6EcgZ9FT77cKNT2sA4PfbO7jMWA3OfBwLxwDmILUAOQ1jSPbv/gHOnvdZiWlXQFulyGrle0GFgUFJOj0ImPgtmhCq+6or/6wbp1TIyzc9imq6oNH/AHJmZS/78fLF7S/fDu20/ah+4UMcJGf7xl8kb9PTscP82wAAAABJRU5ErkJggg==") no-repeat center center;
width: 16px;
height: 16px;
display: block;
margin: 0 10px 0 0;
vertical-align: top;
}
#story_pane .NB-story-comment .NB-story-comment-likes-user {
float: right;
margin: 2px 2px 0 0;
}
#story_pane .NB-story-comment .NB-story-comment-likes-user .NB-user-avatar {
position: static;
}
#story_pane .NB-story-comment .NB-story-comment-likes-user img {
width: 12px;
height: 12px;
border-radius: 2px;
vertical-align: top;
}
#story_pane .NB-story-comment .NB-story-comment-location {
text-transform: uppercase;
font-size: 10px;
color: #BECDD7;
font-weight: bold;
float: left;
clear: both;
margin: 3px 0 6px;
line-height: 12px;
}
#story_pane .NB-story-comment .NB-button-wrapper {
overflow: hidden;
white-space: nowrap;
float: right;
}
#story_pane .NB-story-comment .NB-story-comment-content {
float: left;
color: #303030;
clear: both;
}
#story_pane .NB-story-comment-reply {
border-top: 1px solid #E0E0E0;
padding: 4px 0;
overflow: hidden;
clear: both;
position: relative;
padding: 8px 0 4px 42px;
line-height: 18px;
}
#story_pane .NB-story-comment-reply .NB-story-comment-reply-photo {
width: 32px;
height: 32px;
border-radius: 3px;
position: absolute;
left: 0px;
top: 2px;
cursor: pointer;
}
#story_pane .NB-story-comment-reply-content {
clear: both;
color: #303030;
padding: 2px 0 3px;
}
#story_pane .NB-story-comment-reply-form {
padding-top: 11px;
}
#story_pane .NB-story-comment-reply-form .NB-story-comment-reply-username {
margin: 1px 8px 6px 0;
}
#story_pane .NB-story-comment-reply-form .NB-story-comment-reply-comments {
margin: 0 8px 4px 0;
width: 62%;
display: block;
float: left;
font-size: 12px;
}
#story_pane .NB-story-comment-reply-form .NB-modal-submit-button {
float: left;
font-size: 10px;
padding: 2px 8px;
line-height: 16px;
margin: 0;
}
#story_pane .NB-story-comment-reply-form .NB-error {
font-size: 10px;
color: #6A1000;
padding: 4px 0 0;
line-height: 14px;
font-weight: bold;
clear: both;
}
#story_pane .NB-story-comments-public-teaser-wrapper,
#story_pane .NB-story-comments-public-header-wrapper {
border-top: 1px solid #353535;
padding: 1px 0;
cursor: pointer;
}
#story_pane .NB-story-comments-public-header-wrapper {
cursor: default;
}
#story_pane .NB-story-comments-shares-teaser-wrapper {
border-top: 1px solid #fff;
padding-top: 0;
}
#story_pane .NB-story-comments-public-teaser,
#story_pane .NB-story-comments-public-header {
background-color: #B1B6B4;
color: white;
text-shadow: 0 1px 0 #505050;
font-weight: bold;
text-transform: uppercase;
font-size: 10px;
padding: 2px 12px;
overflow: hidden;
}
#story_pane .NB-story-comments-public-header {
background-color: whiteSmoke;
color: #404040;
text-shadow: 0 1px 0 white;
}
#story_pane .NB-story-comments-shares-teaser {
background-color: whiteSmoke;
color: #202020;
cursor: default;
text-shadow: 0 1px 0 white;
font-weight: bold;
text-transform: uppercase;
font-size: 10px;
padding: 4px 12px 0;
overflow: hidden;
min-height: 26px;
border-bottom: 1px solid #A6A6A6;
background-image: url('data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAAAZCAYAAACM9limAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAKwwAACsMBNCkkqwAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAYOSURBVFiFtVmxkuUoDGy/WkggwYGdbPL+/7M2sqvGBA+CharxBXvNNpq3t9GRjGeMhdSSWhKzvF6vJwB479Fag/cepRT03uGcG3/vvSOlNPbYn6UUcMUYx3NrDa01xBjHnpQSaq3IOeP79++otaK1BgDovSOEMGSHENB7H7Iov5QynZtSQu/9yxneezjnkHOG937Ipax1XXHfN5ZlwXVdQ+/l8/PzmXNGjHFs1lVrRQgBCp7u895Pz1RYgeF367qitYac85BZa0VK6Yvy9nvvPa7rGs/8nefym5wzUkoAAOfc0H9ZlqE7waIjuHLO4+/Lx8fHUw9Tw0opk6c1EhhVKaXpGx5MT5dShjedc0MRylZZjBSVYxVOKYGOJOAEofc+GW/BDSHgPM+RCQRII5LfPfgBFWyt4TiOEf42MujxGOPwjCpIhZdlGYfzYAVFvea9H2nKMwhSKQUhhCG31joBQKflnP8azUoPfFdrHTqFEFBKQSnlFzA8SIVQcefcUJpeDiFMStkULKXgvm/c9z28SeUZQQSK7/RbRttxHJMzCJhz7suZBC6lhBjjRA0amQqYghFjRM4Zvfdfjv34+HgqmYUQRp7Sw8dxDK9xn0ZUa23kMo2yRA5gAHIcxyBHTR+bnmqQRjZJlUCRm9SpBMXSgHMOy7JM70IIUwEA/uWYlNIgJwVBlSIA5AKNFE05gvouz2ut2LYN53kipfSFzLXK6bLcxH1aeTQN6X1NVTqJzmVEqQ0q4xFjnMhn3/cvnMHF9FLuUHJurU3KlFImBelt5xyO48B938NQCyIBIaCUSUBpvJZsphCAsU+XRjxlxxixriuccyN6Y4x4UChLIT1mQ0u9SkOdc5MXGGkEiIZolFHZfd+Ht7UHstGo5f66LvTekXMGMHMU5TAKCBq5R50LYMjIOeO6rsEvPHf5/Px88hDgd6gy7Lm51op936fwYz+iEaaeYY/CSOGzGmK/tc2gpnRKCed5TiBrqpPflOy1D7PyNBt4Nu1eXq/X03pNBagQ60XbAWve285Yv1NFFEjNezWWwCoYWhw0mq2+2liqTrqs/d57fHtHPgqIHshnNZQg8HAlQ61yFgAarQZratIQBUmrIVOFrYPqq2BpZ8s9lntyzti2bewvpeCbLW8aylqOqbQqqWi/I1FWEy211mMElmfp99rvqFE0pPeOWut/pnkIYZqB3qW5BXBd13lW+j9bbPWiVYpGc6lsO7hauZZnaDydeZ4ntm2biN7SxLu0fChSinatFcdxjLKsTM+99IpGFtOGSmuY0yA2cNrO62KK7fs+6caGTp1ou+/WGu77Hj0XZyvqbQlX2xXd99AOlQaQ1fd9RykF27YhpTSmYBXGHKeXlQBZwoE5HQgEAaJMVdR7P0YKTTOVw3fKWTxLxwgF/0+/l1KmdFx+/vz51BC3QxY/ZppQ4Ls92hkrb7wb8bnYAFquo0xygk7VtijYVHs3uvzthsBW2ocizbzXDpaol1JwnufYp2FMJThx23DnRMy9yic6pPJsViqeH2NErXVUI8qm/HVdJ7At56gTtVCoA+wNw/J6vZ4xRvz48WPcpqlAvVTi3xR9CrX8w3dsmPie70iMTCNGIquNnmEjTtPfnqmAa3Tb9xp1GqUjYkIIOI4D+75PxvFjzhzaqtuKol4lD3AW0UFPjWMIKz8QYJ3I+awOox46amjvo98rd9kIp07Ug3bGGPHQ0knhNjdZ9zWK1FP8SW7Re1oazf06UL7rQLlv27a3s456lVeX6nXb/isVMPos153nOfVArTV84yxBr/MFAbDRQcWIPDCTqr0g4oFMKQuKdrpaUpVo1QkEnd5l2tmxRCufLr0D0p6Nv1P2Y9/3cQ2pJKTdrnrZ/jeAZZyAMYUYaTSY0zRlvBtF3s1XTFG+ZwFQQmeksXSTqGm02mAjQyOMupVS8Liua2zQNNEwVjK0DM/FWYl3NBpZrbXphl+vHqmkyuVPlnJGmjaHOiPZCFQqIHh29NAIekfOC/+vxPVu0owxToZZhP90p0pv2KtSGmKjQ5/tXKajiVYk5TMWC+VDu1SOrUb6/A/IUX/pFmu08AAAAABJRU5ErkJggg=='), -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0.10, #EBEBEC),
color-stop(0.84, #F5F5F5)
);
}
#story_pane .NB-story-comments-public-teaser-wrapper:hover .NB-story-comments-public-teaser {
background-color: #2B478C;
background-image: none;
}
#story_pane .NB-story-comments-public-teaser b {
padding: 0 1px;
font-size: 12px;
}
#story_pane .NB-story-share-profiles {
float: left;
vertical-align: top;
min-height: 24px;
padding-top: 2px;
margin-top: 0px;
overflow: hidden;
}
#story_pane .NB-story-share-profiles.NB-story-share-profiles-public {
float: right;
}
#story_pane .NB-story-share-profiles.NB-story-share-profiles-public .NB-story-share-profile {
float: right;
}
#story_pane .NB-story-share-profile {
display: inline-block;
}
#story_pane .NB-story-share-profile .NB-user-avatar {
float: left;
font-size: 0;
vertical-align: middle;
height: 22px;
width: 22px;
margin: 0 4px 6px 0;
cursor: pointer;
}
#story_pane .NB-story-share-profile .NB-public-user {
opacity: 0.5;
}
#story_pane .NB-story-share-profile .NB-user-avatar img {
width: 22px;
height: 22px;
border-radius: 3px;
}
#story_pane .NB-story-share-profile .NB-user-username {
float: left;
}
#story_pane .NB-story-comment .NB-story-comment-content {
clear: both;
padding: 2px 0 3px 0;
}
#story_pane .NB-feed-stories .NB-feed-story {
margin: 0;
clear: both;
overflow: hidden;
}
#story_pane .NB-feed-stories.NB-feed-view-story .NB-feed-story {
padding: 0;
}
#story_pane .audiojs audio {
display: none;
}

View file

@ -0,0 +1,602 @@
/**
* Font Size Style
*/
.NB-story {
line-height: 1.5em;
}
.NB-extra-small .NB-story,
.NB-extra-small .NB-story-comment {
font-size: 12px;
}
.NB-small .NB-story,
.NB-small .NB-story-comment {
font-size: 13px;
}
.NB-medium .NB-story,
.NB-medium .NB-story-comment {
font-size: 14px;
}
.NB-large .NB-story,
.NB-large .NB-story-comment {
font-size: 15px;
}
.NB-extra-large .NB-story,
.NB-extra-large .NB-story-comment {
font-size: 17px;
}
.NB-san-serif {
font-family: Helvetica;
}
.NB-serif {
font-family: georgia;
}
/**
* iPad Wide Style
*/
.NB-ipad-wide .NB-header {
padding: 15px 0 5px;
}
.NB-ipad-wide .NB-header .NB-header-inner {
margin: 0px 90px;
}
.NB-ipad-wide .NB-story {
margin: 30px 90px;
}
.NB-ipad-wide .NB-share-inner-wrapper {
margin: 0 90px;
}
.NB-ipad-wide#story_pane .nb-story-comments-public-teaser,
.NB-ipad-wide#story_pane .nb-story-comments-public-header,
.NB-ipad-wide#story_pane .NB-story-comments-shares-teaser {
padding-left: 90px;
padding-right: 90px;
}
.NB-ipad-wide#story_pane .nb-story-comment {
padding: 0 90px 2px 150px;
}
.NB-ipad-wide#story_pane .nb-story-comment .nb-user-avatar {
left: 88px;
}
.NB-ipad-wide#story_pane .nb-story-comment .nb-story-comment-reshares .nb-user-avatar {
left: 108px;
}
/**
* iPad Narrow Style
*/
.NB-ipad-narrow .NB-header {
padding: 15px 30px 5px;
}
.NB-ipad-narrow .NB-story {
margin: 25px 30px;
}
.NB-ipad-narrow .NB-share-inner-wrapper {
margin: 0 30px;
}
.NB-ipad-narrow#story_pane .nb-story-comments-public-teaser,
.NB-ipad-narrow#story_pane .nb-story-comments-public-header,
.NB-ipad-narrow#story_pane .NB-story-comments-shares-teaser {
padding-left: 30px;
padding-right: 30px;
}
.NB-ipad-narrow#story_pane .nb-story-comment {
padding: 0 30px 2px 90px;
}
.NB-ipad-narrow#story_pane .NB-story-comment .NB-user-avatar {
left: 26px;
}
.NB-ipad-narrow#story_pane .nb-story-comment .nb-story-comment-reshares .nb-user-avatar {
left: 45px;
}
.NB-ipad-narrow#story_pane .NB-story-comment .NB-user-avatar.NB-story-comment-reshare {
left: 26px;
}
/**
* iPhone Style
*/
.NB-iphone .NB-header {
padding: 12px 12px 5px;
}
.NB-iphone .NB-story {
margin: 12px 12px;
}
.NB-iphone .NB-share-inner-wrapper {
margin: 0 12px;
}
/*
.NB-iphone .NB-story img {
max-width: 290px !important;
}
/**
* Universal Style
*/
body {
text-rendering: optimizeLegibility;
margin: 0;
-webkit-text-size-adjust: none;
font-size: 14px;
line-height: 120%;
overflow: hidden;
font-family: Helvetica;
}
body.NB-iphone {
line-height: 110%;
}
.NB-story-author {
color: #969696;
text-decoration: none;
text-transform: uppercase;
margin: 4px 8px 0px 0;
margin-bottom: 8px;
text-shadow: 0 1px 0 #F9F9F9;
float: left;
position: relative;
}
.NB-story-author-positive {
color: #58A353;
text-shadow: 0 1px 0 #E9F6E9;
}
.NB-story-author-negative {
color: #B85044;
text-shadow: 0 1px 0 #F6E9E9;
}
.NB-story-tags {
overflow: hidden;
padding: 5px 2px 2px 2px;
margin-bottom: 8px;
text-transform: uppercase;
}
.NB-story-tag {
float: left;
text-decoration: none;
font-weight: normal;
padding: 0px 4px 0px;
margin: 0px 4px 4px 0;
background-color: #C9CBC6;
color: #505050;
text-shadow: 0 1px 0 #E0E8DB;
-webkit-border-radius: 4px;
position: relative;
}
.NB-story-tag-positive {
background-color: #88B383;
color: white;
text-shadow: 0 1px 0 #588353;
}
.NB-story-tag-negative {
background-color: #C89094;
color: white;
text-shadow: 0 1px 0 #986064;
}
.NB-story-tag .NB-highlight,
.NB-story-author .NB-highlight,
.NB-show-profile .NB-highlight {
position: absolute;
top: -2px;
left: -2px;
bottom: -2px;
right: -2px;
background-color: #000;
opacity: .25;
border-radius: 4px;
display: none;
}
.NB-story-tag.pressed .NB-highlight,
.NB-story-author.pressed .NB-highlight,
.pressed .NB-show-profile .NB-highlight,
.NB-show-profile.pressed .NB-highlight {
display: block;
}
.NB-story-date {
color: #454D6C;
}
.NB-story-author {
font-size: 10px;
line-height: 12px
}
.NB-story-tags {
line-height: 12px;
height: 12px;
}
.NB-story-tag {
font-size: 9px;
}
.NB-story-date {
font-size: 11px;
line-height: 13px;
}
.NB-story h1,
.NB-story h2,
.NB-story h3,
.NB-story h4,
.NB-story h5,
.NB-story h6,
.NB-story div,
.NB-story table,
.NB-story span,
.NB-story pre,
.NB-story code {
overflow: auto;
clear: both;
}
.NB-story h1,
.NB-story h2,
.NB-story h3,
.NB-story h4,
.NB-story h5,
.NB-story h6 {
line-height: 140%;
}
.NB-iphone .NB-story h1,
.NB-iphone .NB-story h2,
.NB-iphone .NB-story h3,
.NB-iphone .NB-story h4,
.NB-iphone .NB-story h5,
.NB-iphone .NB-story h6 {
line-height: 120%;
font-size: 1.1em;
}
a {
text-decoration: none;
}
.NB-story blockquote {
background-color: #F0F0F0;
border-left: 1px solid #9B9B9B;
padding: 10px 20px;
margin: 20px 0;
}
div + p {
margin: 20px 0px 20px 0;
}
.NB-story p {
margin: 0px 0px 20px 0;
clear: both;
}
.NB-story small {
font-size: 11px;
}
.NB-header {
font-size: 22px;
line-height: 1.2em;
font-weight: 600;
background-color: #E0E0E0;
border-bottom: 1px solid #A0A0A0;
text-shadow: 1px 1px 0 #EFEFEF;
overflow: hidden;
max-width: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.NB-iphone .NB-header {
font-size: 14px;
}
.NB-story {
overflow: hidden;
}
.NB-story a.NB-contains-image {
display: block;
}
.NB-story div {
max-width: 100% !important;
}
.NB-story img {
margin: 20px auto;
height: auto !important;
width: auto;
max-width: 100% !important;
display: none;
}
.NB-story img.NB-large-image,
.NB-story img.NB-medium-image {
margin: 20px auto !important;
display: block !important;
width: auto;
height: auto;
display: block;
float: none;
clear: both;
}
.NB-story img.NB-small-image {
display: inline-block;
margin: 2px;
}
.NB-feed-story-comments {
clear: both;
}
.NB-story-title {
clear: left;
margin: 8px 0 4px;
}
.NB-story-title-positive {
color: #58A353;
text-shadow: 0 1px 0 #E9F6E9;
}
.NB-story-title-negative {
color: #B85044;
text-shadow: 0 1px 0 #F6E9E9;
}
ins {
text-decoration: none;
}
del {
display: none;
}
/* Comments */
.NB-feed-story-comments {
margin: 0 !important;
clear: both !important;
max-width: none !important;
}
#story_pane .NB-story-comments-shares-teaser.NB-highlighted {
-webkit-transition: background-color .6s linear;
background-color: #FBE5C7;
}
#story_pane .NB-story-comments-shares-teaser {
-webkit-transition: background-color .6s linear;
}
#story_pane .NB-story-comment-reply.NB-highlighted {
background-color: #FBE5C7;
}
#story_pane .NB-story-comment.NB-highlighted {
-webkit-transition: background-color .6s linear;
background-color: #FBE5C7;
}
#story_pane .NB-story-comment-reply,
#story_pane .NB-story-comment {
-webkit-transition: background-color .6s linear;
}
#story_pane .NB-story-comment-reply .NB-show-profile {
float: left;
left: -42px;
}
.NB-story-comment:last-child {
border-bottom: 1px solid #A6A6A6;
}
#story_pane .NB-story-comment .NB-story-comment-content {
float: none;
}
.nb-story-comments-label {
float: left;
margin-right: 2px;
margin-top: 6px;
}
.nb-story-share-label {
float: right;
margin-top: 6px;
margin-left: 2px;
}
/* Sharing */
.NB-share-wrapper {
margin-top: 36px;
}
a.NB-show-profile {
margin: 0 0 0 2px;
position: relative;
display: inline-block;
}
/* Disable certain interactions on touch devices */
.NB-button,
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);
}
.NB-header,
#NB-share-bar-wrapper {
-webkit-user-select: none;
-webkit-highlight: none;
-webkit-touch-callout: none;
}
.NB-button.NB-share-button {
line-height: 20px;
font-family: Helvetica, sans-serif;
font-size: 14px;
color: #606060;
float: none;
display: block;
text-shadow:
0px 0px 0px rgba(000,000,000,0),
0px 1px 0px rgba(255,255,255,1);
}
.NB-button.NB-share-button a {
display: block;
}
.NB-share-button.active,
.NB-save-button.active {
background: #217412;
box-shadow: none;
}
/* Edit, Like, and Reply buttons */
.NB-button {
padding: 0px 0px 4px 4px;
cursor: pointer;
white-space: nowrap;
float: right;
}
.NB-button.NB-share-button {
float: left;
padding: 0px 0px 20px 0px;
margin: 0 0 0 0;
display: block;
width: 48%;
}
.NB-button.NB-train-button {
float: right;
}
.NB-button div {
padding: 2px 6px;
background: -webkit-gradient(
linear, left top, left bottom,
from(#F0F0F0),
color-stop(0.50, #EBEBEB),
color-stop(0.50, #E4E4E4),
to(#C0C0C0));
border-radius: 3px;
border: 1px solid #949494;
box-shadow: inset 0px 1px 0px rgba(255,255,255,1);
}
.NB-iphone .NB-button div {
padding: 0px 5px 0px;
}
.NB-iphone #story_pane .NB-story-comment .NB-story-comment-content {
padding-bottom: 0px;
}
.NB-button.NB-share-button div {
padding: 8px 0px 8px;
border-radius: 5px;
text-align: center;
background: -webkit-gradient(
linear, left top, left bottom,
from(#42aaff),
to(#1e78c1));
box-shadow: inset 0 0 0px 1px rgba(255, 255, 255, .6);
text-shadow: 0 1px 0 #003366;
}
.NB-button.NB-share-button .NB-icon {
background: url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACGUlEQVR42o2TTUsbYRSFZ+MvcNn/0EU1rSCCgkSxVRCLFgyiqFSE1tDSpNKoEaQJiAvBryDZGCVQKWIpWIeAFMHWL1qKjUJ3KRQSNUgSJzPR5PieMRMqjuLibAaec+577xkJwJ30p0jqEloXiglpQlmh3F3AaaEogWhnMw6dPTgafIljtx3i262gTSgSKbuPQ8dzxEfsSEw6kHBV4NRThrOVAaSd0o2wX0iJdj9D3PMaJ/1WKF4LtMlHyExf6jw0CO2dZAovCmWOBl4gMeFAyv0AmTyoLVRCXa1H+nsLtMgUktslpsmZ46E+JGedUMcsl2CwCumvTVD22qAcdOjS/vmQ3LVce7PCZMLa+EMdVj/XQflp06G/ezZ8/NYAV6gKUz+GcM8vXTGI8M0c20hW5QYo4XacCnhxox5dK+Wo+1SKmuUSeHdcKJ6VrpwqF3//Cqnh0kIy4djvNoyuVeNJHmwNONDrCyIg/8LjN0sFgyhPlfK/5cL4Zo7NZB2uFWDjh6fonQliXg5jQd7H3JcwDQoNy/LOPBXT9YXlx2YyYXcgpIPe+S10emTCqtAmDdbZMJaEd+apuG0ujG/m2EwmbB9fM0CrsTsaxFhPNozpvDPTuW0ujG/m2EwmbID/G2jsNutJA5aEBjyVnu7T042xrWYGWf4Y7DbryYaxJLwzT8Vtc2FGuplBTog/BrvNerJhLAnvTNDQppnBBZHZrv4ITeFuAAAAAElFTkSuQmCC") no-repeat top right;
width: 23px;
display: inline-block;
height: 16px;
margin: 0 2px -2px -12px;
}
.NB-button.NB-train-button .NB-icon {
background: url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACxElEQVR42qWT30+SURzG6a553d/QdffOq7Y2t4xEhgSYvCIM5ddUfi0QdcsMp6Ij8EcFgkoiGqFUCGYyp2Kp02XOzDUVGypcsKJYNzy9nM0517jq4tnOzvt9Pt/ne95zGAD+S/9sfHjcWhQ1qovfqRX8uIjCUWU5UbymGnNq+f2oQV2crykI2NA3xVIqJZJSCU5pU1qvR1qtJuukrA6pBhXWdI2xgoCvPHbuh9GIrLUfp9JaxLksInpN9vLfvnDKchGN4joBtDpNRRpbU7HKIufrxpU4YN1Gxu1GSl6PMxFFTOewM4kYCUpAj8QEDbhBAA8cmlh70AiTXwu9T4VD9h38CQTxy25Dsl5CIqcalfgu4OXNZJQjbvkFQDZcm7OEH8G9PAjjlDpPJ4CMvR8ZqxXp7i6cCAXEfEJVEcClBGJnFeZ3ZmEJd0A/2YD9exWkc8Y+gJ99fUh3mpGo5ueNBHBMd99ll+a0JvtNXZeniCEdFmL12xJmt4OYWB3FkK8NiwoKB9y75E/QKchZHPO52GeVYl4kRIvZg4ahKGTtYzFGnasGS3sLmNmcQmDDB0/MAe2EAiaDDTGKhzgNOuAwEavm4qG+F5LeCCwzWzC4l8HUe3MMmVOU8697MbnmgXfVDefSINReGZjGl3gV20eb8z3E3SFiokUA0r45CDvfgmUKgNFoUZbI7dKEfESCwQUrRlcc5CzKTX7MbcbhW9yDI/QJzSMrUA1EITC/IeaanjDYrQGQy5PNZq+oehUisY3K1LtFqHNRhB5aP8TYwi5e0Hoe3oawK0TMVHeIAEgC4LKUPfJrSov8KqdlKueMfIYjsoNndIIndPwqM+lMAJy2aZRpxnMFX5nE2F/CNwwnuHSh2fcRtuAWRD0R8Dpeo0znA0fzNElprbcKAC5GkzYP1VbqXRlO6zQd2Y8Kjeu3UGdrOq/5C7Lktmdg8uWWAAAAAElFTkSuQmCC") no-repeat top right;
}
.NB-iphone .NB-button .NB-story-comment-like-button-wrapper,
.NB-button .NB-story-comment-like-button-wrapper {
position: relative;
overflow: hidden;
}
.NB-button .NB-story-comment-like-button-wrapper span {
background: transparent url("data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNui8sowAAAGmSURBVCiRbZG/axRBHMXfzO7tbvYua+IFJQa0sRBs1IiQFIEIgvgDEUSs7CSFCJLaA1MJYjrFI5VgmkBiCDYWNgZJlaB/wgknxOT0Lnt7s7Mz852xOgzxXvne98OXxwMG6PXm3PSbzblbgzJ21Kit3mcT4ye34NyI0AeT87PvxeGcHwVGj0d34lJyJQ6Gz1myDwZ+eLpxOapGp8Y95lVPVM4sDUdjF8kSOmK3sZ81HhpGP4XMfi3e3eqx+tcnzyM/ngVjlzjjlcgvIy1aUEaiHIwg110oynNjzXep0h1OVjeMVTNJWK34Xoh2vous6ECoA7R6TRhSiEvJEFk1Rc40+OOZ+rtU7l9r9ZopA1BoAecsHIBcZ/B4CZ18L8uK9r3a9Y+LHADmry5/TmXrmabCDQUJFEkYKhD6EcgZ9FT77cKNT2sA4PfbO7jMWA3OfBwLxwDmILUAOQ1jSPbv/gHOnvdZiWlXQFulyGrle0GFgUFJOj0ImPgtmhCq+6or/6wbp1TIyzc9imq6oNH/AHJmZS/78fLF7S/fDu20/ah+4UMcJGf7xl8kb9PTscP82wAAAABJRU5ErkJggg==") no-repeat center center;
width: 16px;
height: 16px;
display: block;
float: left;
margin: 0 6px;
}
.NB-button.selected div {
background: #2379bf;
background: -webkit-gradient(
linear, left top, left bottom,
from(#1e78c1),
to(#2379bf));
box-shadow: none;
}
.NB-button.NB-share-button.selected div{
box-shadow: inset 0 0 1px 1px rgba(255, 255, 255, 1);
}
.NB-button a {
display: inline-block;
padding-top: 1px;
line-height: 16px;
font-family: Helvetica, sans-serif;
font-size: 11px;
color: #606060;
text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.8);
}
.NB-button.NB-share-button a {
color: #FFF;
text-shadow: none;
font-size: 14px;
}
.NB-button.selected a {
color: rgba(255,255,255,1);
text-shadow: none;
}

View file

@ -14,6 +14,14 @@ $('a.NB-show-profile').live('click', function () {
return false;
});
$('.NB-train-button a').live('click', function () {
var offset = $(this).offset();
console.log(offset);
var url = $(this).attr('href') + "/" + offset.left + "/" + (offset.top - window.pageYOffset) + "/" + offset.width + "/" + offset.height;
window.location = url;
return false;
});
$('.NB-button').live('touchstart', function () {
$(this).addClass('selected');
});

View file

@ -0,0 +1,262 @@
#trainer {
margin: 0;
padding: 0;
font-family: "Lucida Grande", Helvetica;
background-color: transparent;
}
.NB-trainer {
}
.NB-trainer-section {
font-size: 13px;
-webkit-text-size-adjust: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.NB-trainer-section-inner {
margin: 16px 0 0;
overflow: hidden;
box-shadow: 0 0 8px rgba(0, 0, 0, .6);
}
.NB-trainer-publisher .NB-trainer-section-inner {
margin-bottom: 16px;
}
.NB-trainer-section-title {
font-size: 15px;
text-transform: uppercase;
padding: 4px 12px;
background-color: #F0F8F2;
padding: 8px 12px 8px;
background-color: #E3ECF8;
background-image: -webkit-gradient(linear, left top, left bottom, from(#E3ECF8), to(#CBDBF5));
text-shadow: 0 1px 0 #F2F6FE;
border-top: 1px solid #D1DAF7;
border-bottom: 1px solid #A1A9CF;
color: #505050;
font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif, inherit;
font-weight: normal;
position: relative;
}
.NB-trainer-section-body {
padding: 12px 12px;
overflow: hidden;
background-color: #EFF3F6;
border-bottom: 1px solid #D0D0D9;
}
.NB-trainer-title .NB-trainer-section-body {
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-title-info {
text-transform: uppercase;
color: #C0C0C0;
font-weight: bold;
font-size: 12px;
padding: 2px 0 4px;
-webkit-user-select: none;
}
.NB-story-title-positive {
color: #58A353;
text-shadow: 0 1px 0 #E9F6E9;
}
.NB-story-title-negative {
color: #B85044;
text-shadow: 0 1px 0 #F6E9E9;
}
.NB-classifier-container {
white-space: nowrap;
float: left;
display: block;
}
/*
* Classifiers
*/
.NB-classifier {
white-space: nowrap;
float: left;
display: block;
margin: 2px 6px 6px 0;
cursor: pointer;
padding: 0 8px 0 26px;
font-size: 12px;
text-transform: uppercase;
background-color: #D8DEE2;
position: relative;
border: 1px solid transparent;
border-radius: 12px;
}
.NB-classifier input[type=checkbox] {
margin: 3px 6px 2px 4px;
cursor: pointer;
float: left;
}
.NB-classifier label {
cursor: pointer;
color: black;
float: left;
display: block;
padding: 2px 0;
}
.NB-classifier label b {
color: rgba(0,0,0,.4);
text-shadow: none;
font-weight: normal;
}
.NB-classifier label span {
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5);
}
.NB-classifier.NB-classifier-facet-disabled {
background-color: white;
}
.NB-classifier input {
display: none;
}
.NB-classifier .feed_favicon {
margin-top: -2px;
width: 16px;
height: 16px;
}
.NB-classifier .NB-classifier-icon-like {
width: 16px;
height: 16px;
background: transparent url('data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAUCAMAAACK2/weAAAAP1BMVEX////+/v79/f3x8fHw8PD76tb35M/14s3u3Mfl1MDaybbRwa+/v7++vr62tra1tbW9r560p5d6enp5eXkAAABfVS57AAAAFXRSTlMA//////////////////////////9QsPovAAAAU0lEQVR42rXNOQ6AMAxEUbPvJIS5/1kZO1gpkOjyCitfU0QchEoN+GYp5s+YQCKJB0zD592+YTMaptPsMJq89js2tS7zxDxOXCrmNQSmif6Rk3oecZYHCr5fF4MAAAAASUVORK5CYII=') no-repeat 0 0;
position: absolute;
left: 6px;
top: 2px;
opacity: .2;
}
.NB-classifier.NB-classifier-like .NB-classifier-icon-like {
opacity: 1;
}
.pressed .NB-classifier .NB-classifier-icon-like,
.pressed .NB-classifier.NB-classifier-dislike .NB-classifier-icon-like,
.pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-dislike {
opacity: .8;
}
.pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-like {
opacity: 0;
}
.pressed .NB-classifier.NB-classifier-dislike .NB-classifier-icon-like {
opacity: .2;
display: block;
}
.NB-classifier .NB-classifier-icon-dislike {
width: 27px;
height: 22px;
position: absolute;
top: -2px;
left: 2px;
background: transparent url('data:image/png;charset=utf-8;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAUCAMAAACK2/weAAAAWlBMVEX////+/v79/f3x8fHw8PD97Nj76tb66dX56NT35M/2487u3Mfl1MDaybbRwa/QwK+/v7++vr62tra1tbW+sJ+1qJi0p5ezppd7e3t6enp4eHgCAgIBAQEAAABkZKaJAAAAHnRSTlMA///////////////////////////////////////0OBo3AAAAaklEQVR42rXMyRKDMAwDUIe2kELLFhqWSP//myQmOfbIO3ik0YzlPjsgKEToHP1PLalOM1cfLTrjOXLoo++na2M1D7bvBCSFVcWmziA4DKxNWdcNkBCaC0QFa3d9nKHOscwv7f/m/Kag3hMwawmoH/Rk6gAAAABJRU5ErkJggg==') no-repeat 5px -1px;
display: none;
}
.NB-classifier .NB-classifier-icon-dislike-inner {
margin: 4px 4px 0 0;
width: 18px;
height: 13px;
}
.NB-classifier.NB-classifier-like {
background-color: #34912E;
border: 1px solid #202020;
-webkit-box-shadow: 1px 1px 1px #BDC0D7;
-moz-box-shadow: 1px 1px 1px #BDC0D7;
box-shadow: 1px 1px 1px #BDC0D7;
}
.NB-classifier.NB-classifier-dislike {
background-color: #A90103;
border: 1px solid #000;
-webkit-box-shadow: 1px 1px 1px #BDC0D7;
-moz-box-shadow: 1px 1px 1px #BDC0D7;
box-shadow: 1px 1px 1px #BDC0D7;
}
.NB-classifier.NB-classifier-dislike .NB-classifier-icon-like {
display: none;
}
.NB-classifier.NB-classifier-dislike .NB-classifier-icon-dislike {
display: block;
}
.NB-classifier.NB-classifier-like label b,
.pressed .NB-classifier label b {
color: white;
}
.NB-classifier.NB-classifier-like label span,
.pressed .NB-classifier label span {
color: white;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
}
.pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-like,
.pressed .NB-classifier.NB-classifier-dislike .NB-classifier-icon-dislike {
display: none;
}
.pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-dislike,
.pressed .NB-classifier.NB-classifier-like .NB-classifier-icon-like {
display: block;
}
.pressed .NB-classifier {
background-color: #54A54E;
border: 1px solid transparent;
box-shadow: none;
}
.pressed .NB-classifier.NB-classifier-like {
background-color: #C92123;
border: 1px solid transparent;
box-shadow: none;
}
.pressed .NB-classifier.NB-classifier-dislike {
border: 1px solid transparent;
background-color: #C6C6C6;
box-shadow: none;
}
.NB-classifier.NB-classifier-dislike label b,
.pressed .NB-classifier.NB-classifier-like label b {
color: white;
}
.NB-classifier.NB-classifier-dislike label span,
.pressed .NB-classifier.NB-classifier-like label span {
color: white;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5);
}
.NB-classifier-count {
font-size: 10px;
margin: 0 6px 0 -2px;
line-height: 26px;
color: #A0A0A0;
}

View file

@ -0,0 +1,20 @@
var Trainer = function() {
this.highlightTitle();
}
Trainer.prototype = {
highlightTitle: function() {
var $title = $(".NB-title").get(0);
}
};
Zepto(function($) {
new Trainer();
attachFastClick({
skipEvent: true
});
});

33
media/ios/static/zepto.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -90,7 +90,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
return;
}
NEWSBLUR.log(['AJAX Error', e, e.status, textStatus, errorThrown,
!!error_callback, error_callback]);
!!error_callback, error_callback, $.isFunction(callback)]);
if (error_callback) {
error_callback(e, textStatus, errorThrown);
@ -99,7 +99,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
if (NEWSBLUR.Globals.is_authenticated) {
message = "Sorry, there was an unhandled error.";
}
callback({'message': message});
callback({'message': message, status_code: e.status});
}
}
}, options));
@ -706,7 +706,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
social_feed.set(feed);
}, this));
callback && callback();
callback && callback(data);
},
refresh_feed: function(feed_id, callback) {
@ -1172,8 +1172,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
'feed_link': feed_link
}, function(data) {
// NEWSBLUR.log(['save_exception_change_feed_link pre_callback', feed_id, feed_link, data]);
if (data.code < 0 || data.status_code != 200) {
return callback(data);
}
self.post_refresh_feeds(data, callback);
NEWSBLUR.reader.force_feed_refresh(feed_id, data.new_feed_id);
}, error_callback);
} else {
if ($.isFunction(callback)) callback();
@ -1189,8 +1191,10 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
'feed_address': feed_address
}, function(data) {
// NEWSBLUR.log(['save_exception_change_feed_address pre_callback', feed_id, feed_address, data]);
if (data.code < 0 || data.status_code != 200) {
return callback(data);
}
self.post_refresh_feeds(data, callback);
NEWSBLUR.reader.force_feed_refresh(feed_id, data.new_feed_id);
}, error_callback);
} else {
if ($.isFunction(callback)) callback();

View file

@ -1735,6 +1735,7 @@
message = "NewsBlur is down right now. <br> Try again soon.";
} else if (data.status == 503) {
message = "NewsBlur is in maintenace mode. <br> Try again soon.";
this.show_maintenance_page();
}
}
var $error = $.make('div', { className: 'NB-feed-error' }, [
@ -1768,6 +1769,10 @@
});
},
show_maintenance_page: function() {
this.switch_taskbar_view('page', {skip_save_type: 'maintenance'});
},
// ==========================
// = Story Pane - All Views =
// ==========================
@ -1858,6 +1863,9 @@
if (story_id && feed_id) {
options['feed_loaded'] = !this.flags['river_view'];
if (this.flags['social_view']) {
options['feed_loaded'] = true;
}
if (this.flags['social_view'] && !_.string.contains(this.active_feed, 'river:')) {
options['social_feed_id'] = this.active_feed;
} else if (this.flags['social_view'] && story.get('friend_user_ids')) {
@ -3493,6 +3501,21 @@
this.slide_intelligence_slider(unread_view, true);
},
toggle_focus_in_slider: function() {
var $slider = this.$s.$intelligence_slider;
var $focus = $(".NB-intelligence-slider-green", $slider);
var show_focus = NEWSBLUR.assets.feeds.any(function(feed) {
return feed.get('ps');
});
$focus.css('display', show_focus ? 'block' : 'none');
if (!show_focus) {
if (NEWSBLUR.assets.preference('unread_view') > 0) {
this.slide_intelligence_slider(0);
}
}
},
slide_intelligence_slider: function(value, initial_load) {
var $slider = this.$s.$intelligence_slider;
var real_value = value;
@ -3745,7 +3768,8 @@
if (this.socket && !this.socket.socket.connected) {
this.socket.socket.connect();
} else if (force || !this.socket || !this.socket.socket.connected) {
var server = window.location.protocol + '//' + window.location.hostname + ':8888';
var port = _.string.startsWith(window.location.protocol, 'https') ? 8889 : 8888;
var server = window.location.protocol + '//' + window.location.hostname + ':' + port;
this.socket = this.socket || io.connect(server);
// this.socket.refresh_feeds = _.debounce(_.bind(this.force_feeds_refresh, this), 1000*10);
@ -3864,8 +3888,8 @@
force_feed_refresh: function(feed_id, new_feed_id) {
var self = this;
feed_id = feed_id || this.active_feed;
new_feed_id = new_feed_id || feed_id;
new_feed_id = _.isNumber(new_feed_id) && new_feed_id || feed_id;
console.log(["force_feed_refresh", feed_id, new_feed_id]);
this.force_feeds_refresh(function() {
// Open the feed back up if it is being refreshed and is still open.
if (self.active_feed == feed_id || self.active_feed == new_feed_id) {
@ -3885,7 +3909,7 @@
this.flags['pause_feed_refreshing'] = true;
this.model.refresh_feeds(_.bind(function(updated_feeds) {
this.post_feed_refresh(updated_feeds, replace_active_feed, feed_id);
this.post_feed_refresh(updated_feeds, replace_active_feed, feed_id);
}, this), this.flags['has_unfetched_feeds'], feed_id, error_callback);
},
@ -3900,6 +3924,7 @@
this.flags['refresh_inline_feed_delay'] = false;
this.flags['pause_feed_refreshing'] = false;
this.check_feed_fetch_progress();
this.toggle_focus_in_slider();
},
feed_unread_count: function(feed_id) {

View file

@ -227,7 +227,7 @@ _.extend(NEWSBLUR.ReaderAddFeed.prototype, {
$loading.addClass('NB-active');
$submit.addClass('NB-disabled').val('Adding...');
this.model.save_add_url(url, folder, $.rescope(this.post_save_add_url, this));
this.model.save_add_url(url, folder, $.rescope(this.post_save_add_url, this), $.rescope(this.error, this));
},
post_save_add_url: function(e, data) {
@ -250,13 +250,18 @@ _.extend(NEWSBLUR.ReaderAddFeed.prototype, {
this.model.preference('has_setup_feeds', true);
NEWSBLUR.reader.check_hide_getting_started();
} else {
var $error = $('.NB-error', '.NB-fieldset.NB-add-add-url');
$error.text(data.message);
$error.slideDown(300);
$submit.val('Add Site');
this.error(data);
}
},
error: function(data) {
var $submit = $('.NB-add-add-url input[type=submit]', this.$modal);
var $error = $('.NB-error', '.NB-fieldset.NB-add-add-url');
$error.text(data.message || "Oh no, there was a problem grabbing that URL and there's no good explanation for what happened.");
$error.slideDown(300);
$submit.val('Add Site');
},
save_add_folder: function() {
var $submit = $('.NB-add-add-folder input[type=submit]', this.$modal);
var $error = $('.NB-error', '.NB-fieldset.NB-add-add-folder');

View file

@ -52,7 +52,8 @@ NEWSBLUR.ReaderClassifierStory = function(story_id, feed_id, options) {
this.cache = {};
this.story_id = story_id;
this.feed_id = feed_id;
if (options.social_feed_id) this.feed_id = options.social_feed_id;
this.original_feed_id = feed_id;
// if (options.social_feed_id) this.feed_id = options.social_feed_id;
this.trainer_iterator = -1;
this.options = $.extend({}, defaults, options);
this.model = NEWSBLUR.assets;
@ -885,7 +886,9 @@ var classifier_prototype = {
var $save = $('.NB-modal-submit-save', this.$modal);
var data = this.serialize_classifier();
var feed_id = this.feed_id;
if (this.options.social_feed && this.story_id) {
feed_id = this.original_feed_id;
}
if (this.options['training']) {
this.cache[this.feed_id] = this.$modal.clone();
@ -899,7 +902,7 @@ var classifier_prototype = {
NEWSBLUR.assets.recalculate_story_scores(feed_id);
this.model.save_classifier(data, function() {
if (!keep_modal_open) {
NEWSBLUR.reader.force_feeds_refresh(null, true);
NEWSBLUR.reader.feed_unread_count(feed_id);
// NEWSBLUR.reader.force_feed_refresh();
// NEWSBLUR.reader.open_feed(self.feed_id, true);
// TODO: Update counts in active feed.

View file

@ -59,9 +59,15 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
settings_fn.call(this.model, this.feed_id, _.bind(this.populate_settings, this));
},
populate_settings: function() {
populate_settings: function(data) {
var $submit = $('.NB-modal-submit-save', this.$modal);
var $loading = $('.NB-modal-loading', this.$modal);
var $page_history = $(".NB-exception-page-history", this.$modal);
var $feed_history = $(".NB-exception-feed-history", this.$modal);
$feed_history.html(this.make_history(data, 'feed_fetch'));
$page_history.html(this.make_history(data, 'page_fetch'));
$loading.removeClass('NB-active');
this.resize();
},
@ -148,7 +154,8 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
]),
(!this.options.social_feed && $.make('div', { className: 'NB-exception-submit-wrapper' }, [
$.make('input', { type: 'submit', value: 'Parse this RSS/XML Feed', className: 'NB-modal-submit-green NB-modal-submit-address' }),
$.make('div', { className: 'NB-error' })
$.make('div', { className: 'NB-error' }),
$.make('div', { className: 'NB-exception-feed-history' })
]))
])
]),
@ -169,7 +176,8 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
]),
(!this.options.social_feed && $.make('div', { className: 'NB-exception-submit-wrapper' }, [
$.make('input', { type: 'submit', value: 'Fetch Feed From Website', className: 'NB-modal-submit-green NB-modal-submit-link' }),
$.make('div', { className: 'NB-error' })
$.make('div', { className: 'NB-error' }),
$.make('div', { className: 'NB-exception-page-history' })
]))
])
]),
@ -189,6 +197,27 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
]);
},
make_history: function(data, fetch_type) {
var fetches = data[fetch_type+'_history'];
var $history;
if (fetches && fetches.length) {
$history = _.map(fetches, function(fetch) {
var feed_ok = _.contains([200, 304], fetch.status_code) || !fetch.status_code;
var status_class = feed_ok ? ' NB-ok ' : ' NB-errorcode ';
return $.make('div', { className: 'NB-history-fetch' + status_class, title: feed_ok ? '' : fetch.exception }, [
$.make('div', { className: 'NB-history-fetch-date' }, fetch.fetch_date || fetch.push_date),
$.make('div', { className: 'NB-history-fetch-message' }, [
fetch.message,
(fetch.status_code && $.make('div', { className: 'NB-history-fetch-code' }, ' ('+fetch.status_code+')'))
])
]);
});
}
return $.make('div', $history);
},
show_recommended_options_meta: function() {
var $meta_retry = $('.NB-exception-option-retry .NB-exception-option-meta', this.$modal);
var $meta_page = $('.NB-exception-option-page .NB-exception-option-meta', this.$modal);
@ -264,24 +293,25 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
$error.hide().html('');
if (feed_address.length) {
this.model.save_exception_change_feed_address(feed_id, feed_address, function(code) {
NEWSBLUR.reader.force_feed_refresh(feed_id);
$.modal.close();
}, function(data) {
if (data.new_feed_id) {
this.model.save_exception_change_feed_address(feed_id, feed_address, _.bind(function(data) {
console.log(["return to change address", data]);
if (data && data.new_feed_id) {
NEWSBLUR.reader.force_feed_refresh(feed_id, data.new_feed_id);
}
var feed = NEWSBLUR.assets.get_feed(data.new_feed_id);
var feed = NEWSBLUR.assets.get_feed(data.new_feed_id || feed_id);
console.log(["feed address", feed, NEWSBLUR.assets.get_feed(feed_id)]);
var error = "There was a problem fetching the feed from this URL.";
if (parseInt(feed.get('exception_code'), 10) == 404) {
error = "URL gives a 404 - page not found.";
if (!data || data.code < 0 || !data.new_feed_id) {
var error = data.message || "There was a problem fetching the feed from this URL.";
if (parseInt(feed.get('exception_code'), 10) == 404) {
error = "URL gives a 404 - page not found.";
}
$error.show().html((data && data.message) || error);
}
$error.show().html((data && data.message) || error);
$loading.removeClass('NB-active');
$submit.removeClass('NB-disabled').attr('value', 'Parse this RSS/XML Feed');
});
this.populate_settings(data);
}, this));
}
},
@ -298,23 +328,24 @@ _.extend(NEWSBLUR.ReaderFeedException.prototype, {
$error.hide().html('');
if (feed_link.length) {
this.model.save_exception_change_feed_link(feed_id, feed_link, function(code) {
NEWSBLUR.reader.force_feed_refresh(feed_id);
$.modal.close();
}, function(data) {
this.model.save_exception_change_feed_link(feed_id, feed_link, _.bind(function(data) {
if (data.new_feed_id) {
NEWSBLUR.reader.force_feed_refresh(feed_id, data.new_feed_id);
}
var feed = NEWSBLUR.assets.get_feed(data.new_feed_id);
var error = "There was a problem fetching the feed from this URL.";
if (feed.get('exception_code') == '404') {
error = "URL gives a 404 - page not found.";
if (!data || data.code < 0 || !data.new_feed_id) {
var error = data.message || "There was a problem fetching the feed from this URL.";
if (feed.get('exception_code') == '404') {
error = "URL gives a 404 - page not found.";
}
$error.show().html((data && data.message) || error);
}
$error.show().html((data && data.message) || error);
$loading.removeClass('NB-active');
$submit.removeClass('NB-disabled').attr('value', 'Fetch Feed from Website');
});
this.populate_settings(data);
}, this));
}
},

View file

@ -221,16 +221,16 @@ _.extend(NEWSBLUR.ReaderStatistics.prototype, {
var $history;
if (!fetches || !fetches.length) {
$history = $.make('div', { className: 'NB-statistics-history-empty' }, "Nothing recorded.");
$history = $.make('div', { className: 'NB-history-empty' }, "Nothing recorded.");
} else {
$history = _.map(fetches, function(fetch) {
var feed_ok = _.contains([200, 304], fetch.status_code) || !fetch.status_code;
var status_class = feed_ok ? ' NB-ok ' : ' NB-errorcode ';
return $.make('div', { className: 'NB-statistics-history-fetch' + status_class, title: feed_ok ? '' : fetch.exception }, [
$.make('div', { className: 'NB-statistics-history-fetch-date' }, fetch.fetch_date || fetch.push_date),
$.make('div', { className: 'NB-statistics-history-fetch-message' }, [
return $.make('div', { className: 'NB-history-fetch' + status_class, title: feed_ok ? '' : fetch.exception }, [
$.make('div', { className: 'NB-history-fetch-date' }, fetch.fetch_date || fetch.push_date),
$.make('div', { className: 'NB-history-fetch-message' }, [
fetch.message,
(fetch.status_code && $.make('div', { className: 'NB-statistics-history-fetch-code' }, ' ('+fetch.status_code+')'))
(fetch.status_code && $.make('div', { className: 'NB-history-fetch-code' }, ' ('+fetch.status_code+')'))
])
]);
});

View file

@ -101,6 +101,7 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
if (!this.options.feed_chooser) {
_.defer(_.bind(function() {
NEWSBLUR.reader.open_dialog_after_feeds_loaded();
NEWSBLUR.reader.toggle_focus_in_slider();
this.selected();
if (NEWSBLUR.reader.socket) {
NEWSBLUR.reader.send_socket_active_feeds();

View file

@ -458,6 +458,7 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({
var options = {};
if (NEWSBLUR.reader.flags['social_view']) {
options['social_feed'] = true;
options['feed_loaded'] = true;
}
NEWSBLUR.reader.open_story_trainer(this.model.id, feed_id, options);
},

View file

@ -1,13 +1,22 @@
express = require 'express'
mongo = require 'mongodb'
MONGODB_SERVER = if process.env.NODE_ENV == 'development' then 'localhost' else 'db04'
DEV = process.env.NODE_ENV == 'development'
MONGODB_SERVER = if DEV then 'localhost' else 'db04'
MONGODB_PORT = parseInt(process.env.MONGODB_PORT or 27017, 10)
server = new mongo.Server(MONGODB_SERVER, MONGODB_PORT,
auto_reconnect: true
poolSize: 12)
db = new mongo.Db('newsblur', server)
if DEV
server = new mongo.Server(MONGODB_SERVER, MONGODB_PORT,
auto_reconnect: true
poolSize: 12)
else
server = new mongo.ReplSetServers(
[new mongo.Server( MONGODB_SERVER, MONGODB_PORT, { auto_reconnect: true } )]
{rs_name: 'nbset'})
db = new mongo.Db('newsblur', server,
readPreference: mongo.ReadPreference.SECONDARY_PREFERRED
safe: false)
app = express.createServer()
app.use express.bodyParser()

View file

@ -1,22 +1,37 @@
// Generated by CoffeeScript 1.4.0
(function() {
var MONGODB_PORT, MONGODB_SERVER, app, db, express, mongo, server,
var DEV, MONGODB_PORT, MONGODB_SERVER, app, db, express, mongo, server,
_this = this;
express = require('express');
mongo = require('mongodb');
MONGODB_SERVER = process.env.NODE_ENV === 'development' ? 'localhost' : 'db04';
DEV = process.env.NODE_ENV === 'development';
MONGODB_SERVER = DEV ? 'localhost' : 'db04';
MONGODB_PORT = parseInt(process.env.MONGODB_PORT || 27017, 10);
server = new mongo.Server(MONGODB_SERVER, MONGODB_PORT, {
auto_reconnect: true,
poolSize: 12
});
if (DEV) {
server = new mongo.Server(MONGODB_SERVER, MONGODB_PORT, {
auto_reconnect: true,
poolSize: 12
});
} else {
server = new mongo.ReplSetServers([
new mongo.Server(MONGODB_SERVER, MONGODB_PORT, {
auto_reconnect: true
})
], {
rs_name: 'nbset'
});
}
db = new mongo.Db('newsblur', server);
db = new mongo.Db('newsblur', server, {
readPreference: mongo.ReadPreference.SECONDARY_PREFERRED,
safe: false
});
app = express.createServer();

View file

@ -1,5 +1,5 @@
language: node_js
node_js:
- 0.4
- 0.6
- 0.7 # development version of 0.8, may be unstable
- 0.8
- 0.9 # development version of 0.8, may be unstable

23
node/node_modules/mongodb/CONTRIBUTING.md generated vendored Normal file
View file

@ -0,0 +1,23 @@
## Contributing to the driver
### Bugfixes
- Before starting to write code, look for existing [tickets](https://github.com/mongodb/node-mongodb-native/issues) or [create one](https://github.com/mongodb/node-mongodb-native/issues/new) for your specific issue. That way you avoid working on something that might not be of interest or that has been addressed already in a different branch.
- Fork the [repo](https://github.com/mongodb/node-mongodb-native) _or_ for small documentation changes, navigate to the source on github and click the [Edit](https://github.com/blog/844-forking-with-the-edit-button) button.
- Follow the general coding style of the rest of the project:
- 2 space tabs
- no trailing whitespace
- comma last
- inline documentation for new methods, class members, etc
- 0 space between conditionals/functions, and their parenthesis and curly braces
- `if(..) {`
- `for(..) {`
- `while(..) {`
- `function(err) {`
- Write tests and make sure they pass (execute `make test` from the cmd line to run the test suite).
### Documentation
To contribute to the [API documentation](http://mongodb.github.com/node-mongodb-native/) just make your changes to the inline documentation of the appropriate [source code](https://github.com/mongodb/node-mongodb-native/tree/master/docs) in the master branch and submit a [pull request](https://help.github.com/articles/using-pull-requests/). You might also use the github [Edit](https://github.com/blog/844-forking-with-the-edit-button) button.
If you'd like to preview your documentation changes, first commit your changes to your local master branch, then execute `make generate_docs`. Make sure you have the python documentation framework sphinx installed `easy_install sphinx`. The docs are generated under `docs/build'. If all looks good, submit a [pull request](https://help.github.com/articles/using-pull-requests/) to the master branch with your changes.

31
node/node_modules/mongodb/Makefile generated vendored
View file

@ -6,20 +6,12 @@ name = all
total: build_native
test-coverage:
rm -rf lib-cov/
jscoverage lib/ lib-cov/
@TEST_COVERAGE=true nodeunit test/ test/gridstore test/connection
build_native:
# $(MAKE) -C ./external-libs/bson all
build_native_debug:
$(MAKE) -C ./external-libs/bson all_debug
build_native_clang:
$(MAKE) -C ./external-libs/bson clang
build_native_clang_debug:
$(MAKE) -C ./external-libs/bson clang_debug
clean_native:
$(MAKE) -C ./external-libs/bson clean
test: build_native
@echo "\n == Run All tests minus replicaset tests=="
@ -27,26 +19,27 @@ test: build_native
test_pure: build_native
@echo "\n == Run All tests minus replicaset tests=="
$(NODE) dev/tools/test_all.js --noreplicaset --boot --noactive
$(NODE) dev/tools/test_all.js --noreplicaset --boot --nonative
test_junit: build_native
@echo "\n == Run All tests minus replicaset tests=="
$(NODE) dev/tools/test_all.js --junit --noreplicaset
$(NODE) dev/tools/test_all.js --junit --noreplicaset --nokill
jenkins: build_native
@echo "\n == Run All tests minus replicaset tests=="
$(NODE) dev/tools/test_all.js --junit --noreplicaset --nokill
test_nodeunit_pure:
@echo "\n == Execute Test Suite using Pure JS BSON Parser == "
@$(NODEUNIT) test/ test/gridstore test/bson
test_js:
@$(NODEUNIT) $(TESTS)
test_nodeunit_replicaset_pure:
@echo "\n == Execute Test Suite using Pure JS BSON Parser == "
@$(NODEUNIT) test/replicaset
test_nodeunit_native:
@echo "\n == Execute Test Suite using Native BSON Parser == "
@TEST_NATIVE=TRUE $(NODEUNIT) test/ test/gridstore test/bson
@TEST_NATIVE=TRUE $(NODEUNIT) test/ test/gridstore test/bson
test_nodeunit_replicaset_native:
@echo "\n == Execute Test Suite using Native BSON Parser == "

442
node/node_modules/mongodb/Readme.md generated vendored Normal file
View file

@ -0,0 +1,442 @@
Up to date documentation
========================
[Documentation](http://mongodb.github.com/node-mongodb-native/)
Install
=======
To install the most recent release from npm, run:
npm install mongodb
That may give you a warning telling you that bugs['web'] should be bugs['url'], it would be safe to ignore it (this has been fixed in the development version)
To install the latest from the repository, run::
npm install path/to/node-mongodb-native
Community
=========
Check out the google group [node-mongodb-native](http://groups.google.com/group/node-mongodb-native) for questions/answers from users of the driver.
Try it live
============
<a href="https://runnable.com/#mongodb/node-mongodb-native/server.js/launch" target="_blank"><img src="https://runnable.com/external/styles/assets/runnablebtn.png" style="width:67px;height:25px;"></a>
Introduction
============
This is a node.js driver for MongoDB. It's a port (or close to a port) of the library for ruby at http://github.com/mongodb/mongo-ruby-driver/.
A simple example of inserting a document.
```javascript
var client = new Db('test', new Server("127.0.0.1", 27017, {}), {w: 1}),
test = function (err, collection) {
collection.insert({a:2}, function(err, docs) {
collection.count(function(err, count) {
test.assertEquals(1, count);
});
// Locate all the entries using find
collection.find().toArray(function(err, results) {
test.assertEquals(1, results.length);
test.assertTrue(results[0].a === 2);
// Let's close the db
client.close();
});
});
};
client.open(function(err, p_client) {
client.collection('test_insert', test);
});
```
Data types
==========
To store and retrieve the non-JSON MongoDb primitives ([ObjectID](http://www.mongodb.org/display/DOCS/Object+IDs), Long, Binary, [Timestamp](http://www.mongodb.org/display/DOCS/Timestamp+data+type), [DBRef](http://www.mongodb.org/display/DOCS/Database+References#DatabaseReferences-DBRef), Code).
In particular, every document has a unique `_id` which can be almost any type, and by default a 12-byte ObjectID is created. ObjectIDs can be represented as 24-digit hexadecimal strings, but you must convert the string back into an ObjectID before you can use it in the database. For example:
```javascript
// Get the objectID type
var ObjectID = require('mongodb').ObjectID;
var idString = '4e4e1638c85e808431000003';
collection.findOne({_id: new ObjectID(idString)}, console.log) // ok
collection.findOne({_id: idString}, console.log) // wrong! callback gets undefined
```
Here are the constructors the non-Javascript BSON primitive types:
```javascript
// Fetch the library
var mongo = require('mongodb');
// Create new instances of BSON types
new mongo.Long(numberString)
new mongo.ObjectID(hexString)
new mongo.Timestamp() // the actual unique number is generated on insert.
new mongo.DBRef(collectionName, id, dbName)
new mongo.Binary(buffer) // takes a string or Buffer
new mongo.Code(code, [context])
new mongo.Symbol(string)
new mongo.MinKey()
new mongo.MaxKey()
new mongo.Double(number) // Force double storage
```
The C/C++ bson parser/serializer
--------------------------------
If you are running a version of this library has the C/C++ parser compiled, to enable the driver to use the C/C++ bson parser pass it the option native_parser:true like below
```javascript
// using native_parser:
var client = new Db('integration_tests_20',
new Server("127.0.0.1", 27017),
{native_parser:true});
```
The C++ parser uses the js objects both for serialization and deserialization.
GitHub information
==================
The source code is available at http://github.com/mongodb/node-mongodb-native.
You can either clone the repository or download a tarball of the latest release.
Once you have the source you can test the driver by running
$ make test
in the main directory. You will need to have a mongo instance running on localhost for the integration tests to pass.
Examples
========
For examples look in the examples/ directory. You can execute the examples using node.
$ cd examples
$ node queries.js
GridStore
=========
The GridStore class allows for storage of binary files in mongoDB using the mongoDB defined files and chunks collection definition.
For more information have a look at [Gridstore](https://github.com/mongodb/node-mongodb-native/blob/master/docs/gridfs.md)
Replicasets
===========
For more information about how to connect to a replicaset have a look at [Replicasets](https://github.com/mongodb/node-mongodb-native/blob/master/docs/replicaset.md)
Primary Key Factories
---------------------
Defining your own primary key factory allows you to generate your own series of id's
(this could f.ex be to use something like ISBN numbers). The generated the id needs to be a 12 byte long "string".
Simple example below
```javascript
// Custom factory (need to provide a 12 byte array);
CustomPKFactory = function() {}
CustomPKFactory.prototype = new Object();
CustomPKFactory.createPk = function() {
return new ObjectID("aaaaaaaaaaaa");
}
var p_client = new Db('integration_tests_20', new Server("127.0.0.1", 27017, {}), {'pk':CustomPKFactory});
p_client.open(function(err, p_client) {
p_client.dropDatabase(function(err, done) {
p_client.createCollection('test_custom_key', function(err, collection) {
collection.insert({'a':1}, function(err, docs) {
collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) {
cursor.toArray(function(err, items) {
test.assertEquals(1, items.length);
// Let's close the db
p_client.close();
});
});
});
});
});
});
```
Strict mode
-----------
Each database has an optional strict mode. If it is set then asking for a collection
that does not exist will return an Error object in the callback. Similarly if you
attempt to create a collection that already exists. Strict is provided for convenience.
```javascript
var error_client = new Db('integration_tests_', new Server("127.0.0.1", 27017, {auto_reconnect: false}), {strict:true});
test.assertEquals(true, error_client.strict);
error_client.open(function(err, error_client) {
error_client.collection('does-not-exist', function(err, collection) {
test.assertTrue(err instanceof Error);
test.assertEquals("Collection does-not-exist does not exist. Currently in strict mode.", err.message);
});
error_client.createCollection('test_strict_access_collection', function(err, collection) {
error_client.collection('test_strict_access_collection', function(err, collection) {
test.assertTrue(collection instanceof Collection);
// Let's close the db
error_client.close();
});
});
});
```
Documentation
=============
If this document doesn't answer your questions, see the source of
[Collection](https://github.com/mongodb/node-mongodb-native/blob/master/lib/mongodb/collection.js)
or [Cursor](https://github.com/mongodb/node-mongodb-native/blob/master/lib/mongodb/cursor.js),
or the documentation at MongoDB for query and update formats.
Find
----
The find method is actually a factory method to create
Cursor objects. A Cursor lazily uses the connection the first time
you call `nextObject`, `each`, or `toArray`.
The basic operation on a cursor is the `nextObject` method
that fetches the next matching document from the database. The convenience
methods `each` and `toArray` call `nextObject` until the cursor is exhausted.
Signatures:
```javascript
var cursor = collection.find(query, [fields], options);
cursor.sort(fields).limit(n).skip(m).
cursor.nextObject(function(err, doc) {});
cursor.each(function(err, doc) {});
cursor.toArray(function(err, docs) {});
cursor.rewind() // reset the cursor to its initial state.
```
Useful chainable methods of cursor. These can optionally be options of `find` instead of method calls:
* `.limit(n).skip(m)` to control paging.
* `.sort(fields)` Order by the given fields. There are several equivalent syntaxes:
* `.sort({field1: -1, field2: 1})` descending by field1, then ascending by field2.
* `.sort([['field1', 'desc'], ['field2', 'asc']])` same as above
* `.sort([['field1', 'desc'], 'field2'])` same as above
* `.sort('field1')` ascending by field1
Other options of `find`:
* `fields` the fields to fetch (to avoid transferring the entire document)
* `tailable` if true, makes the cursor [tailable](http://www.mongodb.org/display/DOCS/Tailable+Cursors).
* `batchSize` The number of the subset of results to request the database
to return for every request. This should initially be greater than 1 otherwise
the database will automatically close the cursor. The batch size can be set to 1
with `batchSize(n, function(err){})` after performing the initial query to the database.
* `hint` See [Optimization: hint](http://www.mongodb.org/display/DOCS/Optimization#Optimization-Hint).
* `explain` turns this into an explain query. You can also call
`explain()` on any cursor to fetch the explanation.
* `snapshot` prevents documents that are updated while the query is active
from being returned multiple times. See more
[details about query snapshots](http://www.mongodb.org/display/DOCS/How+to+do+Snapshotted+Queries+in+the+Mongo+Database).
* `timeout` if false, asks MongoDb not to time out this cursor after an
inactivity period.
For information on how to create queries, see the
[MongoDB section on querying](http://www.mongodb.org/display/DOCS/Querying).
```javascript
var mongodb = require('mongodb');
var server = new mongodb.Server("127.0.0.1", 27017, {});
new mongodb.Db('test', server, {}).open(function (error, client) {
if (error) throw error;
var collection = new mongodb.Collection(client, 'test_collection');
collection.find({}, {limit:10}).toArray(function(err, docs) {
console.dir(docs);
});
});
```
Insert
------
Signature:
```javascript
collection.insert(docs, options, [callback]);
```
where `docs` can be a single document or an array of documents.
Useful options:
* `safe:true` Should always set if you have a callback.
See also: [MongoDB docs for insert](http://www.mongodb.org/display/DOCS/Inserting).
```javascript
var mongodb = require('mongodb');
var server = new mongodb.Server("127.0.0.1", 27017, {});
new mongodb.Db('test', server, {w: 1}).open(function (error, client) {
if (error) throw error;
var collection = new mongodb.Collection(client, 'test_collection');
collection.insert({hello: 'world'}, {safe:true},
function(err, objects) {
if (err) console.warn(err.message);
if (err && err.message.indexOf('E11000 ') !== -1) {
// this _id was already inserted in the database
}
});
});
```
Note that there's no reason to pass a callback to the insert or update commands
unless you use the `safe:true` option. If you don't specify `safe:true`, then
your callback will be called immediately.
Update; update and insert (upsert)
----------------------------------
The update operation will update the first document that matches your query
(or all documents that match if you use `multi:true`).
If `safe:true`, `upsert` is not set, and no documents match, your callback will return 0 documents updated.
See the [MongoDB docs](http://www.mongodb.org/display/DOCS/Updating) for
the modifier (`$inc`, `$set`, `$push`, etc.) formats.
Signature:
```javascript
collection.update(criteria, objNew, options, [callback]);
```
Useful options:
* `safe:true` Should always set if you have a callback.
* `multi:true` If set, all matching documents are updated, not just the first.
* `upsert:true` Atomically inserts the document if no documents matched.
Example for `update`:
```javascript
var mongodb = require('mongodb');
var server = new mongodb.Server("127.0.0.1", 27017, {});
new mongodb.Db('test', server, {w: 1}).open(function (error, client) {
if (error) throw error;
var collection = new mongodb.Collection(client, 'test_collection');
collection.update({hi: 'here'}, {$set: {hi: 'there'}}, {safe:true},
function(err) {
if (err) console.warn(err.message);
else console.log('successfully updated');
});
});
```
Find and modify
---------------
`findAndModify` is like `update`, but it also gives the updated document to
your callback. But there are a few key differences between findAndModify and
update:
1. The signatures differ.
2. You can only findAndModify a single item, not multiple items.
Signature:
```javascript
collection.findAndModify(query, sort, update, options, callback)
```
The sort parameter is used to specify which object to operate on, if more than
one document matches. It takes the same format as the cursor sort (see
Connection.find above).
See the
[MongoDB docs for findAndModify](http://www.mongodb.org/display/DOCS/findAndModify+Command)
for more details.
Useful options:
* `remove:true` set to a true to remove the object before returning
* `new:true` set to true if you want to return the modified object rather than the original. Ignored for remove.
* `upsert:true` Atomically inserts the document if no documents matched.
Example for `findAndModify`:
```javascript
var mongodb = require('mongodb');
var server = new mongodb.Server("127.0.0.1", 27017, {});
new mongodb.Db('test', server, {w: 1}).open(function (error, client) {
if (error) throw error;
var collection = new mongodb.Collection(client, 'test_collection');
collection.findAndModify({hello: 'world'}, [['_id','asc']], {$set: {hi: 'there'}}, {},
function(err, object) {
if (err) console.warn(err.message);
else console.dir(object); // undefined if no matching object exists.
});
});
```
Save
----
The `save` method is a shorthand for upsert if the document contains an
`_id`, or an insert if there is no `_id`.
Sponsors
========
Just as Felix Geisendörfer I'm also working on the driver for my own startup and this driver is a big project that also benefits other companies who are using MongoDB.
If your company could benefit from a even better-engineered node.js mongodb driver I would appreciate any type of sponsorship you may be able to provide. All the sponsors will get a lifetime display in this readme, priority support and help on problems and votes on the roadmap decisions for the driver. If you are interested contact me on [christkv AT g m a i l.com](mailto:christkv@gmail.com) for details.
And I'm very thankful for code contributions. If you are interested in working on features please contact me so we can discuss API design and testing.
Release Notes
=============
See HISTORY
Credits
=======
1. [10gen](http://github.com/mongodb/mongo-ruby-driver/)
2. [Google Closure Library](http://code.google.com/closure/library/)
3. [Jonas Raoni Soares Silva](http://jsfromhell.com/classes/binary-parser)
Contributors
============
Aaron Heckmann, Christoph Pojer, Pau Ramon Revilla, Nathan White, Emmerman, Seth LaForge, Boris Filipov, Stefan Schärmeli, Tedde Lundgren, renctan, Sergey Ukustov, Ciaran Jessup, kuno, srimonti, Erik Abele, Pratik Daga, Slobodan Utvic, Kristina Chodorow, Yonathan Randolph, Brian Noguchi, Sam Epstein, James Harrison Fisher, Vladimir Dronnikov, Ben Hockey, Henrik Johansson, Simon Weare, Alex Gorbatchev, Shimon Doodkin, Kyle Mueller, Eran Hammer-Lahav, Marcin Ciszak, François de Metz, Vinay Pulim, nstielau, Adam Wiggins, entrinzikyl, Jeremy Selier, Ian Millington, Public Keating, andrewjstone, Christopher Stott, Corey Jewett, brettkiefer, Rob Holland, Senmiao Liu, heroic, gitfy
License
=======
Copyright 2009 - 2012 Christian Amor Kvalheim.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,45 +0,0 @@
NODE = node
name = all
JOBS = 1
all:
rm -rf build .lock-wscript bson.node
node-waf configure build
cp -R ./build/Release/bson.node . || true
@$(NODE) --expose-gc test/test_bson.js
@$(NODE) --expose-gc test/test_full_bson.js
# @$(NODE) --expose-gc test/test_stackless_bson.js
all_debug:
rm -rf build .lock-wscript bson.node
node-waf --debug configure build
cp -R ./build/Release/bson.node . || true
@$(NODE) --expose-gc test/test_bson.js
@$(NODE) --expose-gc test/test_full_bson.js
# @$(NODE) --expose-gc test/test_stackless_bson.js
test:
@$(NODE) --expose-gc test/test_bson.js
@$(NODE) --expose-gc test/test_full_bson.js
# @$(NODE) --expose-gc test/test_stackless_bson.js
clang:
rm -rf build .lock-wscript bson.node
CXX=clang node-waf configure build
cp -R ./build/Release/bson.node . || true
@$(NODE) --expose-gc test/test_bson.js
@$(NODE) --expose-gc test/test_full_bson.js
# @$(NODE) --expose-gc test/test_stackless_bson.js
clang_debug:
rm -rf build .lock-wscript bson.node
CXX=clang node-waf --debug configure build
cp -R ./build/Release/bson.node . || true
@$(NODE) --expose-gc test/test_bson.js
@$(NODE) --expose-gc test/test_full_bson.js
# @$(NODE) --expose-gc test/test_stackless_bson.js
clean:
rm -rf build .lock-wscript bson.node
.PHONY: all

File diff suppressed because it is too large Load diff

View file

@ -1,105 +0,0 @@
#ifndef BSON_H_
#define BSON_H_
#include <node.h>
#include <node_object_wrap.h>
#include <v8.h>
using namespace v8;
using namespace node;
class BSON : public ObjectWrap {
public:
BSON() : ObjectWrap() {}
~BSON() {}
static void Initialize(Handle<Object> target);
static Handle<Value> BSONDeserializeStream(const Arguments &args);
// JS based objects
static Handle<Value> BSONSerialize(const Arguments &args);
static Handle<Value> BSONDeserialize(const Arguments &args);
// Calculate size of function
static Handle<Value> CalculateObjectSize(const Arguments &args);
static Handle<Value> SerializeWithBufferAndIndex(const Arguments &args);
// Experimental
static Handle<Value> CalculateObjectSize2(const Arguments &args);
static Handle<Value> BSONSerialize2(const Arguments &args);
// Constructor used for creating new BSON objects from C++
static Persistent<FunctionTemplate> constructor_template;
private:
static Handle<Value> New(const Arguments &args);
static Handle<Value> deserialize(BSON *bson, char *data, uint32_t dataLength, uint32_t startIndex, bool is_array_item);
static uint32_t serialize(BSON *bson, char *serialized_object, uint32_t index, Handle<Value> name, Handle<Value> value, bool check_key, bool serializeFunctions);
static char* extract_string(char *data, uint32_t offset);
static const char* ToCString(const v8::String::Utf8Value& value);
static uint32_t calculate_object_size(BSON *bson, Handle<Value> object, bool serializeFunctions);
static void write_int32(char *data, uint32_t value);
static void write_int64(char *data, int64_t value);
static void write_double(char *data, double value);
static uint16_t deserialize_int8(char *data, uint32_t offset);
static uint32_t deserialize_int32(char* data, uint32_t offset);
static char *check_key(Local<String> key);
// BSON type instantiate functions
Persistent<Function> longConstructor;
Persistent<Function> objectIDConstructor;
Persistent<Function> binaryConstructor;
Persistent<Function> codeConstructor;
Persistent<Function> dbrefConstructor;
Persistent<Function> symbolConstructor;
Persistent<Function> doubleConstructor;
Persistent<Function> timestampConstructor;
Persistent<Function> minKeyConstructor;
Persistent<Function> maxKeyConstructor;
// Equality Objects
Persistent<String> longString;
Persistent<String> objectIDString;
Persistent<String> binaryString;
Persistent<String> codeString;
Persistent<String> dbrefString;
Persistent<String> symbolString;
Persistent<String> doubleString;
Persistent<String> timestampString;
Persistent<String> minKeyString;
Persistent<String> maxKeyString;
// Equality speed up comparision objects
Persistent<String> _bsontypeString;
Persistent<String> _longLowString;
Persistent<String> _longHighString;
Persistent<String> _objectIDidString;
Persistent<String> _binaryPositionString;
Persistent<String> _binarySubTypeString;
Persistent<String> _binaryBufferString;
Persistent<String> _doubleValueString;
Persistent<String> _symbolValueString;
Persistent<String> _dbRefRefString;
Persistent<String> _dbRefIdRefString;
Persistent<String> _dbRefDbRefString;
Persistent<String> _dbRefNamespaceString;
Persistent<String> _dbRefDbString;
Persistent<String> _dbRefOidString;
// Decode JS function
static Handle<Value> decodeLong(BSON *bson, char *data, uint32_t index);
static Handle<Value> decodeTimestamp(BSON *bson, char *data, uint32_t index);
static Handle<Value> decodeOid(BSON *bson, char *oid);
static Handle<Value> decodeBinary(BSON *bson, uint32_t sub_type, uint32_t number_of_bytes, char *data);
static Handle<Value> decodeCode(BSON *bson, char *code, Handle<Value> scope);
static Handle<Value> decodeDBref(BSON *bson, Local<Value> ref, Local<Value> oid, Local<Value> db);
// Experimental
static uint32_t calculate_object_size2(Handle<Value> object);
static uint32_t serialize2(char *serialized_object, uint32_t index, Handle<Value> name, Handle<Value> value, uint32_t object_size, bool check_key);
};
#endif // BSON_H_

View file

@ -1,20 +0,0 @@
var bson = require('./bson');
exports.BSON = bson.BSON;
exports.Long = require('../../lib/mongodb/bson/long').Long;
exports.ObjectID = require('../../lib/mongodb/bson/objectid').ObjectID;
exports.DBRef = require('../../lib/mongodb/bson/db_ref').DBRef;
exports.Code = require('../../lib/mongodb/bson/code').Code;
exports.Timestamp = require('../../lib/mongodb/bson/timestamp').Timestamp;
exports.Binary = require('../../lib/mongodb/bson/binary').Binary;
exports.Double = require('../../lib/mongodb/bson/double').Double;
exports.MaxKey = require('../../lib/mongodb/bson/max_key').MaxKey;
exports.MinKey = require('../../lib/mongodb/bson/min_key').MinKey;
exports.Symbol = require('../../lib/mongodb/bson/symbol').Symbol;
// Just add constants tot he Native BSON parser
exports.BSON.BSON_BINARY_SUBTYPE_DEFAULT = 0;
exports.BSON.BSON_BINARY_SUBTYPE_FUNCTION = 1;
exports.BSON.BSON_BINARY_SUBTYPE_BYTE_ARRAY = 2;
exports.BSON.BSON_BINARY_SUBTYPE_UUID = 3;
exports.BSON.BSON_BINARY_SUBTYPE_MD5 = 4;
exports.BSON.BSON_BINARY_SUBTYPE_USER_DEFINED = 128;

View file

@ -1,349 +0,0 @@
var sys = require('util'),
debug = require('util').debug,
inspect = require('util').inspect,
Buffer = require('buffer').Buffer,
BSON = require('../bson').BSON,
Buffer = require('buffer').Buffer,
BSONJS = require('../../../lib/mongodb/bson/bson').BSON,
BinaryParser = require('../../../lib/mongodb/bson/binary_parser').BinaryParser,
Long = require('../../../lib/mongodb/bson/long').Long,
ObjectID = require('../../../lib/mongodb/bson/bson').ObjectID,
Binary = require('../../../lib/mongodb/bson/bson').Binary,
Code = require('../../../lib/mongodb/bson/bson').Code,
DBRef = require('../../../lib/mongodb/bson/bson').DBRef,
Symbol = require('../../../lib/mongodb/bson/bson').Symbol,
Double = require('../../../lib/mongodb/bson/bson').Double,
MaxKey = require('../../../lib/mongodb/bson/bson').MaxKey,
MinKey = require('../../../lib/mongodb/bson/bson').MinKey,
Timestamp = require('../../../lib/mongodb/bson/bson').Timestamp,
assert = require('assert');
if(process.env['npm_package_config_native'] != null) return;
sys.puts("=== EXECUTING TEST_BSON ===");
// Should fail due to illegal key
assert.throws(function() { new ObjectID('foo'); })
assert.throws(function() { new ObjectID('foo'); })
// Parsers
var bsonC = new BSON([Long, ObjectID, Binary, Code, DBRef, Symbol, Double, Timestamp, MaxKey, MinKey]);
var bsonJS = new BSONJS([Long, ObjectID, Binary, Code, DBRef, Symbol, Double, Timestamp, MaxKey, MinKey]);
// Simple serialization and deserialization of edge value
var doc = {doc:0x1ffffffffffffe};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
var doc = {doc:-0x1ffffffffffffe};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
//
// Assert correct toJSON
//
var a = Long.fromNumber(10);
assert.equal(10, a);
var a = Long.fromNumber(9223372036854775807);
assert.equal(9223372036854775807, a);
// Simple serialization and deserialization test for a Single String value
var doc = {doc:'Serialize'};
var simple_string_serialized = bsonC.serialize(doc, true, false);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
// Nested doc
var doc = {a:{b:{c:1}}};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
// Simple integer serialization/deserialization test, including testing boundary conditions
var doc = {doc:-1};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
var doc = {doc:2147483648};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
var doc = {doc:-2147483648};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
// Simple serialization and deserialization test for a Long value
var doc = {doc:Long.fromNumber(9223372036854775807)};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize({doc:Long.fromNumber(9223372036854775807)}, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
var doc = {doc:Long.fromNumber(-9223372036854775807)};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize({doc:Long.fromNumber(-9223372036854775807)}, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
// Simple serialization and deserialization for a Float value
var doc = {doc:2222.3333};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
var doc = {doc:-2222.3333};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
// Simple serialization and deserialization for a null value
var doc = {doc:null};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
// Simple serialization and deserialization for a boolean value
var doc = {doc:true};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
// Simple serialization and deserialization for a date value
var date = new Date();
var doc = {doc:date};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')), bsonC.deserialize(simple_string_serialized));
// Simple serialization and deserialization for a boolean value
var doc = {doc:/abcd/mi};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.equal(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')).doc.toString(), bsonC.deserialize(simple_string_serialized).doc.toString());
var doc = {doc:/abcd/};
var simple_string_serialized = bsonC.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc, false, true));
assert.equal(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')).doc.toString(), bsonC.deserialize(simple_string_serialized).doc.toString());
// Simple serialization and deserialization for a objectId value
var doc = {doc:new ObjectID()};
var simple_string_serialized = bsonC.serialize(doc, false, true);
var doc2 = {doc:ObjectID.createFromHexString(doc.doc.toHexString())};
assert.deepEqual(simple_string_serialized, bsonJS.serialize(doc2, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')).doc.toString(), bsonC.deserialize(simple_string_serialized).doc.toString());
// Simple serialization and deserialization for a Binary value
var binary = new Binary();
var string = 'binstring'
for(var index = 0; index < string.length; index++) { binary.put(string.charAt(index)); }
var Binary = new Binary();
var string = 'binstring'
for(var index = 0; index < string.length; index++) { Binary.put(string.charAt(index)); }
var simple_string_serialized = bsonC.serialize({doc:binary}, false, true);
assert.deepEqual(simple_string_serialized, bsonJS.serialize({doc:Binary}, false, true));
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized, 'binary')).doc.value(), bsonC.deserialize(simple_string_serialized).doc.value());
// Simple serialization and deserialization for a Code value
var code = new Code('this.a > i', {'i': 1});
var Code = new Code('this.a > i', {'i': 1});
var simple_string_serialized_2 = bsonJS.serialize({doc:Code}, false, true);
var simple_string_serialized = bsonC.serialize({doc:code}, false, true);
assert.deepEqual(simple_string_serialized, simple_string_serialized_2);
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized_2, 'binary')).doc.scope, bsonC.deserialize(simple_string_serialized).doc.scope);
// Simple serialization and deserialization for an Object
var simple_string_serialized = bsonC.serialize({doc:{a:1, b:{c:2}}}, false, true);
var simple_string_serialized_2 = bsonJS.serialize({doc:{a:1, b:{c:2}}}, false, true);
assert.deepEqual(simple_string_serialized, simple_string_serialized_2)
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized_2, 'binary')).doc, bsonC.deserialize(simple_string_serialized).doc);
// Simple serialization and deserialization for an Array
var simple_string_serialized = bsonC.serialize({doc:[9, 9, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1]}, false, true);
var simple_string_serialized_2 = bsonJS.serialize({doc:[9, 9, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1]}, false, true);
assert.deepEqual(simple_string_serialized, simple_string_serialized_2)
assert.deepEqual(bsonJS.deserialize(new Buffer(simple_string_serialized_2, 'binary')).doc, bsonC.deserialize(simple_string_serialized).doc);
// Simple serialization and deserialization for a DBRef
var oid = new ObjectID()
var oid2 = new ObjectID.createFromHexString(oid.toHexString())
var simple_string_serialized = bsonJS.serialize({doc:new DBRef('namespace', oid2, 'integration_tests_')}, false, true);
var simple_string_serialized_2 = bsonC.serialize({doc:new DBRef('namespace', oid, 'integration_tests_')}, false, true);
assert.deepEqual(simple_string_serialized, simple_string_serialized_2)
// Ensure we have the same values for the dbref
var object_js = bsonJS.deserialize(new Buffer(simple_string_serialized_2, 'binary'));
var object_c = bsonC.deserialize(simple_string_serialized);
assert.equal(object_js.doc.namespace, object_c.doc.namespace);
assert.equal(object_js.doc.oid.toHexString(), object_c.doc.oid.toHexString());
assert.equal(object_js.doc.db, object_c.doc.db);
// Serialized document
var bytes = [47,0,0,0,2,110,97,109,101,0,6,0,0,0,80,97,116,116,121,0,16,97,103,101,0,34,0,0,0,7,95,105,100,0,76,100,12,23,11,30,39,8,89,0,0,1,0];
var serialized_data = '';
// Convert to chars
for(var i = 0; i < bytes.length; i++) {
serialized_data = serialized_data + BinaryParser.fromByte(bytes[i]);
}
var object = bsonC.deserialize(new Buffer(serialized_data, 'binary'));
assert.equal('Patty', object.name)
assert.equal(34, object.age)
assert.equal('4c640c170b1e270859000001', object._id.toHexString())
// Serialize utf8
var doc = { "name" : "本荘由利地域に洪水警報", "name1" : "öüóőúéáűíÖÜÓŐÚÉÁŰÍ", "name2" : "abcdedede"};
var simple_string_serialized = bsonC.serialize(doc, false, true);
var simple_string_serialized2 = bsonJS.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, simple_string_serialized2)
var object = bsonC.deserialize(simple_string_serialized);
assert.equal(doc.name, object.name)
assert.equal(doc.name1, object.name1)
assert.equal(doc.name2, object.name2)
// Serialize object with array
var doc = {b:[1, 2, 3]};
var simple_string_serialized = bsonC.serialize(doc, false, true);
var simple_string_serialized_2 = bsonJS.serialize(doc, false, true);
assert.deepEqual(simple_string_serialized, simple_string_serialized_2)
var object = bsonC.deserialize(simple_string_serialized);
assert.deepEqual(doc, object)
// Test equality of an object ID
var object_id = new ObjectID();
var object_id_2 = new ObjectID();
assert.ok(object_id.equals(object_id));
assert.ok(!(object_id.equals(object_id_2)))
// Test same serialization for Object ID
var object_id = new ObjectID();
var object_id2 = ObjectID.createFromHexString(object_id.toString())
var simple_string_serialized = bsonJS.serialize({doc:object_id}, false, true);
var simple_string_serialized_2 = bsonC.serialize({doc:object_id2}, false, true);
assert.equal(simple_string_serialized_2.length, simple_string_serialized.length);
assert.deepEqual(simple_string_serialized, simple_string_serialized_2)
var object = bsonJS.deserialize(new Buffer(simple_string_serialized_2, 'binary'));
var object2 = bsonC.deserialize(simple_string_serialized);
assert.equal(object.doc.id, object2.doc.id)
// JS Object
var c1 = { _id: new ObjectID, comments: [], title: 'number 1' };
var c2 = { _id: new ObjectID, comments: [], title: 'number 2' };
var doc = {
numbers: []
, owners: []
, comments: [c1, c2]
, _id: new ObjectID
};
var simple_string_serialized = bsonJS.serialize(doc, false, true);
// C++ Object
var c1 = { _id: ObjectID.createFromHexString(c1._id.toHexString()), comments: [], title: 'number 1' };
var c2 = { _id: ObjectID.createFromHexString(c2._id.toHexString()), comments: [], title: 'number 2' };
var doc = {
numbers: []
, owners: []
, comments: [c1, c2]
, _id: ObjectID.createFromHexString(doc._id.toHexString())
};
var simple_string_serialized_2 = bsonC.serialize(doc, false, true);
for(var i = 0; i < simple_string_serialized_2.length; i++) {
// debug(i + "[" + simple_string_serialized_2[i] + "] = [" + simple_string_serialized[i] + "]")
assert.equal(simple_string_serialized_2[i], simple_string_serialized[i]);
}
// Deserialize the string
var doc1 = bsonJS.deserialize(new Buffer(simple_string_serialized_2));
var doc2 = bsonC.deserialize(new Buffer(simple_string_serialized_2));
assert.equal(doc._id.id, doc1._id.id)
assert.equal(doc._id.id, doc2._id.id)
assert.equal(doc1._id.id, doc2._id.id)
var doc = {
_id: 'testid',
key1: { code: 'test1', time: {start:1309323402727,end:1309323402727}, x:10, y:5 },
key2: { code: 'test1', time: {start:1309323402727,end:1309323402727}, x:10, y:5 }
};
var simple_string_serialized = bsonJS.serialize(doc, false, true);
var simple_string_serialized_2 = bsonC.serialize(doc, false, true);
// Deserialize the string
var doc1 = bsonJS.deserialize(new Buffer(simple_string_serialized_2));
var doc2 = bsonC.deserialize(new Buffer(simple_string_serialized_2));
assert.deepEqual(doc2, doc1)
assert.deepEqual(doc, doc2)
assert.deepEqual(doc, doc1)
// Serialize function
var doc = {
_id: 'testid',
key1: function() {}
}
var simple_string_serialized = bsonJS.serialize(doc, false, true, true);
var simple_string_serialized_2 = bsonC.serialize(doc, false, true, true);
// Deserialize the string
var doc1 = bsonJS.deserialize(new Buffer(simple_string_serialized_2));
var doc2 = bsonC.deserialize(new Buffer(simple_string_serialized_2));
assert.equal(doc1.key1.code.toString(), doc2.key1.code.toString())
var doc = {"user_id":"4e9fc8d55883d90100000003","lc_status":{"$ne":"deleted"},"owner_rating":{"$exists":false}};
var simple_string_serialized = bsonJS.serialize(doc, false, true, true);
var simple_string_serialized_2 = bsonC.serialize(doc, false, true, true);
// Should serialize to the same value
assert.equal(simple_string_serialized_2.toString('base64'), simple_string_serialized.toString('base64'))
var doc1 = bsonJS.deserialize(simple_string_serialized_2);
var doc2 = bsonC.deserialize(simple_string_serialized);
assert.deepEqual(doc1, doc2)
// Hex Id
var hexId = new ObjectID().toString();
var docJS = {_id: ObjectID.createFromHexString(hexId), 'funds.remaining': {$gte: 1.222}, 'transactions.id': {$ne: ObjectID.createFromHexString(hexId)}};
var docC = {_id: ObjectID.createFromHexString(hexId), 'funds.remaining': {$gte: 1.222}, 'transactions.id': {$ne: ObjectID.createFromHexString(hexId)}};
var docJSBin = bsonJS.serialize(docJS, false, true, true);
var docCBin = bsonC.serialize(docC, false, true, true);
assert.equal(docCBin.toString('base64'), docJSBin.toString('base64'));
// // Complex document serialization
// doc = {"DateTime": "Tue Nov 40 2011 17:27:55 GMT+0000 (WEST)","isActive": true,"Media": {"URL": "http://videos.sapo.pt/Tc85NsjaKjj8o5aV7Ubb"},"Title": "Lisboa fecha a ganhar 0.19%","SetPosition": 60,"Type": "videos","Thumbnail": [{"URL": "http://rd3.videos.sapo.pt/Tc85NsjaKjj8o5aV7Ubb/pic/320x240","Dimensions": {"Height": 240,"Width": 320}}],"Source": {"URL": "http://videos.sapo.pt","SetID": "1288","SourceID": "http://videos.sapo.pt/tvnet/rss2","SetURL": "http://noticias.sapo.pt/videos/tv-net_1288/","ItemID": "Tc85NsjaKjj8o5aV7Ubb","Name": "SAPO Vídeos"},"Category": "Tec_ciencia","Description": "Lisboa fecha a ganhar 0.19%","GalleryID": new ObjectID("4eea2a634ce8573200000000"),"InternalRefs": {"RegisterDate": "Thu Dec 15 2011 17:12:51 GMT+0000 (WEST)","ChangeDate": "Thu Dec 15 2011 17:12:51 GMT+0000 (WEST)","Hash": 332279244514},"_id": new ObjectID("4eea2a96e52778160000003a")}
// var docJSBin = bsonJS.serialize(docJS, false, true, true);
// var docCBin = bsonC.serialize(docC, false, true, true);
//
//
// // Force garbage collect
// global.gc();

View file

@ -1,218 +0,0 @@
var sys = require('util'),
fs = require('fs'),
Buffer = require('buffer').Buffer,
BSON = require('../bson').BSON,
Buffer = require('buffer').Buffer,
assert = require('assert'),
BinaryParser = require('../../../lib/mongodb/bson/binary_parser').BinaryParser,
BSONJS = require('../../../lib/mongodb/bson/bson').BSON,
Long = require('../../../lib/mongodb/bson/long').Long,
ObjectID = require('../../../lib/mongodb/bson/bson').ObjectID,
Binary = require('../../../lib/mongodb/bson/bson').Binary,
Code = require('../../../lib/mongodb/bson/bson').Code,
DBRef = require('../../../lib/mongodb/bson/bson').DBRef,
Symbol = require('../../../lib/mongodb/bson/bson').Symbol,
Double = require('../../../lib/mongodb/bson/bson').Double,
MaxKey = require('../../../lib/mongodb/bson/bson').MaxKey,
MinKey = require('../../../lib/mongodb/bson/bson').MinKey,
Timestamp = require('../../../lib/mongodb/bson/bson').Timestamp;
if(process.env['npm_package_config_native'] != null) return;
sys.puts("=== EXECUTING TEST_FULL_BSON ===");
// Parsers
var bsonC = new BSON([Long, ObjectID, Binary, Code, DBRef, Symbol, Double, Timestamp, MaxKey, MinKey]);
var bsonJS = new BSONJS([Long, ObjectID, Binary, Code, DBRef, Symbol, Double, Timestamp, MaxKey, MinKey]);
// Should Correctly Deserialize object
var bytes = [95,0,0,0,2,110,115,0,42,0,0,0,105,110,116,101,103,114,97,116,105,111,110,95,116,101,115,116,115,95,46,116,101,115,116,95,105,110,100,101,120,95,105,110,102,111,114,109,97,116,105,111,110,0,8,117,110,105,113,117,101,0,0,3,107,101,121,0,12,0,0,0,16,97,0,1,0,0,0,0,2,110,97,109,101,0,4,0,0,0,97,95,49,0,0];
var serialized_data = '';
// Convert to chars
for(var i = 0; i < bytes.length; i++) {
serialized_data = serialized_data + BinaryParser.fromByte(bytes[i]);
}
var object = bsonC.deserialize(serialized_data);
assert.equal("a_1", object.name);
assert.equal(false, object.unique);
assert.equal(1, object.key.a);
// Should Correctly Deserialize object with all types
var bytes = [26,1,0,0,7,95,105,100,0,161,190,98,75,118,169,3,0,0,3,0,0,4,97,114,114,97,121,0,26,0,0,0,16,48,0,1,0,0,0,16,49,0,2,0,0,0,16,50,0,3,0,0,0,0,2,115,116,114,105,110,103,0,6,0,0,0,104,101,108,108,111,0,3,104,97,115,104,0,19,0,0,0,16,97,0,1,0,0,0,16,98,0,2,0,0,0,0,9,100,97,116,101,0,161,190,98,75,0,0,0,0,7,111,105,100,0,161,190,98,75,90,217,18,0,0,1,0,0,5,98,105,110,97,114,121,0,7,0,0,0,2,3,0,0,0,49,50,51,16,105,110,116,0,42,0,0,0,1,102,108,111,97,116,0,223,224,11,147,169,170,64,64,11,114,101,103,101,120,112,0,102,111,111,98,97,114,0,105,0,8,98,111,111,108,101,97,110,0,1,15,119,104,101,114,101,0,25,0,0,0,12,0,0,0,116,104,105,115,46,120,32,61,61,32,51,0,5,0,0,0,0,3,100,98,114,101,102,0,37,0,0,0,2,36,114,101,102,0,5,0,0,0,116,101,115,116,0,7,36,105,100,0,161,190,98,75,2,180,1,0,0,2,0,0,0,10,110,117,108,108,0,0];
var serialized_data = '';
// Convert to chars
for(var i = 0; i < bytes.length; i++) {
serialized_data = serialized_data + BinaryParser.fromByte(bytes[i]);
}
var object = bsonJS.deserialize(new Buffer(serialized_data, 'binary'));
assert.equal("hello", object.string);
assert.deepEqual([1, 2, 3], object.array);
assert.equal(1, object.hash.a);
assert.equal(2, object.hash.b);
assert.ok(object.date != null);
assert.ok(object.oid != null);
assert.ok(object.binary != null);
assert.equal(42, object.int);
assert.equal(33.3333, object.float);
assert.ok(object.regexp != null);
assert.equal(true, object.boolean);
assert.ok(object.where != null);
assert.ok(object.dbref != null);
assert.ok(object['null'] == null);
// Should Serialize and Deserialze String
var test_string = {hello: 'world'}
var serialized_data = bsonC.serialize(test_string)
assert.deepEqual(test_string, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize Integer
var test_number = {doc: 5}
var serialized_data = bsonC.serialize(test_number)
assert.deepEqual(test_number, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize null value
var test_null = {doc:null}
var serialized_data = bsonC.serialize(test_null)
var object = bsonC.deserialize(serialized_data);
assert.deepEqual(test_null, object);
// Should Correctly Serialize and Deserialize undefined value
var test_undefined = {doc:undefined}
var serialized_data = bsonC.serialize(test_undefined)
var object = bsonJS.deserialize(new Buffer(serialized_data, 'binary'));
assert.equal(null, object.doc)
// Should Correctly Serialize and Deserialize Number
var test_number = {doc: 5.5}
var serialized_data = bsonC.serialize(test_number)
assert.deepEqual(test_number, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize Integer
var test_int = {doc: 42}
var serialized_data = bsonC.serialize(test_int)
assert.deepEqual(test_int, bsonC.deserialize(serialized_data));
test_int = {doc: -5600}
serialized_data = bsonC.serialize(test_int)
assert.deepEqual(test_int, bsonC.deserialize(serialized_data));
test_int = {doc: 2147483647}
serialized_data = bsonC.serialize(test_int)
assert.deepEqual(test_int, bsonC.deserialize(serialized_data));
test_int = {doc: -2147483648}
serialized_data = bsonC.serialize(test_int)
assert.deepEqual(test_int, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize Object
var doc = {doc: {age: 42, name: 'Spongebob', shoe_size: 9.5}}
var serialized_data = bsonC.serialize(doc)
assert.deepEqual(doc, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize Array
var doc = {doc: [1, 2, 'a', 'b']}
var serialized_data = bsonC.serialize(doc)
assert.deepEqual(doc, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize Array with added on functions
var doc = {doc: [1, 2, 'a', 'b']}
var serialized_data = bsonC.serialize(doc)
assert.deepEqual(doc, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize A Boolean
var doc = {doc: true}
var serialized_data = bsonC.serialize(doc)
assert.deepEqual(doc, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize a Date
var date = new Date()
//(2009, 11, 12, 12, 00, 30)
date.setUTCDate(12)
date.setUTCFullYear(2009)
date.setUTCMonth(11 - 1)
date.setUTCHours(12)
date.setUTCMinutes(0)
date.setUTCSeconds(30)
var doc = {doc: date}
var serialized_data = bsonC.serialize(doc)
assert.deepEqual(doc, bsonC.deserialize(serialized_data));
// // Should Correctly Serialize and Deserialize Oid
var doc = {doc: new ObjectID()}
var serialized_data = bsonC.serialize(doc)
assert.deepEqual(doc.doc.toHexString(), bsonC.deserialize(serialized_data).doc.toHexString())
// Should Correctly encode Empty Hash
var test_code = {}
var serialized_data = bsonC.serialize(test_code)
assert.deepEqual(test_code, bsonC.deserialize(serialized_data));
// Should Correctly Serialize and Deserialize Ordered Hash
var doc = {doc: {b:1, a:2, c:3, d:4}}
var serialized_data = bsonC.serialize(doc)
var decoded_hash = bsonC.deserialize(serialized_data).doc
var keys = []
for(name in decoded_hash) keys.push(name)
assert.deepEqual(['b', 'a', 'c', 'd'], keys)
// Should Correctly Serialize and Deserialize Regular Expression
// Serialize the regular expression
var doc = {doc: /foobar/mi}
var serialized_data = bsonC.serialize(doc)
var doc2 = bsonC.deserialize(serialized_data);
assert.equal(doc.doc.toString(), doc2.doc.toString())
// Should Correctly Serialize and Deserialize a Binary object
var bin = new Binary()
var string = 'binstring'
for(var index = 0; index < string.length; index++) {
bin.put(string.charAt(index))
}
var doc = {doc: bin}
var serialized_data = bsonC.serialize(doc)
var deserialized_data = bsonC.deserialize(serialized_data);
assert.equal(doc.doc.value(), deserialized_data.doc.value())
// Should Correctly Serialize and Deserialize a big Binary object
var data = fs.readFileSync("../../test/gridstore/test_gs_weird_bug.png", 'binary');
var bin = new Binary()
bin.write(data)
var doc = {doc: bin}
var serialized_data = bsonC.serialize(doc)
var deserialized_data = bsonC.deserialize(serialized_data);
assert.equal(doc.doc.value(), deserialized_data.doc.value())

View file

@ -1,132 +0,0 @@
var Buffer = require('buffer').Buffer,
BSON = require('../bson').BSON,
Buffer = require('buffer').Buffer,
BSONJS = require('../../../lib/mongodb/bson/bson').BSON,
BinaryParser = require('../../../lib/mongodb/bson/binary_parser').BinaryParser,
Long = require('../../../lib/mongodb/bson/long').Long,
ObjectID = require('../../../lib/mongodb/bson/bson').ObjectID,
Binary = require('../../../lib/mongodb/bson/bson').Binary,
Code = require('../../../lib/mongodb/bson/bson').Code,
DBRef = require('../../../lib/mongodb/bson/bson').DBRef,
Symbol = require('../../../lib/mongodb/bson/bson').Symbol,
Double = require('../../../lib/mongodb/bson/bson').Double,
MaxKey = require('../../../lib/mongodb/bson/bson').MaxKey,
MinKey = require('../../../lib/mongodb/bson/bson').MinKey,
Timestamp = require('../../../lib/mongodb/bson/bson').Timestamp;
assert = require('assert');
if(process.env['npm_package_config_native'] != null) return;
console.log("=== EXECUTING TEST_STACKLESS_BSON ===");
// Parsers
var bsonC = new BSON([Long, ObjectID, Binary, Code, DBRef, Symbol, Double, Timestamp, MaxKey, MinKey]);
var bsonJS = new BSONJS([Long, ObjectID, Binary, Code, DBRef, Symbol, Double, Timestamp, MaxKey, MinKey]);
// Number of iterations for the benchmark
var COUNT = 10000;
// var COUNT = 1;
// Sample simple doc
var doc = {key:"Hello world", key2:"šđžčćŠĐŽČĆ", key3:'客家话', key4:'how are you doing dog!!'};
// var doc = {};
// for(var i = 0; i < 100; i++) {
// doc['string' + i] = "dumdyms fsdfdsfdsfdsfsdfdsfsdfsdfsdfsdfsdfsdfsdffsfsdfs";
// }
// // Calculate size
console.log(bsonC.calculateObjectSize2(doc));
console.log(bsonJS.calculateObjectSize(doc));
// assert.equal(bsonJS.calculateObjectSize(doc), bsonC.calculateObjectSize2(doc));
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// Benchmark calculateObjectSize
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// Benchmark 1 JS BSON
console.log(COUNT + "x (objectBSON = bsonC.calculateObjectSize(object))")
start = new Date
for (j=COUNT; --j>=0; ) {
var objectBSON = bsonJS.calculateObjectSize(doc);
}
end = new Date
var opsprsecond = COUNT / ((end - start)/1000);
console.log("time = ", end - start, "ms -", COUNT / ((end - start)/1000), " ops/sec");
// Benchmark 2 C++ BSON calculateObjectSize
console.log(COUNT + "x (objectBSON = bsonC.calculateObjectSize(object))")
start = new Date
for (j=COUNT; --j>=0; ) {
var objectBSON = bsonC.calculateObjectSize(doc);
}
end = new Date
var opsprsecond = COUNT / ((end - start)/1000);
console.log("time = ", end - start, "ms -", COUNT / ((end - start)/1000), " ops/sec");
// Benchmark 3 C++ BSON calculateObjectSize2
console.log(COUNT + "x (objectBSON = bsonC.calculateObjectSize2(object))")
start = new Date
for (j=COUNT; --j>=0; ) {
var objectBSON = bsonC.calculateObjectSize2(doc);
}
end = new Date
var opsprsecond = COUNT / ((end - start)/1000);
console.log("time = ", end - start, "ms -", COUNT / ((end - start)/1000), " ops/sec");
// // Serialize the content
// var _serializedDoc1 = bsonJS.serialize(doc, true, false);
// var _serializedDoc2 = bsonC.serialize2(doc, true, false);
// console.dir(_serializedDoc1);
// console.dir(_serializedDoc2);
// assert.equal(_serializedDoc1.toString('base64'), _serializedDoc2.toString('base64'))
//
//
// // Benchmark 1
// console.log(COUNT + "x (objectBSON = bsonC.serialize(object))")
// start = new Date
//
// for (j=COUNT; --j>=0; ) {
// // var objectBSON = bsonC.serialize2(doc, true, false);
// var objectBSON = bsonJS.serialize(doc, true, false);
// }
//
// end = new Date
// var opsprsecond = COUNT / ((end - start)/1000);
// console.log("bson size (bytes): ", objectbsonC.length);
// console.log("time = ", end - start, "ms -", COUNT / ((end - start)/1000), " ops/sec");
// console.log("MB/s = " + ((opsprsecond*objectbsonC.length)/1024));
//
// // Benchmark 2
// console.log(COUNT + "x (objectBSON = bsonC.serialize(object))")
// start = new Date
//
// for (j=COUNT; --j>=0; ) {
// var objectBSON = bsonC.serialize2(doc, true, false);
// }
//
// end = new Date
// var opsprsecond = COUNT / ((end - start)/1000);
// console.log("bson size (bytes): ", objectbsonC.length);
// console.log("time = ", end - start, "ms -", COUNT / ((end - start)/1000), " ops/sec");
// console.log("MB/s = " + ((opsprsecond*objectbsonC.length)/1024));
//
// // Benchmark 3
// console.log(COUNT + "x (objectBSON = bsonC.serialize(object))")
// start = new Date
//
// for (j=COUNT; --j>=0; ) {
// var objectBSON = bsonC.serialize(doc, true, false);
// }
//
// end = new Date
// var opsprsecond = COUNT / ((end - start)/1000);
// console.log("bson size (bytes): ", objectbsonC.length);
// console.log("time = ", end - start, "ms -", COUNT / ((end - start)/1000), " ops/sec");
// console.log("MB/s = " + ((opsprsecond*objectbsonC.length)/1024));

View file

@ -1,39 +0,0 @@
import Options
from os import unlink, symlink, popen
from os.path import exists
srcdir = "."
blddir = "build"
VERSION = "0.1.0"
def set_options(opt):
opt.tool_options("compiler_cxx")
opt.add_option( '--debug'
, action='store_true'
, default=False
, help='Build debug variant [Default: False]'
, dest='debug'
)
def configure(conf):
conf.check_tool("compiler_cxx")
conf.check_tool("node_addon")
conf.env.append_value('CXXFLAGS', ['-O3', '-funroll-loops'])
# conf.env.append_value('CXXFLAGS', ['-DDEBUG', '-g', '-O0', '-Wall', '-Wextra'])
# conf.check(lib='node', libpath=['/usr/lib', '/usr/local/lib'], uselib_store='NODE')
def build(bld):
obj = bld.new_task_gen("cxx", "shlib", "node_addon")
obj.target = "bson"
obj.source = ["bson.cc"]
# obj.uselib = "NODE"
def shutdown():
# HACK to get compress.node out of build directory.
# better way to do this?
if Options.commands['clean']:
if exists('bson.node'): unlink('bson.node')
else:
if exists('build/default/bson.node') and not exists('bson.node'):
symlink('build/default/bson.node', 'bson.node')

View file

@ -12,17 +12,16 @@ var Collection = require('./collection').Collection,
* @param {Object} db Current db instance we wish to perform Admin operations on.
* @return {Function} Constructor for Admin type.
*/
function Admin(db) {
function Admin(db) {
if(!(this instanceof Admin)) return new Admin(db);
this.db = db;
};
/**
* Retrieve the server information for the current
* instance of the db client
*
* @param {Function} callback Callback function of format `function(err, result) {}`.
*
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from buildInfo or null if an error occured.
* @return {null} Returns no result
* @api public
*/
@ -33,15 +32,13 @@ Admin.prototype.buildInfo = function(callback) {
/**
* Retrieve the server information for the current
* instance of the db client
*
* @param {Function} callback Callback function of format `function(err, result) {}`.
*
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from serverInfo or null if an error occured.
* @return {null} Returns no result
* @api private
*/
Admin.prototype.serverInfo = function(callback) {
var self = this;
var command = {buildinfo:1};
this.command(command, function(err, doc) {
this.db.executeDbAdminCommand({buildinfo:1}, function(err, doc) {
if(err != null) return callback(err, null);
return callback(null, doc.documents[0]);
});
@ -50,51 +47,42 @@ Admin.prototype.serverInfo = function(callback) {
/**
* Retrieve this db's server status.
*
* @param {Function} callback returns the server status.
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from serverStatus or null if an error occured.
* @return {null}
* @api public
*/
Admin.prototype.serverStatus = function(callback) {
var self = this;
this.command({serverStatus: 1}, function(err, result) {
if (err == null && result.documents[0].ok == 1) {
callback(null, result.documents[0]);
this.db.executeDbAdminCommand({serverStatus: 1}, function(err, doc) {
if(err == null && doc.documents[0].ok === 1) {
callback(null, doc.documents[0]);
} else {
if (err) {
callback(err, false);
} else {
callback(self.wrap(result.documents[0]), false);
}
if(err) return callback(err, false);
return callback(self.db.wrap(doc.documents[0]), false);
}
});
};
/**
* Retrieve the current profiling Level for MongoDB
*
* @param {Function} callback Callback function of format `function(err, result) {}`.
*
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from profilingLevel or null if an error occured.
* @return {null} Returns no result
* @api public
*/
Admin.prototype.profilingLevel = function(callback) {
var self = this;
var command = {profile:-1};
this.command(command, function(err, doc) {
this.db.executeDbAdminCommand({profile:-1}, function(err, doc) {
doc = doc.documents[0];
if(err == null && (doc.ok == 1 || typeof doc.was === 'number')) {
if(err == null && doc.ok === 1) {
var was = doc.was;
if(was == 0) {
callback(null, "off");
} else if(was == 1) {
callback(null, "slow_only");
} else if(was == 2) {
callback(null, "all");
} else {
callback(new Error("Error: illegal profiling level value " + was), null);
}
if(was == 0) return callback(null, "off");
if(was == 1) return callback(null, "slow_only");
if(was == 2) return callback(null, "all");
return callback(new Error("Error: illegal profiling level value " + was), null);
} else {
err != null ? callback(err, null) : callback(new Error("Error with profile command"), null);
}
@ -104,8 +92,7 @@ Admin.prototype.profilingLevel = function(callback) {
/**
* Ping the MongoDB server and retrieve results
*
* @param {Object} [options] Optional parameters to the command.
* @param {Function} callback Callback function of format `function(err, result) {}`.
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from ping or null if an error occured.
* @return {null} Returns no result
* @api public
*/
@ -113,53 +100,37 @@ Admin.prototype.ping = function(options, callback) {
// Unpack calls
var args = Array.prototype.slice.call(arguments, 0);
callback = args.pop();
options = args.length ? args.shift() : {};
// Set self
var self = this;
var databaseName = this.db.databaseName;
this.db.databaseName = 'admin';
this.db.executeDbCommand({ping:1}, options, function(err, result) {
self.db.databaseName = databaseName;
return callback(err, result);
})
this.db.executeDbAdminCommand({ping: 1}, callback);
}
/**
* Authenticate against MongoDB
*
*
* @param {String} username The user name for the authentication.
* @param {String} password The password for the authentication.
* @param {Function} callback Callback function of format `function(err, result) {}`.
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from authenticate or null if an error occured.
* @return {null} Returns no result
* @api public
*/
Admin.prototype.authenticate = function(username, password, callback) {
var self = this;
var databaseName = this.db.databaseName;
this.db.databaseName = 'admin';
this.db.authenticate(username, password, function(err, result) {
self.db.databaseName = databaseName;
return callback(err, result);
this.db.authenticate(username, password, {authdb: 'admin'}, function(err, doc) {
return callback(err, doc);
})
}
/**
* Logout current authenticated user
*
* @param {Object} [options] Optional parameters to the command.
* @param {Function} callback Callback function of format `function(err, result) {}`.
* @param {Object} [options] Optional parameters to the command.
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from logout or null if an error occured.
* @return {null} Returns no result
* @api public
*/
Admin.prototype.logout = function(callback) {
var self = this;
var databaseName = this.db.databaseName;
this.db.databaseName = 'admin';
this.db.logout(function(err, result) {
return callback(err, result);
})
self.db.databaseName = databaseName;
this.db.logout({authdb: 'admin'}, function(err, doc) {
return callback(err, doc);
})
}
/**
@ -172,23 +143,20 @@ Admin.prototype.logout = function(callback) {
* @param {String} username The user name for the authentication.
* @param {String} password The password for the authentication.
* @param {Object} [options] additional options during update.
* @param {Function} callback Callback function of format `function(err, result) {}`.
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from addUser or null if an error occured.
* @return {null} Returns no result
* @api public
*/
Admin.prototype.addUser = function(username, password, options, callback) {
var self = this;
var args = Array.prototype.slice.call(arguments, 2);
callback = args.pop();
options = args.length ? args.shift() : {};
var self = this;
var databaseName = this.db.databaseName;
this.db.databaseName = 'admin';
this.db.addUser(username, password, options, function(err, result) {
self.db.databaseName = databaseName;
return callback(err, result);
})
options.dbName = 'admin';
// Add user
this.db.addUser(username, password, options, function(err, doc) {
return callback(err, doc);
})
}
/**
@ -199,7 +167,7 @@ Admin.prototype.addUser = function(username, password, options, callback) {
*
* @param {String} username The user name for the authentication.
* @param {Object} [options] additional options during update.
* @param {Function} callback Callback function of format `function(err, result) {}`.
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from removeUser or null if an error occured.
* @return {null} Returns no result
* @api public
*/
@ -208,21 +176,18 @@ Admin.prototype.removeUser = function(username, options, callback) {
var args = Array.prototype.slice.call(arguments, 1);
callback = args.pop();
options = args.length ? args.shift() : {};
options.dbName = 'admin';
var self = this;
var databaseName = this.db.databaseName;
this.db.databaseName = 'admin';
this.db.removeUser(username, options, function(err, result) {
self.db.databaseName = databaseName;
return callback(err, result);
})
this.db.removeUser(username, options, function(err, doc) {
return callback(err, doc);
})
}
/**
* Set the current profiling level of MongoDB
*
*
* @param {String} level The new profiling level (off, slow_only, all)
* @param {Function} callback Callback function of format `function(err, result) {}`.
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from setProfilingLevel or null if an error occured.
* @return {null} Returns no result
* @api public
*/
@ -242,48 +207,40 @@ Admin.prototype.setProfilingLevel = function(level, callback) {
}
// Set up the profile number
command['profile'] = profile;
// Execute the command to set the profiling level
this.command(command, function(err, doc) {
command['profile'] = profile;
this.db.executeDbAdminCommand(command, function(err, doc) {
doc = doc.documents[0];
if(err == null && (doc.ok == 1 || typeof doc.was === 'number')) {
if(err == null && doc.ok === 1)
return callback(null, level);
} else {
return err != null ? callback(err, null) : callback(new Error("Error with profile command"), null);
}
return err != null ? callback(err, null) : callback(new Error("Error with profile command"), null);
});
};
/**
* Retrive the current profiling information for MongoDB
*
* @param {Function} callback Callback function of format `function(err, result) {}`.
*
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from profilingInfo or null if an error occured.
* @return {null} Returns no result
* @api public
*/
Admin.prototype.profilingInfo = function(callback) {
var self = this;
var databaseName = this.db.databaseName;
this.db.databaseName = 'admin';
try {
new Cursor(this.db, new Collection(this.db, DbCommand.SYSTEM_PROFILE_COLLECTION), {}).toArray(function(err, items) {
return callback(err, items);
});
new Cursor(this.db, new Collection(this.db, DbCommand.SYSTEM_PROFILE_COLLECTION), {}, {}, {dbName: 'admin'}).toArray(function(err, items) {
return callback(err, items);
});
} catch (err) {
return callback(err, null);
}
self.db.databaseName = databaseName;
};
/**
* Execute a db command against the Admin database
*
*
* @param {Object} command A command object `{ping:1}`.
* @param {Object} [options] Optional parameters to the command.
* @param {Function} callback Callback function of format `function(err, result) {}`.
* @param {Object} [options] Optional parameters to the command.
* @param {Function} callback this will be called after executing this method. The command always return the whole result of the command as the second parameter.
* @return {null} Returns no result
* @api public
*/
@ -294,18 +251,18 @@ Admin.prototype.command = function(command, options, callback) {
options = args.length ? args.shift() : {};
// Execute a command
this.db.executeDbAdminCommand(command, options, function(err, result) {
this.db.executeDbAdminCommand(command, options, function(err, doc) {
// Ensure change before event loop executes
return callback != null ? callback(err, result) : null;
return callback != null ? callback(err, doc) : null;
});
}
/**
* Validate an existing collection
*
*
* @param {String} collectionName The name of the collection to validate.
* @param {Object} [options] Optional parameters to the command.
* @param {Function} callback Callback function of format `function(err, result) {}`.
* @param {Object} [options] Optional parameters to the command.
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from validateCollection or null if an error occured.
* @return {null} Returns no result
* @api public
*/
@ -317,7 +274,7 @@ Admin.prototype.validateCollection = function(collectionName, options, callback)
var self = this;
var command = {validate: collectionName};
var keys = Object.keys(options);
// Decorate command with extra options
for(var i = 0; i < keys.length; i++) {
if(options.hasOwnProperty(keys[i])) {
@ -326,61 +283,52 @@ Admin.prototype.validateCollection = function(collectionName, options, callback)
}
this.db.executeDbCommand(command, function(err, doc) {
if(err != null) return callback(err, null);
if(err != null) return callback(err, null);
doc = doc.documents[0];
if(doc.ok == 0) {
if(doc.ok === 0)
return callback(new Error("Error with validate command"), null);
} else if(doc.result != null && doc.result.constructor != String) {
if(doc.result != null && doc.result.constructor != String)
return callback(new Error("Error with validation data"), null);
} else if(doc.result != null && doc.result.match(/exception|corrupt/) != null) {
if(doc.result != null && doc.result.match(/exception|corrupt/) != null)
return callback(new Error("Error: invalid collection " + collectionName), null);
} else if(doc.valid != null && !doc.valid) {
return callback(new Error("Error: invalid collection " + collectionName), null);
} else {
return callback(null, doc);
}
if(doc.valid != null && !doc.valid)
return callback(new Error("Error: invalid collection " + collectionName), null);
return callback(null, doc);
});
};
/**
* List the available databases
*
* @param {Function} callback Callback function of format `function(err, result) {}`.
*
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from listDatabases or null if an error occured.
* @return {null} Returns no result
* @api public
*/
Admin.prototype.listDatabases = function(callback) {
// Execute the listAllDatabases command
this.db.executeDbAdminCommand({listDatabases:1}, {}, function(err, result) {
if(err != null) {
callback(err, null);
} else {
callback(null, result.documents[0]);
}
});
this.db.executeDbAdminCommand({listDatabases:1}, {}, function(err, doc) {
if(err != null) return callback(err, null);
return callback(null, doc.documents[0]);
});
}
/**
* Get ReplicaSet status
*
* @param {Function} callback returns the replica set status (if available).
* @param {Function} callback this will be called after executing this method. The first parameter will contain the Error object if an error occured, or null otherwise. While the second parameter will contain the results from replSetGetStatus or null if an error occured.
* @return {null}
* @api public
*/
Admin.prototype.replSetGetStatus = function(callback) {
var self = this;
this.command({replSetGetStatus:1}, function(err, result) {
if (err == null && result.documents[0].ok == 1) {
callback(null, result.documents[0]);
} else {
if (err) {
callback(err, false);
} else {
callback(self.db.wrap(result.documents[0]), false);
}
}
this.db.executeDbAdminCommand({replSetGetStatus:1}, function(err, doc) {
if(err == null && doc.documents[0].ok === 1)
return callback(null, doc.documents[0]);
if(err) return callback(err, false);
return callback(self.db.wrap(doc.documents[0]), false);
});
};

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,17 @@
/**
Base object used for common functionality
**/
var BaseCommand = exports.BaseCommand = function() {
var BaseCommand = exports.BaseCommand = function BaseCommand() {
};
var id = 1;
BaseCommand.prototype.getRequestId = function() {
BaseCommand.prototype.getRequestId = function getRequestId() {
if (!this.requestId) this.requestId = id++;
return this.requestId;
};
BaseCommand.prototype.setMongosReadPreference = function setMongosReadPreference(readPreference, tags) {}
BaseCommand.prototype.updateRequestId = function() {
this.requestId = id++;
return this.requestId;

View file

@ -1,6 +1,7 @@
var QueryCommand = require('./query_command').QueryCommand,
InsertCommand = require('./insert_command').InsertCommand,
inherits = require('util').inherits,
utils = require('../utils'),
crypto = require('crypto');
/**
@ -32,6 +33,7 @@ DbCommand.SYSTEM_INDEX_COLLECTION = "system.indexes";
DbCommand.SYSTEM_PROFILE_COLLECTION = "system.profile";
DbCommand.SYSTEM_USER_COLLECTION = "system.users";
DbCommand.SYSTEM_COMMAND_COLLECTION = "$cmd";
DbCommand.SYSTEM_JS_COLLECTION = "system.js";
// New commands
DbCommand.NcreateIsMasterCommand = function(db, databaseName) {
@ -47,11 +49,11 @@ DbCommand.createCollectionInfoCommand = function(db, selector) {
return new DbCommand(db, db.databaseName + "." + DbCommand.SYSTEM_NAMESPACE_COLLECTION, QueryCommand.OPTS_NO_CURSOR_TIMEOUT, 0, 0, selector, null);
};
DbCommand.createGetNonceCommand = function(db) {
DbCommand.createGetNonceCommand = function(db, options) {
return new DbCommand(db, db.databaseName + "." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NO_CURSOR_TIMEOUT, 0, -1, {'getnonce':1}, null);
};
DbCommand.createAuthenticationCommand = function(db, username, password, nonce) {
DbCommand.createAuthenticationCommand = function(db, username, password, nonce, authdb) {
// Use node md5 generator
var md5 = crypto.createHash('md5');
// Generate keys used for authentication
@ -60,11 +62,11 @@ DbCommand.createAuthenticationCommand = function(db, username, password, nonce)
// Final key
md5 = crypto.createHash('md5');
md5.update(nonce + username + hash_password);
var key = md5.digest('hex');
var key = md5.digest('hex');
// Creat selector
var selector = {'authenticate':1, 'user':username, 'nonce':nonce, 'key':key};
// Create db command
return new DbCommand(db, db.databaseName + "." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NONE, 0, -1, selector, null);
return new DbCommand(db, authdb + "." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NONE, 0, -1, selector, null);
};
DbCommand.createLogoutCommand = function(db) {
@ -92,10 +94,12 @@ DbCommand.createRenameCollectionCommand = function(db, fromCollectionName, toCol
};
DbCommand.createGetLastErrorCommand = function(options, db) {
var args = Array.prototype.slice.call(arguments, 0);
db = args.pop();
options = args.length ? args.shift() : {};
// Final command
if (typeof db === 'undefined') {
db = options;
options = {};
}
// Final command
var command = {'getlasterror':1};
// If we have an options Object let's merge in the fields (fsync/wtimeout/w)
if('object' === typeof options) {
@ -103,7 +107,12 @@ DbCommand.createGetLastErrorCommand = function(options, db) {
command[name] = options[name]
}
}
// Special case for w == 1, remove the w
if(1 == command.w) {
delete command.w;
}
// Execute command
return new DbCommand(db, db.databaseName + "." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NO_CURSOR_TIMEOUT, 0, -1, command, null);
};
@ -122,62 +131,84 @@ DbCommand.createCreateIndexCommand = function(db, collectionName, fieldOrSpec, o
var fieldHash = {};
var indexes = [];
var keys;
// Get all the fields accordingly
if (fieldOrSpec.constructor === String) { // 'type'
if('string' == typeof fieldOrSpec) {
// 'type'
indexes.push(fieldOrSpec + '_' + 1);
fieldHash[fieldOrSpec] = 1;
} else if (fieldOrSpec.constructor === Array) { // [{location:'2d'}, ...]
} else if(utils.isArray(fieldOrSpec)) {
fieldOrSpec.forEach(function(f) {
if (f.constructor === String) { // [{location:'2d'}, 'type']
if('string' == typeof f) {
// [{location:'2d'}, 'type']
indexes.push(f + '_' + 1);
fieldHash[f] = 1;
} else if (f.constructor === Array) { // [['location', '2d'],['type', 1]]
} else if(utils.isArray(f)) {
// [['location', '2d'],['type', 1]]
indexes.push(f[0] + '_' + (f[1] || 1));
fieldHash[f[0]] = f[1] || 1;
} else if (f.constructor === Object) { // [{location:'2d'}, {type:1}]
} else if(utils.isObject(f)) {
// [{location:'2d'}, {type:1}]
keys = Object.keys(f);
keys.forEach(function(k) {
indexes.push(k + '_' + f[k]);
fieldHash[k] = f[k];
});
});
} else {
// undefined
// undefined (ignore)
}
});
} else if (fieldOrSpec.constructor === Object) { // {location:'2d', type:1}
} else if(utils.isObject(fieldOrSpec)) {
// {location:'2d', type:1}
keys = Object.keys(fieldOrSpec);
keys.forEach(function(key) {
indexes.push(key + '_' + fieldOrSpec[key]);
fieldHash[key] = fieldOrSpec[key];
});
}
// Generate the index name
var indexName = indexes.join("_");
// Build the selector
var selector = {'ns':(db.databaseName + "." + collectionName), 'key':fieldHash, 'name':indexName};
var indexName = typeof options.name == 'string'
? options.name
: indexes.join("_");
var selector = {
'ns': db.databaseName + "." + collectionName,
'key': fieldHash,
'name': indexName
}
// Ensure we have a correct finalUnique
var finalUnique = options == null || 'object' === typeof options ? false : options;
var finalUnique = options == null || 'object' === typeof options
? false
: options;
// Set up options
options = options == null || typeof options == 'boolean' ? {} : options;
options = options == null || typeof options == 'boolean'
? {}
: options;
// Add all the options
var keys = Object.keys(options);
// Add all the fields to the selector
for(var i = 0; i < keys.length; i++) {
selector[keys[i]] = options[keys[i]];
}
// If we don't have the unique property set on the selector
if(selector['unique'] == null) selector['unique'] = finalUnique;
// Create the insert command for the index and return the document
return new InsertCommand(db, db.databaseName + "." + DbCommand.SYSTEM_INDEX_COLLECTION, false).add(selector);
if(selector['unique'] == null)
selector['unique'] = finalUnique;
var name = db.databaseName + "." + DbCommand.SYSTEM_INDEX_COLLECTION;
var cmd = new InsertCommand(db, name, false);
return cmd.add(selector);
};
DbCommand.logoutCommand = function(db, command_hash) {
return new DbCommand(db, db.databaseName + "." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NO_CURSOR_TIMEOUT, 0, -1, command_hash, null);
DbCommand.logoutCommand = function(db, command_hash, options) {
var dbName = options != null && options['authdb'] != null ? options['authdb'] : db.databaseName;
// Create logout command
return new DbCommand(db, dbName + "." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NO_CURSOR_TIMEOUT, 0, -1, command_hash, null);
}
DbCommand.createDropIndexCommand = function(db, collectionName, indexName) {
@ -200,6 +231,10 @@ DbCommand.createAdminDbCommand = function(db, command_hash) {
return new DbCommand(db, "admin." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NO_CURSOR_TIMEOUT, 0, -1, command_hash, null);
};
DbCommand.createAdminDbCommandSlaveOk = function(db, command_hash) {
return new DbCommand(db, "admin." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NO_CURSOR_TIMEOUT | QueryCommand.OPTS_SLAVE, 0, -1, command_hash, null);
};
DbCommand.createDbSlaveOkCommand = function(db, command_hash, options) {
return new DbCommand(db, db.databaseName + "." + DbCommand.SYSTEM_COMMAND_COLLECTION, QueryCommand.OPTS_NO_CURSOR_TIMEOUT | QueryCommand.OPTS_SLAVE, 0, -1, command_hash, null, options);
};
};

View file

@ -4,7 +4,7 @@ var BaseCommand = require('./base_command').BaseCommand,
/**
Insert Document Command
**/
var DeleteCommand = exports.DeleteCommand = function(db, collectionName, selector) {
var DeleteCommand = exports.DeleteCommand = function(db, collectionName, selector, flags) {
BaseCommand.call(this);
// Validate correctness off the selector
@ -18,6 +18,7 @@ var DeleteCommand = exports.DeleteCommand = function(db, collectionName, selecto
}
}
this.flags = flags;
this.collectionName = collectionName;
this.selector = selector;
this.db = db;
@ -79,11 +80,13 @@ DeleteCommand.prototype.toBinary = function() {
_index = _index + _command.write(this.collectionName, _index, 'utf8') + 1;
_command[_index - 1] = 0;
// Write zero
_command[_index++] = 0;
_command[_index++] = 0;
_command[_index++] = 0;
_command[_index++] = 0;
// Write the flags
_command[_index + 3] = (this.flags >> 24) & 0xff;
_command[_index + 2] = (this.flags >> 16) & 0xff;
_command[_index + 1] = (this.flags >> 8) & 0xff;
_command[_index] = this.flags & 0xff;
// Adjust index
_index = _index + 4;
// Document binary length
var documentLength = 0

View file

@ -13,7 +13,7 @@ var InsertCommand = exports.InsertCommand = function(db, collectionName, checkKe
this.db = db;
this.flags = 0;
this.serializeFunctions = false;
// Ensure valid options hash
options = options == null ? {} : options;
@ -22,7 +22,13 @@ var InsertCommand = exports.InsertCommand = function(db, collectionName, checkKe
// This will finish inserting all non-index violating documents even if it returns an error
this.flags = 1;
}
// Check if we have keepGoing set -> set flag if it's the case
if(options['continueOnError'] != null && options['continueOnError']) {
// This will finish inserting all non-index violating documents even if it returns an error
this.flags = 1;
}
// Let us defined on a command basis if we want functions to be serialized or not
if(options['serializeFunctions'] != null && options['serializeFunctions']) {
this.serializeFunctions = true;
@ -36,14 +42,14 @@ InsertCommand.OP_INSERT = 2002;
InsertCommand.prototype.add = function(document) {
if(Buffer.isBuffer(document)) {
var object_size = document[0] | document[1] << 8 | document[2] << 16 | document[3] << 24;
var object_size = document[0] | document[1] << 8 | document[2] << 16 | document[3] << 24;
if(object_size != document.length) {
var error = new Error("insert raw message size does not match message header size [" + document.length + "] != [" + object_size + "]");
error.name = 'MongoError';
throw error;
}
}
this.documents.push(document);
return this;
};
@ -68,19 +74,19 @@ InsertCommand.prototype.toBinary = function() {
totalLengthOfCommand += this.db.bson.calculateObjectSize(this.documents[i], this.serializeFunctions, true);
}
}
// Let's build the single pass buffer command
var _index = 0;
var _command = new Buffer(totalLengthOfCommand);
// Write the header information to the buffer
_command[_index + 3] = (totalLengthOfCommand >> 24) & 0xff;
_command[_index + 3] = (totalLengthOfCommand >> 24) & 0xff;
_command[_index + 2] = (totalLengthOfCommand >> 16) & 0xff;
_command[_index + 1] = (totalLengthOfCommand >> 8) & 0xff;
_command[_index] = totalLengthOfCommand & 0xff;
// Adjust index
_index = _index + 4;
// Write the request ID
_command[_index + 3] = (this.requestId >> 24) & 0xff;
_command[_index + 3] = (this.requestId >> 24) & 0xff;
_command[_index + 2] = (this.requestId >> 16) & 0xff;
_command[_index + 1] = (this.requestId >> 8) & 0xff;
_command[_index] = this.requestId & 0xff;
@ -92,14 +98,14 @@ InsertCommand.prototype.toBinary = function() {
_command[_index++] = 0;
_command[_index++] = 0;
// Write the op_code for the command
_command[_index + 3] = (InsertCommand.OP_INSERT >> 24) & 0xff;
_command[_index + 3] = (InsertCommand.OP_INSERT >> 24) & 0xff;
_command[_index + 2] = (InsertCommand.OP_INSERT >> 16) & 0xff;
_command[_index + 1] = (InsertCommand.OP_INSERT >> 8) & 0xff;
_command[_index] = InsertCommand.OP_INSERT & 0xff;
// Adjust index
_index = _index + 4;
// Write flags if any
_command[_index + 3] = (this.flags >> 24) & 0xff;
_command[_index + 3] = (this.flags >> 24) & 0xff;
_command[_index + 2] = (this.flags >> 16) & 0xff;
_command[_index + 1] = (this.flags >> 8) & 0xff;
_command[_index] = this.flags & 0xff;
@ -127,15 +133,15 @@ InsertCommand.prototype.toBinary = function() {
}
// Write the length to the document
_command[_index + 3] = (documentLength >> 24) & 0xff;
_command[_index + 3] = (documentLength >> 24) & 0xff;
_command[_index + 2] = (documentLength >> 16) & 0xff;
_command[_index + 1] = (documentLength >> 8) & 0xff;
_command[_index] = documentLength & 0xff;
// Update index in buffer
_index = _index + documentLength;
// Add terminating 0 for the object
_command[_index - 1] = 0;
_command[_index - 1] = 0;
}
return _command;
};

View file

@ -8,26 +8,27 @@ var QueryCommand = exports.QueryCommand = function(db, collectionName, queryOpti
BaseCommand.call(this);
// Validate correctness off the selector
var object = query;
var object = query,
object_size;
if(Buffer.isBuffer(object)) {
var object_size = object[0] | object[1] << 8 | object[2] << 16 | object[3] << 24;
if(object_size != object.length) {
object_size = object[0] | object[1] << 8 | object[2] << 16 | object[3] << 24;
if(object_size != object.length) {
var error = new Error("query selector raw message size does not match message header size [" + object.length + "] != [" + object_size + "]");
error.name = 'MongoError';
throw error;
}
}
var object = returnFieldSelector;
object = returnFieldSelector;
if(Buffer.isBuffer(object)) {
var object_size = object[0] | object[1] << 8 | object[2] << 16 | object[3] << 24;
if(object_size != object.length) {
object_size = object[0] | object[1] << 8 | object[2] << 16 | object[3] << 24;
if(object_size != object.length) {
var error = new Error("query fields raw message size does not match message header size [" + object.length + "] != [" + object_size + "]");
error.name = 'MongoError';
throw error;
}
}
// Make sure we don't get a null exception
options = options == null ? {} : options;
// Set up options
@ -35,20 +36,69 @@ var QueryCommand = exports.QueryCommand = function(db, collectionName, queryOpti
this.queryOptions = queryOptions;
this.numberToSkip = numberToSkip;
this.numberToReturn = numberToReturn;
// Ensure we have no null query
query = query == null ? {} : query;
// Wrap query in the $query parameter so we can add read preferences for mongos
this.query = query;
this.returnFieldSelector = returnFieldSelector;
this.db = db;
// Let us defined on a command basis if we want functions to be serialized or not
if(options['serializeFunctions'] != null && options['serializeFunctions']) {
this.serializeFunctions = true;
}
}
};
inherits(QueryCommand, BaseCommand);
QueryCommand.OP_QUERY = 2004;
/*
* Adds the read prefrence to the current command
*/
QueryCommand.prototype.setMongosReadPreference = function(readPreference, tags) {
// If we have readPreference set to true set to secondary prefered
if(readPreference == true) {
readPreference = 'secondaryPreferred';
} else if(readPreference == 'false') {
readPreference = 'primary';
}
// Force the slave ok flag to be set if we are not using primary read preference
if(readPreference != false && readPreference != 'primary') {
this.queryOptions |= QueryCommand.OPTS_SLAVE;
}
// Backward compatibility, ensure $query only set on read preference so 1.8.X works
if((readPreference != null || tags != null) && this.query['$query'] == null) {
this.query = {'$query': this.query};
}
// If we have no readPreference set and no tags, check if the slaveOk bit is set
if(readPreference == null && tags == null) {
// If we have a slaveOk bit set the read preference for MongoS
if(this.queryOptions & QueryCommand.OPTS_SLAVE) {
this.query['$readPreference'] = {mode: 'secondary'}
} else {
this.query['$readPreference'] = {mode: 'primary'}
}
}
// Build read preference object
if(typeof readPreference == 'object' && readPreference['_type'] == 'ReadPreference') {
this.query['$readPreference'] = readPreference.toObject();
} else if(readPreference != null) {
// Add the read preference
this.query['$readPreference'] = {mode: readPreference};
// If we have tags let's add them
if(tags != null) {
this.query['$readPreference']['tags'] = tags;
}
}
}
/*
struct {
MsgHeader header; // standard message header
@ -61,14 +111,15 @@ struct {
}
*/
QueryCommand.prototype.toBinary = function() {
// Total length of the command
var totalLengthOfCommand = 0;
// Calculate total length of the document
if(Buffer.isBuffer(this.query)) {
totalLengthOfCommand = 4 + Buffer.byteLength(this.collectionName) + 1 + 4 + 4 + this.query.length + (4 * 4);
totalLengthOfCommand = 4 + Buffer.byteLength(this.collectionName) + 1 + 4 + 4 + this.query.length + (4 * 4);
} else {
totalLengthOfCommand = 4 + Buffer.byteLength(this.collectionName) + 1 + 4 + 4 + this.db.bson.calculateObjectSize(this.query, this.serializeFunctions, true) + (4 * 4);
totalLengthOfCommand = 4 + Buffer.byteLength(this.collectionName) + 1 + 4 + 4 + this.db.bson.calculateObjectSize(this.query, this.serializeFunctions, true) + (4 * 4);
}
// Calculate extra fields size
if(this.returnFieldSelector != null && !(Buffer.isBuffer(this.returnFieldSelector))) {
if(Object.keys(this.returnFieldSelector).length > 0) {
@ -82,14 +133,14 @@ QueryCommand.prototype.toBinary = function() {
var _index = 0;
var _command = new Buffer(totalLengthOfCommand);
// Write the header information to the buffer
_command[_index + 3] = (totalLengthOfCommand >> 24) & 0xff;
_command[_index + 3] = (totalLengthOfCommand >> 24) & 0xff;
_command[_index + 2] = (totalLengthOfCommand >> 16) & 0xff;
_command[_index + 1] = (totalLengthOfCommand >> 8) & 0xff;
_command[_index] = totalLengthOfCommand & 0xff;
// Adjust index
_index = _index + 4;
// Write the request ID
_command[_index + 3] = (this.requestId >> 24) & 0xff;
_command[_index + 3] = (this.requestId >> 24) & 0xff;
_command[_index + 2] = (this.requestId >> 16) & 0xff;
_command[_index + 1] = (this.requestId >> 8) & 0xff;
_command[_index] = this.requestId & 0xff;
@ -101,7 +152,7 @@ QueryCommand.prototype.toBinary = function() {
_command[_index++] = 0;
_command[_index++] = 0;
// Write the op_code for the command
_command[_index + 3] = (QueryCommand.OP_QUERY >> 24) & 0xff;
_command[_index + 3] = (QueryCommand.OP_QUERY >> 24) & 0xff;
_command[_index + 2] = (QueryCommand.OP_QUERY >> 16) & 0xff;
_command[_index + 1] = (QueryCommand.OP_QUERY >> 8) & 0xff;
_command[_index] = QueryCommand.OP_QUERY & 0xff;
@ -109,7 +160,7 @@ QueryCommand.prototype.toBinary = function() {
_index = _index + 4;
// Write the query options
_command[_index + 3] = (this.queryOptions >> 24) & 0xff;
_command[_index + 3] = (this.queryOptions >> 24) & 0xff;
_command[_index + 2] = (this.queryOptions >> 16) & 0xff;
_command[_index + 1] = (this.queryOptions >> 8) & 0xff;
_command[_index] = this.queryOptions & 0xff;
@ -118,10 +169,10 @@ QueryCommand.prototype.toBinary = function() {
// Write the collection name to the command
_index = _index + _command.write(this.collectionName, _index, 'utf8') + 1;
_command[_index - 1] = 0;
_command[_index - 1] = 0;
// Write the number of documents to skip
_command[_index + 3] = (this.numberToSkip >> 24) & 0xff;
_command[_index + 3] = (this.numberToSkip >> 24) & 0xff;
_command[_index + 2] = (this.numberToSkip >> 16) & 0xff;
_command[_index + 1] = (this.numberToSkip >> 8) & 0xff;
_command[_index] = this.numberToSkip & 0xff;
@ -129,7 +180,7 @@ QueryCommand.prototype.toBinary = function() {
_index = _index + 4;
// Write the number of documents to return
_command[_index + 3] = (this.numberToReturn >> 24) & 0xff;
_command[_index + 3] = (this.numberToReturn >> 24) & 0xff;
_command[_index + 2] = (this.numberToReturn >> 16) & 0xff;
_command[_index + 1] = (this.numberToReturn >> 8) & 0xff;
_command[_index] = this.numberToReturn & 0xff;
@ -151,28 +202,28 @@ QueryCommand.prototype.toBinary = function() {
}
// Write the length to the document
_command[_index + 3] = (documentLength >> 24) & 0xff;
_command[_index + 3] = (documentLength >> 24) & 0xff;
_command[_index + 2] = (documentLength >> 16) & 0xff;
_command[_index + 1] = (documentLength >> 8) & 0xff;
_command[_index] = documentLength & 0xff;
// Update index in buffer
_index = _index + documentLength;
// Add terminating 0 for the object
_command[_index - 1] = 0;
_command[_index - 1] = 0;
// Push field selector if available
if(this.returnFieldSelector != null && !(Buffer.isBuffer(this.returnFieldSelector))) {
if(Object.keys(this.returnFieldSelector).length > 0) {
var documentLength = this.db.bson.serializeWithBufferAndIndex(this.returnFieldSelector, this.checkKeys, _command, _index, this.serializeFunctions) - _index + 1;
// Write the length to the document
_command[_index + 3] = (documentLength >> 24) & 0xff;
_command[_index + 3] = (documentLength >> 24) & 0xff;
_command[_index + 2] = (documentLength >> 16) & 0xff;
_command[_index + 1] = (documentLength >> 8) & 0xff;
_command[_index] = documentLength & 0xff;
// Update index in buffer
_index = _index + documentLength;
// Add terminating 0 for the object
_command[_index - 1] = 0;
_command[_index - 1] = 0;
}
} if(this.returnFieldSelector != null && Buffer.isBuffer(this.returnFieldSelector)) {
// Document binary length
@ -182,19 +233,19 @@ QueryCommand.prototype.toBinary = function() {
// Serialize the selector
documentLength = object.length;
// Copy the data into the current buffer
object.copy(_command, _index);
object.copy(_command, _index);
// Write the length to the document
_command[_index + 3] = (documentLength >> 24) & 0xff;
_command[_index + 3] = (documentLength >> 24) & 0xff;
_command[_index + 2] = (documentLength >> 16) & 0xff;
_command[_index + 1] = (documentLength >> 8) & 0xff;
_command[_index] = documentLength & 0xff;
// Update index in buffer
_index = _index + documentLength;
// Add terminating 0 for the object
_command[_index - 1] = 0;
_command[_index - 1] = 0;
}
// Return finished command
return _command;
};
@ -206,4 +257,5 @@ QueryCommand.OPTS_SLAVE = 4;
QueryCommand.OPTS_OPLOG_REPLY = 8;
QueryCommand.OPTS_NO_CURSOR_TIMEOUT = 16;
QueryCommand.OPTS_AWAIT_DATA = 32;
QueryCommand.OPTS_EXHAUST = 64;
QueryCommand.OPTS_EXHAUST = 64;
QueryCommand.OPTS_PARTIAL = 128;

View file

@ -0,0 +1,50 @@
var EventEmitter = require('events').EventEmitter
, inherits = require('util').inherits;
var Base = function Base() {
EventEmitter.call(this);
}
/**
* @ignore
*/
inherits(Base, EventEmitter);
/**
* Fire all the errors
* @ignore
*/
Base.prototype.__executeAllCallbacksWithError = function(err) {
// Locate all the possible callbacks that need to return
for(var i = 0; i < this.dbInstances.length; i++) {
// Fetch the db Instance
var dbInstance = this.dbInstances[i];
// Check all callbacks
var keys = Object.keys(dbInstance._callBackStore._notReplied);
// For each key check if it's a callback that needs to be returned
for(var j = 0; j < keys.length; j++) {
var info = dbInstance._callBackStore._notReplied[keys[j]];
// Check if we have a chained command (findAndModify)
if(info && info['chained'] && Array.isArray(info['chained']) && info['chained'].length > 0) {
var chained = info['chained'];
// Only callback once and the last one is the right one
var finalCallback = chained.pop();
// Emit only the last event
dbInstance._callBackStore.emit(finalCallback, err, null);
// Put back the final callback to ensure we don't call all commands in the chain
chained.push(finalCallback);
// Remove all chained callbacks
for(var i = 0; i < chained.length; i++) {
delete dbInstance._callBackStore._notReplied[chained[i]];
}
} else {
dbInstance._callBackStore.emit(keys[j], err, null);
}
}
}
}
exports.Base = Base;

View file

@ -8,18 +8,22 @@ var utils = require('./connection_utils'),
var Connection = exports.Connection = function(id, socketOptions) {
// Set up event emitter
EventEmitter.call(this);
EventEmitter.call(this);
// Store all socket options
this.socketOptions = socketOptions ? socketOptions : {host:'localhost', port:27017};
this.socketOptions = socketOptions ? socketOptions : {host:'localhost', port:27017, domainSocket:false};
// Set keep alive default if not overriden
if(this.socketOptions.keepAlive == null && (process.platform !== "sunos" || process.platform !== "win32")) this.socketOptions.keepAlive = 100;
// Id for the connection
this.id = id;
// State of the connection
this.connected = false;
// Set if this is a domain socket
this.domainSocket = this.socketOptions.domainSocket;
//
// Connection parsing state
//
this.maxBsonSize = socketOptions.maxBsonSize ? socketOptions.maxBsonSize : Connection.DEFAULT_MAX_BSON_SIZE;
this.maxBsonSize = socketOptions.maxBsonSize ? socketOptions.maxBsonSize : Connection.DEFAULT_MAX_BSON_SIZE;
// Contains the current message bytes
this.buffer = null;
// Contains the current message size
@ -37,7 +41,7 @@ var Connection = exports.Connection = function(id, socketOptions) {
}
// Set max bson size
Connection.DEFAULT_MAX_BSON_SIZE = 4 * 1024 * 1024 * 4 * 3;
Connection.DEFAULT_MAX_BSON_SIZE = 1024 * 1024 * 4;
// Inherit event emitter so we can emit stuff wohoo
inherits(Connection, EventEmitter);
@ -46,9 +50,9 @@ Connection.prototype.start = function() {
// If we have a normal connection
if(this.socketOptions.ssl) {
// Create a new stream
this.connection = new net.Socket();
// Set options on the socket
this.connection.setTimeout(this.socketOptions.timeout);
this.connection = new net.Socket();
// Set timeout allowing backward compatibility to timeout if no connectTimeoutMS is set
this.connection.setTimeout(this.socketOptions.connectTimeoutMS != null ? this.socketOptions.connectTimeoutMS : this.socketOptions.timeout);
// Work around for 0.4.X
if(process.version.indexOf("v0.4") == -1) this.connection.setNoDelay(this.socketOptions.noDelay);
// Set keep alive if defined
@ -57,15 +61,15 @@ Connection.prototype.start = function() {
this.connection.setKeepAlive(true, this.socketOptions.keepAlive);
} else {
this.connection.setKeepAlive(false);
}
}
}
// Set up pair for tls with server, accept self-signed certificates as well
var pair = this.pair = tls.createSecurePair(false);
// Set up encrypted streams
this.pair.encrypted.pipe(this.connection);
this.connection.pipe(this.pair.encrypted);
// Setup clearText stream
this.writeSteam = this.pair.cleartext;
// Add all handlers to the socket to manage it
@ -80,14 +84,21 @@ Connection.prototype.start = function() {
this.writeSteam.on("close", closeHandler(this));
// Start socket
this.connection.connect(this.socketOptions.port, this.socketOptions.host);
if(this.logger != null && this.logger.doDebug){
this.logger.debug("opened connection", this.socketOptions);
}
} else {
// // Create a new stream
// this.connection = new net.Stream();
// // Create new connection instance
// this.connection = new net.Socket();
this.connection = net.createConnection(this.socketOptions.port, this.socketOptions.host);
// Create new connection instance
if(this.domainSocket) {
this.connection = net.createConnection(this.socketOptions.host);
} else {
this.connection = net.createConnection(this.socketOptions.port, this.socketOptions.host);
}
if(this.logger != null && this.logger.doDebug){
this.logger.debug("opened connection", this.socketOptions);
}
// Set options on the socket
this.connection.setTimeout(this.socketOptions.timeout);
this.connection.setTimeout(this.socketOptions.connectTimeoutMS != null ? this.socketOptions.connectTimeoutMS : this.socketOptions.timeout);
// Work around for 0.4.X
if(process.version.indexOf("v0.4") == -1) this.connection.setNoDelay(this.socketOptions.noDelay);
// Set keep alive if defined
@ -96,7 +107,7 @@ Connection.prototype.start = function() {
this.connection.setKeepAlive(true, this.socketOptions.keepAlive);
} else {
this.connection.setKeepAlive(false);
}
}
}
// Set up write stream
@ -110,9 +121,7 @@ Connection.prototype.start = function() {
this.connection.on("timeout", timeoutHandler(this));
this.connection.on("drain", drainHandler(this));
this.connection.on("close", closeHandler(this));
// // Start socket
// this.connection.connect(this.socketOptions.port, this.socketOptions.host);
}
}
}
// Check if the sockets are live
@ -127,40 +136,52 @@ Connection.prototype.write = function(command, callback) {
if(Array.isArray(command)) {
for(var i = 0; i < command.length; i++) {
var binaryCommand = command[i].toBinary()
if(this.logger != null && this.logger.doDebug) this.logger.debug("writing command to mongodb", binaryCommand);
if(!this.socketOptions['disableDriverBSONSizeCheck'] && binaryCommand.length > this.maxBsonSize)
return callback(new Error("Document exceeds maximal allowed bson size of " + this.maxBsonSize + " bytes"));
if(this.logger != null && this.logger.doDebug)
this.logger.debug("writing command to mongodb", {binary: binaryCommand, json: command[i]});
var r = this.writeSteam.write(binaryCommand);
}
} else {
var binaryCommand = command.toBinary()
if(this.logger != null && this.logger.doDebug) this.logger.debug("writing command to mongodb", binaryCommand);
if(!this.socketOptions['disableDriverBSONSizeCheck'] && binaryCommand.length > this.maxBsonSize)
return callback(new Error("Document exceeds maximal allowed bson size of " + this.maxBsonSize + " bytes"));
if(this.logger != null && this.logger.doDebug)
this.logger.debug("writing command to mongodb", {binary: binaryCommand, json: command});
var r = this.writeSteam.write(binaryCommand);
}
} catch (err) {
if(typeof callback === 'function') callback(err);
}
} catch (err) {
if(typeof callback === 'function') callback(err);
}
}
// Force the closure of the connection
Connection.prototype.close = function() {
Connection.prototype.close = function() {
// clear out all the listeners
resetHandlers(this, true);
// Add a dummy error listener to catch any weird last moment errors (and ignore them)
this.connection.on("error", function() {})
// destroy connection
this.connection.destroy();
if(this.logger != null && this.logger.doDebug){
this.logger.debug("closed connection", this.connection);
}
}
// Reset all handlers
var resetHandlers = function(self, clearListeners) {
var resetHandlers = function(self, clearListeners) {
self.eventHandlers = {error:[], connect:[], close:[], end:[], timeout:[], parseError:[], message:[]};
// If we want to clear all the listeners
if(clearListeners && self.connection != null) {
var keys = Object.keys(self.eventHandlers);
var keys = Object.keys(self.eventHandlers);
// Remove all listeners
for(var i = 0; i < keys.length; i++) {
self.connection.removeAllListeners(keys[i]);
}
}
}
}
@ -170,9 +191,11 @@ var resetHandlers = function(self, clearListeners) {
// Connect handler
var connectHandler = function(self) {
return function() {
return function() {
// Set connected
self.connected = true;
// Now that we are connected set the socket timeout
self.connection.setTimeout(self.socketOptions.socketTimeoutMS != null ? self.socketOptions.socketTimeoutMS : self.socketOptions.timeout);
// Emit the connect event with no error
self.emit("connect", null, self);
}
@ -202,34 +225,34 @@ var createDataHandler = exports.Connection.createDataHandler = function(self) {
data.copy(self.buffer, self.bytesRead, 0, remainingBytesToRead);
// Slice the overflow into a new buffer that we will then re-parse
data = data.slice(remainingBytesToRead);
// Emit current complete message
try {
self.emit("message", self.buffer, self);
var emitBuffer = self.buffer;
// Reset state of buffer
self.buffer = null;
self.sizeOfMessage = 0;
self.bytesRead = 0;
self.stubBuffer = null;
// Emit the buffer
self.emit("message", emitBuffer, self);
} catch(err) {
var errorObject = {err:"socketHandler", trace:err, bin:buffer, parseState:{
sizeOfMessage:self.sizeOfMessage,
sizeOfMessage:self.sizeOfMessage,
bytesRead:self.bytesRead,
stubBuffer:self.stubBuffer}};
if(self.logger != null && self.logger.doError) self.logger.error("parseError", errorObject);
// We got a parse Error fire it off then keep going
self.emit("parseError", errorObject, self);
}
// Reset state of buffer
self.buffer = null;
self.sizeOfMessage = 0;
self.bytesRead = 0;
self.stubBuffer = null;
}
} else {
// Stub buffer is kept in case we don't get enough bytes to determine the
// size of the message (< 4 bytes)
if(self.stubBuffer != null && self.stubBuffer.length > 0) {
if(self.stubBuffer != null && self.stubBuffer.length > 0) {
// If we have enough bytes to determine the message size let's do it
if(self.stubBuffer.length + data.length > 4) {
if(self.stubBuffer.length + data.length > 4) {
// Prepad the data
var newData = new Buffer(self.stubBuffer.length + data.length);
self.stubBuffer.copy(newData, 0);
@ -261,9 +284,9 @@ var createDataHandler = exports.Connection.createDataHandler = function(self) {
// If we have a negative sizeOfMessage emit error and return
if(sizeOfMessage < 0 || sizeOfMessage > self.maxBsonSize) {
var errorObject = {err:"socketHandler", trace:'', bin:self.buffer, parseState:{
sizeOfMessage:sizeOfMessage,
bytesRead:self.bytesRead,
stubBuffer:self.stubBuffer}};
sizeOfMessage: sizeOfMessage,
bytesRead: self.bytesRead,
stubBuffer: self.stubBuffer}};
if(self.logger != null && self.logger.doError) self.logger.error("parseError", errorObject);
// We got a parse Error fire it off then keep going
self.emit("parseError", errorObject, self);
@ -283,10 +306,10 @@ var createDataHandler = exports.Connection.createDataHandler = function(self) {
self.stubBuffer = null;
// Exit parsing loop
data = new Buffer(0);
} else if(sizeOfMessage > 4 && sizeOfMessage < self.maxBsonSize && sizeOfMessage == data.length) {
try {
self.emit("message", data, self);
var emitBuffer = data;
// Reset state of buffer
self.buffer = null;
self.sizeOfMessage = 0;
@ -294,27 +317,28 @@ var createDataHandler = exports.Connection.createDataHandler = function(self) {
self.stubBuffer = null;
// Exit parsing loop
data = new Buffer(0);
// Emit the message
self.emit("message", emitBuffer, self);
} catch (err) {
var errorObject = {err:"socketHandler", trace:err, bin:self.buffer, parseState:{
sizeOfMessage:self.sizeOfMessage,
sizeOfMessage:self.sizeOfMessage,
bytesRead:self.bytesRead,
stubBuffer:self.stubBuffer}};
if(self.logger != null && self.logger.doError) self.logger.error("parseError", errorObject);
// We got a parse Error fire it off then keep going
self.emit("parseError", errorObject, self);
}
}
} else if(sizeOfMessage <= 4 || sizeOfMessage > self.maxBsonSize) {
var errorObject = {err:"socketHandler", trace:null, bin:data, parseState:{
sizeOfMessage:sizeOfMessage,
sizeOfMessage:sizeOfMessage,
bytesRead:0,
buffer:null,
buffer:null,
stubBuffer:null}};
if(self.logger != null && self.logger.doError) self.logger.error("parseError", errorObject);
// We got a parse Error fire it off then keep going
self.emit("parseError", errorObject, self);
// Clear out the state of the parser
// Clear out the state of the parser
self.buffer = null;
self.sizeOfMessage = 0;
self.bytesRead = 0;
@ -324,17 +348,19 @@ var createDataHandler = exports.Connection.createDataHandler = function(self) {
} else {
try {
self.emit("message", data.slice(0, sizeOfMessage), self);
var emitBuffer = data.slice(0, sizeOfMessage);
// Reset state of buffer
self.buffer = null;
self.sizeOfMessage = 0;
self.bytesRead = 0;
self.stubBuffer = null;
// Copy rest of message
data = data.slice(sizeOfMessage);
data = data.slice(sizeOfMessage);
// Emit the message
self.emit("message", emitBuffer, self);
} catch (err) {
var errorObject = {err:"socketHandler", trace:err, bin:self.buffer, parseState:{
sizeOfMessage:sizeOfMessage,
sizeOfMessage:sizeOfMessage,
bytesRead:self.bytesRead,
stubBuffer:self.stubBuffer}};
if(self.logger != null && self.logger.doError) self.logger.error("parseError", errorObject);
@ -342,7 +368,7 @@ var createDataHandler = exports.Connection.createDataHandler = function(self) {
self.emit("parseError", errorObject, self);
}
}
}
} else {
// Create a buffer that contains the space for the non-complete message
self.stubBuffer = new Buffer(data.length)
@ -352,7 +378,7 @@ var createDataHandler = exports.Connection.createDataHandler = function(self) {
data = new Buffer(0);
}
}
}
}
}
}
}
@ -362,11 +388,11 @@ var endHandler = function(self) {
// Set connected to false
self.connected = false;
// Emit end event
self.emit("end", {err: 'connection received Fin packet from [' + self.socketOptions.host + ':' + self.socketOptions.port + ']'}, self);
self.emit("end", {err: 'connection received Fin packet from [' + self.socketOptions.host + ':' + self.socketOptions.port + ']'}, self);
}
}
var timeoutHandler = function(self) {
var timeoutHandler = function(self) {
return function() {
self.emit("timeout", {err: 'connection to [' + self.socketOptions.host + ':' + self.socketOptions.port + '] timed out'}, self);
}
@ -377,7 +403,7 @@ var drainHandler = function(self) {
}
}
var errorHandler = function(self) {
var errorHandler = function(self) {
return function(err) {
// Set connected to false
self.connected = false;
@ -387,9 +413,9 @@ var errorHandler = function(self) {
}
var closeHandler = function(self) {
return function(hadError) {
return function(hadError) {
// If we have an error during the connection phase
if(hadError && !self.connected) {
if(hadError && !self.connected) {
// Set disconnected
self.connected = false;
// Emit error
@ -398,7 +424,7 @@ var closeHandler = function(self) {
// Set disconnected
self.connected = false;
// Emit close
self.emit("close", {err: 'connection closed to [' + self.socketOptions.host + ':' + self.socketOptions.port + ']'}, self);
self.emit("close", {err: 'connection closed to [' + self.socketOptions.host + ':' + self.socketOptions.port + ']'}, self);
}
}
}

View file

@ -7,18 +7,31 @@ var utils = require('./connection_utils'),
Connection = require("./connection").Connection;
var ConnectionPool = exports.ConnectionPool = function(host, port, poolSize, bson, socketOptions) {
if(typeof host !== 'string' || typeof port !== 'number') throw "host and port must be specified [" + host + ":" + port + "]";
if(typeof host !== 'string') {
throw new Error("host must be specified [" + host + "]");
}
// Set up event emitter
EventEmitter.call(this);
// Keep all options for the socket in a specific collection allowing the user to specify the
EventEmitter.call(this);
// Keep all options for the socket in a specific collection allowing the user to specify the
// Wished upon socket connection parameters
this.socketOptions = typeof socketOptions === 'object' ? socketOptions : {};
this.socketOptions.host = host;
this.socketOptions.port = port;
this.socketOptions.domainSocket = false;
this.bson = bson;
// PoolSize is always + 1 for special reserved "measurment" socket (like ping, stats etc)
this.poolSize = poolSize;
this.minPoolSize = Math.floor(this.poolSize / 2) + 1;
// Check if the host is a socket
if(host.match(/^\//)) {
this.socketOptions.domainSocket = true;
} else if(typeof port !== 'number') {
throw new Error("port must be specified [" + port + "]");
}
// Set default settings for the socket options
utils.setIntegerParameter(this.socketOptions, 'timeout', 0);
// Delay before writing out the data to the server
@ -28,19 +41,17 @@ var ConnectionPool = exports.ConnectionPool = function(host, port, poolSize, bso
// Set the encoding of the data read, default is binary == null
utils.setStringParameter(this.socketOptions, 'encoding', null);
// Allows you to set a throttling bufferSize if you need to stop overflows
utils.setIntegerParameter(this.socketOptions, 'bufferSize', 0);
utils.setIntegerParameter(this.socketOptions, 'bufferSize', 0);
// Internal structures
this.openConnections = [];
this.connections = [];
// Assign connection id's
this.connectionId = 0;
// Current index for selection of pool connection
this.currentConnectionIndex = 0;
// The pool state
this._poolState = 'disconnected';
this._poolState = 'disconnected';
// timeout control
this._timeout = false;
}
@ -50,17 +61,16 @@ inherits(ConnectionPool, EventEmitter);
ConnectionPool.prototype.setMaxBsonSize = function(maxBsonSize) {
if(maxBsonSize == null){
maxBsonSize = Connection.DEFAULT_MAX_BSON_SIZE;
}
}
for(var i = 0; i < this.openConnections.length; i++) {
this.openConnections[i].maxBsonSize = maxBsonSize;
}
}
}
// Start a function
var _connect = function(_self) {
return new function() {
var connectionStatus = _self._poolState;
// Create a new connection instance
var connection = new Connection(_self.connectionId++, _self.socketOptions);
// Set logger on pool
@ -69,15 +79,13 @@ var _connect = function(_self) {
connection.on("connect", function(err, connection) {
// Add connection to list of open connections
_self.openConnections.push(connection);
_self.connections.push(connection)
// If the number of open connections is equal to the poolSize signal ready pool
if(_self.connections.length === _self.poolSize && _self._poolState !== 'disconnected') {
if(_self.openConnections.length === _self.poolSize && _self._poolState !== 'disconnected') {
// Set connected
_self._poolState = 'connected';
// Emit pool ready
_self.emit("poolReady");
} else if(_self.connections.length < _self.poolSize) {
} else if(_self.openConnections.length < _self.poolSize) {
// We need to open another connection, make sure it's in the next
// tick so we don't get a cascade of errors
process.nextTick(function() {
@ -92,76 +100,62 @@ var _connect = function(_self) {
connection.on("error", function(err, connection) {
numberOfErrors++;
// If we are already disconnected ignore the event
if(connectionStatus != 'disconnected' && _self.listeners("error").length > 0) {
_self.emit("error", err);
if(_self._poolState != 'disconnected' && _self.listeners("error").length > 0) {
_self.emit("error", err);
}
// Set disconnected
connectionStatus = 'disconnected';
// Set disconnected
_self._poolState = 'disconnected';
// Clean up
_self.openConnections = [];
_self.connections = [];
_self._poolState = 'disconnected';
// Stop
_self.stop();
});
// Close handler
connection.on("close", function() {
// If we are already disconnected ignore the event
if(connectionStatus !== 'disconnected' && _self.listeners("close").length > 0) {
_self.emit("close");
if(_self._poolState !== 'disconnected' && _self.listeners("close").length > 0) {
_self.emit("close");
}
// Set disconnected
connectionStatus = 'disconnected';
// Set disconnected
_self._poolState = 'disconnected';
// Clean up
_self.openConnections = [];
_self.connections = [];
_self._poolState = 'disconnected';
// Stop
_self.stop();
});
// Timeout handler
connection.on("timeout", function(err, connection) {
// If we are already disconnected ignore the event
if(connectionStatus !== 'disconnected' && _self.listeners("timeout").length > 0) {
_self.emit("timeout", err);
if(_self._poolState !== 'disconnected' && _self.listeners("timeout").length > 0) {
_self.emit("timeout", err);
}
// Set disconnected
connectionStatus = 'disconnected';
// Set disconnected
_self._poolState = 'disconnected';
// Clean up
_self.openConnections = [];
_self.connections = [];
_self._poolState = 'disconnected';
// Stop
_self.stop();
});
// Parse error, needs a complete shutdown of the pool
connection.on("parseError", function() {
// If we are already disconnected ignore the event
if(connectionStatus !== 'disconnected' && _self.listeners("parseError").length > 0) {
// if(connectionStatus == 'connected') {
_self.emit("parseError", new Error("parseError occured"));
if(_self._poolState !== 'disconnected' && _self.listeners("parseError").length > 0) {
_self.emit("parseError", new Error("parseError occured"));
}
// Set disconnected
connectionStatus = 'disconnected';
_self.stop();
});
connection.on("message", function(message) {
connection.on("message", function(message) {
_self.emit("message", message);
});
// Start connection in the next tick
connection.start();
connection.start();
}();
}
// Start method, will throw error if no listeners are available
// Pass in an instance of the listener that contains the api for
// Pass in an instance of the listener that contains the api for
// finding callbacks for a given message etc.
ConnectionPool.prototype.start = function() {
var markerDate = new Date().getTime();
@ -170,9 +164,9 @@ ConnectionPool.prototype.start = function() {
if(this.listeners("poolReady").length == 0) {
throw "pool must have at least one listener ready that responds to the [poolReady] event";
}
// Set pool state to connecting
this._poolState = 'connecting';
this._poolState = 'connecting';
this._timeout = false;
_connect(self);
@ -190,22 +184,20 @@ ConnectionPool.prototype.restart = function() {
ConnectionPool.prototype.stop = function(removeListeners) {
removeListeners = removeListeners == null ? true : removeListeners;
// Set disconnected
this._poolState = 'disconnected';
this._poolState = 'disconnected';
// Clear all listeners if specified
if(removeListeners) {
this.removeAllEventListeners();
this.removeAllEventListeners();
}
// Close all connections
for(var i = 0; i < this.connections.length; i++) {
this.connections[i].close();
for(var i = 0; i < this.openConnections.length; i++) {
this.openConnections[i].close();
}
// Clean up
// this.connectionsWithErrors = [];
this.openConnections = [];
this.connections = [];
this.openConnections = [];
}
// Check the status of the connection
@ -221,7 +213,7 @@ ConnectionPool.prototype.checkoutConnection = function(id) {
}
ConnectionPool.prototype.getAllConnections = function() {
return this.connections;
return this.openConnections;
}
// Remove all non-needed event listeners

View file

@ -0,0 +1,333 @@
var ReadPreference = require('./read_preference').ReadPreference
, Base = require('./base').Base
, inherits = require('util').inherits;
/**
* Mongos constructor provides a connection to a mongos proxy including failover to additional servers
*
* Options
* - **socketOptions** {Object, default:null}, an object containing socket options to use (noDelay:(boolean), keepAlive:(number), connectTimeoutMS:(number), socketTimeoutMS:(number))
* - **ha** {Boolean, default:true}, turn on high availability, attempts to reconnect to down proxies
* - **haInterval** {Number, default:2000}, time between each replicaset status check.
*
* @class Represents a Mongos connection with failover to backup proxies
* @param {Array} list of mongos server objects
* @param {Object} [options] additional options for the mongos connection
*/
var Mongos = function Mongos(servers, options) {
// Set up basic
if(!(this instanceof Mongos))
return new Mongos(servers, options);
// Set up event emitter
Base.call(this);
// Throw error on wrong setup
if(servers == null || !Array.isArray(servers) || servers.length == 0)
throw new Error("At least one mongos proxy must be in the array");
// Ensure we have at least an empty options object
this.options = options == null ? {} : options;
// Set default connection pool options
this.socketOptions = this.options.socketOptions != null ? this.options.socketOptions : {};
// Enabled ha
this.haEnabled = this.options['ha'] == null ? true : this.options['ha'];
// How often are we checking for new servers in the replicaset
this.mongosStatusCheckInterval = this.options['haInterval'] == null ? 2000 : this.options['haInterval'];
// Save all the server connections
this.servers = servers;
// Servers we need to attempt reconnect with
this.downServers = [];
// Just contains the current lowest ping time and server
this.lowestPingTimeServer = null;
this.lowestPingTime = 0;
// Add options to servers
for(var i = 0; i < this.servers.length; i++) {
var server = this.servers[i];
// Default empty socket options object
var socketOptions = {host: server.host, port: server.port};
// If a socket option object exists clone it
if(this.socketOptions != null) {
var keys = Object.keys(this.socketOptions);
for(var k = 0; k < keys.length;k++) socketOptions[keys[i]] = this.socketOptions[keys[i]];
}
// Set socket options
server.socketOptions = socketOptions;
}
}
/**
* @ignore
*/
inherits(Mongos, Base);
/**
* @ignore
*/
Mongos.prototype.isMongos = function() {
return true;
}
/**
* @ignore
*/
Mongos.prototype.connect = function(db, options, callback) {
if('function' === typeof options) callback = options, options = {};
if(options == null) options = {};
if(!('function' === typeof callback)) callback = null;
var self = this;
// Keep reference to parent
this.db = db;
// Set server state to connecting
this._serverState = 'connecting';
// Number of total servers that need to initialized (known servers)
this._numberOfServersLeftToInitialize = this.servers.length;
// Default to the first proxy server as the first one to use
this._currentMongos = this.servers[0];
// Connect handler
var connectHandler = function(_server) {
return function(err, result) {
self._numberOfServersLeftToInitialize = self._numberOfServersLeftToInitialize - 1;
if(self._numberOfServersLeftToInitialize == 0) {
// Start ha function if it exists
if(self.haEnabled) {
// Setup the ha process
self._replicasetTimeoutId = setTimeout(self.mongosCheckFunction, self.mongosStatusCheckInterval);
}
// Set the mongos to connected
self._serverState = "connected";
// Emit the open event
self.db.emit("open", null, self.db);
// Callback
callback(null, self.db);
}
}
};
// Error handler
var errorOrCloseHandler = function(_server) {
return function(err, result) {
// Create current mongos comparision
var currentUrl = self._currentMongos.host + ":" + self._currentMongos.port;
var serverUrl = this.host + ":" + this.port;
// We need to check if the server that closed is the actual current proxy we are using, otherwise
// just ignore
if(currentUrl == serverUrl) {
// Remove the server from the list
if(self.servers.indexOf(_server) != -1) {
self.servers.splice(self.servers.indexOf(_server), 1);
}
// Pick the next one on the list if there is one
for(var i = 0; i < self.servers.length; i++) {
// Grab the server out of the array (making sure there is no servers in the list if none available)
var server = self.servers[i];
// Generate url for comparision
var serverUrl = server.host + ":" + server.port;
// It's not the current one and connected set it as the current db
if(currentUrl != serverUrl && server.isConnected()) {
self._currentMongos = server;
break;
}
}
}
// Ensure we don't store the _server twice
if(self.downServers.indexOf(_server) == -1) {
// Add the server instances
self.downServers.push(_server);
}
}
}
// Mongo function
this.mongosCheckFunction = function() {
// If we have down servers let's attempt a reconnect
if(self.downServers.length > 0) {
var numberOfServersLeft = self.downServers.length;
// Attempt to reconnect
for(var i = 0; i < self.downServers.length; i++) {
var downServer = self.downServers.pop();
// Attemp to reconnect
downServer.connect(self.db, {returnIsMasterResults: true}, function(_server) {
// Return a function to check for the values
return function(err, result) {
// Adjust the number of servers left
numberOfServersLeft = numberOfServersLeft - 1;
if(err != null) {
self.downServers.push(_server);
} else {
// Add server event handlers
_server.on("close", errorOrCloseHandler(_server));
_server.on("error", errorOrCloseHandler(_server));
// Add to list of servers
self.servers.push(_server);
}
if(numberOfServersLeft <= 0) {
// Perfom another ha
self._replicasetTimeoutId = setTimeout(self.mongosCheckFunction, self.mongosStatusCheckInterval);
}
}
}(downServer));
}
} else if(self.servers.length > 0) {
var numberOfServersLeft = self.servers.length;
var _s = new Date().getTime()
// Else let's perform a ping command
for(var i = 0; i < self.servers.length; i++) {
var executePing = function(_server) {
// Get a read connection
var _connection = _server.checkoutReader();
// Execute ping command
self.db.command({ping:1}, {connection:_connection}, function(err, result) {
var pingTime = new Date().getTime() - _s;
// If no server set set the first one, otherwise check
// the lowest ping time and assign the server if it's got a lower ping time
if(self.lowestPingTimeServer == null) {
self.lowestPingTimeServer = _server;
self.lowestPingTime = pingTime;
self._currentMongos = _server;
} else if(self.lowestPingTime > pingTime
&& (_server.host != self.lowestPingTimeServer.host || _server.port != self.lowestPingTimeServer.port)) {
self.lowestPingTimeServer = _server;
self.lowestPingTime = pingTime;
self._currentMongos = _server;
}
// Number of servers left
numberOfServersLeft = numberOfServersLeft - 1;
// All active mongos's pinged
if(numberOfServersLeft == 0) {
// Perfom another ha
self._replicasetTimeoutId = setTimeout(self.mongosCheckFunction, self.mongosStatusCheckInterval);
}
})
}
// Execute the function
executePing(self.servers[i]);
}
} else {
self._replicasetTimeoutId = setTimeout(self.mongosCheckFunction, self.mongosStatusCheckInterval);
}
}
// Connect all the server instances
for(var i = 0; i < this.servers.length; i++) {
// Get the connection
var server = this.servers[i];
server.mongosInstance = this;
// Add server event handlers
server.on("close", errorOrCloseHandler(server));
server.on("timeout", errorOrCloseHandler(server));
server.on("error", errorOrCloseHandler(server));
// Connect the instance
server.connect(self.db, {returnIsMasterResults: true}, connectHandler(server));
}
}
/**
* @ignore
* Just return the currently picked active connection
*/
Mongos.prototype.allServerInstances = function() {
return this.servers;
}
/**
* Always ourselves
* @ignore
*/
Mongos.prototype.setReadPreference = function() {}
/**
* @ignore
*/
Mongos.prototype.allRawConnections = function() {
// Neeed to build a complete list of all raw connections, start with master server
var allConnections = [];
// Add all connections
for(var i = 0; i < this.servers.length; i++) {
allConnections = allConnections.concat(this.servers[i].allRawConnections());
}
// Return all the conections
return allConnections;
}
/**
* @ignore
*/
Mongos.prototype.isConnected = function() {
return this._serverState == "connected";
}
/**
* @ignore
*/
Mongos.prototype.checkoutWriter = function() {
// No current mongo, just pick first server
if(this._currentMongos == null && this.servers.length > 0) {
return this.servers[0].checkoutWriter();
}
return this._currentMongos.checkoutWriter();
}
/**
* @ignore
*/
Mongos.prototype.checkoutReader = function(read) {
// If we have a read preference object unpack it
if(typeof read == 'object' && read['_type'] == 'ReadPreference') {
// Validate if the object is using a valid mode
if(!read.isValid()) throw new Error("Illegal readPreference mode specified, " + read.mode);
} else if(!ReadPreference.isValid(read)) {
throw new Error("Illegal readPreference mode specified, " + read);
}
// No current mongo, just pick first server
if(this._currentMongos == null && this.servers.length > 0) {
return this.servers[0].checkoutReader();
}
return this._currentMongos.checkoutReader();
}
/**
* @ignore
*/
Mongos.prototype.close = function(callback) {
var self = this;
// Set server status as disconnected
this._serverState = 'disconnected';
// Number of connections to close
var numberOfConnectionsToClose = self.servers.length;
// If we have a ha process running kill it
if(self._replicasetTimeoutId != null) clearTimeout(self._replicasetTimeoutId);
// Close all proxy connections
for(var i = 0; i < self.servers.length; i++) {
self.servers[i].close(function(err, result) {
numberOfConnectionsToClose = numberOfConnectionsToClose - 1;
// Callback if we have one defined
if(numberOfConnectionsToClose == 0 && typeof callback == 'function') {
callback(null);
}
});
}
}
/**
* @ignore
* Return the used state
*/
Mongos.prototype._isUsed = function() {
return this._used;
}
exports.Mongos = Mongos;

View file

@ -0,0 +1,66 @@
/**
* A class representation of the Read Preference.
*
* Read Preferences
* - **ReadPreference.PRIMARY**, Read from primary only. All operations produce an error (throw an exception where applicable) if primary is unavailable. Cannot be combined with tags (This is the default.).
* - **ReadPreference.PRIMARY_PREFERRED**, Read from primary if available, otherwise a secondary.
* - **ReadPreference.SECONDARY**, Read from secondary if available, otherwise error.
* - **ReadPreference.SECONDARY_PREFERRED**, Read from a secondary if available, otherwise read from the primary.
* - **ReadPreference.NEAREST**, All modes read from among the nearest candidates, but unlike other modes, NEAREST will include both the primary and all secondaries in the random selection.
*
* @class Represents a Read Preference.
* @param {String} the read preference type
* @param {Object} tags
* @return {ReadPreference}
*/
var ReadPreference = function(mode, tags) {
if(!(this instanceof ReadPreference))
return new ReadPreference(mode, tags);
this._type = 'ReadPreference';
this.mode = mode;
this.tags = tags;
}
/**
* @ignore
*/
ReadPreference.isValid = function(_mode) {
return (_mode == ReadPreference.PRIMARY || _mode == ReadPreference.PRIMARY_PREFERRED
|| _mode == ReadPreference.SECONDARY || _mode == ReadPreference.SECONDARY_PREFERRED
|| _mode == ReadPreference.NEAREST);
}
/**
* @ignore
*/
ReadPreference.prototype.isValid = function(mode) {
var _mode = typeof mode == 'string' ? mode : this.mode;
return ReadPreference.isValid(_mode);
}
/**
* @ignore
*/
ReadPreference.prototype.toObject = function() {
var object = {mode:this.mode};
if(this.tags != null) {
object['tags'] = this.tags;
}
return object;
}
/**
* @ignore
*/
ReadPreference.PRIMARY = 'primary';
ReadPreference.PRIMARY_PREFERRED = 'primaryPreferred';
ReadPreference.SECONDARY = 'secondary';
ReadPreference.SECONDARY_PREFERRED = 'secondaryPreferred';
ReadPreference.NEAREST = 'nearest'
/**
* @ignore
*/
exports.ReadPreference = ReadPreference;

File diff suppressed because it is too large Load diff

View file

@ -1,972 +0,0 @@
var Connection = require('./connection').Connection,
DbCommand = require('../commands/db_command').DbCommand,
MongoReply = require('../responses/mongo_reply').MongoReply,
debug = require('util').debug,
EventEmitter = require('events').EventEmitter,
inherits = require('util').inherits,
inspect = require('util').inspect,
Server = require('./server').Server,
PingStrategy = require('./strategies/ping_strategy').PingStrategy,
StatisticsStrategy = require('./strategies/statistics_strategy').StatisticsStrategy;
const STATE_STARTING_PHASE_1 = 0;
const STATE_PRIMARY = 1;
const STATE_SECONDARY = 2;
const STATE_RECOVERING = 3;
const STATE_FATAL_ERROR = 4;
const STATE_STARTING_PHASE_2 = 5;
const STATE_UNKNOWN = 6;
const STATE_ARBITER = 7;
const STATE_DOWN = 8;
const STATE_ROLLBACK = 9;
/**
* ReplSetServers constructor provides master-slave functionality
*
* @param serverArr{Array of type Server}
* @return constructor of ServerCluster
*
*/
var ReplSetServers = exports.ReplSetServers = function(servers, options) {
// Set up event emitter
EventEmitter.call(this);
// Set up basic
if(!(this instanceof ReplSetServers)) return new ReplSetServers(server, options);
var self = this;
// Contains the master server entry
this.options = options == null ? {} : options;
this.reconnectWait = this.options["reconnectWait"] != null ? this.options["reconnectWait"] : 1000;
this.retries = this.options["retries"] != null ? this.options["retries"] : 30;
this.replicaSet = this.options["rs_name"];
// Are we allowing reads from secondaries ?
this.readSecondary = this.options["read_secondary"];
this.slaveOk = true;
this.closedConnectionCount = 0;
this._used = false;
// Default poolSize for new server instances
this.poolSize = this.options.poolSize == null ? 1 : this.options.poolSize;
// Set up ssl connections
this.ssl = this.options.ssl == null ? false : this.options.ssl;
// Just keeps list of events we allow
this.eventHandlers = {error:[], parseError:[], poolReady:[], message:[], close:[], timeout:[]};
// Internal state of server connection
this._serverState = 'disconnected';
// Read preference
this._readPreference = null;
// Do we record server stats or not
this.recordQueryStats = false;
// Get the readPreference
var readPreference = this.options['readPreference'];
// Read preference setting
if(readPreference != null) {
if(readPreference != Server.READ_PRIMARY && readPreference != Server.READ_SECONDARY_ONLY
&& readPreference != Server.READ_SECONDARY) {
throw new Error("Illegal readPreference mode specified, " + readPreference);
}
// Set read Preference
this._readPreference = readPreference;
} else {
this._readPreference = null;
}
// Strategy for picking a secondary
this.strategy = this.options['strategy'] == null ? 'statistical' : this.options['strategy'];
// Make sure strategy is one of the two allowed
if(this.strategy != null && (this.strategy != 'ping' && this.strategy != 'statistical')) throw new Error("Only ping or statistical strategies allowed");
// Let's set up our strategy object for picking secodaries
if(this.strategy == 'ping') {
// Create a new instance
this.strategyInstance = new PingStrategy(this);
} else if(this.strategy == 'statistical') {
// Set strategy as statistical
this.strategyInstance = new StatisticsStrategy(this);
// Add enable query information
this.enableRecordQueryStats(true);
}
// Set default connection pool options
this.socketOptions = this.options.socketOptions != null ? this.options.socketOptions : {};
// Set up logger if any set
this.logger = this.options.logger != null
&& (typeof this.options.logger.debug == 'function')
&& (typeof this.options.logger.error == 'function')
&& (typeof this.options.logger.debug == 'function')
? this.options.logger : {error:function(message, object) {}, log:function(message, object) {}, debug:function(message, object) {}};
// Ensure all the instances are of type server and auto_reconnect is false
if(!Array.isArray(servers) || servers.length == 0) {
throw Error("The parameter must be an array of servers and contain at least one server");
} else if(Array.isArray(servers) || servers.length > 0) {
var count = 0;
servers.forEach(function(server) {
if(server instanceof Server) count = count + 1;
// Ensure no server has reconnect on
server.options.auto_reconnect = false;
});
if(count < servers.length) {
throw Error("All server entries must be of type Server");
} else {
this.servers = servers;
}
}
// Auto Reconnect property
Object.defineProperty(this, "autoReconnect", { enumerable: true
, get: function () {
return true;
}
});
// Get Read Preference method
Object.defineProperty(this, "readPreference", { enumerable: true
, get: function () {
if(this._readPreference == null && this.readSecondary) {
return Server.READ_SECONDARY;
} else if(this._readPreference == null && !this.readSecondary) {
return Server.READ_PRIMARY;
} else {
return this._readPreference;
}
}
});
// Db Instances
Object.defineProperty(this, "dbInstances", {enumerable:true
, get: function() {
var servers = this.allServerInstances();
return servers[0].dbInstances;
}
})
// Auto Reconnect property
Object.defineProperty(this, "host", { enumerable: true
, get: function () {
if (this.primary != null) return this.primary.host;
}
});
Object.defineProperty(this, "port", { enumerable: true
, get: function () {
if (this.primary != null) return this.primary.port;
}
});
Object.defineProperty(this, "read", { enumerable: true
, get: function () {
return this.secondaries.length > 0 ? this.secondaries[0] : null;
}
});
// Get list of secondaries
Object.defineProperty(this, "secondaries", {enumerable: true
, get: function() {
var keys = Object.keys(this._state.secondaries);
var array = new Array(keys.length);
// Convert secondaries to array
for(var i = 0; i < keys.length; i++) {
array[i] = this._state.secondaries[keys[i]];
}
return array;
}
});
// Get list of all secondaries including passives
Object.defineProperty(this, "allSecondaries", {enumerable: true
, get: function() {
return this.secondaries.concat(this.passives);
}
});
// Get list of arbiters
Object.defineProperty(this, "arbiters", {enumerable: true
, get: function() {
var keys = Object.keys(this._state.arbiters);
var array = new Array(keys.length);
// Convert arbiters to array
for(var i = 0; i < keys.length; i++) {
array[i] = this._state.arbiters[keys[i]];
}
return array;
}
});
// Get list of passives
Object.defineProperty(this, "passives", {enumerable: true
, get: function() {
var keys = Object.keys(this._state.passives);
var array = new Array(keys.length);
// Convert arbiters to array
for(var i = 0; i < keys.length; i++) {
array[i] = this._state.passives[keys[i]];
}
return array;
}
});
// Master connection property
Object.defineProperty(this, "primary", { enumerable: true
, get: function () {
return this._state != null ? this._state.master : null;
}
});
};
inherits(ReplSetServers, EventEmitter);
// Allow setting the read preference at the replicaset level
ReplSetServers.prototype.setReadPreference = function(preference) {
// Set read preference
this._readPreference = preference;
// Ensure slaveOk is correct for secodnaries read preference and tags
if((this._readPreference == Server.READ_SECONDARY || this._readPreference == Server.READ_SECONDARY_ONLY)
|| (this._readPreference != null && typeof this._readPreference == 'object')) {
this.slaveOk = true;
}
}
// Return the used state
ReplSetServers.prototype._isUsed = function() {
return this._used;
}
ReplSetServers.prototype.setTarget = function(target) {
this.target = target;
};
ReplSetServers.prototype.isConnected = function() {
// Return the state of the replicaset server
return this.primary != null && this._state.master != null && this._state.master.isConnected();
}
Server.prototype.isSetMember = function() {
return false;
}
ReplSetServers.prototype.isPrimary = function(config) {
return this.readSecondary && this.secondaries.length > 0 ? false : true;
}
ReplSetServers.prototype.isReadPrimary = ReplSetServers.prototype.isPrimary;
// Clean up dead connections
var cleanupConnections = ReplSetServers.cleanupConnections = function(connections, addresses, byTags) {
// Ensure we don't have entries in our set with dead connections
var keys = Object.keys(connections);
for(var i = 0; i < keys.length; i++) {
var server = connections[keys[i]];
// If it's not connected remove it from the list
if(!server.isConnected()) {
// Remove from connections and addresses
delete connections[keys[i]];
delete addresses[keys[i]];
// Clean up tags if needed
if(server.tags != null && typeof server.tags === 'object') {
cleanupTags(server, byTags);
}
}
}
}
var cleanupTags = ReplSetServers._cleanupTags = function(server, byTags) {
var serverTagKeys = Object.keys(server.tags);
// Iterate over all server tags and remove any instances for that tag that matches the current
// server
for(var i = 0; i < serverTagKeys.length; i++) {
// Fetch the value for the tag key
var value = server.tags[serverTagKeys[i]];
// If we got an instance of the server
if(byTags[serverTagKeys[i]] != null
&& byTags[serverTagKeys[i]][value] != null
&& Array.isArray(byTags[serverTagKeys[i]][value])) {
// List of clean servers
var cleanInstances = [];
// We got instances for the particular tag set
var instances = byTags[serverTagKeys[i]][value];
for(var j = 0; j < instances.length; j++) {
var serverInstance = instances[j];
// If we did not find an instance add it to the clean instances
if((serverInstance.host + ":" + serverInstance.port) !== (server.host + ":" + server.port)) {
cleanInstances.push(serverInstance);
}
}
// Update the byTags list
byTags[serverTagKeys[i]][value] = cleanInstances;
}
}
}
ReplSetServers.prototype.allServerInstances = function() {
var self = this;
// Close all the servers (concatenate entire list of servers first for ease)
var allServers = self._state.master != null ? [self._state.master] : [];
// Secondary keys
var keys = Object.keys(self._state.secondaries);
// Add all secondaries
for(var i = 0; i < keys.length; i++) {
allServers.push(self._state.secondaries[keys[i]]);
}
// Arbiter keys
var keys = Object.keys(self._state.arbiters);
// Add all arbiters
for(var i = 0; i < keys.length; i++) {
allServers.push(self._state.arbiters[keys[i]]);
}
// Passive keys
var keys = Object.keys(self._state.passives);
// Add all arbiters
for(var i = 0; i < keys.length; i++) {
allServers.push(self._state.passives[keys[i]]);
}
// Return complete list of all servers
return allServers;
}
// Ensure no callback is left hanging when we have an error
var __executeAllCallbacksWithError = function(dbInstance, error) {
var keys = Object.keys(dbInstance._callBackStore._notReplied);
// Iterate over all callbacks
for(var i = 0; i < keys.length; i++) {
// Delete info object
delete dbInstance._callBackStore._notReplied[keys[i]];
// Emit the error
dbInstance._callBackStore.emit(keys[i], error);
}
}
ReplSetServers.prototype.connect = function(parent, options, callback) {
var self = this;
var dateStamp = new Date().getTime();
if('function' === typeof options) callback = options, options = {};
if(options == null) options = {};
if(!('function' === typeof callback)) callback = null;
// Keep reference to parent
this.db = parent;
// Set server state to connecting
this._serverState = 'connecting';
// Reference to the instance
var replSetSelf = this;
var serverConnections = this.servers;
// Ensure parent can do a slave query if it's set
parent.slaveOk = this.slaveOk ? this.slaveOk : parent.slaveOk;
// Number of total servers that need to initialized (known servers)
var numberOfServersLeftToInitialize = serverConnections.length;
// Clean up state
replSetSelf._state = {'master':null, 'secondaries':{}, 'arbiters':{}, 'passives':{}, 'errors':{}, 'addresses':{}, 'byTags':{}, 'setName':null, 'errorMessages':[], 'members':[]};
// Create a connection handler
var connectionHandler = function(instanceServer) {
return function(err, result) {
// Don't attempt to connect if we are done
// if(replSetSelf._serverState === 'disconnected') return;
// Remove a server from the list of intialized servers we need to perform
numberOfServersLeftToInitialize = numberOfServersLeftToInitialize - 1;
// Add enable query information
instanceServer.enableRecordQueryStats(replSetSelf.recordQueryStats);
if(err == null && result.documents[0].hosts != null) {
// Fetch the isMaster command result
var document = result.documents[0];
// Break out the results
var setName = document.setName;
var isMaster = document.ismaster;
var secondary = document.secondary;
var passive = document.passive;
var arbiterOnly = document.arbiterOnly;
var hosts = Array.isArray(document.hosts) ? document.hosts : [];
var arbiters = Array.isArray(document.arbiters) ? document.arbiters : [];
var passives = Array.isArray(document.passives) ? document.passives : [];
var tags = document.tags ? document.tags : {};
var primary = document.primary;
var me = document.me;
// Only add server to our internal list if it's a master, secondary or arbiter
if(isMaster == true || secondary == true || arbiterOnly == true) {
// Handle a closed connection
var closeHandler = function(err, server) {
var closeServers = function() {
// Set the state to disconnected
parent._state = 'disconnected';
// Shut down the replicaset for now and Fire off all the callbacks sitting with no reply
if(replSetSelf._serverState == 'connected') {
// Close the replicaset
replSetSelf.close(function() {
__executeAllCallbacksWithError(parent, err);
// Ensure single callback only
if(callback != null) {
// Single callback only
var internalCallback = callback;
callback = null;
// Return the error
internalCallback(err, null);
} else {
// If the parent has listeners trigger an event
if(parent.listeners("close").length > 0) {
parent.emit("close", err);
}
}
});
}
}
// Check if this is the primary server, then disconnect otherwise keep going
if(replSetSelf._state.master != null) {
var primaryAddress = replSetSelf._state.master.host + ":" + replSetSelf._state.master.port;
var errorServerAddress = server.host + ":" + server.port;
// Only shut down the set if we have a primary server error
if(primaryAddress == errorServerAddress) {
closeServers();
} else {
// Remove from the list of servers
delete replSetSelf._state.addresses[errorServerAddress];
// Locate one of the lists and remove
if(replSetSelf._state.secondaries[errorServerAddress] != null) {
delete replSetSelf._state.secondaries[errorServerAddress];
} else if(replSetSelf._state.arbiters[errorServerAddress] != null) {
delete replSetSelf._state.arbiters[errorServerAddress];
} else if(replSetSelf._state.passives[errorServerAddress] != null) {
delete replSetSelf._state.passives[errorServerAddress];
}
// Check if we are reading from Secondary only
if(replSetSelf._readPreference == Server.READ_SECONDARY_ONLY && Object.keys(replSetSelf._state.secondaries).length == 0) {
closeServers();
}
}
} else {
closeServers();
}
}
// Handle a connection timeout
var timeoutHandler = function(err, server) {
var closeServers = function() {
// Set the state to disconnected
parent._state = 'disconnected';
// Shut down the replicaset for now and Fire off all the callbacks sitting with no reply
if(replSetSelf._serverState == 'connected') {
// Close the replicaset
replSetSelf.close(function() {
__executeAllCallbacksWithError(parent, err);
// Ensure single callback only
if(callback != null) {
// Single callback only
var internalCallback = callback;
callback = null;
// Return the error
internalCallback(new Error("connection timed out"), null);
} else {
// If the parent has listeners trigger an event
if(parent.listeners("error").length > 0) {
parent.emit("timeout", new Error("connection timed out"));
}
}
});
}
}
// Check if this is the primary server, then disconnect otherwise keep going
if(replSetSelf._state.master != null) {
var primaryAddress = replSetSelf._state.master.host + ":" + replSetSelf._state.master.port;
var errorServerAddress = server.host + ":" + server.port;
// Only shut down the set if we have a primary server error
if(primaryAddress == errorServerAddress) {
closeServers();
} else {
// Remove from the list of servers
delete replSetSelf._state.addresses[errorServerAddress];
// Locate one of the lists and remove
if(replSetSelf._state.secondaries[errorServerAddress] != null) {
delete replSetSelf._state.secondaries[errorServerAddress];
} else if(replSetSelf._state.arbiters[errorServerAddress] != null) {
delete replSetSelf._state.arbiters[errorServerAddress];
} else if(replSetSelf._state.passives[errorServerAddress] != null) {
delete replSetSelf._state.passives[errorServerAddress];
}
// Check if we are reading from Secondary only
if(replSetSelf._readPreference == Server.READ_SECONDARY_ONLY && Object.keys(replSetSelf._state.secondaries).length == 0) {
closeServers();
}
}
} else {
closeServers();
}
}
// Handle an error
var errorHandler = function(err, server) {
var closeServers = function() {
// Set the state to disconnected
parent._state = 'disconnected';
// Shut down the replicaset for now and Fire off all the callbacks sitting with no reply
if(replSetSelf._serverState == 'connected') {
// Close the replicaset
replSetSelf.close(function() {
__executeAllCallbacksWithError(parent, err);
// Ensure single callback only
if(callback != null) {
// Single callback only
var internalCallback = callback;
callback = null;
// Return the error
internalCallback(err, null);
} else {
// If the parent has listeners trigger an event
if(parent.listeners("error").length > 0) {
parent.emit("error", err);
}
}
});
}
}
// Check if this is the primary server, then disconnect otherwise keep going
if(replSetSelf._state.master != null) {
var primaryAddress = replSetSelf._state.master.host + ":" + replSetSelf._state.master.port;
var errorServerAddress = server.host + ":" + server.port;
// Only shut down the set if we have a primary server error
if(primaryAddress == errorServerAddress) {
closeServers();
} else {
// Remove from the list of servers
delete replSetSelf._state.addresses[errorServerAddress];
// Locate one of the lists and remove
if(replSetSelf._state.secondaries[errorServerAddress] != null) {
delete replSetSelf._state.secondaries[errorServerAddress];
} else if(replSetSelf._state.arbiters[errorServerAddress] != null) {
delete replSetSelf._state.arbiters[errorServerAddress];
} else if(replSetSelf._state.passives[errorServerAddress] != null) {
delete replSetSelf._state.passives[errorServerAddress];
}
// Check if we are reading from Secondary only
if(replSetSelf._readPreference == Server.READ_SECONDARY_ONLY && Object.keys(replSetSelf._state.secondaries).length == 0) {
closeServers();
}
}
} else {
closeServers();
}
}
// Ensure we don't have duplicate handlers
instanceServer.removeAllListeners("close");
instanceServer.removeAllListeners("error");
instanceServer.removeAllListeners("timeout");
// Add error handler to the instance of the server
instanceServer.on("close", closeHandler);
// Add error handler to the instance of the server
instanceServer.on("error", errorHandler);
// instanceServer.on("timeout", errorHandler);
instanceServer.on("timeout", timeoutHandler);
// Add tag info
instanceServer.tags = tags;
// For each tag in tags let's add the instance Server to the list for that tag
if(tags != null && typeof tags === 'object') {
var tagKeys = Object.keys(tags);
// For each tag file in the server add it to byTags
for(var i = 0; i < tagKeys.length; i++) {
var value = tags[tagKeys[i]];
// Check if we have a top level tag object
if(replSetSelf._state.byTags[tagKeys[i]] == null) replSetSelf._state.byTags[tagKeys[i]] = {};
// For the value check if we have an array of server instances
if(!Array.isArray(replSetSelf._state.byTags[tagKeys[i]][value])) replSetSelf._state.byTags[tagKeys[i]][value] = [];
// Check that the instance is not already registered there
var valueArray = replSetSelf._state.byTags[tagKeys[i]][value];
var found = false;
// Iterate over all values
for(var j = 0; j < valueArray.length; j++) {
if(valueArray[j].host == instanceServer.host && valueArray[j].port == instanceServer.port) {
found = true;
break;
}
}
// If it was not found push the instance server to the list
if(!found) valueArray.push(instanceServer);
}
}
// Remove from error list
delete replSetSelf._state.errors[me];
// Add our server to the list of finished servers
replSetSelf._state.addresses[me] = instanceServer;
// Assign the set name
if(replSetSelf.replicaSet == null) {
replSetSelf._state.setName = setName;
} else if(replSetSelf.replicaSet != setName && replSetSelf._serverState != 'disconnected') {
replSetSelf._state.errorMessages.push(new Error("configured mongodb replicaset does not match provided replicaset [" + setName + "] != [" + replSetSelf.replicaSet + "]"));
// Set done
replSetSelf._serverState = 'disconnected';
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Return error message ignoring rest of calls
return internalCallback(replSetSelf._state.errorMessages[0], parent);
}
// Let's add the server to our list of server types
if(secondary == true && (passive == false || passive == null)) {
replSetSelf._state.secondaries[me] = instanceServer;
} else if(arbiterOnly == true) {
replSetSelf._state.arbiters[me] = instanceServer;
} else if(secondary == true && passive == true) {
replSetSelf._state.passives[me] = instanceServer;
} else if(isMaster == true) {
replSetSelf._state.master = instanceServer;
} else if(isMaster == false && primary != null && replSetSelf._state.addresses[primary]) {
replSetSelf._state.master = replSetSelf._state.addresses[primary];
}
// Let's go throught all the "possible" servers in the replicaset
var candidateServers = hosts.concat(arbiters).concat(passives);
// If we have new servers let's add them
for(var i = 0; i < candidateServers.length; i++) {
// Fetch the server string
var candidateServerString = candidateServers[i];
// Add the server if it's not defined
if(replSetSelf._state.addresses[candidateServerString] == null) {
// Split the server string
var parts = candidateServerString.split(/:/);
if(parts.length == 1) {
parts = [parts[0], Connection.DEFAULT_PORT];
}
// Default empty socket options object
var socketOptions = {};
// If a socket option object exists clone it
if(replSetSelf.socketOptions != null) {
var keys = Object.keys(replSetSelf.socketOptions);
for(var i = 0; i < keys.length;i++) socketOptions[keys[i]] = replSetSelf.socketOptions[keys[i]];
}
// Add host information to socket options
socketOptions['host'] = parts[0];
socketOptions['port'] = parseInt(parts[1]);
// Create a new server instance
var newServer = new Server(parts[0], parseInt(parts[1]), {auto_reconnect:false, 'socketOptions':socketOptions
, logger:replSetSelf.logger, ssl:replSetSelf.ssl, poolSize:replSetSelf.poolSize});
// Set the replicaset instance
newServer.replicasetInstance = replSetSelf;
// Add handlers
newServer.on("close", closeHandler);
newServer.on("timeout", timeoutHandler);
newServer.on("error", errorHandler);
// Add server to list, ensuring we don't get a cascade of request to the same server
replSetSelf._state.addresses[candidateServerString] = newServer;
// Add a new server to the total number of servers that need to initialized before we are done
numberOfServersLeftToInitialize = numberOfServersLeftToInitialize + 1;
// Let's set up a new server instance
newServer.connect(parent, {returnIsMasterResults: true, eventReceiver:newServer}, connectionHandler(newServer));
}
}
} else {
// Remove the instance from out list of servers
delete replSetSelf._state.addresses[me];
}
}
// If done finish up
if((numberOfServersLeftToInitialize == 0) && replSetSelf._serverState === 'connecting' && replSetSelf._state.errorMessages.length == 0) {
// Set db as connected
replSetSelf._serverState = 'connected';
// If we don't expect a master let's call back, otherwise we need a master before
// the connection is successful
if(replSetSelf.masterNotNeeded || replSetSelf._state.master != null) {
// If we have a read strategy boot it
if(replSetSelf.strategyInstance != null) {
// Ensure we have a proper replicaset defined
replSetSelf.strategyInstance.replicaset = replSetSelf;
// Start strategy
replSetSelf.strategyInstance.start(function(err) {
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Perform callback
internalCallback(null, parent);
})
} else {
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Perform callback
internalCallback(null, parent);
}
} else if(replSetSelf.readSecondary == true && Object.keys(replSetSelf._state.secondaries).length > 0) {
// If we have a read strategy boot it
if(replSetSelf.strategyInstance != null) {
// Ensure we have a proper replicaset defined
replSetSelf.strategyInstance.replicaset = replSetSelf;
// Start strategy
replSetSelf.strategyInstance.start(function(err) {
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Perform callback
internalCallback(null, parent);
})
} else {
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Perform callback
internalCallback(null, parent);
}
} else if(replSetSelf.readSecondary == true && Object.keys(replSetSelf._state.secondaries).length == 0) {
replSetSelf._serverState = 'disconnected';
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Force close all server instances
replSetSelf.close();
// Perform callback
internalCallback(new Error("no secondary server found"), null);
} else if(typeof callback === 'function'){
replSetSelf._serverState = 'disconnected';
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Force close all server instances
replSetSelf.close();
// Perform callback
internalCallback(new Error("no primary server found"), null);
}
} else if((numberOfServersLeftToInitialize == 0) && replSetSelf._state.errorMessages.length > 0 && replSetSelf._serverState != 'disconnected') {
// Set done
replSetSelf._serverState = 'disconnected';
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Force close all server instances
replSetSelf.close();
// Callback to signal we are done
internalCallback(replSetSelf._state.errorMessages[0], null);
}
}
}
// Ensure we have all registered servers in our set
for(var i = 0; i < serverConnections.length; i++) {
replSetSelf._state.addresses[serverConnections[i].host + ':' + serverConnections[i].port] = serverConnections[i];
}
// Initialize all the connections
for(var i = 0; i < serverConnections.length; i++) {
// Set up the logger for the server connection
serverConnections[i].logger = replSetSelf.logger;
// Default empty socket options object
var socketOptions = {};
// If a socket option object exists clone it
if(this.socketOptions != null && typeof this.socketOptions === 'object') {
var keys = Object.keys(this.socketOptions);
for(var j = 0; j < keys.length;j++) socketOptions[keys[j]] = this.socketOptions[keys[j]];
}
// If ssl is specified
if(replSetSelf.ssl) serverConnections[i].ssl = true;
// Add host information to socket options
socketOptions['host'] = serverConnections[i].host;
socketOptions['port'] = serverConnections[i].port;
// Set the socket options
serverConnections[i].socketOptions = socketOptions;
// Set the replicaset instance
serverConnections[i].replicasetInstance = replSetSelf;
// Connect to server
serverConnections[i].connect(parent, {returnIsMasterResults: true, eventReceiver:serverConnections[i]}, connectionHandler(serverConnections[i]));
}
// Check if we have an error in the inital set of servers and callback with error
if(replSetSelf._state.errorMessages.length > 0 && typeof callback === 'function') {
// ensure no callbacks get called twice
var internalCallback = callback;
callback = null;
// Perform callback
internalCallback(replSetSelf._state.errorMessages[0], null);
}
}
ReplSetServers.prototype.checkoutWriter = function() {
// Establish connection
var connection = this._state.master != null ? this._state.master.checkoutWriter() : null;
// Return the connection
return connection;
}
ReplSetServers.prototype.checkoutReader = function() {
var connection = null;
// If we have specified to read from a secondary server grab a random one and read
// from it, otherwise just pass the primary connection
if((this.readSecondary == true || this._readPreference == Server.READ_SECONDARY || this._readPreference == Server.READ_SECONDARY_ONLY) && Object.keys(this._state.secondaries).length > 0) {
// Checkout a secondary server from the passed in set of servers
if(this.strategyInstance != null) {
connection = this.strategyInstance.checkoutSecondary();
} else {
// Pick a random key
var keys = Object.keys(this._state.secondaries);
var key = keys[Math.floor(Math.random() * keys.length)];
connection = this._state.secondaries[key].checkoutReader();
}
} else if(this._readPreference == Server.READ_SECONDARY_ONLY && Object.keys(this._state.secondaries).length == 0) {
connection = null;
} else if(this._readPreference != null && typeof this._readPreference === 'object') {
// Get all tag keys (used to try to find a server that is valid)
var keys = Object.keys(this._readPreference);
// final instance server
var instanceServer = null;
// for each key look for an avilable instance
for(var i = 0; i < keys.length; i++) {
// Grab subkey value
var value = this._readPreference[keys[i]];
// Check if we have any servers for the tag, if we do pick a random one
if(this._state.byTags[keys[i]] != null
&& this._state.byTags[keys[i]][value] != null
&& Array.isArray(this._state.byTags[keys[i]][value])
&& this._state.byTags[keys[i]][value].length > 0) {
// Let's grab an available server from the list using a random pick
var serverInstances = this._state.byTags[keys[i]][value];
// Set instance to return
instanceServer = serverInstances[Math.floor(Math.random() * serverInstances.length)];
break;
}
}
// Return the instance of the server
connection = instanceServer != null ? instanceServer.checkoutReader() : this.checkoutWriter();
} else {
connection = this.checkoutWriter();
}
// Return the connection
return connection;
}
ReplSetServers.prototype.allRawConnections = function() {
// Neeed to build a complete list of all raw connections, start with master server
var allConnections = [];
// Get connection object
var allMasterConnections = this._state.master.connectionPool.getAllConnections();
// Add all connections to list
allConnections = allConnections.concat(allMasterConnections);
// If we have read secondary let's add all secondary servers
if(this.readSecondary && Object.keys(this._state.secondaries).length > 0) {
// Get all the keys
var keys = Object.keys(this._state.secondaries);
// For each of the secondaries grab the connections
for(var i = 0; i < keys.length; i++) {
// Get connection object
var secondaryPoolConnections = this._state.secondaries[keys[i]].connectionPool.getAllConnections();
// Add all connections to list
allConnections = allConnections.concat(secondaryPoolConnections);
}
}
// Return all the conections
return allConnections;
}
ReplSetServers.prototype.enableRecordQueryStats = function(enable) {
// Set the global enable record query stats
this.recordQueryStats = enable;
// Ensure all existing servers already have the flag set, even if the
// connections are up already or we have not connected yet
if(this._state != null && this._state.addresses != null) {
var keys = Object.keys(this._state.addresses);
// Iterate over all server instances and set the enableRecordQueryStats flag
for(var i = 0; i < keys.length; i++) {
this._state.addresses[keys[i]].enableRecordQueryStats(enable);
}
} else if(Array.isArray(this.servers)) {
for(var i = 0; i < this.servers.length; i++) {
this.servers[i].enableRecordQueryStats(enable);
}
}
}
ReplSetServers.prototype.disconnect = function(callback) {
this.close(callback);
}
ReplSetServers.prototype.close = function(callback) {
var self = this;
// Set server status as disconnected
this._serverState = 'disconnected';
// Get all the server instances and close them
var allServers = [];
// Make sure we have servers
if(this._state['addresses'] != null) {
var keys = Object.keys(this._state.addresses);
for(var i = 0; i < keys.length; i++) {
allServers.push(this._state.addresses[keys[i]]);
}
}
// Let's process all the closing
var numberOfServersToClose = allServers.length;
// Remove all the listeners
self.removeAllListeners();
// Special case where there are no servers
if(allServers.length == 0 && typeof callback === 'function') return callback(null, null);
// Close the servers
for(var i = 0; i < allServers.length; i++) {
var server = allServers[i];
if(server.isConnected()) {
// Close each server
server.close(function() {
numberOfServersToClose = numberOfServersToClose - 1;
// Clear out state if we are done
if(numberOfServersToClose == 0) {
// Clear out state
self._state = {'master':null, 'secondaries':{}, 'arbiters':{}, 'passives':{}, 'errors':{}, 'addresses':{}, 'byTags':{}, 'setName':null, 'errorMessages':[], 'members':[]};
}
// If we are finished perform the call back
if(numberOfServersToClose == 0 && typeof callback === 'function') {
callback(null);
}
})
} else {
numberOfServersToClose = numberOfServersToClose - 1;
// If we have no more servers perform the callback
if(numberOfServersToClose == 0 && typeof callback === 'function') {
callback(null);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -3,123 +3,186 @@ var Server = require("../server").Server;
// The ping strategy uses pings each server and records the
// elapsed time for the server so it can pick a server based on lowest
// return time for the db command {ping:true}
var PingStrategy = exports.PingStrategy = function(replicaset) {
var PingStrategy = exports.PingStrategy = function(replicaset, secondaryAcceptableLatencyMS) {
this.replicaset = replicaset;
this.secondaryAcceptableLatencyMS = secondaryAcceptableLatencyMS;
this.state = 'disconnected';
this.pingInterval = 5000;
// Class instance
this.Db = require("../../db").Db;
}
// Starts any needed code
PingStrategy.prototype.start = function(callback) {
// already running?
if ('connected' == this.state) return;
this.state = 'connected';
// Start ping server
this._pingServer(callback);
this._pingServer(callback);
}
// Stops and kills any processes running
PingStrategy.prototype.stop = function(callback) {
PingStrategy.prototype.stop = function(callback) {
// Stop the ping process
this.state = 'disconnected';
// Call the callback
callback(null, null);
// optional callback
callback && callback(null, null);
}
PingStrategy.prototype.checkoutSecondary = function() {
// Get all secondary server keys
var keys = Object.keys(this.replicaset._state.secondaries);
// Contains the picked instance
var minimumPingMs = null;
var selectedInstance = null;
// Pick server key by the lowest ping time
for(var i = 0; i < keys.length; i++) {
// Fetch a server
var server = this.replicaset._state.secondaries[keys[i]];
// If we don't have a ping time use it
if(server.runtimeStats['pingMs'] == null) {
// Set to 0 ms for the start
server.runtimeStats['pingMs'] = 0;
// Pick server
selectedInstance = server;
break;
} else {
// If the next server's ping time is less than the current one choose than one
if(minimumPingMs == null || server.runtimeStats['pingMs'] < minimumPingMs) {
minimumPingMs = server.runtimeStats['pingMs'];
selectedInstance = server;
PingStrategy.prototype.checkoutSecondary = function(tags, secondaryCandidates) {
// Servers are picked based on the lowest ping time and then servers that lower than that + secondaryAcceptableLatencyMS
// Create a list of candidat servers, containing the primary if available
var candidateServers = [];
// If we have not provided a list of candidate servers use the default setup
if(!Array.isArray(secondaryCandidates)) {
candidateServers = this.replicaset._state.master != null ? [this.replicaset._state.master] : [];
// Add all the secondaries
var keys = Object.keys(this.replicaset._state.secondaries);
for(var i = 0; i < keys.length; i++) {
candidateServers.push(this.replicaset._state.secondaries[keys[i]])
}
} else {
candidateServers = secondaryCandidates;
}
// Final list of eligable server
var finalCandidates = [];
// If we have tags filter by tags
if(tags != null && typeof tags == 'object') {
// If we have an array or single tag selection
var tagObjects = Array.isArray(tags) ? tags : [tags];
// Iterate over all tags until we find a candidate server
for(var _i = 0; _i < tagObjects.length; _i++) {
// Grab a tag object
var tagObject = tagObjects[_i];
// Matching keys
var matchingKeys = Object.keys(tagObject);
// Remove any that are not tagged correctly
for(var i = 0; i < candidateServers.length; i++) {
var server = candidateServers[i];
// If we have tags match
if(server.tags != null) {
var matching = true;
// Ensure we have all the values
for(var j = 0; j < matchingKeys.length; j++) {
if(server.tags[matchingKeys[j]] != tagObject[matchingKeys[j]]) {
matching = false;
break;
}
}
// If we have a match add it to the list of matching servers
if(matching) {
finalCandidates.push(server);
}
}
}
}
} else {
// Final array candidates
var finalCandidates = candidateServers;
}
// Sort by ping time
finalCandidates.sort(function(a, b) {
return a.runtimeStats['pingMs'] > b.runtimeStats['pingMs'];
});
if(0 === finalCandidates.length)
return new Error("No replica set members available for query");
// handle undefined pingMs
var lowestPing = finalCandidates[0].runtimeStats['pingMs'] | 0;
// determine acceptable latency
var acceptable = lowestPing + this.secondaryAcceptableLatencyMS;
// remove any server responding slower than acceptable
var len = finalCandidates.length;
while(len--) {
if(finalCandidates[len].runtimeStats['pingMs'] > acceptable) {
finalCandidates.splice(len, 1);
}
}
// Return the selected instance
return selectedInstance != null ? selectedInstance.checkoutReader() : null;
// If no candidates available return an error
if(finalCandidates.length == 0)
return new Error("No replica set members available for query");
// Pick a random acceptable server
return finalCandidates[Math.round(Math.random(1000000) * (finalCandidates.length - 1))].checkoutReader();
}
PingStrategy.prototype._pingServer = function(callback) {
var self = this;
// Ping server function
var pingFunction = function() {
if(self.state == 'disconnected') return;
var addresses = self.replicaset._state != null && self.replicaset._state.addresses != null ? self.replicaset._state.addresses : null;
var addresses = self.replicaset._state.addresses;
// Grab all servers
var serverKeys = Object.keys(addresses);
// Number of server entries
var numberOfEntries = serverKeys.length;
// We got keys
for(var i = 0; i < serverKeys.length; i++) {
// We got a server instance
var server = addresses[serverKeys[i]];
// Create a new server object, avoid using internal connections as they might
// be in an illegal state
new function(serverInstance) {
var server = new Server(serverInstance.host, serverInstance.port, {poolSize:1, timeout:500});
var db = new self.Db(self.replicaset.db.databaseName, server);
// Add error listener
db.on("error", function(err) {
// Adjust the number of checks
numberOfEntries = numberOfEntries - 1;
// Close connection
db.close();
// If we are done with all results coming back trigger ping again
if(numberOfEntries == 0 && self.state == 'connected') {
setTimeout(pingFunction, 1000);
}
})
var options = { poolSize: 1, timeout: 500, auto_reconnect: false };
var server = new Server(serverInstance.host, serverInstance.port, options);
var db = new self.Db(self.replicaset.db.databaseName, server, { safe: true });
db.on("error", done);
// Open the db instance
db.open(function(err, p_db) {
if(err != null) {
db.close();
} else {
// Startup time of the command
var startTime = new Date().getTime();
// Execute ping on this connection
p_db.executeDbCommand({ping:1}, function(err, result) {
// Adjust the number of checks
numberOfEntries = numberOfEntries - 1;
// Get end time of the command
var endTime = new Date().getTime();
// Store the ping time in the server instance state variable, if there is one
if(serverInstance != null && serverInstance.runtimeStats != null && serverInstance.isConnected()) {
serverInstance.runtimeStats['pingMs'] = (endTime - startTime);
}
// Close server
p_db.close();
// If we are done with all results coming back trigger ping again
if(numberOfEntries == 0 && self.state == 'connected') {
setTimeout(pingFunction, 1000);
}
})
}
db.open(function(err, _db) {
if(err) return done(err, _db);
// Startup time of the command
var startTime = Date.now();
// Execute ping on this connection
db.executeDbCommand({ping:1}, {failFast:true}, function() {
if(null != serverInstance.runtimeStats && serverInstance.isConnected()) {
serverInstance.runtimeStats['pingMs'] = Date.now() - startTime;
}
done(null, _db);
})
})
function done (err, _db) {
// Close connection
if (_db) _db.close(true);
// Adjust the number of checks
numberOfEntries--;
// If we are done with all results coming back trigger ping again
if(0 === numberOfEntries && 'connected' == self.state) {
setTimeout(pingFunction, self.pingInterval);
}
}
}(server);
}
}
// Start pingFunction
setTimeout(pingFunction, 1000);
// Do the callback
callback(null);
callback && callback(null);
}

Some files were not shown because too many files have changed in this diff Show more