Merge branch 'master' into requests

* master: (66 commits)
  Too many stories? Don't animate transitions when switching intelligence levels.
  Fixing typo in iphone app that preventing mark folder as read when the visible stories option was showing.
  Updating the mongo db copy util to also look for updated stories.
  Globals.is_staff on user, not profile. Make the stats referesh every minute instead of every 10 minutes for staff.
  Fixing offsets in river of news.
  Adding ciphering to usernames in log, so I can finally take screenshots of the most colorful logs of all time.
  Adding my very special mongo serialization backup utility to get around the damn unrepairable mongo database. This is taking 14 hours to run.
  Auto-refreshing feedback (1 min for staff, 10 min for everybody).
  Fixing exception around multiple feeds during Google Reader import process. Also switching rate limit to status code 429.
  Adding Nokia MeeGo client to user agents.
  Only show raw feeds in feed autocomplete.
  Handling iphone version for new users on ios app.
  FIXING THE WORST BUG OF MY LIFE -- finally figured out what was causing the story-shows-as-unread bug. Also fixed enclosures on certain types of feeds.
  Fixing menu manage open position to compensate for additional menu items.
  Reducing the amount of work done by feed fetching when there are no new stories.
  Fixing emergency bug around trimming feeds where the cursor is changing. Dammit mongo.
  Simplifying ufw ports in fabfile. Adding env.user.
  Launching the iPhone app on the front-page. Big deal.
  Minor cleanup of river stories view.
  Cleaning up mongoengine imports and settings for default MongoDB.
  ...

Conflicts:
	local_settings.py.template
This commit is contained in:
Samuel Clay 2011-12-20 21:47:55 -08:00
commit b5efad9142
288 changed files with 29644 additions and 1779 deletions

View file

@ -136,6 +136,7 @@ class OPMLImporter(Importer):
feed_data['active_subscribers'] = 1
feed_data['num_subscribers'] = 1
feed_db, _ = Feed.objects.get_or_create(feed_address=feed_address,
feed_link=feed_link,
defaults=dict(**feed_data))
us, _ = UserSubscription.objects.get_or_create(
@ -228,8 +229,12 @@ class GoogleReaderImporter(Importer):
feed_data = dict(feed_address=feed_address, feed_link=feed_link, feed_title=feed_title)
feed_data['active_subscribers'] = 1
feed_data['num_subscribers'] = 1
feed_db, _ = Feed.objects.get_or_create(feed_address=feed_address,
defaults=dict(**feed_data))
feeds = Feed.objects.filter(feed_address=feed_address,
branch_from_feed__isnull=True).order_by('-num_subscribers')
if feeds:
feed_db = feeds[0]
else:
feed_db = Feed.objects.create(**feed_data)
us, _ = UserSubscription.objects.get_or_create(
feed=feed_db,

View file

@ -43,7 +43,7 @@ class UserSubscription(models.Model):
def __unicode__(self):
return '[' + self.feed.feed_title + '] '
def canonical(self, full=False, include_favicon=True):
def canonical(self, full=False, include_favicon=True, classifiers=None):
feed = self.feed.canonical(full=full, include_favicon=include_favicon)
feed['feed_title'] = self.user_title or feed['feed_title']
feed['ps'] = self.unread_count_positive
@ -51,6 +51,8 @@ class UserSubscription(models.Model):
feed['ng'] = self.unread_count_negative
feed['active'] = self.active
feed['feed_opens'] = self.feed_opens
if classifiers:
feed['classifiers'] = classifiers
if not self.active and self.user.profile.is_premium:
feed['active'] = True
self.active = True
@ -136,7 +138,8 @@ class UserSubscription(models.Model):
self.unread_count_negative = 0
self.unread_count_positive = 0
self.unread_count_neutral = 0
self.unread_count_updated = latest_story_date
self.unread_count_updated = now
self.oldest_unread_story_date = now
self.needs_unread_recalc = False
MUserStory.delete_marked_as_read_stories(self.user.pk, self.feed.pk)
@ -297,7 +300,7 @@ class UserSubscription(models.Model):
if not silent:
logging.info(' ---> [%s] Computing scores: %s (%s/%s/%s)' % (self.user, self.feed, feed_scores['negative'], feed_scores['neutral'], feed_scores['positive']))
return
return self
def switch_feed(self, new_feed, old_feed):
# Rewrite feed in subscription folders
@ -317,8 +320,47 @@ class UserSubscription(models.Model):
except (IntegrityError, OperationError):
logging.info(" !!!!> %s already subscribed" % self.user)
self.delete()
return
# Switch read stories
user_stories = MUserStory.objects(user_id=self.user.pk, feed_id=old_feed.pk)
logging.info(" ---> %s read stories" % user_stories.count())
for user_story in user_stories:
user_story.feed_id = new_feed.pk
duplicate_story = user_story.story
story_guid = duplicate_story.story_guid if hasattr(duplicate_story, 'story_guid') else duplicate_story.id
original_story = MStory.objects(story_feed_id=new_feed.pk,
story_guid=story_guid)
if original_story:
user_story.story = original_story[0]
try:
user_story.save()
except OperationError:
# User read the story in the original feed, too. Ugh, just ignore it.
pass
else:
logging.info(" ***> Can't find original story: %s" % duplicate_story.id)
user_story.delete()
def switch_feed_for_classifier(model):
duplicates = model.objects(feed_id=old_feed.pk, user_id=self.user.pk)
if duplicates.count():
logging.info(" ---> Switching %s %s" % (duplicates.count(), model))
for duplicate in duplicates:
duplicate.feed_id = new_feed.pk
try:
duplicate.save()
pass
except (IntegrityError, OperationError):
logging.info(" !!!!> %s already exists" % duplicate)
duplicate.delete()
switch_feed_for_classifier(MClassifierTitle)
switch_feed_for_classifier(MClassifierAuthor)
switch_feed_for_classifier(MClassifierFeed)
switch_feed_for_classifier(MClassifierTag)
class Meta:
unique_together = ("user", "feed")
@ -403,7 +445,7 @@ class UserSubscriptionFolders(models.Model):
new_folders.append({f_k: nf})
return new_folders, multiples_found, deleted
user_sub_folders = json.decode(self.folders)
user_sub_folders, multiples_found, deleted = _find_feed_in_folders(user_sub_folders)
self.folders = json.encode(user_sub_folders)
@ -447,7 +489,7 @@ class UserSubscriptionFolders(models.Model):
user_sub_folders, feeds_to_delete, deleted_folder = _find_folder_in_folders(user_sub_folders, '', feed_ids_in_folder)
self.folders = json.encode(user_sub_folders)
self.save()
if commit_delete:
UserSubscription.objects.filter(user=self.user, feed__in=feeds_to_delete).delete()
@ -476,6 +518,8 @@ class UserSubscriptionFolders(models.Model):
self.save()
def move_feed_to_folder(self, feed_id, in_folder=None, to_folder=None):
logging.user(self.user, "~FBMoving feed '~SB%s~SN' in '%s' to: ~SB%s" % (
feed_id, in_folder, to_folder))
user_sub_folders = json.decode(self.folders)
self.delete_feed(feed_id, in_folder, commit_delete=False)
user_sub_folders = json.decode(self.folders)
@ -486,6 +530,8 @@ class UserSubscriptionFolders(models.Model):
return self
def move_folder_to_folder(self, folder_name, in_folder=None, to_folder=None):
logging.user(self.user, "~FBMoving folder '~SB%s~SN' in '%s' to: ~SB%s" % (
folder_name, in_folder, to_folder))
user_sub_folders = json.decode(self.folders)
deleted_folder = self.delete_folder(folder_name, in_folder, [], commit_delete=False)
user_sub_folders = json.decode(self.folders)

View file

@ -154,7 +154,7 @@ def autologin(request, username, secret):
return HttpResponseRedirect(reverse('index') + next)
@ratelimit(minutes=1, requests=20)
@ratelimit(minutes=1, requests=12)
@json.json_view
def load_feeds(request):
user = get_user(request)
@ -164,6 +164,10 @@ def load_feeds(request):
flat = request.REQUEST.get('flat', False)
update_counts = request.REQUEST.get('update_counts', False)
if include_favicons == 'false': include_favicons = False
if update_counts == 'false': update_counts = False
if flat == 'false': flat = False
if flat: return load_feeds_flat(request)
try:
@ -222,6 +226,9 @@ def load_feeds_flat(request):
user = request.user
include_favicons = request.REQUEST.get('include_favicons', False)
feeds = {}
iphone_version = "1.2"
if include_favicons == 'false': include_favicons = False
if not user.is_authenticated():
return HttpResponseForbidden()
@ -229,7 +236,7 @@ def load_feeds_flat(request):
try:
folders = UserSubscriptionFolders.objects.get(user=user)
except UserSubscriptionFolders.DoesNotExist:
data = dict(folders=[])
data = dict(folders=[], iphone_version=iphone_version)
return data
user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True)
@ -263,7 +270,7 @@ def load_feeds_flat(request):
make_feeds_folder(folder, flat_folder_name, depth+1)
make_feeds_folder(folders)
data = dict(flat_folders=flat_folders, feeds=feeds, user=user.username)
data = dict(flat_folders=flat_folders, feeds=feeds, user=user.username, iphone_version=iphone_version)
return data
@ratelimit(minutes=1, requests=10)
@ -280,13 +287,14 @@ def refresh_feeds(request):
UNREAD_CUTOFF = datetime.datetime.utcnow() - datetime.timedelta(days=settings.DAYS_OF_UNREAD)
favicons_fetching = [int(f) for f in request.REQUEST.getlist('favicons_fetching') if f]
feed_icons = dict([(i.feed_id, i) for i in MFeedIcon.objects(feed_id__in=favicons_fetching)])
for sub in user_subs:
for i, sub in enumerate(user_subs):
pk = sub.feed.pk
if (sub.needs_unread_recalc or
sub.unread_count_updated < UNREAD_CUTOFF or
sub.oldest_unread_story_date < UNREAD_CUTOFF):
sub.calculate_feed_scores(silent=True)
sub = sub.calculate_feed_scores(silent=True)
if not sub: continue # TODO: Figure out the correct sub and give it a new feed_id
feeds[pk] = {
'ps': sub.unread_count_positive,
'nt': sub.unread_count_neutral,
@ -303,8 +311,9 @@ def refresh_feeds(request):
if sub.feed.pk in favicons_fetching and sub.feed.pk in feed_icons:
feeds[pk]['favicon'] = feed_icons[sub.feed.pk].data
feeds[pk]['favicon_color'] = feed_icons[sub.feed.pk].color
feeds[pk]['favicon_fetching'] = bool(not (feed_icons[sub.feed.pk].not_found or
feed_icons[sub.feed.pk].data))
feeds[pk]['favicon_fetching'] = sub.feed.favicon_fetching
user_subs = UserSubscription.objects.select_related('feed').filter(user=user, active=True)
if favicons_fetching:
sub_feed_ids = [s.feed.pk for s in user_subs]
@ -315,10 +324,10 @@ def refresh_feeds(request):
feeds[moved_feed_id] = feeds[duplicate_feeds[0].feed.pk]
feeds[moved_feed_id]['dupe_feed_id'] = duplicate_feeds[0].feed.pk
if settings.DEBUG:
if settings.DEBUG or request.REQUEST.get('check_fetch_status'):
diff = datetime.datetime.utcnow()-start
timediff = float("%s.%.2s" % (diff.seconds, (diff.microseconds / 1000)))
logging.user(request, "~FBRefreshing %s feeds (%s seconds)" % (user_subs.count(), timediff))
logging.user(request, "~FBRefreshing %s feeds (%s seconds) (%s/%s)" % (user_subs.count(), timediff, request.REQUEST.get('check_fetch_status', False), len(favicons_fetching)))
return {'feeds': feeds}
@ -341,7 +350,7 @@ def load_single_feed(request, feed_id):
page = int(request.REQUEST.get('page', 1))
dupe_feed_id = None
userstories_db = None
if page: offset = limit * (page-1)
if not feed_id: raise Http404
@ -485,15 +494,15 @@ def load_starred_stories(request):
@json.json_view
def load_river_stories(request):
limit = 18
offset = 0
start = time.time()
user = get_user(request)
feed_ids = [int(feed_id) for feed_id in request.REQUEST.getlist('feeds') if feed_id]
original_feed_ids = list(feed_ids)
page = int(request.REQUEST.get('page', 1))
read_stories_count = int(request.REQUEST.get('read_stories_count', 0))
bottom_delta = datetime.timedelta(days=settings.DAYS_OF_UNREAD)
limit = 18
offset = int(request.REQUEST.get('offset', 0))
start = time.time()
user = get_user(request)
feed_ids = [int(feed_id) for feed_id in request.REQUEST.getlist('feeds') if feed_id]
original_feed_ids = list(feed_ids)
page = int(request.REQUEST.get('page', 1))
read_stories_count = int(request.REQUEST.get('read_stories_count', 0))
days_to_keep_unreads = datetime.timedelta(days=settings.DAYS_OF_UNREAD)
if not feed_ids:
logging.user(request, "~FCLoading empty river stories: page %s" % (page))
@ -502,14 +511,14 @@ def load_river_stories(request):
# Fetch all stories at and before the page number.
# Not a single page, because reading stories can move them up in the unread order.
# `read_stories_count` is an optimization, works best when all 25 stories before have been read.
limit = limit * page - read_stories_count
offset = (page-1) * limit - read_stories_count
limit = page * limit - read_stories_count
# Read stories to exclude
read_stories = MUserStory.objects(user_id=user.pk, feed_id__in=feed_ids).only('story_id')
read_stories = [rs.story_id for rs in read_stories]
# Determine mark_as_read dates for all feeds to ignore all stories before this date.
# max_feed_count = 0
feed_counts = {}
feed_last_reads = {}
for feed_id in feed_ids:
@ -521,9 +530,8 @@ def load_river_stories(request):
feed_counts[feed_id] = (usersub.unread_count_negative * 1 +
usersub.unread_count_neutral * 10 +
usersub.unread_count_positive * 20)
# if feed_counts[feed_id] > max_feed_count:
# max_feed_count = feed_counts[feed_id]
feed_last_reads[feed_id] = int(time.mktime(usersub.mark_read_date.timetuple()))
feed_counts = sorted(feed_counts.items(), key=itemgetter(1))[:40]
feed_ids = [f[0] for f in feed_counts]
feed_last_reads = dict([(str(feed_id), feed_last_reads[feed_id]) for feed_id in feed_ids
@ -535,7 +543,7 @@ def load_river_stories(request):
mstories = MStory.objects(
story_guid__nin=read_stories,
story_feed_id__in=feed_ids,
# story_date__gte=start - bottom_delta
# story_date__gte=start - days_to_keep_unreads
).map_reduce("""function() {
var d = feed_last_reads[this[~story_feed_id]];
if (this[~story_date].getTime()/1000 > d) {
@ -555,8 +563,10 @@ def load_river_stories(request):
except OperationFailure, e:
raise e
mstories = sorted(mstories, cmp=lambda x, y: cmp(story_score(y, bottom_delta), story_score(x, bottom_delta)))
mstories = sorted(mstories, cmp=lambda x, y: cmp(story_score(y, days_to_keep_unreads),
story_score(x, days_to_keep_unreads)))
# Prune the river to only include a set number of stories per feed
# story_feed_counts = defaultdict(int)
# mstories_pruned = []
# for story in mstories:
@ -564,21 +574,26 @@ def load_river_stories(request):
# if story_feed_counts[story['story_feed_id']] >= 3: continue
# mstories_pruned.append(story)
# story_feed_counts[story['story_feed_id']] += 1
stories = []
for i, story in enumerate(mstories):
if i < offset: continue
if i >= offset + limit: break
if i >= limit: break
stories.append(bunch(story))
stories = Feed.format_stories(stories)
found_feed_ids = list(set([story['story_feed_id'] for story in stories]))
# Find starred stories
starred_stories = MStarredStory.objects(
user_id=user.pk,
story_feed_id__in=found_feed_ids
).only('story_guid', 'starred_date')
starred_stories = dict([(story.story_guid, story.starred_date)
for story in starred_stories])
try:
starred_stories = MStarredStory.objects(
user_id=user.pk,
story_feed_id__in=found_feed_ids
).only('story_guid', 'starred_date')
starred_stories = dict([(story.story_guid, story.starred_date)
for story in starred_stories])
except OperationFailure:
logging.info(" ***> Starred stories failure")
starred_stories = {}
# Intelligence classifiers for all feeds involved
def sort_by_feed(classifiers):
@ -586,17 +601,20 @@ def load_river_stories(request):
for classifier in classifiers:
feed_classifiers[classifier.feed_id].append(classifier)
return feed_classifiers
classifier_feeds = sort_by_feed(MClassifierFeed.objects(user_id=user.pk, feed_id__in=found_feed_ids))
classifier_authors = sort_by_feed(MClassifierAuthor.objects(user_id=user.pk, feed_id__in=found_feed_ids))
classifier_titles = sort_by_feed(MClassifierTitle.objects(user_id=user.pk, feed_id__in=found_feed_ids))
classifier_tags = sort_by_feed(MClassifierTag.objects(user_id=user.pk, feed_id__in=found_feed_ids))
classifiers = {}
for feed_id in found_feed_ids:
classifiers[feed_id] = get_classifiers_for_user(user, feed_id, classifier_feeds[feed_id],
classifier_authors[feed_id],
classifier_titles[feed_id],
classifier_tags[feed_id])
try:
classifier_feeds = sort_by_feed(MClassifierFeed.objects(user_id=user.pk, feed_id__in=found_feed_ids))
classifier_authors = sort_by_feed(MClassifierAuthor.objects(user_id=user.pk, feed_id__in=found_feed_ids))
classifier_titles = sort_by_feed(MClassifierTitle.objects(user_id=user.pk, feed_id__in=found_feed_ids))
classifier_tags = sort_by_feed(MClassifierTag.objects(user_id=user.pk, feed_id__in=found_feed_ids))
except OperationFailure:
logging.info(" ***> Classifiers failure")
else:
for feed_id in found_feed_ids:
classifiers[feed_id] = get_classifiers_for_user(user, feed_id, classifier_feeds[feed_id],
classifier_authors[feed_id],
classifier_titles[feed_id],
classifier_tags[feed_id])
# Just need to format stories
for story in stories:
@ -822,7 +840,7 @@ def delete_feed(request):
@ajax_login_required
@json.json_view
def delete_folder(request):
folder_to_delete = request.POST['folder_name']
folder_to_delete = request.POST.get('folder_name') or request.POST.get('folder_to_delete')
in_folder = request.POST.get('in_folder', '')
feed_ids_in_folder = [int(f) for f in request.REQUEST.getlist('feed_id') if f]
@ -851,17 +869,21 @@ def rename_feed(request):
@ajax_login_required
@json.json_view
def rename_folder(request):
folder_to_rename = request.POST['folder_name']
folder_to_rename = request.POST.get('folder_name') or request.POST.get('folder_to_rename')
new_folder_name = request.POST['new_folder_name']
in_folder = request.POST.get('in_folder', '')
code = 0
# Works piss poor with duplicate folder titles, if they are both in the same folder.
# renames all, but only in the same folder parent. But nobody should be doing that, right?
if new_folder_name:
if folder_to_rename and new_folder_name:
user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user)
user_sub_folders.rename_folder(folder_to_rename, new_folder_name, in_folder)
return dict(code=1)
code = 1
else:
code = -1
return dict(code=code)
@ajax_login_required
@json.json_view
@ -869,7 +891,7 @@ def move_feed_to_folder(request):
feed_id = int(request.POST['feed_id'])
in_folder = request.POST.get('in_folder', '')
to_folder = request.POST.get('to_folder', '')
user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user)
user_sub_folders = user_sub_folders.move_feed_to_folder(feed_id, in_folder=in_folder, to_folder=to_folder)
@ -965,10 +987,11 @@ def save_feed_chooser(request):
for sub in usersubs:
try:
if sub.feed.pk in approved_feeds:
sub.active = True
activated += 1
sub.save()
sub.feed.count_subscribers()
if not sub.active:
sub.active = True
sub.save()
sub.feed.count_subscribers()
elif sub.active:
sub.active = False
sub.save()

View file

@ -65,12 +65,6 @@ class Command(BaseCommand):
options['compute_scores'] = True
import pymongo
db = pymongo.Connection(settings.MONGODB_SLAVE['host'], slave_okay=True, replicaset='nbset').newsblur
options['slave_db'] = db
disp = feed_fetcher.Dispatcher(options, num_workers)
feeds_queue = []

View file

@ -90,7 +90,7 @@ class Feed(models.Model):
'favicon_color': self.favicon_color,
'favicon_fade': self.favicon_fade(),
'favicon_text_color': self.favicon_text_color(),
'favicon_fetching': bool(not (self.favicon_not_found or self.favicon_color))
'favicon_fetching': self.favicon_fetching,
}
if include_favicon:
@ -149,6 +149,10 @@ class Feed(models.Model):
# Feed has been deleted. Just ignore it.
return
@property
def favicon_fetching(self):
return bool(not (self.favicon_not_found or self.favicon_color))
@classmethod
def get_feed_from_url(cls, url, create=True, aggressive=False, fetch=True, offset=0):
feed = None
@ -217,11 +221,12 @@ class Feed(models.Model):
publisher.connection.close()
def update_all_statistics(self):
def update_all_statistics(self, full=True):
self.count_subscribers()
self.count_stories()
self.save_popular_authors()
self.save_popular_tags()
if full:
self.save_popular_authors()
self.save_popular_tags()
def setup_feed_for_premium_subscribers(self):
self.count_subscribers()
@ -259,7 +264,7 @@ class Feed(models.Model):
self.active = True
self.save()
except IntegrityError:
original_feed = Feed.objects.get(feed_address=feed_address)
original_feed = Feed.objects.get(feed_address=feed_address, feed_link=self.feed_link)
original_feed.has_feed_exception = False
original_feed.active = True
original_feed.save()
@ -575,7 +580,7 @@ class Feed(models.Model):
self.data.feed_classifier_counts = json.encode(scores)
self.data.save()
def update(self, verbose=False, force=False, single_threaded=True, compute_scores=True, slave_db=None):
def update(self, verbose=False, force=False, single_threaded=True, compute_scores=True):
from utils import feed_fetcher
try:
self.feed_address = self.feed_address % {'NEWSBLUR_DIR': settings.NEWSBLUR_DIR}
@ -591,7 +596,6 @@ class Feed(models.Model):
'single_threaded': single_threaded,
'force': force,
'compute_scores': compute_scores,
'slave_db': slave_db,
}
disp = feed_fetcher.Dispatcher(options, 1)
disp.add_jobs([[self.pk]])
@ -621,6 +625,7 @@ class Feed(models.Model):
if story.get('title'):
story_content = story.get('story_content')
story_tags = self.get_tags(story)
story_link = self.get_permalink(story)
existing_story, story_has_changed = self._exists_story(story, story_content, existing_stories)
if existing_story is None:
@ -629,14 +634,13 @@ class Feed(models.Model):
story_title = story.get('title'),
story_content = story_content,
story_author_name = story.get('author'),
story_permalink = story.get('link'),
story_permalink = story_link,
story_guid = story.get('guid'),
story_tags = story_tags
)
try:
s.save()
ret_values[ENTRY_NEW] += 1
cache.set('updated_feed:%s' % self.id, 1)
except (IntegrityError, OperationError), e:
ret_values[ENTRY_ERR] += 1
if verbose:
@ -674,20 +678,19 @@ 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'))
existing_story.story_feed = self.pk
existing_story.story_date = story.get('published')
existing_story.story_title = story.get('title')
existing_story.story_content = story_content_diff
existing_story.story_original_content = original_content
existing_story.story_author_name = story.get('author')
existing_story.story_permalink = story.get('link')
existing_story.story_permalink = story_link
existing_story.story_guid = story.get('guid')
existing_story.story_tags = story_tags
try:
existing_story.save()
ret_values[ENTRY_UPDATED] += 1
cache.set('updated_feed:%s' % self.id, 1)
except (IntegrityError, OperationError):
ret_values[ENTRY_ERR] += 1
if verbose:
@ -768,32 +771,32 @@ class Feed(models.Model):
story_feed_id=self.pk,
).order_by('-story_date')
if stories.count() > trim_cutoff:
if verbose:
print 'Found %s stories in %s. Trimming to %s...' % (stories.count(), self, trim_cutoff)
story_trim_date = stories[trim_cutoff].story_date
logging.debug(' ---> [%-30s] Found %s stories. Trimming to %s...' % (self, stories.count(), trim_cutoff))
try:
story_trim_date = stories[trim_cutoff].story_date
except IndexError, e:
logging.debug(' ***> [%-30s] Error trimming feed: %s' % (self, e))
return
extra_stories = MStory.objects(story_feed_id=self.pk, story_date__lte=story_trim_date)
extra_stories_count = extra_stories.count()
extra_stories.delete()
# print "Deleted stories, %s left." % MStory.objects(story_feed_id=self.pk).count()
userstories = MUserStory.objects(feed_id=self.pk, read_date__lte=story_trim_date)
print "Deleted %s stories, %s left." % (extra_stories_count, MStory.objects(story_feed_id=self.pk).count())
userstories = MUserStory.objects(feed_id=self.pk, story_date__lte=story_trim_date)
if userstories.count():
# print "Found %s user stories. Deleting..." % userstories.count()
print "Found %s user stories. Deleting..." % userstories.count()
userstories.delete()
def get_stories(self, offset=0, limit=25, force=False, slave=False):
stories = cache.get('feed_stories:%s-%s-%s' % (self.id, offset, limit), [])
if not stories or force:
if slave:
import pymongo
db = pymongo.Connection(['db01'], slave_okay=True, replicaset='nbset').newsblur
stories_db_orig = db.stories.find({"story_feed_id": self.pk})[offset:offset+limit]
stories_db = []
for story in stories_db_orig:
stories_db.append(bunch(story))
else:
stories_db = MStory.objects(story_feed_id=self.pk)[offset:offset+limit]
stories = Feed.format_stories(stories_db, self.pk)
cache.set('feed_stories:%s-%s-%s' % (self.id, offset, limit), stories)
if slave:
import pymongo
db = pymongo.Connection(['db01'], slave_okay=True, replicaset='nbset').newsblur
stories_db_orig = db.stories.find({"story_feed_id": self.pk})[offset:offset+limit]
stories_db = []
for story in stories_db_orig:
stories_db.append(bunch(story))
else:
stories_db = MStory.objects(story_feed_id=self.pk)[offset:offset+limit]
stories = Feed.format_stories(stories_db, self.pk)
return stories
@ -855,12 +858,23 @@ class Feed(models.Model):
fcat.append(tagname)
fcat = [t[:250] for t in fcat]
return fcat[:12]
def get_permalink(self, entry):
link = entry.get('link')
if not link:
links = entry.get('links')
if links:
link = links[0].get('href')
if not link:
link = entry.get('id')
return link
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)
@ -870,22 +884,22 @@ class Feed(models.Model):
# 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 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
elif story.get('link') and story.get('link') == existing_story.story_permalink:
story_in_system = existing_story
# Title distance + content distance, checking if story changed
story_title_difference = levenshtein_distance(story.get('title'),
existing_story.story_title)
if '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
# Title distance + content distance, checking if story changed
story_title_difference = levenshtein_distance(story.get('title'),
existing_story.story_title)
seq = difflib.SequenceMatcher(None, story_content, existing_story_content)
@ -909,11 +923,14 @@ class Feed(models.Model):
story_in_system = existing_story
story_has_changed = True
break
if story_in_system:
if story_content != existing_story_content:
story_has_changed = True
if story_link != existing_story.story_permalink:
story_has_changed = True
break
# if story_has_changed or not story_in_system:
# print 'New/updated story: %s' % (story),
@ -1094,7 +1111,7 @@ class MFeedPage(mongo.Document):
class MStory(mongo.Document):
'''A feed item'''
story_feed_id = mongo.IntField()
story_feed_id = mongo.IntField(unique_with='story_guid')
story_date = mongo.DateTimeField()
story_title = mongo.StringField(max_length=1024)
story_content = mongo.StringField()
@ -1110,6 +1127,7 @@ class MStory(mongo.Document):
meta = {
'collection': 'stories',
'indexes': [('story_feed_id', '-story_date')],
'index_drop_dups': True,
'ordering': ['-story_date'],
'allow_inheritance': False,
}
@ -1150,7 +1168,8 @@ class MStarredStory(mongo.Document):
meta = {
'collection': 'starred_stories',
'indexes': [('user_id', '-starred_date'), 'story_feed_id'],
'indexes': [('user_id', '-starred_date'), ('user_id', 'story_feed_id'), 'user_id', 'story_feed_id'],
'index_drop_dups': True,
'ordering': ['-starred_date'],
'allow_inheritance': False,
}
@ -1247,8 +1266,10 @@ class DuplicateFeed(models.Model):
return "%s: %s" % (self.feed, self.duplicate_address)
def merge_feeds(original_feed_id, duplicate_feed_id, force=False):
from apps.reader.models import UserSubscription, MUserStory
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
from apps.reader.models import UserSubscription
if original_feed_id == duplicate_feed_id:
logging.info(" ***> Merging the same feed. Ignoring...")
return
if original_feed_id > duplicate_feed_id and not force:
original_feed_id, duplicate_feed_id = duplicate_feed_id, original_feed_id
try:
@ -1267,52 +1288,14 @@ def merge_feeds(original_feed_id, duplicate_feed_id, force=False):
for user_sub in user_subs:
user_sub.switch_feed(original_feed, duplicate_feed)
# Switch read stories
user_stories = MUserStory.objects(feed_id=duplicate_feed.pk)
logging.info(" ---> %s read stories" % user_stories.count())
for user_story in user_stories:
user_story.feed_id = original_feed.pk
duplicate_story = user_story.story
story_guid = duplicate_story.story_guid if hasattr(duplicate_story, 'story_guid') else duplicate_story.id
original_story = MStory.objects(story_feed_id=original_feed.pk,
story_guid=story_guid)
if original_story:
user_story.story = original_story[0]
try:
user_story.save()
except OperationError:
# User read the story in the original feed, too. Ugh, just ignore it.
pass
else:
logging.info(" ***> Can't find original story: %s" % duplicate_story.id)
user_story.delete()
def delete_story_feed(model, feed_field='feed_id'):
duplicate_stories = model.objects(**{feed_field: duplicate_feed.pk})
# if duplicate_stories.count():
# logging.info(" ---> Deleting %s %s" % (duplicate_stories.count(), model))
duplicate_stories.delete()
def switch_feed(model):
duplicates = model.objects(feed_id=duplicate_feed.pk)
if duplicates.count():
logging.info(" ---> Switching %s %s" % (duplicates.count(), model))
for duplicate in duplicates:
duplicate.feed_id = original_feed.pk
try:
duplicate.save()
pass
except (IntegrityError, OperationError):
logging.info(" !!!!> %s already exists" % duplicate)
duplicate.delete()
delete_story_feed(MStory, 'story_feed_id')
delete_story_feed(MFeedPage, 'feed_id')
switch_feed(MClassifierTitle)
switch_feed(MClassifierAuthor)
switch_feed(MClassifierFeed)
switch_feed(MClassifierTag)
try:
DuplicateFeed.objects.create(

View file

@ -12,13 +12,10 @@ class UpdateFeeds(Task):
if not isinstance(feed_pks, list):
feed_pks = [feed_pks]
import pymongo
db = pymongo.Connection(settings.MONGODB_SLAVE['host'], slave_okay=True).newsblur
for feed_pk in feed_pks:
try:
feed = Feed.objects.get(pk=feed_pk)
feed.update(slave_db=db)
feed.update()
except Feed.DoesNotExist:
logging.info(" ---> Feed doesn't exist: [%s]" % feed_pk)
# logging.debug(' Updating: [%s] %s' % (feed_pks, feed))

View file

@ -41,13 +41,17 @@ def load_single_feed(request, feed_id):
@json.json_view
def feed_autocomplete(request):
query = request.GET['term']
query = request.GET.get('term')
if not query:
return dict(code=-1, message="Specify a search 'term'.")
feeds = []
for field in ['feed_address', 'feed_link', 'feed_title']:
if not feeds:
feeds = Feed.objects.filter(**{
'%s__icontains' % field: query,
'num_subscribers__gt': 1,
'branch_from_feed__isnull': True,
}).exclude(
Q(**{'%s__icontains' % field: 'token'}) |
Q(**{'%s__icontains' % field: 'private'})
@ -135,7 +139,7 @@ def exception_retry(request):
feed.fetched_once = True
feed.save()
feed = feed.update(force=True, compute_scores=False)
feed = feed.update(force=True, compute_scores=False, verbose=True)
usersub = UserSubscription.objects.get(user=user, feed=feed)
usersub.calculate_feed_scores(silent=False)
@ -150,6 +154,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']
code = -1
if feed.has_page_exception or feed.has_feed_exception:
# Fix broken feed
@ -160,6 +165,7 @@ def exception_change_feed_address(request):
feed.feed_address = feed_address
feed.next_scheduled_update = datetime.datetime.utcnow()
duplicate_feed = feed.save()
code = 1
if duplicate_feed:
new_feed = Feed.objects.get(pk=duplicate_feed.pk)
feed = new_feed
@ -179,18 +185,29 @@ 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.objects.get(pk=feed.pk)
usersub = UserSubscription.objects.get(user=request.user, feed=original_feed)
usersub.switch_feed(feed, original_feed)
if usersub:
usersub.switch_feed(feed, original_feed)
usersub = UserSubscription.objects.get(user=request.user, feed=feed)
usersub.calculate_feed_scores(silent=False)
feed.update_all_statistics()
classifiers = get_classifiers_for_user(usersub.user, usersub.feed.pk)
feeds = {original_feed.pk: usersub.canonical(full=True)}
return {'code': 1, 'feeds': feeds}
feeds = {
original_feed.pk: usersub.canonical(full=True, classifiers=classifiers),
}
return {
'code': code,
'feeds': feeds,
'new_feed_id': usersub.feed.pk,
}
@ajax_login_required
@json.json_view
@ -232,18 +249,29 @@ 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.objects.get(pk=feed.pk)
usersub = UserSubscription.objects.get(user=request.user, feed=original_feed)
usersub.switch_feed(feed, original_feed)
if usersub:
usersub.switch_feed(feed, original_feed)
usersub = UserSubscription.objects.get(user=request.user, feed=feed)
usersub.calculate_feed_scores(silent=False)
feed.update_all_statistics()
classifiers = get_classifiers_for_user(usersub.user, usersub.feed.pk)
feeds = {original_feed.pk: usersub.canonical(full=True)}
return {'code': code, 'feeds': feeds}
feeds = {
original_feed.pk: usersub.canonical(full=True, classifiers=classifiers),
}
return {
'code': code,
'feeds': feeds,
'new_feed_id': usersub.feed.pk,
}
@login_required
def status(request):

View file

@ -34,3 +34,7 @@ def press(request):
def feedback(request):
return render_to_response('static/feedback.xhtml', {},
context_instance=RequestContext(request))
def iphone(request):
return render_to_response('static/iphone.xhtml', {},
context_instance=RequestContext(request))

View file

@ -12,4 +12,8 @@ def render_statistics_graphs(statistics):
def format_graph(n, max_value, height=30):
if n == 0 or max_value == 0:
return 1
return max(1, height * (n/float(max_value)))
return max(1, height * (n/float(max_value)))
@register.inclusion_tag('statistics/render_feedback_table.xhtml')
def render_feedback_table(feedbacks):
return dict(feedbacks=feedbacks)

View file

@ -3,4 +3,5 @@ from apps.statistics import views
urlpatterns = patterns('',
url(r'^dashboard_graphs', views.dashboard_graphs, name='statistics-graphs'),
url(r'^feedback_table', views.feedback_table, name='feedback-table'),
)

View file

@ -1,9 +1,15 @@
from django.template import RequestContext
from django.shortcuts import render_to_response
from apps.statistics.models import MStatistics
from apps.statistics.models import MStatistics, MFeedback
def dashboard_graphs(request):
statistics = MStatistics.all()
return render_to_response('statistics/render_statistics_graphs.xhtml', {
'statistics': statistics,
}, context_instance=RequestContext(request))
def feedback_table(request):
feedbacks = MFeedback.all()
return render_to_response('statistics/render_feedback_table.xhtml', {
'feedbacks': feedbacks,
}, context_instance=RequestContext(request))

View file

@ -1,14 +1,9 @@
127.0.0.1 localhost
# 10.10 app01
# 10.5.1.100 db01
# 10.10 task01
# 10.5.1.101 task02
199.15.250.228 app01 app01.newsblur.com
199.15.250.229 app02 app02.newsblur.com
199.15.253.218 db01 db01.newsblur.com
199.15.253.162 db02 db02.newsblur.com
199.15.252.50 db02 db02.newsblur.com
199.15.253.226 db03 db03.newsblur.com
199.15.250.231 task01 task01.newsblur.com
199.15.250.250 task02 task02.newsblur.com

57
config/munin/aws_elb_latency Executable file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env python
import datetime
import os
import sys
import boto
from boto.ec2.cloudwatch import CloudWatchConnection
from munin import MuninPlugin
class AWSCloudWatchELBLatencyPlugin(MuninPlugin):
category = "AWS"
args = "-l 0 --base 1000"
vlabel = "Seconds"
info = "Show latency for requests"
@property
def title(self):
return "Seconds of latency for ELBs"
@property
def fields(self):
return [
("maximum", dict(
label = "Maximum latency",
type = "GAUGE",
)),
("minimum", dict(
label = "Minimum latency",
type = "GAUGE",
)),
("average", dict(
label = "Average latency",
type = "GAUGE",
)),
]
def __init__(self):
self.api_key = os.environ['AWS_KEY']
self.secret_key = os.environ['AWS_SECRET']
def execute(self):
minutes = 5
end_date = datetime.datetime.utcnow()
start_date = end_date - datetime.timedelta(minutes=minutes)
cw = CloudWatchConnection(self.api_key, self.secret_key)
metrics = cw.get_metric_statistics(5*60, start_date, end_date, "Latency", "AWS/ELB", ["Average", "Minimum", "Maximum"])
m = metrics[0]
return dict(
maximum = m['Maximum'],
minimum = m['Minimum'],
average = m['Average'],
)
if __name__ == "__main__":
AWSCloudWatchELBLatencyPlugin().run()

47
config/munin/aws_elb_requests Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env python
import datetime
import os
import sys
import boto
from boto.ec2.cloudwatch import CloudWatchConnection
from munin import MuninPlugin
class AWSCloudWatchELBRequestsPlugin(MuninPlugin):
category = "AWS"
args = "-l 0 --base 1000"
vlabel = "Requests/sec"
info = "Show number of requests per second"
@property
def title(self):
return "Requests/sec for ELBs '%s'" % ",".join(self.elb_names)
@property
def fields(self):
return [
(n, dict(
label = "requests on ELB %s" % n,
type = "ABSOLUTE",
)) for n in self.elb_names
]
def __init__(self):
self.api_key = os.environ['AWS_KEY']
self.secret_key = os.environ['AWS_SECRET']
self.elb_names = (sys.argv[0].rsplit('_', 1)[-1] or os.environ['ELB_NAME']).split(',')
def execute(self):
minutes = 5
end_date = datetime.datetime.utcnow()
start_date = end_date - datetime.timedelta(minutes=minutes)
cw = CloudWatchConnection(self.api_key, self.secret_key)
return dict(
(n, sum(x['Sum'] for x in cw.get_metric_statistics(60, start_date, end_date, "RequestCount", "AWS/ELB", ["Sum"])))
for n in self.elb_names
)
if __name__ == "__main__":
AWSCloudWatchELBRequestsPlugin().run()

View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
import os
import sys
import boto
from boto.sqs.connection import SQSConnection
from munin import MuninPlugin
class AWSSQSQueueLengthPlugin(MuninPlugin):
category = "AWS"
args = "-l 0 --base 1000"
vlabel = "Messages"
info = "Show number of messages in an SQS queue"
@property
def title(self):
return "Length of AWS SQS queues '%s'" % ",".join(self.queues)
@property
def fields(self):
return [
(q, dict(
label = "messages in %s" % q,
type = "GAUGE",
min = "0",
)) for q in self.queues
]
def __init__(self):
self.api_key = os.environ['AWS_KEY']
self.secret_key = os.environ['AWS_SECRET']
self.queues = (sys.argv[0].rsplit('_', 1)[-1] or os.environ['SQS_QUEUES']).split(',')
def execute(self):
conn = SQSConnection(self.api_key, self.secret_key)
return dict(
(qname, conn.get_queue(qname).count())
for qname in self.queues
)
if __name__ == "__main__":
AWSSQSQueueLengthPlugin().run()

50
config/munin/cassandra_cfcounts Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env python
import os
from munin.cassandra import MuninCassandraPlugin
class CassandraCFCountsPlugin(MuninCassandraPlugin):
title = "read/write rate"
args = "--base 1000 -l 0"
vlabel = "ops per ${graph_period}"
@property
def fields(self):
fs = []
cfstats = self.cfstats()
for kf, kfstats in cfstats.items():
if not self.keyspaces or kf not in self.keyspaces:
continue
for cf, cfstats in kfstats['cf'].items():
name = "%s_%s_reads" % (kf, cf)
label = "%s.%s reads" % (kf, cf)
fs.append((name, dict(
label = label,
info = label,
type = "DERIVE",
min = "0",
)))
name = "%s_%s_writes" % (kf, cf)
label = "%s.%s writes" % (kf, cf)
fs.append((name, dict(
label = label,
info = label,
type = "DERIVE",
min = "0",
)))
return fs
def execute(self):
cfstats = self.cfstats()
values = {}
for kf, kfstats in cfstats.items():
if not self.keyspaces or kf not in self.keyspaces:
continue
for cf, cfstats in kfstats['cf'].items():
name = "%s_%s" % (kf, cf)
values["%s_reads" % name] = cfstats['Read Count']
values["%s_writes" % name] = cfstats['Write Count']
return values
if __name__ == "__main__":
CassandraCFCountsPlugin().run()

View file

@ -0,0 +1,45 @@
#!/usr/bin/env python
import os
from munin.cassandra import MuninCassandraPlugin
class CassandraKeyCacheRatioPlugin(MuninCassandraPlugin):
title = "key cache hit ratio"
args = "--base 1000 -l 0"
vlabel = "ratio"
scale = False
@property
def fields(self):
fs = []
cfstats = self.cfstats()
for kf, kfstats in cfstats.items():
if not self.keyspaces or kf not in self.keyspaces:
continue
for cf, cfstats in kfstats['cf'].items():
name = "%s_%s" % (kf, cf)
label = "%s.%s" % (kf, cf)
fs.append((name, dict(
label = label,
info = label,
type = "GAUGE",
max = "1",
min = "0",
)))
return fs
def execute(self):
cfstats = self.cfstats()
values = {}
for kf, kfstats in cfstats.items():
if not self.keyspaces or kf not in self.keyspaces:
continue
for cf, cfstats in kfstats['cf'].items():
if cfstats['Key cache hit rate'] != 'NaN':
values["%s_%s" % (kf, cf)] = cfstats['Key cache hit rate']
else:
values["%s_%s" % (kf, cf)] = "U"
return values
if __name__ == "__main__":
CassandraKeyCacheRatioPlugin().run()

55
config/munin/cassandra_latency Executable file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env python
import os
from munin.cassandra import MuninCassandraPlugin
class CassandraLatencyPlugin(MuninCassandraPlugin):
title = "read/write latency"
args = "--base 1000 -l 0"
vlabel = "seconds"
@property
def fields(self):
fs = []
cfstats = self.cfstats()
for kf, kfstats in cfstats.items():
if not self.keyspaces or kf not in self.keyspaces:
continue
for cf, cfstats in kfstats['cf'].items():
name = "%s_%s_read" % (kf, cf)
label = "%s.%s read latency" % (kf, cf)
fs.append((name, dict(
label = label,
info = label,
type = "GAUGE",
min = "0",
)))
name = "%s_%s_write" % (kf, cf)
label = "%s.%s write latency" % (kf, cf)
fs.append((name, dict(
label = label,
info = label,
type = "GAUGE",
min = "0",
)))
return fs
def execute(self):
cfstats = self.cfstats()
values = {}
for kf, kfstats in cfstats.items():
if not self.keyspaces or kf not in self.keyspaces:
continue
for cf, cfstats in kfstats['cf'].items():
name = "%s_%s" % (kf, cf)
for k, n in (('read', 'Read Latency'), ('write', 'Write Latency')):
latency = cfstats[n].split(' ')[0]
if latency == 'NaN':
latency = 'U'
else:
latency = float(latency) / 1000
values["%s_%s" % (name, k)] = latency
return values
if __name__ == "__main__":
CassandraLatencyPlugin().run()

24
config/munin/cassandra_load Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env python
import os
from munin.cassandra import MuninCassandraPlugin
class CassandraLoadPlugin(MuninCassandraPlugin):
title = "load (data stored in node)"
args = "--base 1024 -l 0"
vlabel = "bytes"
fields = [
('load', dict(
label = "load",
info = "data stored in node",
type = "GAUGE",
min = "0",
)),
]
def execute(self):
info = self.cinfo()
return dict(load = info['Load'])
if __name__ == "__main__":
CassandraLoadPlugin().run()

33
config/munin/cassandra_pending Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env python
import os
from munin.cassandra import MuninCassandraPlugin
class CassandraPendingPlugin(MuninCassandraPlugin):
title = "thread pool pending tasks"
args = "--base 1000 -l 0"
vlabel = "pending tasks"
scale = False
@property
def fields(self):
tpstats = self.tpstats()
fs = []
for name, stats in tpstats.items():
fs.append((name.lower().replace('-', '_'), dict(
label = name,
info = name,
type = "GAUGE",
min = "0",
)))
return fs
def execute(self):
tpstats = self.tpstats()
values = {}
for name, stats in tpstats.items():
values[name.lower().replace('-', '_')] = stats['pending']
return values
if __name__ == "__main__":
CassandraPendingPlugin().run()

153
config/munin/cpu Executable file
View file

@ -0,0 +1,153 @@
#!/usr/bin/env python
import os
import re
from munin import MuninPlugin
class CPUPlugin(MuninPlugin):
title = "CPU usage"
args = "--base 1000 -r --lower-limit 0 --upper-limit 100" #" --upper-limit $graphlimit"
vlabel = "%"
category = "system"
period = "second" # TODO: I think this is the default anyway
info = "This graph shows how CPU time is spent."
@property
def order(self):
return ("system user nice idle " + self.extinfo).strip()
@property
def extinfo(self):
if hasattr(self, '_extinfo'):
return self._extinfo
fp = open("/proc/stat", "rb")
stat = fp.read()
fp.close()
if bool(re.match(r"^cpu +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+", stat)):
self._extinfo = "iowait irq softirq"
else:
self._extinfo = ""
return self._extinfo
@property
def fields(self):
warning = os.environ.get('load_warn', 10)
critical = os.environ.get('load_crit', 120)
fields = [
("system", dict(
label = "system"
draw = "AREA",
max = max,
min = min,
type = "DERIVE",
warning = syswarning,
critical = syscritical,
info = "CPU time spent by the kernel in system activities",
)),
("user", dict(
label = "user"
draw = "STACK",
max = max,
min = "0",
type = "DERIVE",
warning = usrwarning,
info = "CPU time spent by normal programs and daemons",
))
]
return [("load", dict(
label = "load",
info = 'The load average of the machine describes how many processes are in the run-queue (scheduled to run "immediately").',
type = "GAUGE",
min = "0",
warning = str(warning),
critical = str(critical)))]
def execute(self):
if os.path.exists("/proc/loadavg"):
loadavg = open("/proc/loadavg", "r").read().strip().split(' ')
else:
from subprocess import Popen, PIPE
output = Popen(["uptime"], stdout=PIPE).communicate()[0]
loadavg = output.rsplit(':', 1)[1].strip().split(' ')[:3]
print loadavg
print "load.value %s" % loadavg[1]
if __name__ == "__main__":
CPUPlugin().run()
if (`egrep '^cpu +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+' /proc/stat 2>/dev/null >/dev/null`)
then
extinfo="iowait irq softirq"
fi
if [ "$1" = "config" ]; then
NCPU=$(egrep '^cpu[0-9]+ ' /proc/stat | wc -l)
PERCENT=$(($NCPU * 100))
MAX=$(($NCPU * 100))
if [ "$scaleto100" = "yes" ]; then
graphlimit=100
else
graphlimit=$PERCENT
fi
SYSWARNING=`expr $PERCENT '*' 30 / 100`
SYSCRITICAL=`expr $PERCENT '*' 50 / 100`
USRWARNING=`expr $PERCENT '*' 80 / 100`
echo 'nice.label nice'
echo 'nice.draw STACK'
echo 'nice.min 0'
echo "nice.max $MAX"
echo 'nice.type DERIVE'
echo 'nice.info CPU time spent by nice(1)d programs'
echo 'idle.label idle'
echo 'idle.draw STACK'
echo 'idle.min 0'
echo "idle.max $MAX"
echo 'idle.type DERIVE'
echo 'idle.info Idle CPU time'
if [ "$scaleto100" = "yes" ]; then
echo "system.cdef system,$NCPU,/"
echo "user.cdef user,$NCPU,/"
echo "nice.cdef nice,$NCPU,/"
echo "idle.cdef idle,$NCPU,/"
fi
if [ ! -z "$extinfo" ]
then
echo 'iowait.label iowait'
echo 'iowait.draw STACK'
echo 'iowait.min 0'
echo "iowait.max $MAX"
echo 'iowait.type DERIVE'
echo 'iowait.info CPU time spent waiting for I/O operations to finish'
echo 'irq.label irq'
echo 'irq.draw STACK'
echo 'irq.min 0'
echo "irq.max $MAX"
echo 'irq.type DERIVE'
echo 'irq.info CPU time spent handling interrupts'
echo 'softirq.label softirq'
echo 'softirq.draw STACK'
echo 'softirq.min 0'
echo "softirq.max $MAX"
echo 'softirq.type DERIVE'
echo 'softirq.info CPU time spent handling "batched" interrupts'
if [ "$scaleto100" = "yes" ]; then
echo "iowait.cdef iowait,$NCPU,/"
echo "irq.cdef irq,$NCPU,/"
echo "softirq.cdef softirq,$NCPU,/"
fi
fi
exit 0
fi
HZ=`getconf CLK_TCK`
if [ ! -z "$extinfo" ]
then
awk -v HZ=$HZ 'BEGIN { factor=100/HZ } /^cpu / { for (i=2; i<=8; i++) { $i = int($i * factor) }; print "user.value " $2 "\nnice.value " $3 "\nsystem.value " $4 "\nidle.value " $5 "\niowait.value " $6 "\nirq.value " $7 "\nsoftirq.value " $8 }' < /proc/stat
else
awk -v HZ=$HZ 'BEGIN { factor=100/HZ } /^cpu / { for (i=2; i<=5; i++) { $i = int($i * factor) }; print "user.value " $2 "\nnice.value " $3 "\nsystem.value " $4 "\nidle.value " $5 }' < /proc/stat
fi

23
config/munin/ddwrt_wl_rate Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env python
from munin.ddwrt import DDWrtPlugin
class DDWrtWirelessRate(DDWrtPlugin):
title = "Wireless rate"
args = "--base 1000 -l 0"
vlabel = "Mbps"
info = "rate"
fields = (
('rate', dict(
label = "rate",
info = "rate",
type = "GAUGE",
)),
)
def execute(self):
info = self.get_info()
return dict(rate=info['wl_rate'].split(' ')[0])
if __name__ == "__main__":
DDWrtWirelessRate().run()

33
config/munin/ddwrt_wl_signal Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env python
from munin.ddwrt import DDWrtPlugin
class DDWrtWirelessSignalPlugin(DDWrtPlugin):
title = "Wireless signal"
args = "--base 1000 -l 0"
vlabel = "units"
info = "signal quality"
fields = (
('signal', dict(
label = "signal",
info = "signal",
type = "GAUGE",
)),
('noise', dict(
label = "noise",
info = "noise",
type = "GAUGE",
)),
)
def execute(self):
info = self.get_info()
active = info['active_wireless']
signal, noise = active.split(',')[1:3]
return dict(
signal = signal[1:-1],
noise = noise[1:-1],
)
if __name__ == "__main__":
DDWrtWirelessSignalPlugin().run()

View file

@ -0,0 +1,39 @@
#!/usr/bin/env python
from munin.gearman import MuninGearmanPlugin
class MuninGearmanConnectionsPlugin(MuninGearmanPlugin):
title = "Gearman Connections"
args = "--base 1000"
vlabel = "Connections"
fields = (
('total', dict(
label = "Total",
type = "GAUGE",
draw = "LINE2",
min = "0",
)),
('workers', dict(
label = "Workers",
type = "GAUGE",
draw = "LINE2",
min = "0",
)),
('clients', dict(
label = "Clients",
type = "GAUGE",
draw = "LINE2",
min = "0",
)),
)
def execute(self):
workers = self.get_workers()
return dict(
total = len(workers),
workers = sum(1 for x in workers if x['abilities']),
clients = sum(1 for x in workers if not x['abilities']),
)
if __name__ == "__main__":
MuninGearmanConnectionsPlugin().run()

40
config/munin/gearman_queues Executable file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env python
import os
from munin.gearman import MuninGearmanPlugin
class MuninGearmanQueuesPlugin(MuninGearmanPlugin):
title = "Gearman Queues"
args = "--base 1000"
vlabel = "tasks"
_info_keys = ('total', 'running', 'workers')
def __init__(self):
super(MuninGearmanQueuesPlugin, self).__init__()
self.queues = os.environ['GEARMAN_QUEUES'].split(',')
@property
def fields(self):
fs = []
for q in self.queues:
for k in self._info_keys:
label = "%s %s" % (q, k)
fs.append(("%s_%s" % (q.replace('.', '_'), k), dict(
label = label,
info = label,
type = "GAUGE",
)))
return fs
def execute(self):
status = self.get_status()
values = {}
for q in self.queues:
if q in status:
for k in self._info_keys:
values["%s_%s" % (q.replace('.', '_'), k)] = status[q][k]
return values
if __name__ == "__main__":
MuninGearmanQueuesPlugin().run()

62
config/munin/hookbox Executable file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python
import os
import json
import urllib
import urllib2
from munin import MuninPlugin
class HookboxPlugin(MuninPlugin):
title = 'hookbox'
args = "--base 1000"
vlabel = "Y"
info = "Subscibed users"
scale = False
def get_channels(self):
return os.environ.get('HOOKBOX_CHANNELS', '').split(',')
def get_url(self):
return os.environ.get('HOOKBOX_URL', 'http://localhost:8001/rest')
def get_secret(self):
return os.environ.get('HOOKBOX_SECRET', '')
@property
def fields(self):
return (
(channel, dict(
label=channel,
info="%s - users" % channel,
type="GAUGE",
))
for channel in self.get_channels()
)
def get_channel_info(self, channel_name):
values = {
'channel_name': channel_name,
'secret': self.get_secret(),
}
req = urllib2.Request("%s/get_channel_info?%s" % (self.get_url(), urllib.urlencode(values)))
resp = urllib2.urlopen(req)
return json.loads(resp.read())
def get_subscribers(self, channel_name):
try:
return len(self.get_channel_info(channel_name)[1]['subscribers'])
except (urllib2.URLError, KeyError), e:
return 'U'
def execute(self):
return dict(
(channel_name, self.get_subscribers(channel_name))
for channel_name in self.get_channels()
)
if __name__ == "__main__":
HookboxPlugin().run()

44
config/munin/loadavg Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
"""
Load average plugin for Munin.
Based on the default "load" plugin in Munin
First tries reading from /proc/loadavg if it exists. Otherwise, execute
`uptime` and parse out the load average.
"""
import os
from munin import MuninPlugin
class LoadAVGPlugin(MuninPlugin):
title = "Load average"
args = "--base 1000 -l 0"
vlabel = "load"
scale = False
category = "system"
@property
def fields(self):
warning = os.environ.get('load_warn', 10)
critical = os.environ.get('load_crit', 120)
return [("load", dict(
label = "load",
info = 'The load average of the machine describes how many processes are in the run-queue (scheduled to run "immediately").',
type = "GAUGE",
min = "0",
warning = str(warning),
critical = str(critical)))]
def execute(self):
if os.path.exists("/proc/loadavg"):
loadavg = open("/proc/loadavg", "r").read().strip().split(' ')
else:
from subprocess import Popen, PIPE
output = Popen(["uptime"], stdout=PIPE).communicate()[0]
loadavg = output.rsplit(':', 1)[1].strip().split(' ')[:3]
return dict(load=loadavg[1])
if __name__ == "__main__":
LoadAVGPlugin().run()

26
config/munin/memcached_bytes Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env python
from munin.memcached import MuninMemcachedPlugin
class MuninMemcachedBytesPlugin(MuninMemcachedPlugin):
title = "Memcached bytes read/written stats"
args = "--base 1024"
vlabel = "bytes read (-) / written (+) per ${graph_period}"
info = "bytes read/writter stats"
order = ("bytes_read", "bytes_written")
fields = (
('bytes_read', dict(
label = "bytes read",
type = "COUNTER",
graph = "no",
)),
('bytes_written', dict(
label = "Bps",
info = "Bytes read/written",
type = "COUNTER",
negative = "bytes_read",
)),
)
if __name__ == "__main__":
MuninMemcachedBytesPlugin().run()

View file

@ -0,0 +1,19 @@
#!/usr/bin/env python
from munin.memcached import MuninMemcachedPlugin
class MuninMemcachedConnectionsPlugin(MuninMemcachedPlugin):
title = "Memcached connections stats"
args = "--base 1000"
vlabel = "Connections"
info = "connections stats"
fields = (
('curr_connections', dict(
label = "connections",
info = "connections",
type = "GAUGE",
)),
)
if __name__ == "__main__":
MuninMemcachedConnectionsPlugin().run()

View file

@ -0,0 +1,19 @@
#!/usr/bin/env python
from munin.memcached import MuninMemcachedPlugin
class MuninMemcachedCurrentItemsPlugin(MuninMemcachedPlugin):
title = "Memcached current items stats"
args = "--base 1000"
vlabel = "Current Items"
info = "current items stats"
fields = (
('curr_items', dict(
label = "items",
info = "number current items",
type = "GAUGE",
)),
)
if __name__ == "__main__":
MuninMemcachedCurrentItemsPlugin().run()

19
config/munin/memcached_items Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env python
from munin.memcached import MuninMemcachedPlugin
class MuninMemcachedItemsPlugin(MuninMemcachedPlugin):
title = "Memcached new items stats"
args = "--base 1000"
vlabel = "Items"
info = "items stats"
fields = (
('total_items', dict(
label = "items",
info = "number of new items",
type = "COUNTER",
)),
)
if __name__ == "__main__":
MuninMemcachedItemsPlugin().run()

44
config/munin/memcached_queries Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
from munin.memcached import MuninMemcachedPlugin
class MuninMemcachedQueriesPlugin(MuninMemcachedPlugin):
title = "Memcached query stats"
args = "--base 1000"
vlabel = "queries per ${graph_period}"
info = "get/set stats"
fields = (
('cmd_get', dict(
label = "Gets",
info = "Gets",
type = "DERIVE",
min = "0",
)),
('cmd_set', dict(
label = "Sets",
info = "Sets",
type = "DERIVE",
min = "0",
)),
('get_hits', dict(
label = "Get hits",
info = "Get hits",
type = "DERIVE",
min = "0",
)),
('get_misses', dict(
label = "Get misses",
info = "Get misses",
type = "DERIVE",
min = "0",
)),
('evictions', dict(
label = "Evictions",
info = "Evictions",
type = "DERIVE",
min = "0",
)),
)
if __name__ == "__main__":
MuninMemcachedQueriesPlugin().run()

29
config/munin/mongodb_flush_avg Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBFlushAvg(MuninMongoDBPlugin):
args = "-l 0 --base 1000"
vlabel = "seconds"
title = "MongoDB background flush interval"
info = "The average time between background flushes"
fields = (
('total_ms', dict(
label = "Flush interval",
info = "The time interval for background flushes",
type = "DERIVE",
min = "0",
)),
)
def execute(self):
status = self.connection.admin.command('serverStatus')
try:
value = float(status["backgroundFlushing"]["total_ms"])/1000
except KeyError:
value = "U"
return dict(total_ms=value)
if __name__ == "__main__":
MongoDBFlushAvg().run()

29
config/munin/mongodb_heap_usage Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBHeapUsagePlugin(MuninMongoDBPlugin):
args = "-l 0 --base 1024"
vlabel = "bytes"
title = "MongoDB heap usage"
info = "Heap usage"
fields = (
('heap_usage', dict(
label = "heap usage",
info = "heap usage",
type = "GAUGE",
min = "0",
)),
)
def execute(self):
status = self.connection.admin.command('serverStatus')
try:
value = status['extra_info']['heap_usage_bytes']
except KeyError:
value = "U"
return dict(heap_usage=value)
if __name__ == "__main__":
MongoDBHeapUsagePlugin().run()

View file

@ -0,0 +1,29 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBIndexMissesPlugin(MuninMongoDBPlugin):
args = "-l 0 --base 1000"
vlabel = "misses"
title = "MongoDB index misses"
info = "Number of index cache misses"
fields = (
('misses', dict(
label = "misses",
info = "Index cache misses",
type = "DERIVE",
min = "0",
)),
)
def execute(self):
status = self.connection.admin.command('serverStatus')
try:
value = status['indexCounters']['btree']['misses']
except KeyError:
value = "U"
return dict(misses=value)
if __name__ == "__main__":
MongoDBIndexMissesPlugin().run()

29
config/munin/mongodb_lock_ratio Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBLockRatio(MuninMongoDBPlugin):
args = "-l 0 --base 1000"
vlabel = "ratio"
title = "MongoDB global lock time ratio"
info = "How long the global lock has been held compared to the global execution time"
fields = (
('lockratio', dict(
label = "Global lock time ratio",
info = "How long the global lock has been held compared to the global execution time",
type = "GAUGE",
min = "0",
)),
)
def execute(self):
status = self.connection.admin.command('serverStatus')
try:
value = status["globalLock"]["ratio"]
except KeyError:
value = "U"
return dict(lockratio=value)
if __name__ == "__main__":
MongoDBLockRatio().run()

29
config/munin/mongodb_lock_time Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBLockTime(MuninMongoDBPlugin):
args = "-l 0 --base 1000"
vlabel = "time"
title = "MongoDB global lock time"
info = "How long the global lock has been held"
fields = (
('locktime', dict(
label = "Global lock time",
info = "How long the global lock has been held",
type = "COUNTER",
min = "0",
)),
)
def execute(self):
status = self.connection.admin.command('serverStatus')
try:
value = int(status["globalLock"]["lockTime"])
except KeyError:
value = "U"
return dict(locktime=value)
if __name__ == "__main__":
MongoDBLockTime().run()

44
config/munin/mongodb_memory Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBMemoryPlugin(MuninMongoDBPlugin):
args = "-l 0 --base 1024"
vlabel = "bytes"
title = "MongoDB memory usage"
info = "Memory usage"
fields = (
('virtual', dict(
label = "virtual",
info = "Bytes of virtual memory",
type = "GAUGE",
min = "0",
)),
('resident', dict(
label = "resident",
info = "Bytes of resident memory",
type = "GAUGE",
min = "0",
)),
('mapped', dict(
label = "mapped",
info = "Bytes of mapped memory",
type = "GAUGE",
min = "0",
)),
)
def execute(self):
status = self.connection.admin.command('serverStatus')
values = {}
for k in ("virtual", "resident", "mapped"):
try:
value = int(status["mem"][k]) * 1024 * 1024
except KeyError:
value = "U"
values[k] = value
return values
if __name__ == "__main__":
MongoDBMemoryPlugin().run()

29
config/munin/mongodb_objects_ Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBObjectsPlugin(MuninMongoDBPlugin):
args = "--base 1000"
vlabel = "objects"
info = "Number of objects stored"
dbname_in_args = True
fields = (
('objects', dict(
label = "objects",
info = "Number of objects stored",
type = "GAUGE",
min = "0",
)),
)
@property
def title(self):
return "MongoDB objects in database %s" % self.dbname
def execute(self):
stats = self.db.command("dbstats")
return dict(objects=stats['objects'])
if __name__ == "__main__":
MongoDBObjectsPlugin().run()

32
config/munin/mongodb_ops Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBOpsPlugin(MuninMongoDBPlugin):
args = "-l 0 --base 1000"
vlabel = "ops/sec"
title = "MongoDB operations"
info = "Operations"
ops = ("query", "update", "insert", "delete", "command", "getmore")
@property
def fields(self):
return [
(op, dict(
label = "%s operations" % op,
info = "%s operations" % op,
type = "DERIVE",
min = "0",
)) for op in self.ops
]
def execute(self):
status = self.connection.admin.command('serverStatus')
return dict(
(op, status["opcounters"].get(op, 0))
for op in self.ops
)
if __name__ == "__main__":
MongoDBOpsPlugin().run()

View file

@ -0,0 +1,29 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBPageFaultsPlugin(MuninMongoDBPlugin):
args = "-l 0 --base 1000"
vlabel = "page faults / sec"
title = "MongoDB page faults"
info = "Page faults"
fields = (
('page_faults', dict(
label = "page faults",
info = "Page faults",
type = "DERIVE",
min = "0",
)),
)
def execute(self):
status = self.connection.admin.command('serverStatus')
try:
value = status['extra_info']['page_faults']
except KeyError:
value = "U"
return dict(page_faults=value)
if __name__ == "__main__":
MongoDBPageFaultsPlugin().run()

32
config/munin/mongodb_queues Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBQueuesPlugin(MuninMongoDBPlugin):
args = "-l 0 --base 1000"
vlabel = "count"
title = "MongoDB queues"
info = "Queues"
queues = ("readers", "writers")
@property
def fields(self):
return [
(q, dict(
label = "%s" % q,
info = "%s" % q,
type = "GAUGE",
min = "0",
)) for q in self.queues
]
def execute(self):
status = self.connection.admin.command('serverStatus')
return dict(
(q, status["globalLock"]["currentQueue"][q])
for q in self.queues
)
if __name__ == "__main__":
MongoDBQueuesPlugin().run()

View file

@ -0,0 +1,51 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
PRIMARY_STATE = 1
SECONDARY_STATE = 2
class MongoReplicaSetLag(MuninMongoDBPlugin):
vlabel = "seconds"
title = "MongoDB Replica Set Lag"
fields = [("optimeLag", {'label': "Oldest secondary lag"}), ("oplogLength", {"label": "Primary oplog length" })]
def _get_oplog_length(self):
oplog = self.connection['local'].oplog.rs
last_op = oplog.find({}, {'ts': 1}).sort([('$natural', -1)]).limit(1)[0]['ts'].time
first_op = oplog.find({}, {'ts': 1}).sort([('$natural', 1)]).limit(1)[0]['ts'].time
oplog_length = last_op - first_op
return oplog_length
def _get_max_replication_lag(self):
status = self.connection.admin.command('replSetGetStatus')
members = status['members']
primary_optime = None
oldest_secondary_optime = None
for member in members:
member_state = member['state']
optime = member['optime']
if member_state == PRIMARY_STATE:
primary_optime = optime.time
elif member_state == SECONDARY_STATE:
if not oldest_secondary_optime or optime.time < oldest_secondary_optime.time:
oldest_secondary_optime = optime.time
if not primary_optime or not oldest_secondary_optime:
raise Exception("Replica set is not healthy")
return primary_optime - oldest_secondary_optime
def execute(self):
oplog_length = self._get_oplog_length()
replication_lag = self._get_max_replication_lag()
return {
"optimeLag": replication_lag,
"oplogLength": oplog_length
}
if __name__ == "__main__":
MongoReplicaSetLag().run()

45
config/munin/mongodb_size_ Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mongodb import MuninMongoDBPlugin
class MongoDBSizePlugin(MuninMongoDBPlugin):
args = "-l 0 --base 1024"
vlabel = "bytes"
info = "Size of database"
dbname_in_args = True
fields = (
('storagesize', dict(
label = "Storage size (bytes)",
info = "Storage size",
type = "GAUGE",
draw = "AREA",
)),
('datasize', dict(
label = "Data size (bytes)",
info = "Data size",
type = "GAUGE",
draw = "AREA",
)),
('indexsize', dict(
label = "Index size (bytes)",
info = "Index size",
type = "GAUGE",
draw = "AREA",
)),
)
@property
def title(self):
return "MongoDB size of database %s" % self.dbname
def execute(self):
stats = self.db.command("dbstats")
return dict(
storagesize = stats["storageSize"],
datasize = stats["dataSize"],
indexsize = stats["indexSize"],
)
if __name__ == "__main__":
MongoDBSizePlugin().run()

33
config/munin/mysql_dbrows_ Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mysql import MuninMySQLPlugin
class MuninMySQLDBRowsPlugin(MuninMySQLPlugin):
dbname_in_args = True
args = "-l 0 --base 1000"
vlabel = "rows"
info = "Rows in database"
fields = (
('rows', dict(
label = "Row count",
info = "Row count",
type = "GAUGE",
)),
)
@property
def title(self):
return "MySQL number of rows in database %s" % self.dbname
def execute(self):
c = self.cursor()
c.execute("SELECT sum(table_rows) FROM information_schema.TABLES WHERE table_schema = %s", (self.dbname,))
row = c.fetchone()
return dict(
rows = row[0],
)
if __name__ == "__main__":
MuninMySQLDBRowsPlugin().run()

41
config/munin/mysql_dbsize_ Executable file
View file

@ -0,0 +1,41 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.mysql import MuninMySQLPlugin
class MuninMySQLDBSizePlugin(MuninMySQLPlugin):
dbname_in_args = True
args = "-l 0 --base 1024"
vlabel = "bytes"
info = "Size of database"
fields = (
('datasize', dict(
label = "Data size (bytes)",
info = "Data size",
type = "GAUGE",
draw = "AREA",
)),
('indexsize', dict(
label = "Index size (bytes)",
info = "Index size",
type = "GAUGE",
draw = "AREA",
)),
)
@property
def title(self):
return "MySQL size of database %s" % self.dbname
def execute(self):
c = self.cursor()
c.execute("SELECT sum(data_length), sum(index_length) FROM information_schema.TABLES WHERE table_schema = %s", (self.dbname,))
row = c.fetchone()
return dict(
datasize = row[0],
indexsize = row[1],
)
if __name__ == "__main__":
MuninMySQLDBSizePlugin().run()

49
config/munin/nginx_connections Executable file
View file

@ -0,0 +1,49 @@
#!/usr/bin/env python
import os
import re
import urllib
from munin.nginx import MuninNginxPlugin
class MuninNginxConnectionsPlugin(MuninNginxPlugin):
title = "Nginx Connections"
args = "--base 1000"
vlabel = "Connections"
fields = (
('total', dict(
label = "Active connections",
type = "GAUGE",
draw = "LINE2",
min = "0",
)),
('reading', dict(
label = "Reading",
type = "GAUGE",
draw = "LINE2",
min = "0",
)),
('writing', dict(
label = "Writing",
type = "GAUGE",
draw = "LINE2",
min = "0",
)),
('waiting', dict(
label = "Waiting",
type = "GAUGE",
draw = "LINE2",
min = "0",
)),
)
def execute(self):
status = self.get_status()
return dict(
total = status['active'],
reading = status['reading'],
writing = status['writing'],
waiting = status['waiting'],
)
if __name__ == "__main__":
MuninNginxConnectionsPlugin().run()

27
config/munin/nginx_requests Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env python
import os
import re
import urllib
from munin.nginx import MuninNginxPlugin
class MuninNginxRequestsPlugin(MuninNginxPlugin):
title = "Nginx Requests"
args = "--base 1000"
vlabel = "Requests per second"
fields = (
('request', dict(
label = "Requests",
type = "DERIVE",
min = "0",
draw = "LINE2",
)),
)
def execute(self):
return dict(
request = self.get_status()['requests'],
)
if __name__ == "__main__":
MuninNginxRequestsPlugin().run()

35
config/munin/path_size Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env python
import os
import subprocess
from munin import MuninPlugin
class PathSizePlugin(MuninPlugin):
args = "--base 1024 -l 0"
vlabel = "bytes"
scale = True
category = "other"
fields = (
('size', dict(
label = "size",
info = "Size",
type = "GAUGE",
)),
)
def __init__(self, *args, **kwargs):
super(PathSizePlugin, self).__init__(*args, **kwargs)
self.path = os.environ["PATHSIZE_PATH"]
@property
def title(self):
return "Size of %s" % self.path
def execute(self):
p = subprocess.Popen("du -sk " + self.path, shell=True, stdout=subprocess.PIPE)
du = p.communicate()[0]
size = int(du.split('\t')[0].strip()) * 1024
return dict(size=size)
if __name__ == "__main__":
PathSizePlugin().run()

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.pgbouncer import MuninPgBouncerPlugin
class MuninPgBouncerPoolsClientPlugin(MuninPgBouncerPlugin):
command = "SHOW POOLS"
vlabel = "Connections"
info = "Shows number of connections to pgbouncer"
fields = (
('cl_active', dict(
label = "active",
info = "Active connections to pgbouncer",
type = "GAUGE",
min = "0",
)),
('cl_waiting', dict(
label = "waiting",
info = "Waiting connections to pgbouncer",
type = "GAUGE",
min = "0",
)),
)
@property
def title(self):
return "PgBouncer client connections on %s" % self.dbwatched
if __name__ == "__main__":
MuninPgBouncerPoolsClientPlugin().run()

View file

@ -0,0 +1,50 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.pgbouncer import MuninPgBouncerPlugin
class MuninPgBouncerPoolsServerPlugin(MuninPgBouncerPlugin):
command = "SHOW POOLS"
vlabel = "Connections"
info = "Shows number of connections to postgresql"
fields = (
('sv_active', dict(
label = "active",
info = "Active connections to Postgresql",
type = "GAUGE",
min = "0",
)),
('sv_idle', dict(
label = "idle",
info = "Idle connections to Postgresql",
type = "GAUGE",
min = "0",
)),
('sv_used', dict(
label = "used",
info = "Used connections to Postgresql",
type = "GAUGE",
min = "0",
)),
('sv_tested', dict(
label = "tested",
info = "Tested connections to Postgresql",
type = "GAUGE",
min = "0",
)),
('sv_login', dict(
label = "login",
info = "Connections logged in to Postgresql",
type = "GAUGE",
min = "0",
)),
)
@property
def title(self):
return "PgBouncer server connections on %s" % self.dbwatched
if __name__ == "__main__":
MuninPgBouncerPoolsServerPlugin().run()

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.pgbouncer import MuninPgBouncerPlugin
class MuninPgBouncerStatsBytesServerPlugin(MuninPgBouncerPlugin):
command = "SHOW STATS"
vlabel = "Bytes"
info = "Shows average bytes per second"
fields = (
('avg_recv', dict(
label = "received",
info = "Average bytes received per second",
type = "GAUGE",
min = "0",
)),
('avg_sent', dict(
label = "sent",
info = "Average bytes sent per second",
type = "GAUGE",
min = "0",
)),
)
@property
def title(self):
return "PgBouncer bytes per second on %s" % self.dbwatched
if __name__ == "__main__":
MuninPgBouncerStatsBytesServerPlugin().run()

View file

@ -0,0 +1,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.pgbouncer import MuninPgBouncerPlugin
class MuninPgBouncerStatsQueryServerPlugin(MuninPgBouncerPlugin):
command = "SHOW STATS"
vlabel = "Microseconds"
info = "Shows average query duration in microseconds"
fields = (
('avg_query', dict(
label = "received",
info = "Average query duration",
type = "GAUGE",
min = "0",
)),
)
@property
def title(self):
return "PgBouncer average query duration on %s" % self.dbwatched
if __name__ == "__main__":
MuninPgBouncerStatsQueryServerPlugin().run()

View file

@ -0,0 +1,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.pgbouncer import MuninPgBouncerPlugin
class MuninPgBouncerStatsRequestsServerPlugin(MuninPgBouncerPlugin):
command = "SHOW STATS"
vlabel = "Requests"
info = "Shows average requests per second"
fields = (
('avg_req', dict(
label = "requests per second",
info = "average requests per second",
type = "GAUGE",
min = "0",
)),
)
@property
def title(self):
return "PgBouncer average requests per second on %s" % self.dbwatched
if __name__ == "__main__":
MuninPgBouncerStatsRequestsServerPlugin().run()

View file

@ -0,0 +1,66 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Based on a plugin by BjØrn Ruberg.
Plugin to monitor PostgreSQL memory usage; gives number of blocks
read from disk and from memory, showing how much of the database is
served from PostgreSQL's memory buffer.
PLEASE NOTE: This plugin may not present the whole truth - the truth
may actually be even better than this plugin will show you! That is
because PostgreSQL statistics only considers memory block reads from
its own allocated memory. When PostgreSQL reads from disk, it may
actually still be read from memory, but from the _kernel_'s
memory. Summarily, your database server may run even better than
this plugin will indicate. See
http://www.postgresql.org/docs/7.4/interactive/monitoring-stats.html
for a (short) description.
"""
from munin.postgres import MuninPostgresPlugin
class MuninPostgresBlockReadPlugin(MuninPostgresPlugin):
dbname_in_args = True
args = "--base 1000"
vlabel = "Blocks read per ${graph_period}"
info = "Shows number of blocks read from disk and from memory"
fields = (
('from_disk', dict(
label = "Read from disk",
info = "Read from disk",
type = "DERIVE",
min = "0",
draw = "AREA",
)),
('from_memory', dict(
label = "Cached in memory",
info = "Cached in memory",
type = "DERIVE",
min = "0",
draw = "STACK",
)),
)
@property
def title(self):
return "Postgres data reads from %s" % self.dbname
def execute(self):
c = self.cursor()
query = (
"SELECT (SUM (heap_blks_read) + SUM (idx_blks_read) + "
" SUM (toast_blks_read) + SUM (tidx_blks_read)) AS disk, "
" (SUM (heap_blks_hit) + SUM (idx_blks_hit) + "
" SUM (toast_blks_hit) + SUM (tidx_blks_hit)) AS mem "
"FROM pg_statio_user_tables")
c.execute(query)
values = {}
for row in c.fetchall():
values['from_disk'] = row[0]
values['from_memory'] = row[1]
return values
if __name__ == "__main__":
MuninPostgresBlockReadPlugin().run()

61
config/munin/postgres_commits_ Executable file
View file

@ -0,0 +1,61 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Based on a plugin by BjØrn Ruberg.
Plugin to monitor PostgreSQL commits/rollbacks.
"Why should I care?"
- Too many commits can really bog down the database, as it checks all
the tables for consitency after each change.
- Software is often set to 'AutoCommit = 1', meaning a commit is done
after each transaction. This is a good idea with brittle code so that
you can get some work done if not all, but when you're inserting 10,000
rows this can really suck.
- If you see a spike in rollbacks, some db programmer is probably
abusing their session, or a stored proceudre has gone horribly wrong
and isn't leaving a trace. Time for the rolled-up newspaper.
Find out more at
http://www.postgresql.org/docs/8.2/interactive/monitoring-stats.html
(where "8.2" can be the version of PostgreSQL you have installed)
"""
from munin.postgres import MuninPostgresPlugin
class MuninPostgresCommitsPlugin(MuninPostgresPlugin):
dbname_in_args = True
args = "--base 1000"
vlabel = "Sessions per ${graph_period}"
info = "Shows number of commits and rollbacks"
fields = (
('commits', dict(
label = "commits",
info = "SQL sessions terminated with a commit command",
type = "DERIVE",
min = "0",
)),
('rollbacks', dict(
label = "rollbacks",
info = "SQL sessions terminated with a rollback command",
type = "DERIVE",
min = "0",
)),
)
@property
def title(self):
return "Postgres commits/rollbacks on %s" % self.dbname
def execute(self):
c = self.cursor()
c.execute("SELECT xact_commit, xact_rollback FROM pg_stat_database WHERE datname = %s", (self.dbname,))
values = {}
for row in c.fetchall():
values["commits"] = row[0]
values["rollbacks"] = row[1]
return values
if __name__ == "__main__":
MuninPostgresCommitsPlugin().run()

View file

@ -0,0 +1,34 @@
#!/usr/bin/env python
from munin.postgres import MuninPostgresPlugin
class MuninPostgresConnectionsPlugin(MuninPostgresPlugin):
dbname_in_args = False
title = "Postgres active connections"
args = "-l 0 --base 1000"
vlabel = "Active connections"
info = "Shows active Postgresql connections"
@property
def fields(self):
c = self.cursor()
c.execute("SHOW max_connections")
row = c.fetchone()
return (
('connections', dict(
label = "Active connections",
info = "Active connections",
type = "GAUGE",
warning = int(int(row[0]) * 0.7),
critical = int(int(row[0]) * 0.8),
)),
)
def execute(self):
c = self.cursor()
c.execute("SELECT COUNT(1) FROM pg_stat_activity")
row = c.fetchone()
return dict(connections = row[0])
if __name__ == "__main__":
MuninPostgresConnectionsPlugin().run()

50
config/munin/postgres_locks Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Based on a Perl plugin by an unknown author.
Show postgres lock statistics.
"""
from munin.postgres import MuninPostgresPlugin
class MuninPostgresLocksPlugin(MuninPostgresPlugin):
dbname_in_args = False
title = "Postgres locks"
args = "--base 1000"
vlabel = "Locks"
info = "Shows Postgresql locks"
fields = (
('locks', dict(
label = "Locks",
info = "Locks",
type = "GAUGE",
warning = 10,
critical = 20,
)),
('exlocks', dict(
label = "Exclusive locks",
info = "Exclusive locks",
type = "GAUGE",
warning = 5,
critical = 10,
)),
)
def execute(self):
c = self.cursor()
c.execute("SELECT mode, COUNT(mode) FROM pg_locks GROUP BY mode ORDER BY mode")
locks = 0
exlocks = 0
for row in c.fetchall():
if 'exclusive' in row[0].lower():
exlocks += row[1]
locks += row[1]
return dict(
locks = locks,
exlocks = exlocks,
)
if __name__ == "__main__":
MuninPostgresLocksPlugin().run()

89
config/munin/postgres_queries_ Executable file
View file

@ -0,0 +1,89 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Based on a plugin by BjØrn Ruberg.
Plugin to monitor PostgreSQL query rate; returns the number of
sequential scans initiated, rows returned by sequential reads,
index scans initiated, rows returned by index scans, inserts,
updates, and deletes.
Find out more at
http://www.postgresql.org/docs/8.2/interactive/monitoring-stats.html
(should work with PostgreSQL 7.x and 8.x)
"""
from munin.postgres import MuninPostgresPlugin
class MuninPostgresQueriesPlugin(MuninPostgresPlugin):
dbname_in_args = True
args = "--base 1000"
vlabel = "Queries per ${graph_period}"
info = "Shows number of select, insert, update and delete queries"
field_types = (
('sel_seq', dict(
label = "s_selects",
info = "Sequential selects on all tables",
column = "seq_scan",
)),
('sel_seq_rows', dict(
label = "s_select rows",
info = "Rows returned from sequential selects",
column = "seq_tup_read",
)),
('sel_idx', dict(
label = "i_selects",
info = "Sequential selects on all indexes",
column = "idx_scan",
)),
('sel_idx_rows', dict(
label = "i_select rows",
info = "Rows returned from index selects",
column = "idx_tup_fetch",
)),
('inserts', dict(
label = "inserts",
info = "Rows inserted on all tables",
column = "n_tup_ins",
)),
('updates', dict(
label = "updates",
info = "Rows updated on all tables",
column = "n_tup_upd",
)),
('deletes', dict(
label = "deletes",
info = "Rows deleted on all tables",
column = "n_tup_del",
)),
)
@property
def title(self):
return "Postgres queries on %s" % self.dbname
@property
def fields(self):
return [
(k, dict(
label = v['label'],
info = v['label'],
type = "DERIVE",
min = "0",
)) for k, v in self.field_types]
def execute(self):
c = self.cursor()
keys = [(k, v['column']) for k, v in self.field_types]
query = "SELECT %s FROM pg_stat_all_tables" % ",".join('SUM("%s")' % col for key, col in keys)
c.execute(query)
row = c.fetchone()
values = {}
for k, v in zip(keys, row):
values[k[0]] = v
return values
if __name__ == "__main__":
MuninPostgresQueriesPlugin().run()

98
config/munin/postgres_space_ Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Based on a plugin by BjØrn Ruberg and Moses Moore.
Plugin to monitor PostgreSQL disk usage.
"""
from munin.postgres import MuninPostgresPlugin
class MuninPostgresSpacePlugin(MuninPostgresPlugin):
dbname_in_args = True
args = "-l 0 --base 1024"
vlabel = "bytes"
info = "Size of database"
fields = (
('size', dict(
label = "Database size (bytes)",
info = "Database size",
type = "GAUGE",
draw = "AREA",
)),
('indexsize', dict(
label = "Index size (bytes)",
info = "Index size",
type = "GAUGE",
draw = "AREA",
)),
('metasize', dict(
label = "Meta size (bytes)",
info = "Meta size",
type = "GAUGE",
draw = "AREA",
)),
('metaindexsize', dict(
label = "Meta Index size (bytes)",
info = "Meta Index size",
type = "GAUGE",
draw = "AREA",
)),
)
@property
def title(self):
return "Postgres size of database %s" % self.dbname
def execute(self):
c = self.cursor()
namespaces = {}
c.execute("SELECT oid, nspname FROM pg_namespace")
for row in c.fetchall():
namespaces[row[0]] = row[1]
query = (
"SELECT relname, relnamespace, relkind, relfilenode, relpages"
" FROM pg_class WHERE relkind IN ('r', 'i')")
database_pages = 0
database_indexes = 0
metadatabase_pages = 0
metadatabase_indexes = 0
c.execute(query)
for row in c.fetchall():
relname, relnamespace, relkind, relfilenode, relpages = row
ns = namespaces[relnamespace]
if ns.startswith('pg_toast'):
continue
meta = ns.startswith('pg_') or ns == "information_schema"
c2 = self.cursor()
c2.execute("SELECT SUM(relpages) FROM pg_class WHERE relname IN (%s, %s)",
("pg_toast_%s" % relfilenode, "pg_toast_%s_index" % relfilenode))
relpages2 = int(c2.fetchone()[0] or '0')
if relkind == "r": # Regular table
if meta:
metadatabase_pages += int(relpages) + relpages2
else:
database_pages += int(relpages) + relpages2
elif relkind == "i": # Index
if meta:
metadatabase_indexes += int(relpages) + relpages2
else:
database_indexes += int(relpages) + relpages2
return dict(
size = database_pages * 8192,
indexsize = database_indexes * 8192,
metasize = metadatabase_pages * 8192,
metaindexsize = metadatabase_indexes * 8192,
)
if __name__ == "__main__":
MuninPostgresSpacePlugin().run()

View file

@ -0,0 +1,26 @@
#!/usr/bin/env python
""" Monitors the total table size (data + indexes)
for all tables in the specified database."""
from munin.postgres import MuninPostgresPlugin
class PostgresTableSizes(MuninPostgresPlugin):
vlabel = "Table Size"
title = "Table Sizes"
@property
def fields(self):
return [(table, {"label": table}) for table in self.tables()]
def execute(self):
tables = {}
for table in self.tables():
cursor = self.cursor()
cursor.execute("SELECT pg_total_relation_size(%s);", (table,))
tables[table] = cursor.fetchone()[0]
return tables
if __name__ == "__main__":
PostgresTableSizes().run()

View file

@ -0,0 +1,19 @@
#!/usr/bin/env python
from munin.redis import MuninRedisPlugin
class MuninRedisActiveConnectionsPlugin(MuninRedisPlugin):
title = "Redis active connections"
args = "--base 1000"
vlabel = "Connections"
info = "active connections"
fields = (
('connected_clients', dict(
label = "connections",
info = "connections",
type = "GAUGE",
)),
)
if __name__ == "__main__":
MuninRedisActiveConnectionsPlugin().run()

19
config/munin/redis_commands Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env python
from munin.redis import MuninRedisPlugin
class MuninRedisCommandsPlugin(MuninRedisPlugin):
title = "Redis commands"
args = "--base 1000"
vlabel = "commands/sec"
info = "total commands"
fields = (
('total_commands_processed', dict(
label = "commands",
info = "commands",
type = "COUNTER",
)),
)
if __name__ == "__main__":
MuninRedisCommandsPlugin().run()

19
config/munin/redis_connects Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env python
from munin.redis import MuninRedisPlugin
class MuninRedisTotalConnectionsPlugin(MuninRedisPlugin):
title = "Redis connects"
args = "--base 1000"
vlabel = "connections/sec"
info = "connections per second"
fields = (
('total_connections_received', dict(
label = "connections",
info = "connections",
type = "COUNTER",
)),
)
if __name__ == "__main__":
MuninRedisTotalConnectionsPlugin().run()

19
config/munin/redis_used_memory Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env python
from munin.redis import MuninRedisPlugin
class MuninRedisUsedMemoryPlugin(MuninRedisPlugin):
title = "Redis used memory"
args = "--base 1024"
vlabel = "Memory"
info = "used memory"
fields = (
('used_memory', dict(
label = "used memory",
info = "used memory",
type = "GAUGE",
)),
)
if __name__ == "__main__":
MuninRedisUsedMemoryPlugin().run()

53
config/munin/request_time Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python
import os
from time import time
from urllib2 import urlopen, Request
from munin import MuninPlugin
class MuninRequestTimePlugin(MuninPlugin):
title = "Request Time"
args = "--base 1000"
vlabel = "seconds"
info = "Time for a request to complete"
def __init__(self):
super(MuninRequestTimePlugin, self).__init__()
self.bound_time = float(os.environ['RT_BOUND_TIME']) if 'RT_BOUND_TIME' in os.environ else None
self.urls = []
for k, v in os.environ.iteritems():
if k.startswith('RT_URL'):
name, url = tuple(v.split('=', 1))
url = url.split('|')
headers = {}
if len(url) > 1:
headers = dict(x.split('=') for x in url[1:])
url = url[0]
self.urls.append((name, url, headers))
self.urls.sort()
@property
def fields(self):
return [
(name, dict(
label = name,
type = "GAUGE",
min = "0",
)) for name, url, headers in self.urls
]
def execute(self):
values = {}
for name, url, headers in self.urls:
t = time()
req = Request(url, headers=headers)
urlopen(req).read()
dt = time() - t
if self.bound_time:
dt = min(dt, self.bound_time)
values[name] = "%.2f" % dt
return values
if __name__ == "__main__":
MuninRequestTimePlugin().run()

34
config/munin/riak_ops Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from munin.riak import MuninRiakPlugin
class RiakOpsPlugin(MuninRiakPlugin):
args = "-l 0 --base 1000"
vlabel = "ops/sec"
title = "Riak operations"
info = "Operations"
fields = (
('gets', dict(
label = "gets",
info = "gets",
type = "DERIVE",
min = "0",
)),
('puts', dict(
label = "puts",
info = "puts",
type = "DERIVE",
min = "0",
)),
)
def execute(self):
status = self.get_status()
return dict(
gets = status['node_gets_total'],
puts = status['node_puts_total'],
)
if __name__ == "__main__":
RiakOpsPlugin().run()

59
config/munin/tc_size Executable file
View file

@ -0,0 +1,59 @@
#!/usr/bin/env python
import os
import subprocess
from munin import MuninPlugin
class MuninTokyoCabinetSizePlugin(MuninPlugin):
title = "Size of Tokyo Cabinet database"
args = "--base 1024"
vlabel = "bytes"
fields = (
("size", dict(
label = "Size",
type = "GAUGE",
min = "0",
)),
)
environ = {
'PATH': "/usr/bin:/usr/local/bin",
}
def __init__(self):
super(MuninTokyoCabinetSizePlugin, self).__init__()
path = os.environ['TC_PATH']
if path.startswith('tt://'):
self.path = None
self.port = None
self.host = path[5:]
if ':' in self.host:
self.host, self.port = path[5:].split(':')
else:
self.path = path
self.host = None
self.port = None
def inform(self):
if self.path:
raise NotImplementedError()
else:
args = ["tcrmgr", "inform"]
if self.port:
args += ["-port", str(self.port)]
args.append(self.host)
p = subprocess.Popen(args, env=self.environ, stdout=subprocess.PIPE)
res = p.communicate()[0]
res = res.split('\n')
return {
'records': int(res[0].split(':')[-1]),
'size': int(res[1].split(':')[-1]),
}
def execute(self):
info = self.inform()
return dict(size=info['size'])
if __name__ == "__main__":
MuninTokyoCabinetSizePlugin().run()

47
fabfile.py vendored
View file

@ -33,7 +33,7 @@ env.roledefs ={
'local': ['localhost'],
'app': ['app01.newsblur.com', 'app02.newsblur.com'],
'web': ['www.newsblur.com', 'app02.newsblur.com'],
'db': ['db01.newsblur.com', 'db03.newsblur.com'],
'db': ['db01.newsblur.com', 'db02.newsblur.com', 'db03.newsblur.com'],
'task': ['task01.newsblur.com', 'task02.newsblur.com', 'task03.newsblur.com'],
}
@ -153,11 +153,13 @@ def compress_media():
def backup_mongo():
with cd(os.path.join(env.NEWSBLUR_PATH, 'utils/backups')):
run('./mongo_backup.sh')
# run('./mongo_backup.sh')
run('python backup_mongo.py')
def backup_postgresql():
with cd(os.path.join(env.NEWSBLUR_PATH, 'utils/backups')):
run('./postgresql_backup.sh')
# run('./postgresql_backup.sh')
run('python backup_psql.py')
# ===============
# = Calibration =
@ -175,6 +177,7 @@ def sync_time():
def setup_common():
setup_installs()
setup_user()
setup_sudoers()
setup_repo()
setup_repo_local_settings()
setup_local_files()
@ -188,7 +191,6 @@ def setup_common():
setup_forked_mongoengine()
setup_pymongo_repo()
setup_logrotate()
setup_sudoers()
setup_nginx()
configure_nginx()
@ -237,7 +239,7 @@ def setup_installs():
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')
sudo('python ez_setup.py -U setuptools && rm ez_setup.py')
sudo('chsh sclay -s /bin/zsh')
sudo('chsh %s -s /bin/zsh' % env.user)
run('mkdir -p %s' % env.VENDOR_PATH)
def setup_user():
@ -253,6 +255,7 @@ def setup_user():
def add_machine_to_ssh():
put("~/.ssh/id_dsa.pub", "local_keys")
run("echo `cat local_keys` >> .ssh/authorized_keys")
run("rm local_keys")
def setup_repo():
with settings(warn_only=True):
@ -296,7 +299,7 @@ def setup_python():
sudo('python setup.py install')
with settings(warn_only=True):
sudo('su -c \'echo "import sys; sys.setdefaultencoding(\\\\"utf-8\\\\")" > /usr/lib/python2.6/sitecustomize.py\'')
sudo('su -c \'echo "import sys; sys.setdefaultencoding(\\\\"utf-8\\\\")" > /usr/lib/python2.7/sitecustomize.py\'')
# PIL - Only if python-imaging didn't install through apt-get, like on Mac OS X.
def setup_imaging():
@ -328,8 +331,8 @@ def setup_mongoengine():
with settings(warn_only=True):
run('rm -fr mongoengine')
run('git clone https://github.com/hmarr/mongoengine.git')
sudo('rm -f /usr/local/lib/python2.6/site-packages/mongoengine')
sudo('ln -s %s /usr/local/lib/python2.6/site-packages/mongoengine' %
sudo('rm -f /usr/local/lib/python2.7/dist-packages/mongoengine')
sudo('ln -s %s /usr/local/lib/python2.7/dist-packages/mongoengine' %
os.path.join(env.VENDOR_PATH, 'mongoengine/mongoengine'))
with cd(os.path.join(env.VENDOR_PATH, 'mongoengine')):
run('git checkout -b dev origin/dev')
@ -344,14 +347,17 @@ def setup_pymongo_repo():
def setup_forked_mongoengine():
with cd(os.path.join(env.VENDOR_PATH, 'mongoengine')):
with settings(warn_only=True):
run('git remote add github http://github.com/samuelclay/mongoengine')
run('git checkout dev')
run('git pull github dev')
run('git checkout master')
run('git branch -D dev')
run('git remote add sclay git://github.com/samuelclay/mongoengine.git')
run('git fetch sclay')
run('git checkout -b dev sclay/dev')
run('git pull sclay dev')
def switch_forked_mongoengine():
with cd(os.path.join(env.VENDOR_PATH, 'mongoengine')):
run('git co dev')
run('git pull github dev --force')
run('git pull sclay dev --force')
# run('git checkout .')
# run('git checkout master')
# run('get branch -D dev')
@ -361,7 +367,7 @@ def setup_logrotate():
put('config/logrotate.conf', '/etc/logrotate.d/newsblur', use_sudo=True)
def setup_sudoers():
sudo('su - root -c "echo \\\\"sclay ALL=(ALL) NOPASSWD: ALL\\\\" >> /etc/sudoers"')
sudo('su - root -c "echo \\\\"%s ALL=(ALL) NOPASSWD: ALL\\\\" >> /etc/sudoers"' % env.user)
def setup_nginx():
with cd(env.VENDOR_PATH):
@ -436,16 +442,11 @@ def setup_db_firewall():
sudo('ufw default deny')
sudo('ufw allow ssh')
sudo('ufw allow 80')
sudo('ufw allow from 199.15.253.0/24 to any port 5432 ') # PostgreSQL
sudo('ufw allow from 199.15.250.0/24 to any port 5432 ') # PostgreSQL
sudo('ufw allow from 199.15.253.0/24 to any port 27017') # MongoDB
sudo('ufw allow from 199.15.250.0/24 to any port 27017') # MongoDB
sudo('ufw allow from 199.15.253.0/24 to any port 5672 ') # RabbitMQ
sudo('ufw allow from 199.15.250.0/24 to any port 5672 ') # RabbitMQ
sudo('ufw allow from 199.15.250.0/24 to any port 6379 ') # Redis
sudo('ufw allow from 199.15.253.0/24 to any port 6379 ') # Redis
sudo('ufw allow from 199.15.250.0/24 to any port 11211 ') # Memcached
sudo('ufw allow from 199.15.253.0/24 to any port 11211 ') # Memcached
sudo('ufw allow from 199.15.250.0/22 to any port 5432 ') # PostgreSQL
sudo('ufw allow from 199.15.250.0/22 to any port 27017') # MongoDB
sudo('ufw allow from 199.15.250.0/22 to any port 5672 ') # RabbitMQ
sudo('ufw allow from 199.15.250.0/22 to any port 6379 ') # Redis
sudo('ufw allow from 199.15.250.0/22 to any port 11211 ') # Memcached
sudo('ufw --force enable')
def setup_db_motd():

View file

@ -1,4 +1,5 @@
import logging
import pymongo
# ===================
# = Server Settings =
@ -68,6 +69,14 @@ REDIS = {
'host': '127.0.0.1',
}
# AMQP - RabbitMQ server
BROKER_HOST = "db01.newsblur.com"
BROKER_PORT = 5672
BROKER_USER = "newsblur"
BROKER_PASSWORD = "newsblur"
BROKER_VHOST = "newsblurvhost"
# ===========
# = Logging =
# ===========

View file

@ -5652,7 +5652,7 @@ background: transparent;
position: fixed;
height: 55px;
font-size: 36px;
line-height: 78px;
line-height: 60px;
margin: 0 2px 12px 0;
text-shadow: 1px 1px 0 #F6F6F6;
color: #1A008D;
@ -5953,4 +5953,199 @@ background: transparent;
.NB-static-api .NB-api-endpoint-param-desc {
width: 100%;
white-space: normal;
}
}
/* =============== */
/* = iPhone Page = */
/* =============== */
.NB-static-iphone .NB-iphone-main {
margin: 24px 36px 24px -36px;
text-align: center;
}
.NB-static-iphone .NB-iphone-title {
font-size: 20px;
margin: 0 0 24px;
}
.NB-static-iphone .NB-iphone-subtitle {
font-size: 16px;
margin: 8px 0 0;
color: #6D7D88;
}
.NB-static-iphone .NB-iphone-stripe-wrapper {
border-top: 1px solid #505050;
border-bottom: 1px solid #505050;
margin-right: -36px;
}
.NB-static-iphone .NB-iphone-stripe {
border-top: 1px solid white;
border-bottom: 1px solid white;
padding: 12px 0;
background: #F0F0F0 url('../img/reader/stripe_background.png');
}
.NB-static-iphone .NB-iphone-download button {
padding: 4px 20px 4px 10px;
letter-spacing: -0.03em;
text-align: center;
background-color: #3A8FCE;
background-image: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0.06, #1E3C54),
color-stop(0.94, #648295)
);
background-image: -moz-linear-gradient(
center bottom,
#1E3C54 6%,
#648295 94%
);
color: white;
text-shadow: 0 -1px 0px #101C3B;
border: 1px solid #2F6EA7;
border-color: #508FCD #4483BF #2F6EA7 #3F7EB9;
border-radius: 6px;
font-size: 1.14em;
line-height: 1.73em;
margin: 0 auto;
cursor: pointer;
}
.NB-static-iphone .NB-iphone-download .NB-big {
font-size: 1.45em;
font-weight: bold;
}
.NB-static-iphone .NB-iphone-download img {
float: left;
margin: 0 8px 0 0;
}
.NB-static-iphone .NB-iphone-download button:hover {
background-color: #3A8FCE;
background-image: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0.06, #182A42),
color-stop(0.94, #516A83)
);
background-image: -moz-linear-gradient(
center bottom,
#182A42 6%,
#516A83 94%
);
color: #C0D7E7;
}
.NB-static-iphone .NB-iphone-download button:active {
background-color: #3A8FCE;
background-image: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0.06, #516A83),
color-stop(0.94, #182A42)
);
background-image: -moz-linear-gradient(
center bottom,
#516A83 6%
#182A42 94%,
);
}
.NB-static-iphone .NB-iphone-features {
list-style-image: none;
list-style-position: outside;
list-style-type: none;
margin: 0 0 0 0;
padding: 0;
font-size: 16px;
text-align: center;
clear: left;
overflow: hidden;
display: inline-block;
}
.NB-static-iphone .NB-iphone-features li {
list-style-image: none;
list-style-position: outside;
list-style-type: none;
}
.NB-static-iphone .NB-iphone-features .NB-iphone-feature {
padding: 12px 12px 12px 12px;
float: left;
width: 160px;
}
.NB-static-iphone .NB-iphone-features .NB-iphone-feature img {
width: 128px;
height: 184px;
border: 1px solid #202020;
border-color: #909090 #808080 #505050 #606060;
}
.NB-static-iphone .NB-iphone-features .NB-iphone-feature .NB-iphone-feature-title {
font-weight: bold;
margin: 8px 0 0;
}
.NB-static-iphone .NB-iphone-features .NB-iphone-feature .NB-iphone-feature-subtitle {
margin: 8px 0 0;
}
.NB-static-iphone .NB-iphone-main .NB-iphone-feature {
-webkit-transition: all .32s ease-out;
-moz-transition: all .32s ease-out;
-o-transition: all .32s ease-out;
-ms-transition: all .32s ease-out;
border-radius: 4px;
border: 2px solid transparent;
}
.NB-static-iphone .NB-iphone-feature.NB-active {
border: 2px solid #39518B;
}
.NB-static-iphone .NB-iphone-mockup {
float: right;
margin: 142px 96px 0 0;
width: 320px;
height: 460px;
position: relative;
}
.NB-static-iphone .NB-iphone-mockup .NB-iphone-skeleton {
position: absolute;
top: -140px;
left: -32px;
width: 381px;
height: 729px;
background: transparent url('../img/iphone/skeleton.png') no-repeat 0 0;
z-index: 0;
}
.NB-static-iphone .NB-iphone-mockup .NB-iphone-features {
padding: 0;
margin: 0;
overflow: hidden;
width: 320px;
height: 460px;
display: block;
position: relative;
}
.NB-static-iphone .NB-iphone-mockup .NB-iphone-feature {
position: absolute;
top: 0;
left: 0;
padding: 0;
border: none;
}
.NB-static-iphone .NB-iphone-mockup .NB-iphone-feature img {
width: 320px;
height: 460px;
border-left: none;
border-top: none;
border-bottom: none;
border-right: 1px solid #505050;
}
@media screen and (max-width: 1050px) {
.NB-static-iphone .NB-iphone-mockup {
margin-right: 36px;
}
.NB-static-iphone .NB-iphone-features .NB-iphone-feature {
padding: 4px;
}
.NB-static-iphone .NB-iphone-main {
margin-bottom: 54px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View file

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View file

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View file

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View file

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,5 +1,5 @@
//
// AddViewController.h
// AddSiteViewController.h
// NewsBlur
//
// Created by Samuel Clay on 10/04/2011.
@ -12,7 +12,7 @@
@class NewsBlurAppDelegate;
@interface AddViewController : UIViewController
@interface AddSiteViewController : UIViewController
<UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource, UITableViewDelegate, UITableViewDataSource, ASIHTTPRequestDelegate> {
NewsBlurAppDelegate *appDelegate;
@ -34,12 +34,6 @@
UILabel *addingLabel;
UILabel *errorLabel;
UISegmentedControl *addTypeControl;
UILabel *usernameLabel;
UILabel *usernameOrEmailLabel;
UILabel *passwordLabel;
UILabel *emailLabel;
UILabel *passwordOptionalLabel;
}
- (void)reload;
@ -51,6 +45,7 @@
- (IBAction)doAddButton;
- (NSString *)extractParentFolder;
- (void)animateLoop;
- (void)showFolderPicker;
- (void)hideFolderPicker;
- (IBAction)checkSiteAddress;
@ -74,10 +69,4 @@
@property (nonatomic, retain) IBOutlet UILabel *errorLabel;
@property (nonatomic, retain) IBOutlet UISegmentedControl *addTypeControl;
@property (nonatomic, retain) IBOutlet UILabel *usernameLabel;
@property (nonatomic, retain) IBOutlet UILabel *usernameOrEmailLabel;
@property (nonatomic, retain) IBOutlet UILabel *passwordLabel;
@property (nonatomic, retain) IBOutlet UILabel *emailLabel;
@property (nonatomic, retain) IBOutlet UILabel *passwordOptionalLabel;
@end

View file

@ -1,19 +1,19 @@
//
// AddViewController.m
// AddSiteViewController.m
// NewsBlur
//
// Created by Samuel Clay on 10/31/10.
// Copyright 2010 NewsBlur. All rights reserved.
//
#import "AddViewController.h"
#import "AddSiteViewController.h"
#import "AddSiteAutocompleteCell.h"
#import "NewsBlurAppDelegate.h"
#import "ASIHTTPRequest.h"
#import "ASIFormDataRequest.h"
#import "JSON.h"
@implementation AddViewController
@implementation AddSiteViewController
@synthesize appDelegate;
@synthesize inFolderInput;
@ -32,11 +32,6 @@
@synthesize addingLabel;
@synthesize errorLabel;
@synthesize addTypeControl;
@synthesize usernameLabel;
@synthesize usernameOrEmailLabel;
@synthesize passwordLabel;
@synthesize emailLabel;
@synthesize passwordOptionalLabel;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
@ -82,6 +77,7 @@
- (void)viewDidAppear:(BOOL)animated {
[self.activityIndicator stopAnimating];
[super viewDidAppear:animated];
[self showFolderPicker];
}
@ -109,7 +105,7 @@
}
- (IBAction)doCancelButton {
[appDelegate.addViewController dismissModalViewControllerAnimated:YES];
[appDelegate.addSiteViewController dismissModalViewControllerAnimated:YES];
}
- (IBAction)doAddButton {
@ -137,15 +133,7 @@
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
[errorLabel setText:@""];
if (textField == inFolderInput && ![inFolderInput isFirstResponder]) {
[siteAddressInput resignFirstResponder];
[addFolderInput resignFirstResponder];
[inFolderInput setInputView:folderPicker];
if (folderPicker.frame.origin.y >= self.view.bounds.size.height) {
folderPicker.hidden = NO;
[UIView animateWithDuration:.35 animations:^{
folderPicker.frame = CGRectMake(0, self.view.bounds.size.height - folderPicker.frame.size.height, folderPicker.frame.size.width, folderPicker.frame.size.height);
}];
}
[self showFolderPicker];
return NO;
} else if (textField == siteAddressInput) {
[self hideFolderPicker];
@ -256,8 +244,8 @@
[self.errorLabel setText:[results valueForKey:@"message"]];
[self.errorLabel setHidden:NO];
} else {
[appDelegate.addViewController dismissModalViewControllerAnimated:YES];
[appDelegate reloadFeedsView];
[appDelegate.addSiteViewController dismissModalViewControllerAnimated:YES];
[appDelegate reloadFeedsView:YES];
}
[results release];
@ -308,8 +296,8 @@
[self.errorLabel setText:[results valueForKey:@"message"]];
[self.errorLabel setHidden:NO];
} else {
[appDelegate.addViewController dismissModalViewControllerAnimated:YES];
[appDelegate reloadFeedsView];
[appDelegate.addSiteViewController dismissModalViewControllerAnimated:YES];
[appDelegate reloadFeedsView:YES];
}
[results release];
@ -402,6 +390,18 @@ numberOfRowsInComponent:(NSInteger)component {
[inFolderInput setText:folder_title];
}
- (void)showFolderPicker {
[siteAddressInput resignFirstResponder];
[addFolderInput resignFirstResponder];
[inFolderInput setInputView:folderPicker];
if (folderPicker.frame.origin.y >= self.view.bounds.size.height) {
folderPicker.hidden = NO;
[UIView animateWithDuration:.35 animations:^{
folderPicker.frame = CGRectMake(0, self.view.bounds.size.height - folderPicker.frame.size.height, folderPicker.frame.size.width, folderPicker.frame.size.height);
}];
}
}
- (void)hideFolderPicker {
[UIView animateWithDuration:.35 animations:^{
folderPicker.frame = CGRectMake(0, self.view.bounds.size.height, folderPicker.frame.size.width, folderPicker.frame.size.height);

View file

@ -226,7 +226,7 @@
<object class="IBUITextField" id="622270256">
<reference key="NSNextResponder" ref="973185930"/>
<int key="NSvFlags">292</int>
<string key="NSFrame">{{20, 86}, {280, 31}}</string>
<string key="NSFrame">{{20, 82}, {280, 31}}</string>
<reference key="NSSuperview" ref="973185930"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="782639577"/>
@ -259,7 +259,7 @@
<object class="IBUITextField" id="782639577">
<reference key="NSNextResponder" ref="973185930"/>
<int key="NSvFlags">292</int>
<string key="NSFrame">{{20, 130}, {280, 31}}</string>
<string key="NSFrame">{{20, 122}, {280, 31}}</string>
<reference key="NSSuperview" ref="973185930"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="919711053"/>
@ -292,7 +292,7 @@
<object class="IBUITextField" id="919711053">
<reference key="NSNextResponder" ref="973185930"/>
<int key="NSvFlags">292</int>
<string key="NSFrame">{{20, 130}, {280, 31}}</string>
<string key="NSFrame">{{20, 122}, {280, 31}}</string>
<reference key="NSSuperview" ref="973185930"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="450177912"/>
@ -326,7 +326,7 @@
<object class="IBUIActivityIndicatorView" id="450177912">
<reference key="NSNextResponder" ref="973185930"/>
<int key="NSvFlags">-2147483356</int>
<string key="NSFrame">{{24, 136}, {20, 20}}</string>
<string key="NSFrame">{{24, 128}, {20, 20}}</string>
<reference key="NSSuperview" ref="973185930"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="39450128"/>
@ -4784,86 +4784,6 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<object class="IBObjectContainer" key="IBDocument.Objects">
<object class="NSMutableArray" key="connectionRecords">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">activityIndicator</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="900763914"/>
</object>
<int key="connectionID">22</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">view</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="973185930"/>
</object>
<int key="connectionID">32</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">folderPicker</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="292705575"/>
</object>
<int key="connectionID">38</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">inFolderInput</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="622270256"/>
</object>
<int key="connectionID">39</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">addButton</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="810673033"/>
</object>
<int key="connectionID">41</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">cancelButton</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="480078390"/>
</object>
<int key="connectionID">42</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">addTypeControl</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="151912820"/>
</object>
<int key="connectionID">43</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">navBar</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="836275523"/>
</object>
<int key="connectionID">46</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">addingLabel</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="30147744"/>
</object>
<int key="connectionID">50</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">errorLabel</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="845365385"/>
</object>
<int key="connectionID">51</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">siteTable</string>
@ -4872,6 +4792,14 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
<int key="connectionID">54</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">siteScrollView</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="39450128"/>
</object>
<int key="connectionID">64</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">siteAddressInput</string>
@ -4890,11 +4818,67 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">siteScrollView</string>
<string key="label">navBar</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="39450128"/>
<reference key="destination" ref="836275523"/>
</object>
<int key="connectionID">64</int>
<int key="connectionID">46</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">inFolderInput</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="622270256"/>
</object>
<int key="connectionID">39</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">folderPicker</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="292705575"/>
</object>
<int key="connectionID">38</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">errorLabel</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="845365385"/>
</object>
<int key="connectionID">51</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">cancelButton</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="480078390"/>
</object>
<int key="connectionID">42</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">addTypeControl</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="151912820"/>
</object>
<int key="connectionID">43</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">addingLabel</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="30147744"/>
</object>
<int key="connectionID">50</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">addButton</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="810673033"/>
</object>
<int key="connectionID">41</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
@ -4904,6 +4888,22 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
<int key="connectionID">66</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">activityIndicator</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="900763914"/>
</object>
<int key="connectionID">22</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">view</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="973185930"/>
</object>
<int key="connectionID">32</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchEventConnection" key="connection">
<string key="label">selectAddTypeSignup</string>
@ -5170,7 +5170,7 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>AddViewController</string>
<string>AddSiteViewController</string>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
<string>UIResponder</string>
<string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
@ -5212,7 +5212,7 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBPartialClassDescription">
<string key="className">AddViewController</string>
<string key="className">AddSiteViewController</string>
<string key="superclassName">UIViewController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -5285,19 +5285,14 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string>addingLabel</string>
<string>appDelegate</string>
<string>cancelButton</string>
<string>emailLabel</string>
<string>errorLabel</string>
<string>folderPicker</string>
<string>inFolderInput</string>
<string>navBar</string>
<string>passwordLabel</string>
<string>passwordOptionalLabel</string>
<string>siteActivityIndicator</string>
<string>siteAddressInput</string>
<string>siteScrollView</string>
<string>siteTable</string>
<string>usernameLabel</string>
<string>usernameOrEmailLabel</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -5309,18 +5304,13 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string>NewsBlurAppDelegate</string>
<string>UIBarButtonItem</string>
<string>UILabel</string>
<string>UILabel</string>
<string>UIPickerView</string>
<string>UITextField</string>
<string>UINavigationBar</string>
<string>UILabel</string>
<string>UILabel</string>
<string>UIActivityIndicatorView</string>
<string>UITextField</string>
<string>UIScrollView</string>
<string>UITableView</string>
<string>UILabel</string>
<string>UILabel</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
@ -5334,19 +5324,14 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string>addingLabel</string>
<string>appDelegate</string>
<string>cancelButton</string>
<string>emailLabel</string>
<string>errorLabel</string>
<string>folderPicker</string>
<string>inFolderInput</string>
<string>navBar</string>
<string>passwordLabel</string>
<string>passwordOptionalLabel</string>
<string>siteActivityIndicator</string>
<string>siteAddressInput</string>
<string>siteScrollView</string>
<string>siteTable</string>
<string>usernameLabel</string>
<string>usernameOrEmailLabel</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -5378,10 +5363,6 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string key="name">cancelButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">emailLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">errorLabel</string>
<string key="candidateClassName">UILabel</string>
@ -5398,14 +5379,6 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string key="name">navBar</string>
<string key="candidateClassName">UINavigationBar</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">passwordLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">passwordOptionalLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">siteActivityIndicator</string>
<string key="candidateClassName">UIActivityIndicatorView</string>
@ -5422,48 +5395,55 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string key="name">siteTable</string>
<string key="candidateClassName">UITableView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">usernameLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">usernameOrEmailLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/AddViewController.h</string>
<string key="minorKey">./Classes/AddSiteViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">BaseViewController</string>
<string key="superclassName">UIViewController</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/BaseViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">FeedDetailViewController</string>
<string key="superclassName">UIViewController</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>markAllRead</string>
<string>doOpenMarkReadActionSheet:</string>
<string>doOpenSettingsActionSheet</string>
<string>selectIntelligence</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<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>markAllRead</string>
<string>doOpenMarkReadActionSheet:</string>
<string>doOpenSettingsActionSheet</string>
<string>selectIntelligence</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">markAllRead</string>
<string key="name">doOpenMarkReadActionSheet:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">doOpenSettingsActionSheet</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
@ -5666,16 +5646,159 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NewsBlurAppDelegate</string>
<string key="superclassName">NSObject</string>
<string key="className">MoveSiteViewController</string>
<string key="superclassName">UIViewController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>doCancelButton</string>
<string>doMoveButton</string>
<string>moveFolder</string>
<string>moveSite</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<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>doCancelButton</string>
<string>doMoveButton</string>
<string>moveFolder</string>
<string>moveSite</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">doCancelButton</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">doMoveButton</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">moveFolder</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">moveSite</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>addViewController</string>
<string>activityIndicator</string>
<string>appDelegate</string>
<string>cancelButton</string>
<string>errorLabel</string>
<string>folderPicker</string>
<string>moveButton</string>
<string>movingLabel</string>
<string>navBar</string>
<string>titleLabel</string>
<string>toFolderInput</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>UIActivityIndicatorView</string>
<string>NewsBlurAppDelegate</string>
<string>UIBarButtonItem</string>
<string>UILabel</string>
<string>UIPickerView</string>
<string>UIBarButtonItem</string>
<string>UILabel</string>
<string>UINavigationBar</string>
<string>UIView</string>
<string>UITextField</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>activityIndicator</string>
<string>appDelegate</string>
<string>cancelButton</string>
<string>errorLabel</string>
<string>folderPicker</string>
<string>moveButton</string>
<string>movingLabel</string>
<string>navBar</string>
<string>titleLabel</string>
<string>toFolderInput</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">activityIndicator</string>
<string key="candidateClassName">UIActivityIndicatorView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">appDelegate</string>
<string key="candidateClassName">NewsBlurAppDelegate</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">cancelButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">errorLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">folderPicker</string>
<string key="candidateClassName">UIPickerView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">moveButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">movingLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">navBar</string>
<string key="candidateClassName">UINavigationBar</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">titleLabel</string>
<string key="candidateClassName">UIView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">toFolderInput</string>
<string key="candidateClassName">UITextField</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/MoveSiteViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NewsBlurAppDelegate</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>addSiteViewController</string>
<string>feedDetailViewController</string>
<string>feedsViewController</string>
<string>loginViewController</string>
<string>moveSiteViewController</string>
<string>navigationController</string>
<string>originalStoryViewController</string>
<string>storyDetailViewController</string>
@ -5683,10 +5806,11 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>AddViewController</string>
<string>AddSiteViewController</string>
<string>FeedDetailViewController</string>
<string>NewsBlurViewController</string>
<string>LoginViewController</string>
<string>MoveSiteViewController</string>
<string>UINavigationController</string>
<string>OriginalStoryViewController</string>
<string>StoryDetailViewController</string>
@ -5697,10 +5821,11 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>addViewController</string>
<string>addSiteViewController</string>
<string>feedDetailViewController</string>
<string>feedsViewController</string>
<string>loginViewController</string>
<string>moveSiteViewController</string>
<string>navigationController</string>
<string>originalStoryViewController</string>
<string>storyDetailViewController</string>
@ -5709,8 +5834,8 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">addViewController</string>
<string key="candidateClassName">AddViewController</string>
<string key="name">addSiteViewController</string>
<string key="candidateClassName">AddSiteViewController</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">feedDetailViewController</string>
@ -5724,6 +5849,10 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string key="name">loginViewController</string>
<string key="candidateClassName">LoginViewController</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">moveSiteViewController</string>
<string key="candidateClassName">MoveSiteViewController</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">navigationController</string>
<string key="candidateClassName">UINavigationController</string>
@ -5749,7 +5878,7 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
<object class="IBPartialClassDescription">
<string key="className">NewsBlurViewController</string>
<string key="superclassName">UIViewController</string>
<string key="superclassName">BaseViewController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
@ -5757,6 +5886,8 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string>doAddButton</string>
<string>doLogoutButton</string>
<string>doSwitchSitesUnread</string>
<string>sectionTapped:</string>
<string>sectionUntapped:</string>
<string>selectIntelligence</string>
</object>
<object class="NSMutableArray" key="dict.values">
@ -5764,6 +5895,8 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string>id</string>
<string>id</string>
<string>id</string>
<string>UIButton</string>
<string>UIButton</string>
<string>id</string>
</object>
</object>
@ -5774,6 +5907,8 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string>doAddButton</string>
<string>doLogoutButton</string>
<string>doSwitchSitesUnread</string>
<string>sectionTapped:</string>
<string>sectionUntapped:</string>
<string>selectIntelligence</string>
</object>
<object class="NSMutableArray" key="dict.values">
@ -5790,6 +5925,14 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string key="name">doSwitchSitesUnread</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">sectionTapped:</string>
<string key="candidateClassName">UIButton</string>
</object>
<object class="IBActionInfo">
<string key="name">sectionUntapped:</string>
<string key="candidateClassName">UIButton</string>
</object>
<object class="IBActionInfo">
<string key="name">selectIntelligence</string>
<string key="candidateClassName">id</string>
@ -5877,7 +6020,7 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
</object>
<object class="IBPartialClassDescription">
<string key="className">OriginalStoryViewController</string>
<string key="superclassName">UIViewController</string>
<string key="superclassName">BaseViewController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
@ -6051,8 +6194,10 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<bool key="EncodedWithXMLCoder">YES</bool>
<string>activity</string>
<string>appDelegate</string>
<string>buttonAction</string>
<string>buttonNext</string>
<string>buttonPrevious</string>
<string>feedTitleGradient</string>
<string>progressView</string>
<string>toolbar</string>
<string>webView</string>
@ -6063,6 +6208,8 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string>NewsBlurAppDelegate</string>
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIView</string>
<string>UIProgressView</string>
<string>UIToolbar</string>
<string>UIWebView</string>
@ -6074,8 +6221,10 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<bool key="EncodedWithXMLCoder">YES</bool>
<string>activity</string>
<string>appDelegate</string>
<string>buttonAction</string>
<string>buttonNext</string>
<string>buttonPrevious</string>
<string>feedTitleGradient</string>
<string>progressView</string>
<string>toolbar</string>
<string>webView</string>
@ -6090,6 +6239,10 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<string key="name">appDelegate</string>
<string key="candidateClassName">NewsBlurAppDelegate</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">buttonAction</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">buttonNext</string>
<string key="candidateClassName">UIBarButtonItem</string>
@ -6098,6 +6251,10 @@ AAgAAAAIAAIACAACAAAAAgAAAAEAAQABAAE</bytes>
<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">progressView</string>
<string key="candidateClassName">UIProgressView</string>

View file

@ -15,6 +15,6 @@
- (void) clearFinishedRequests;
- (void) cancelRequests;
- (void)informError:(NSError *)error;
- (void)informError:(id)error;
@end

View file

@ -53,15 +53,27 @@
#pragma mark -
#pragma mark View methods
- (void)informError:(NSError *)error {
NSString* localizedDescription = [error localizedDescription];
- (void)informError:(id)error {
NSLog(@"Error: %@", error);
NSString *errorMessage;
if ([error isKindOfClass:[NSString class]]) {
errorMessage = error;
} else {
errorMessage = [error localizedDescription];
if ([error code] == 4 &&
[errorMessage rangeOfString:@"cancelled"].location != NSNotFound) {
return;
}
}
[MBProgressHUD hideHUDForView:self.view animated:YES];
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
[HUD setCustomView:[[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"warning.gif"]] autorelease]];
[HUD setCustomView:[[[UIImageView alloc]
initWithImage:[UIImage imageNamed:@"warning.gif"]] autorelease]];
[HUD setMode:MBProgressHUDModeCustomView];
HUD.labelText = localizedDescription;
HUD.labelText = errorMessage;
[HUD hide:YES afterDelay:1];
// UIAlertView* alertView = [[UIAlertView alloc]
// initWithTitle:@"Error"
// message:localizedDescription delegate:nil

View file

@ -37,7 +37,6 @@
- (void)fetchFeedDetail:(int)page withCallback:(void(^)())callback;
- (void)fetchRiverPage:(int)page withCallback:(void(^)())callback;
- (void)finishedLoadingFeed:(ASIHTTPRequest *)request;
- (void)failLoadingFeed:(ASIHTTPRequest *)request;
- (void)renderStories:(NSArray *)newStories;
- (void)scrollViewDidScroll:(UIScrollView *)scroll;
@ -53,12 +52,15 @@
- (IBAction)doOpenSettingsActionSheet;
- (void)confirmDeleteSite;
- (void)deleteSite;
- (void)deleteFolder;
- (void)openMoveView;
@property (nonatomic, retain) IBOutlet NewsBlurAppDelegate *appDelegate;
@property (nonatomic, retain) IBOutlet UITableView *storyTitlesTable;
@property (nonatomic, retain) IBOutlet UIToolbar *feedViewToolbar;
@property (nonatomic, retain) IBOutlet UISlider * feedScoreSlider;
@property (nonatomic, retain) IBOutlet UIBarButtonItem * feedMarkReadButton;
@property (nonatomic, retain) IBOutlet UIBarButtonItem * settingsButton;
@property (nonatomic, retain) IBOutlet UISegmentedControl * intelligenceControl;
@property (nonatomic, retain) PullToRefreshView *pull;

View file

@ -27,6 +27,7 @@
@implementation FeedDetailViewController
@synthesize storyTitlesTable, feedViewToolbar, feedScoreSlider, feedMarkReadButton;
@synthesize settingsButton;
@synthesize stories;
@synthesize appDelegate;
@synthesize feedPage;
@ -55,44 +56,16 @@
self.pageFinished = NO;
[MBProgressHUD hideHUDForView:self.view animated:YES];
UIView *titleView = [[UIView alloc] init];
UILabel *titleLabel = [[[UILabel alloc] init] autorelease];
if (appDelegate.isRiverView) {
self.storyTitlesTable.separatorStyle = UITableViewCellSeparatorStyleNone;
self.storyTitlesTable.separatorColor = [UIColor clearColor];
titleLabel.text = [NSString stringWithFormat:@" %@", appDelegate.activeFolder];
} else {
self.storyTitlesTable.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
self.storyTitlesTable.separatorColor = [UIColor colorWithRed:.9 green:.9 blue:.9 alpha:1.0];
titleLabel.text = [NSString stringWithFormat:@" %@", [appDelegate.activeFeed objectForKey:@"feed_title"]];
}
titleLabel.backgroundColor = [UIColor clearColor];
titleLabel.textAlignment = UITextAlignmentLeft;
titleLabel.font = [UIFont fontWithName:@"Helvetica-Bold" size:15.0];
titleLabel.textColor = [UIColor whiteColor];
titleLabel.lineBreakMode = UILineBreakModeTailTruncation;
titleLabel.shadowColor = [UIColor blackColor];
titleLabel.shadowOffset = CGSizeMake(0, -1);
titleLabel.center = CGPointMake(28, -2);
[titleLabel sizeToFit];
NSString *feedIdStr = [NSString stringWithFormat:@"%@", [appDelegate.activeFeed objectForKey:@"id"]];
UIImage *titleImage;
if (appDelegate.isRiverView) {
titleImage = [UIImage imageNamed:@"folder.png"];
} else {
titleImage = [Utilities getImage:feedIdStr];
}
UIImageView *titleImageView = [[UIImageView alloc] initWithImage:titleImage];
titleImageView.frame = CGRectMake(0.0, 2.0, 16.0, 16.0);
[titleLabel addSubview:titleImageView];
[titleImageView release];
UIView *titleLabel = [appDelegate makeFeedTitle:appDelegate.activeFeed];
self.navigationItem.titleView = titleLabel;
[titleView release];
// Commenting out until training is ready...
// UIBarButtonItem *trainBarButton = [UIBarButtonItem alloc];
@ -123,11 +96,31 @@
[self.intelligenceControl setSelectedSegmentIndex:[appDelegate selectedIntelligence]+1];
[super viewWillAppear:animated];
BOOL pullFound = NO;
for (UIView *view in self.storyTitlesTable.subviews) {
if ([view isKindOfClass:[PullToRefreshView class]]) {
pullFound = YES;
if (appDelegate.isRiverView) {
[view removeFromSuperview];
}
}
}
if (!appDelegate.isRiverView && !pullFound) {
[self.storyTitlesTable addSubview:pull];
}
if (appDelegate.isRiverView && [appDelegate.activeFolder isEqualToString:@"Everything"]) {
settingsButton.enabled = NO;
} else {
settingsButton.enabled = YES;
}
}
- (void)viewDidAppear:(BOOL)animated {
// [[storyTitlesTable cellForRowAtIndexPath:[storyTitlesTable indexPathForSelectedRow]] setSelected:NO]; // TODO: DESELECT CELL --- done, see line below:
[self.storyTitlesTable deselectRowAtIndexPath:[storyTitlesTable indexPathForSelectedRow] animated:YES];
[pull refreshLastUpdatedDate];
[super viewDidAppear:animated];
}
@ -137,6 +130,7 @@
[feedViewToolbar release];
[feedScoreSlider release];
[feedMarkReadButton release];
[settingsButton release];
[stories release];
[appDelegate release];
[intelligenceControl release];
@ -172,14 +166,14 @@
NEWSBLUR_URL,
[appDelegate.activeFeed objectForKey:@"id"],
self.feedPage];
NSURL *urlFeedDetail = [NSURL URLWithString:theFeedDetailURL];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:urlFeedDetail];
[self cancelRequests];
__block ASIHTTPRequest *request = [self requestWithURL:theFeedDetailURL];
[request setDelegate:self];
[request setResponseEncoding:NSUTF8StringEncoding];
[request setDefaultResponseEncoding:NSUTF8StringEncoding];
[request setFailedBlock:^(void) {
[self failLoadingFeed:request];
[self informError:[request error]];
}];
[request setCompletionBlock:^(void) {
[self finishedLoadingFeed:request];
@ -193,22 +187,23 @@
}
}
- (void)failLoadingFeed:(ASIHTTPRequest *)request {
// if (self.feedPage <= 1) {
// [appDelegate.navigationController
// popToViewController:[appDelegate.navigationController.viewControllers
// objectAtIndex:0]
// animated:YES];
// }
[self informError:[request error]];
}
- (void)finishedLoadingFeed:(ASIHTTPRequest *)request {
if ([request responseStatusCode] >= 500) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.15 * NSEC_PER_SEC),
dispatch_get_current_queue(), ^{
[appDelegate.navigationController
popToViewController:[appDelegate.navigationController.viewControllers
objectAtIndex:0]
animated:YES];
});
[self informError:@"The server barfed!"];
return;
}
NSString *responseString = [request responseString];
NSDictionary *results = [[NSDictionary alloc]
initWithDictionary:[responseString JSONValue]];
if (!appDelegate.isRiverView && request.tag != [[results objectForKey:@"feed_id"] intValue]) {
[results release];
return;
@ -261,14 +256,14 @@
[appDelegate.activeFolderFeeds componentsJoinedByString:@"&feeds="],
self.feedPage,
readStoriesCount];
NSURL *urlFeedDetail = [NSURL URLWithString:theFeedDetailURL];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:urlFeedDetail];
[self cancelRequests];
__block ASIHTTPRequest *request = [self requestWithURL:theFeedDetailURL];
[request setDelegate:self];
[request setResponseEncoding:NSUTF8StringEncoding];
[request setDefaultResponseEncoding:NSUTF8StringEncoding];
[request setFailedBlock:^(void) {
[self failLoadingFeed:request];
[self informError:[request error]];
}];
[request setCompletionBlock:^(void) {
[self finishedLoadingFeed:request];
@ -597,13 +592,16 @@
- (void)markFeedsReadWithAllStories:(BOOL)includeHidden {
NSLog(@"mark feeds read: %d %d", appDelegate.isRiverView, includeHidden);
if (appDelegate.isRiverView && includeHidden) {
// Mark folder as read
NSString *urlString = [NSString stringWithFormat:@"http://%@/reader/mark_folder_as_read",
NSString *urlString = [NSString stringWithFormat:@"http://%@/reader/mark_feed_as_read",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:appDelegate.activeFolder forKey:@"folder_name"];
for (id feed_id in [appDelegate.dictFolders objectForKey:appDelegate.activeFolder]) {
[request addPostValue:feed_id forKey:@"feed_id"];
}
[request setDelegate:nil];
[request startAsynchronous];
@ -646,6 +644,12 @@
}
- (IBAction)doOpenMarkReadActionSheet:(id)sender {
// Individual sites just get marked as read, no action sheet needed.
if (!appDelegate.isRiverView) {
[self markFeedsReadWithAllStories:YES];
return;
}
NSString *title = appDelegate.isRiverView ?
appDelegate.activeFolder :
[appDelegate.activeFeed objectForKey:@"feed_title"];
@ -659,25 +663,27 @@
int visibleUnreadCount = appDelegate.visibleUnreadCount;
int totalUnreadCount = [appDelegate unreadCount];
NSArray *buttonTitles = nil;
if (visibleUnreadCount >= totalUnreadCount || visibleUnreadCount <= 0) {
NSString *visibleText = [NSString stringWithFormat:@"Mark %@ read",
appDelegate.isRiverView ?
@"entire folder" :
@"this site"];
buttonTitles = [NSArray arrayWithObjects:visibleText, nil];
options.destructiveButtonIndex = 0;
} else {
NSString *visibleText = [NSString stringWithFormat:@"Mark %@ read",
visibleUnreadCount == 1 ?
@"this story as" :
[NSString stringWithFormat:@"these %d stories",
visibleUnreadCount]];
NSString *entireText = [NSString stringWithFormat:@"Mark %@ read",
appDelegate.isRiverView ?
@"entire folder" :
@"this site"];
BOOL showVisible = YES;
BOOL showEntire = YES;
if ([appDelegate.activeFolder isEqualToString:@"Everything"]) showEntire = NO;
if (visibleUnreadCount >= totalUnreadCount || visibleUnreadCount <= 0) showVisible = NO;
NSString *entireText = [NSString stringWithFormat:@"Mark %@ read",
appDelegate.isRiverView ?
@"entire folder" :
@"this site"];
NSString *visibleText = [NSString stringWithFormat:@"Mark %@ read",
visibleUnreadCount == 1 ? @"this story as" :
[NSString stringWithFormat:@"these %d stories",
visibleUnreadCount]];
if (showVisible && showEntire) {
buttonTitles = [NSArray arrayWithObjects:visibleText, entireText, nil];
options.destructiveButtonIndex = 1;
} else if (showVisible && !showEntire) {
buttonTitles = [NSArray arrayWithObjects:visibleText, nil];
options.destructiveButtonIndex = -1;
} else if (!showVisible && showEntire) {
buttonTitles = [NSArray arrayWithObjects:entireText, nil];
options.destructiveButtonIndex = 0;
}
for (id title in buttonTitles) {
@ -690,48 +696,70 @@
[options release];
}
- (IBAction)doOpenSettingsActionSheet {
UIActionSheet *options = [[UIActionSheet alloc]
initWithTitle:[appDelegate.activeFeed objectForKey:@"feed_title"]
delegate:self
cancelButtonTitle:nil
destructiveButtonTitle:nil
otherButtonTitles:nil];
NSArray *buttonTitles = [NSArray arrayWithObjects:@"Delete this site", nil];
for (id title in buttonTitles) {
[options addButtonWithTitle:title];
}
options.cancelButtonIndex = [options addButtonWithTitle:@"Cancel"];
options.tag = kSettingsActionSheet;
[options showInView:self.view];
[options release];
}
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
NSLog(@"Action option #%d", buttonIndex);
// NSLog(@"Action option #%d on %d", buttonIndex, actionSheet.tag);
if (actionSheet.tag == 1) {
int visibleUnreadCount = appDelegate.visibleUnreadCount;
int totalUnreadCount = [appDelegate unreadCount];
if (visibleUnreadCount >= totalUnreadCount || visibleUnreadCount <= 0) {
if (buttonIndex == 0) {
[self markFeedsReadWithAllStories:YES];
}
} else {
BOOL showVisible = YES;
BOOL showEntire = YES;
if ([appDelegate.activeFolder isEqualToString:@"Everything"]) showEntire = NO;
if (visibleUnreadCount >= totalUnreadCount || visibleUnreadCount <= 0) showVisible = NO;
// NSLog(@"Counts: %d %d = %d", visibleUnreadCount, totalUnreadCount, visibleUnreadCount >= totalUnreadCount || visibleUnreadCount <= 0);
if (showVisible && showEntire) {
if (buttonIndex == 0) {
[self markFeedsReadWithAllStories:NO];
} else if (buttonIndex == 1) {
[self markFeedsReadWithAllStories:YES];
}
} else if (showVisible && !showEntire) {
if (buttonIndex == 0) {
[self markFeedsReadWithAllStories:NO];
}
} else if (!showVisible && showEntire) {
if (buttonIndex == 0) {
[self markFeedsReadWithAllStories:YES];
}
}
} else if (actionSheet.tag == 2) {
if (buttonIndex == 0) {
[self confirmDeleteSite];
} else if (buttonIndex == 1) {
[self openMoveView];
}
}
}
- (IBAction)doOpenSettingsActionSheet {
NSString *title = appDelegate.isRiverView ?
appDelegate.activeFolder :
[appDelegate.activeFeed objectForKey:@"feed_title"];
UIActionSheet *options = [[UIActionSheet alloc]
initWithTitle:title
delegate:self
cancelButtonTitle:nil
destructiveButtonTitle:nil
otherButtonTitles:nil];
if (![title isEqualToString:@"Everything"]) {
NSString *deleteText = [NSString stringWithFormat:@"Delete %@",
appDelegate.isRiverView ?
@"this entire folder" :
@"this site"];
[options addButtonWithTitle:deleteText];
options.destructiveButtonIndex = 0;
NSString *moveText = @"Move to another folder";
[options addButtonWithTitle:moveText];
}
options.cancelButtonIndex = [options addButtonWithTitle:@"Cancel"];
options.tag = kSettingsActionSheet;
[options showInView:self.view];
[options release];
}
- (void)confirmDeleteSite {
UIAlertView *deleteConfirm = [[UIAlertView alloc] initWithTitle:@"Positive?" message:nil delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Delete", nil];
[deleteConfirm show];
@ -744,7 +772,11 @@
if (buttonIndex == 0) {
return;
} else {
[self deleteSite];
if (appDelegate.isRiverView) {
[self deleteFolder];
} else {
[self deleteSite];
}
}
}
}
@ -761,12 +793,12 @@
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:urlFeedDetail];
[request setDelegate:self];
[request addPostValue:[[appDelegate activeFeed] objectForKey:@"id"] forKey:@"feed_id"];
[request addPostValue:[appDelegate activeFolder] forKey:@"in_folder"];
[request addPostValue:[appDelegate extractFolderName:appDelegate.activeFolder] forKey:@"in_folder"];
[request setFailedBlock:^(void) {
[self failLoadingFeed:request];
[self informError:[request error]];
}];
[request setCompletionBlock:^(void) {
[appDelegate reloadFeedsView];
[appDelegate reloadFeedsView:YES];
[appDelegate.navigationController
popToViewController:[appDelegate.navigationController.viewControllers
objectAtIndex:0]
@ -778,17 +810,56 @@
[request startAsynchronous];
}
- (void)deleteFolder {
[MBProgressHUD hideHUDForView:self.view animated:YES];
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
HUD.labelText = @"Deleting...";
NSString *theFeedDetailURL = [NSString stringWithFormat:@"http://%@/reader/delete_folder",
NEWSBLUR_URL];
NSURL *urlFeedDetail = [NSURL URLWithString:theFeedDetailURL];
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:urlFeedDetail];
[request setDelegate:self];
[request addPostValue:[appDelegate extractFolderName:appDelegate.activeFolder]
forKey:@"folder_to_delete"];
[request addPostValue:[appDelegate extractFolderName:[appDelegate extractParentFolderName:appDelegate.activeFolder]]
forKey:@"in_folder"];
[request setFailedBlock:^(void) {
[self informError:[request error]];
}];
[request setCompletionBlock:^(void) {
[appDelegate reloadFeedsView:YES];
[appDelegate.navigationController
popToViewController:[appDelegate.navigationController.viewControllers
objectAtIndex:0]
animated:YES];
[MBProgressHUD hideHUDForView:self.view animated:YES];
}];
[request setTimeOutSeconds:30];
[request startAsynchronous];
}
- (void)openMoveView {
[appDelegate showMoveSite];
}
#pragma mark -
#pragma mark PullToRefresh
// called when the user pulls-to-refresh
- (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view {
- (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view {
if (appDelegate.isRiverView) {
[pull finishedLoading];
return;
}
NSString *urlString = [NSString
stringWithFormat:@"http://%@/reader/refresh_feed/%@",
NEWSBLUR_URL,
[appDelegate.activeFeed objectForKey:@"id"]];
NSURL *url = [NSURL URLWithString:urlString];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[self cancelRequests];
__block ASIHTTPRequest *request = [self requestWithURL:urlString];
[request setDelegate:self];
[request setResponseEncoding:NSUTF8StringEncoding];
[request setDefaultResponseEncoding:NSUTF8StringEncoding];
@ -826,7 +897,7 @@
// called when the date shown needs to be updated, optional
- (NSDate *)pullToRefreshViewLastUpdated:(PullToRefreshView *)view {
NSLog(@"Updated; %@", [appDelegate.activeFeed objectForKey:@"updated_seconds_ago"]);
// NSLog(@"Updated; %@", [appDelegate.activeFeed objectForKey:@"updated_seconds_ago"]);
int seconds = -1 * [[appDelegate.activeFeed objectForKey:@"updated_seconds_ago"] intValue];
return [[[NSDate alloc] initWithTimeIntervalSinceNow:seconds] autorelease];
}

View file

@ -79,7 +79,6 @@
<string key="NSFrame">{{79, 8}, {161, 30}}</string>
<reference key="NSSuperview" ref="929039419"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView"/>
<string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
<int key="IBSegmentControlStyle">2</int>
<int key="IBNumberOfSegments">3</int>
@ -224,6 +223,14 @@
</object>
<int key="connectionID">58</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">settingsButton</string>
<reference key="source" ref="372490531"/>
<reference key="destination" ref="274994040"/>
</object>
<int key="connectionID">63</int>
</object>
<object class="IBConnectionRecord">
<object class="IBCocoaTouchOutletConnection" key="connection">
<string key="label">dataSource</string>
@ -401,13 +408,13 @@
<reference key="dict.values" ref="0"/>
</object>
<nil key="sourceID"/>
<int key="maxID">62</int>
<int key="maxID">63</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBPartialClassDescription">
<string key="className">AddViewController</string>
<string key="className">AddSiteViewController</string>
<string key="superclassName">UIViewController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -480,19 +487,14 @@
<string>addingLabel</string>
<string>appDelegate</string>
<string>cancelButton</string>
<string>emailLabel</string>
<string>errorLabel</string>
<string>folderPicker</string>
<string>inFolderInput</string>
<string>navBar</string>
<string>passwordLabel</string>
<string>passwordOptionalLabel</string>
<string>siteActivityIndicator</string>
<string>siteAddressInput</string>
<string>siteScrollView</string>
<string>siteTable</string>
<string>usernameLabel</string>
<string>usernameOrEmailLabel</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -504,18 +506,13 @@
<string>NewsBlurAppDelegate</string>
<string>UIBarButtonItem</string>
<string>UILabel</string>
<string>UILabel</string>
<string>UIPickerView</string>
<string>UITextField</string>
<string>UINavigationBar</string>
<string>UILabel</string>
<string>UILabel</string>
<string>UIActivityIndicatorView</string>
<string>UITextField</string>
<string>UIScrollView</string>
<string>UITableView</string>
<string>UILabel</string>
<string>UILabel</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
@ -529,19 +526,14 @@
<string>addingLabel</string>
<string>appDelegate</string>
<string>cancelButton</string>
<string>emailLabel</string>
<string>errorLabel</string>
<string>folderPicker</string>
<string>inFolderInput</string>
<string>navBar</string>
<string>passwordLabel</string>
<string>passwordOptionalLabel</string>
<string>siteActivityIndicator</string>
<string>siteAddressInput</string>
<string>siteScrollView</string>
<string>siteTable</string>
<string>usernameLabel</string>
<string>usernameOrEmailLabel</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
@ -573,10 +565,6 @@
<string key="name">cancelButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">emailLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">errorLabel</string>
<string key="candidateClassName">UILabel</string>
@ -593,14 +581,6 @@
<string key="name">navBar</string>
<string key="candidateClassName">UINavigationBar</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">passwordLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">passwordOptionalLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">siteActivityIndicator</string>
<string key="candidateClassName">UIActivityIndicatorView</string>
@ -617,19 +597,11 @@
<string key="name">siteTable</string>
<string key="candidateClassName">UITableView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">usernameLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">usernameOrEmailLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/AddViewController.h</string>
<string key="minorKey">./Classes/AddSiteViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
@ -691,6 +663,7 @@
<string>feedScoreSlider</string>
<string>feedViewToolbar</string>
<string>intelligenceControl</string>
<string>settingsButton</string>
<string>storyTitlesTable</string>
</object>
<object class="NSMutableArray" key="dict.values">
@ -700,6 +673,7 @@
<string>UISlider</string>
<string>UIToolbar</string>
<string>UISegmentedControl</string>
<string>UIBarButtonItem</string>
<string>UITableView</string>
</object>
</object>
@ -712,6 +686,7 @@
<string>feedScoreSlider</string>
<string>feedViewToolbar</string>
<string>intelligenceControl</string>
<string>settingsButton</string>
<string>storyTitlesTable</string>
</object>
<object class="NSMutableArray" key="dict.values">
@ -736,6 +711,10 @@
<string key="name">intelligenceControl</string>
<string key="candidateClassName">UISegmentedControl</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">settingsButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">storyTitlesTable</string>
<string key="candidateClassName">UITableView</string>
@ -875,6 +854,155 @@
<string key="minorKey">./Classes/LoginViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">MoveSiteViewController</string>
<string key="superclassName">UIViewController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>doCancelButton</string>
<string>doMoveButton</string>
<string>moveFolder</string>
<string>moveSite</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<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>doCancelButton</string>
<string>doMoveButton</string>
<string>moveFolder</string>
<string>moveSite</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">doCancelButton</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">doMoveButton</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">moveFolder</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">moveSite</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>activityIndicator</string>
<string>appDelegate</string>
<string>cancelButton</string>
<string>errorLabel</string>
<string>folderPicker</string>
<string>fromFolderInput</string>
<string>moveButton</string>
<string>movingLabel</string>
<string>navBar</string>
<string>titleLabel</string>
<string>toFolderInput</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>UIActivityIndicatorView</string>
<string>NewsBlurAppDelegate</string>
<string>UIBarButtonItem</string>
<string>UILabel</string>
<string>UIPickerView</string>
<string>UITextField</string>
<string>UIBarButtonItem</string>
<string>UILabel</string>
<string>UINavigationBar</string>
<string>UILabel</string>
<string>UITextField</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>activityIndicator</string>
<string>appDelegate</string>
<string>cancelButton</string>
<string>errorLabel</string>
<string>folderPicker</string>
<string>fromFolderInput</string>
<string>moveButton</string>
<string>movingLabel</string>
<string>navBar</string>
<string>titleLabel</string>
<string>toFolderInput</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">activityIndicator</string>
<string key="candidateClassName">UIActivityIndicatorView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">appDelegate</string>
<string key="candidateClassName">NewsBlurAppDelegate</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">cancelButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">errorLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">folderPicker</string>
<string key="candidateClassName">UIPickerView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">fromFolderInput</string>
<string key="candidateClassName">UITextField</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">moveButton</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">movingLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">navBar</string>
<string key="candidateClassName">UINavigationBar</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">titleLabel</string>
<string key="candidateClassName">UILabel</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">toFolderInput</string>
<string key="candidateClassName">UITextField</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">./Classes/MoveSiteViewController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NewsBlurAppDelegate</string>
<string key="superclassName">BaseViewController</string>
@ -882,10 +1010,11 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>addViewController</string>
<string>addSiteViewController</string>
<string>feedDetailViewController</string>
<string>feedsViewController</string>
<string>loginViewController</string>
<string>moveSiteViewController</string>
<string>navigationController</string>
<string>originalStoryViewController</string>
<string>storyDetailViewController</string>
@ -893,10 +1022,11 @@
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>AddViewController</string>
<string>AddSiteViewController</string>
<string>FeedDetailViewController</string>
<string>NewsBlurViewController</string>
<string>LoginViewController</string>
<string>MoveSiteViewController</string>
<string>UINavigationController</string>
<string>OriginalStoryViewController</string>
<string>StoryDetailViewController</string>
@ -907,10 +1037,11 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>addViewController</string>
<string>addSiteViewController</string>
<string>feedDetailViewController</string>
<string>feedsViewController</string>
<string>loginViewController</string>
<string>moveSiteViewController</string>
<string>navigationController</string>
<string>originalStoryViewController</string>
<string>storyDetailViewController</string>
@ -919,8 +1050,8 @@
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">addViewController</string>
<string key="candidateClassName">AddViewController</string>
<string key="name">addSiteViewController</string>
<string key="candidateClassName">AddSiteViewController</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">feedDetailViewController</string>
@ -934,6 +1065,10 @@
<string key="name">loginViewController</string>
<string key="candidateClassName">LoginViewController</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">moveSiteViewController</string>
<string key="candidateClassName">MoveSiteViewController</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">navigationController</string>
<string key="candidateClassName">UINavigationController</string>
@ -968,6 +1103,7 @@
<string>doLogoutButton</string>
<string>doSwitchSitesUnread</string>
<string>sectionTapped:</string>
<string>sectionUntapped:</string>
<string>selectIntelligence</string>
</object>
<object class="NSMutableArray" key="dict.values">
@ -975,7 +1111,8 @@
<string>id</string>
<string>id</string>
<string>id</string>
<string>UITapGestureRecognizer</string>
<string>UIButton</string>
<string>UIButton</string>
<string>id</string>
</object>
</object>
@ -987,6 +1124,7 @@
<string>doLogoutButton</string>
<string>doSwitchSitesUnread</string>
<string>sectionTapped:</string>
<string>sectionUntapped:</string>
<string>selectIntelligence</string>
</object>
<object class="NSMutableArray" key="dict.values">
@ -1005,7 +1143,11 @@
</object>
<object class="IBActionInfo">
<string key="name">sectionTapped:</string>
<string key="candidateClassName">UITapGestureRecognizer</string>
<string key="candidateClassName">UIButton</string>
</object>
<object class="IBActionInfo">
<string key="name">sectionUntapped:</string>
<string key="candidateClassName">UIButton</string>
</object>
<object class="IBActionInfo">
<string key="name">selectIntelligence</string>
@ -1268,6 +1410,7 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<string>activity</string>
<string>appDelegate</string>
<string>buttonAction</string>
<string>buttonNext</string>
<string>buttonPrevious</string>
<string>feedTitleGradient</string>
@ -1281,6 +1424,7 @@
<string>NewsBlurAppDelegate</string>
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIBarButtonItem</string>
<string>UIView</string>
<string>UIProgressView</string>
<string>UIToolbar</string>
@ -1293,6 +1437,7 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<string>activity</string>
<string>appDelegate</string>
<string>buttonAction</string>
<string>buttonNext</string>
<string>buttonPrevious</string>
<string>feedTitleGradient</string>
@ -1310,6 +1455,10 @@
<string key="name">appDelegate</string>
<string key="candidateClassName">NewsBlurAppDelegate</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">buttonAction</string>
<string key="candidateClassName">UIBarButtonItem</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">buttonNext</string>
<string key="candidateClassName">UIBarButtonItem</string>

View file

@ -131,7 +131,7 @@
}
[self.errorLabel setHidden:NO];
} else {
[appDelegate reloadFeedsView];
[appDelegate reloadFeedsView:YES];
}
[results release];
@ -178,7 +178,7 @@
}
[self.errorLabel setHidden:NO];
} else {
[appDelegate reloadFeedsView];
[appDelegate reloadFeedsView:YES];
}
[results release];

View file

@ -0,0 +1,43 @@
//
// MoveSiteViewController.h
// NewsBlur
//
// Created by Samuel Clay on 12/2/2011.
// Copyright 2011 NewsBlur. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "NewsBlurAppDelegate.h"
#import "ASIHTTPRequest.h"
@class NewsBlurAppDelegate;
@interface MoveSiteViewController : UIViewController
<UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource, ASIHTTPRequestDelegate> {
NewsBlurAppDelegate *appDelegate;
}
- (void)reload;
- (IBAction)moveSite;
- (IBAction)moveFolder;
- (IBAction)doCancelButton;
- (IBAction)doMoveButton;
- (NSArray *)pickerFolders;
@property (nonatomic, retain) IBOutlet NewsBlurAppDelegate *appDelegate;
@property (nonatomic, retain) IBOutlet UITextField *fromFolderInput;
@property (nonatomic, retain) IBOutlet UITextField *toFolderInput;
@property (nonatomic, retain) IBOutlet UILabel *titleLabel;
@property (nonatomic, retain) IBOutlet UIBarButtonItem *moveButton;
@property (nonatomic, retain) IBOutlet UIBarButtonItem *cancelButton;
@property (nonatomic, retain) IBOutlet UIPickerView *folderPicker;
@property (nonatomic, retain) IBOutlet UINavigationBar *navBar;
@property (nonatomic, retain) IBOutlet UIActivityIndicatorView *activityIndicator;
@property (nonatomic, retain) IBOutlet UILabel *movingLabel;
@property (nonatomic, retain) IBOutlet UILabel *errorLabel;
@property (nonatomic, retain) NSMutableArray *folders;
@end

View file

@ -0,0 +1,284 @@
//
// MoveSiteViewController.m
// NewsBlur
//
// Created by Samuel Clay on 12/2/11.
// Copyright 2011 NewsBlur. All rights reserved.
//
#import "MoveSiteViewController.h"
#import "NewsBlurAppDelegate.h"
#import "ASIHTTPRequest.h"
#import "ASIFormDataRequest.h"
#import "JSON.h"
#import "StringHelper.h"
@implementation MoveSiteViewController
@synthesize appDelegate;
@synthesize toFolderInput;
@synthesize fromFolderInput;
@synthesize titleLabel;
@synthesize moveButton;
@synthesize cancelButton;
@synthesize folderPicker;
@synthesize navBar;
@synthesize activityIndicator;
@synthesize movingLabel;
@synthesize errorLabel;
@synthesize folders;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
}
return self;
}
- (void)viewDidLoad {
UIImageView *folderImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"folder.png"]];
[toFolderInput setLeftView:folderImage];
[toFolderInput setLeftViewMode:UITextFieldViewModeAlways];
UIImageView *folderImage2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"folder.png"]];
[fromFolderInput setLeftView:folderImage2];
[fromFolderInput setLeftViewMode:UITextFieldViewModeAlways];
[folderImage release];
[folderImage2 release];
navBar.tintColor = [UIColor colorWithRed:0.16f green:0.36f blue:0.46 alpha:0.9];
appDelegate = [NewsBlurAppDelegate sharedAppDelegate];
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated {
[self.errorLabel setHidden:YES];
[self.movingLabel setHidden:YES];
[self.activityIndicator stopAnimating];
for (UIView *subview in [self.titleLabel subviews]) {
[subview removeFromSuperview];
}
UIView *label = [appDelegate makeFeedTitle:appDelegate.activeFeed];
label.frame = CGRectMake(label.frame.origin.x,
label.frame.origin.y,
self.titleLabel.frame.size.width -
(self.titleLabel.frame.origin.x-label.frame.origin.x),
label.frame.size.height);
[self.titleLabel addSubview:label];
[self reload];
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
[self.activityIndicator stopAnimating];
[super viewDidAppear:animated];
}
- (void)dealloc {
[appDelegate release];
[toFolderInput release];
[fromFolderInput release];
[titleLabel release];
[moveButton release];
[cancelButton release];
[folderPicker release];
[navBar release];
[folders release];
[super dealloc];
}
- (void)reload {
BOOL isTopLevel = [[appDelegate.activeFolder trim] isEqualToString:@""];
int row = 0;
[toFolderInput setText:@""];
if (appDelegate.isRiverView) {
NSString *parentFolderName = [appDelegate extractParentFolderName:appDelegate.activeFolder];
row = [[self pickerFolders]
indexOfObject:parentFolderName];
fromFolderInput.text = parentFolderName;
} else {
fromFolderInput.text = isTopLevel ? @"— Top Level —" : appDelegate.activeFolder;
row = isTopLevel ?
0 :
[[self pickerFolders] indexOfObject:appDelegate.activeFolder];
}
self.folders = [NSMutableArray array];
[folderPicker reloadAllComponents];
[folderPicker selectRow:row inComponent:0 animated:NO];
moveButton.enabled = NO;
moveButton.title = appDelegate.isRiverView ? @"Move Folder to Folder" : @"Move Site to Folder";
}
- (IBAction)doCancelButton {
[appDelegate.moveSiteViewController dismissModalViewControllerAnimated:YES];
}
- (IBAction)doMoveButton {
if (appDelegate.isRiverView) {
[self moveFolder];
} else {
[self moveSite];
}
}
#pragma mark -
#pragma mark Move Site
- (IBAction)moveSite {
[self.movingLabel setHidden:NO];
[self.movingLabel setText:@"Moving site..."];
[self.errorLabel setHidden:YES];
[self.activityIndicator startAnimating];
NSString *urlString = [NSString stringWithFormat:@"http://%@/reader/move_feed_to_folder",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
NSString *fromFolder = [appDelegate extractFolderName:[fromFolderInput text]];
NSString *toFolder = [appDelegate extractFolderName:[toFolderInput text]];
[request setPostValue:fromFolder forKey:@"in_folder"];
[request setPostValue:toFolder forKey:@"to_folder"];
[request setPostValue:[appDelegate.activeFeed objectForKey:@"id"] forKey:@"feed_id"];
[request setDelegate:self];
[request setDidFinishSelector:@selector(requestFinished:)];
[request setDidFailSelector:@selector(requestFailed:)];
[request startAsynchronous];
}
- (void)requestFinished:(ASIHTTPRequest *)request {
if ([request responseStatusCode] >= 500) {
return [self requestFailed:request];
}
[self.movingLabel setHidden:YES];
[self.activityIndicator stopAnimating];
NSString *responseString = [request responseString];
NSDictionary *results = [[NSDictionary alloc]
initWithDictionary:[responseString JSONValue]];
// int statusCode = [request responseStatusCode];
int code = [[results valueForKey:@"code"] intValue];
if (code == -1) {
[self.errorLabel setText:[results valueForKey:@"message"]];
[self.errorLabel setHidden:NO];
} else {
appDelegate.activeFolder = [toFolderInput text];
[appDelegate.moveSiteViewController dismissModalViewControllerAnimated:YES];
[appDelegate reloadFeedsView:NO];
}
[results release];
}
#pragma mark -
#pragma mark Move Folder
- (IBAction)moveFolder {
[self.movingLabel setHidden:NO];
[self.movingLabel setText:@"Moving Folder..."];
[self.errorLabel setHidden:YES];
[self.activityIndicator startAnimating];
NSString *urlString = [NSString stringWithFormat:@"http://%@/reader/move_folder_to_folder",
NEWSBLUR_URL];
NSURL *url = [NSURL URLWithString:urlString];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
NSString *folderName = [appDelegate extractFolderName:appDelegate.activeFolder];
NSString *fromFolder = [appDelegate extractFolderName:[fromFolderInput text]];
NSString *toFolder = [appDelegate extractFolderName:[toFolderInput text]];
[request setPostValue:fromFolder forKey:@"in_folder"];
[request setPostValue:toFolder forKey:@"to_folder"];
[request setPostValue:folderName forKey:@"folder_name"];
[request setDelegate:self];
[request setDidFinishSelector:@selector(finishMoveFolder:)];
[request setDidFailSelector:@selector(requestFailed:)];
[request startAsynchronous];
}
- (void)finishMoveFolder:(ASIHTTPRequest *)request {
[self.movingLabel setHidden:YES];
[self.activityIndicator stopAnimating];
NSString *responseString = [request responseString];
NSDictionary *results = [[NSDictionary alloc]
initWithDictionary:[responseString JSONValue]];
// int statusCode = [request responseStatusCode];
int code = [[results valueForKey:@"code"] intValue];
if (code == -1) {
[self.errorLabel setText:[results valueForKey:@"message"]];
[self.errorLabel setHidden:NO];
} else {
[appDelegate.moveSiteViewController dismissModalViewControllerAnimated:YES];
[appDelegate reloadFeedsView:NO];
}
[results release];
}
- (void)requestFailed:(ASIHTTPRequest *)request {
[self.movingLabel setHidden:YES];
[self.errorLabel setHidden:NO];
[self.activityIndicator stopAnimating];
NSError *error = [request error];
NSLog(@"Error: %@", error);
NSLog(@"Error: %@", [request responseString]);
[self.errorLabel setText:error.localizedDescription];
}
#pragma mark -
#pragma mark Folder Picker
- (NSArray *)pickerFolders {
if ([self.folders count]) return self.folders;
self.folders = [NSMutableArray array];
[self.folders addObject:@"— Top Level —"];
for (NSString *folder in appDelegate.dictFoldersArray) {
if ([[folder trim] isEqualToString:@""]) continue;
if (appDelegate.isRiverView) {
if (![folder containsString:appDelegate.activeFolder]) {
[self.folders addObject:folder];
}
} else {
[self.folders addObject:folder];
}
}
return self.folders;
}
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView
numberOfRowsInComponent:(NSInteger)component {
return [[self pickerFolders] count];
}
- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component {
return [[self pickerFolders] objectAtIndex:row];
}
- (void)pickerView:(UIPickerView *)pickerView
didSelectRow:(NSInteger)row
inComponent:(NSInteger)component {
[toFolderInput setText:[[self pickerFolders] objectAtIndex:row]];
moveButton.enabled = YES;
}
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
[errorLabel setText:@""];
if (textField == toFolderInput && ![toFolderInput isFirstResponder]) {
[toFolderInput setInputView:folderPicker];
}
return NO;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
return YES;
}
@end

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,8 @@
@class FeedDetailViewController;
@class StoryDetailViewController;
@class LoginViewController;
@class AddViewController;
@class AddSiteViewController;
@class MoveSiteViewController;
@class OriginalStoryViewController;
@interface NewsBlurAppDelegate : BaseViewController <UIApplicationDelegate> {
@ -23,7 +24,8 @@
FeedDetailViewController *feedDetailViewController;
StoryDetailViewController *storyDetailViewController;
LoginViewController *loginViewController;
AddViewController *addViewController;
AddSiteViewController *addSiteViewController;
MoveSiteViewController *moveSiteViewController;
OriginalStoryViewController *originalStoryViewController;
NSString * activeUsername;
@ -56,7 +58,8 @@
@property (nonatomic, retain) IBOutlet FeedDetailViewController *feedDetailViewController;
@property (nonatomic, retain) IBOutlet StoryDetailViewController *storyDetailViewController;
@property (nonatomic, retain) IBOutlet LoginViewController *loginViewController;
@property (nonatomic, retain) IBOutlet AddViewController *addViewController;
@property (nonatomic, retain) IBOutlet AddSiteViewController *addSiteViewController;
@property (nonatomic, retain) IBOutlet MoveSiteViewController *moveSiteViewController;
@property (nonatomic, retain) IBOutlet OriginalStoryViewController *originalStoryViewController;
@property (readwrite, retain) NSString * activeUsername;
@ -81,12 +84,15 @@
@property (nonatomic, retain) NSDictionary *dictFeeds;
@property (nonatomic, retain) NSMutableArray *dictFoldersArray;
+ (NewsBlurAppDelegate*) sharedAppDelegate;
- (void)showLogin;
- (void)showAdd;
- (void)showMoveSite;
- (void)loadFeedDetailView;
- (void)loadRiverFeedDetailView;
- (void)loadStoryDetailView;
- (void)reloadFeedsView;
- (void)reloadFeedsView:(BOOL)showLoader;
- (void)hideNavigationBar:(BOOL)animated;
- (void)showNavigationBar:(BOOL)animated;
- (void)setTitle:(NSString *)title;
@ -115,8 +121,11 @@
- (void)markFeedAllRead:(id)feedId;
- (void)calculateStoryLocations;
+ (int)computeStoryScore:(NSDictionary *)intelligence;
- (NSString *)extractFolderName:(NSString *)folderName;
- (NSString *)extractParentFolderName:(NSString *)folderName;
+ (UIView *)makeGradientView:(CGRect)rect startColor:(NSString *)start endColor:(NSString *)end;
- (UIView *)makeFeedTitleGradient:(NSDictionary *)feed withRect:(CGRect)rect;
- (UIView *)makeFeedTitle:(NSDictionary *)feed;
@end

View file

@ -11,10 +11,12 @@
#import "FeedDetailViewController.h"
#import "StoryDetailViewController.h"
#import "LoginViewController.h"
#import "AddViewController.h"
#import "AddSiteViewController.h"
#import "MoveSiteViewController.h"
#import "OriginalStoryViewController.h"
#import "MBProgressHUD.h"
#import "Utilities.h"
#import "StringHelper.h"
@implementation NewsBlurAppDelegate
@ -24,7 +26,8 @@
@synthesize feedDetailViewController;
@synthesize storyDetailViewController;
@synthesize loginViewController;
@synthesize addViewController;
@synthesize addSiteViewController;
@synthesize moveSiteViewController;
@synthesize originalStoryViewController;
@synthesize activeUsername;
@ -49,6 +52,10 @@
@synthesize dictFeeds;
@synthesize dictFoldersArray;
+ (NewsBlurAppDelegate*) sharedAppDelegate {
return (NewsBlurAppDelegate*) [UIApplication sharedApplication].delegate;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// [TestFlight takeOff:@"101dd20fb90f7355703b131d9af42633_MjQ0NTgyMDExLTA4LTIxIDIzOjU3OjEzLjM5MDcyOA"];
@ -75,7 +82,8 @@
[feedDetailViewController release];
[storyDetailViewController release];
[loginViewController release];
[addViewController release];
[addSiteViewController release];
[moveSiteViewController release];
[originalStoryViewController release];
[navigationController release];
[window release];
@ -117,14 +125,19 @@
- (void)showAdd {
UINavigationController *navController = self.navigationController;
[addViewController initWithNibName:nil bundle:nil];
[navController presentModalViewController:addViewController animated:YES];
[addViewController reload];
[addSiteViewController initWithNibName:nil bundle:nil];
[navController presentModalViewController:addSiteViewController animated:YES];
[addSiteViewController reload];
}
- (void)reloadFeedsView {
[self setTitle:@"NewsBlur"];
[feedsViewController fetchFeedList:YES];
- (void)showMoveSite {
UINavigationController *navController = self.navigationController;
[moveSiteViewController initWithNibName:nil bundle:nil];
[navController presentModalViewController:moveSiteViewController animated:YES];
}
- (void)reloadFeedsView:(BOOL)showLoader {
[feedsViewController fetchFeedList:showLoader];
[loginViewController dismissModalViewControllerAnimated:YES];
self.navigationController.navigationBar.tintColor = [UIColor colorWithRed:0.16f green:0.36f blue:0.46 alpha:0.9];
}
@ -331,14 +344,20 @@
int total = 0;
NSArray *folder;
if (!folderName) {
folder = [self.dictFolders objectForKey:self.activeFolder];
if (!folderName && self.activeFolder == @"Everything") {
for (id feedId in self.dictFeeds) {
total += [self unreadCountForFeed:feedId];
}
} else {
folder = [self.dictFolders objectForKey:folderName];
}
if (!folderName) {
folder = [self.dictFolders objectForKey:self.activeFolder];
} else {
folder = [self.dictFolders objectForKey:folderName];
}
for (id feedId in folder) {
total += [self unreadCountForFeed:feedId];
for (id feedId in folder) {
total += [self unreadCountForFeed:feedId];
}
}
return total;
@ -437,9 +456,17 @@
[self markFeedAllRead:feedId];
}
- (void)markActiveFolderAllRead {
for (id feedId in [self.dictFolders objectForKey:self.activeFolder]) {
[self markFeedAllRead:feedId];
- (void)markActiveFolderAllRead {
if (self.activeFolder == @"Everything") {
for (NSString *folderName in self.dictFoldersArray) {
for (id feedId in [self.dictFolders objectForKey:folderName]) {
[self markFeedAllRead:feedId];
}
}
} else {
for (id feedId in [self.dictFolders objectForKey:self.activeFolder]) {
[self markFeedAllRead:feedId];
}
}
}
@ -489,6 +516,40 @@
return score;
}
- (NSString *)extractParentFolderName:(NSString *)folderName {
if ([folderName containsString:@"Top Level"]) {
folderName = @"";
}
if ([folderName containsString:@" - "]) {
int lastFolderLoc = [folderName rangeOfString:@" - " options:NSBackwardsSearch].location;
// int secondLastFolderLoc = [[folderName substringToIndex:lastFolderLoc] rangeOfString:@" - " options:NSBackwardsSearch].location;
folderName = [folderName substringToIndex:lastFolderLoc];
} else {
folderName = @"— Top Level —";
}
return folderName;
}
- (NSString *)extractFolderName:(NSString *)folderName {
if ([folderName containsString:@"Top Level"]) {
folderName = @"";
}
if ([folderName containsString:@" - "]) {
int folder_loc = [folderName rangeOfString:@" - " options:NSBackwardsSearch].location;
folderName = [folderName substringFromIndex:(folder_loc + 3)];
}
return folderName;
}
#pragma mark -
#pragma mark Feed Templates
+ (UIView *)makeGradientView:(CGRect)rect startColor:(NSString *)start endColor:(NSString *)end {
UIView *gradientView = [[[UIView alloc] initWithFrame:rect] autorelease];
@ -544,20 +605,23 @@
titleLabel.backgroundColor = [UIColor clearColor];
titleLabel.textAlignment = UITextAlignmentLeft;
titleLabel.lineBreakMode = UILineBreakModeTailTruncation;
titleLabel.numberOfLines = 1;
titleLabel.font = [UIFont fontWithName:@"Helvetica-Bold" size:11.0];
titleLabel.shadowOffset = CGSizeMake(0, 1);
if ([[feed objectForKey:@"favicon_text_color"] class] != [NSNull class]) {
titleLabel.textColor = [[feed objectForKey:@"favicon_text_color"] isEqualToString:@"white"] ?
[UIColor whiteColor] :
[UIColor blackColor];
titleLabel.shadowColor = [[feed objectForKey:@"favicon_text_color"] isEqualToString:@"white"] ?
UIColorFromRGB(0x202020):
UIColorFromRGB(0xe0e0e0);
titleLabel.textColor = [[feed objectForKey:@"favicon_text_color"]
isEqualToString:@"white"] ?
[UIColor whiteColor] :
[UIColor blackColor];
titleLabel.shadowColor = [[feed objectForKey:@"favicon_text_color"]
isEqualToString:@"white"] ?
UIColorFromRGB(0x202020) :
UIColorFromRGB(0xd0d0d0);
} else {
titleLabel.textColor = [UIColor whiteColor];
titleLabel.shadowColor = [UIColor blackColor];
}
titleLabel.frame = CGRectMake(32, 1, window.frame.size.width-20, 20);
titleLabel.frame = CGRectMake(32, 1, rect.size.width-32, 20);
NSString *feedIdStr = [NSString stringWithFormat:@"%@", [feed objectForKey:@"id"]];
UIImage *titleImage = [Utilities getImage:feedIdStr];
@ -580,4 +644,38 @@
return gradientView;
}
- (UIView *)makeFeedTitle:(NSDictionary *)feed {
UILabel *titleLabel = [[[UILabel alloc] init] autorelease];
if (self.isRiverView) {
titleLabel.text = [NSString stringWithFormat:@" %@", self.activeFolder];
} else {
titleLabel.text = [NSString stringWithFormat:@" %@", [feed objectForKey:@"feed_title"]];
}
titleLabel.backgroundColor = [UIColor clearColor];
titleLabel.textAlignment = UITextAlignmentLeft;
titleLabel.font = [UIFont fontWithName:@"Helvetica-Bold" size:15.0];
titleLabel.textColor = [UIColor whiteColor];
titleLabel.lineBreakMode = UILineBreakModeTailTruncation;
titleLabel.numberOfLines = 1;
titleLabel.shadowColor = [UIColor blackColor];
titleLabel.shadowOffset = CGSizeMake(0, -1);
titleLabel.center = CGPointMake(28, -2);
[titleLabel sizeToFit];
NSString *feedIdStr = [NSString stringWithFormat:@"%@", [feed objectForKey:@"id"]];
UIImage *titleImage;
if (self.isRiverView) {
titleImage = [UIImage imageNamed:@"folder.png"];
} else {
titleImage = [Utilities getImage:feedIdStr];
}
UIImageView *titleImageView = [[UIImageView alloc] initWithImage:titleImage];
titleImageView.frame = CGRectMake(0.0, 2.0, 16.0, 16.0);
[titleLabel addSubview:titleImageView];
[titleImageView release];
return titleLabel;
}
@end

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