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
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
@ -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))
|
|
@ -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
|
@ -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
|
@ -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()
|
44
config/munin/aws_sqs_queue_length_
Executable 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
|
@ -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()
|
45
config/munin/cassandra_key_cache_ratio
Executable 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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()
|
39
config/munin/gearman_connections
Executable 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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()
|
19
config/munin/memcached_connections
Executable 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()
|
19
config/munin/memcached_curr_items
Executable 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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()
|
29
config/munin/mongodb_index_misses
Executable 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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()
|
29
config/munin/mongodb_page_faults
Executable 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
|
@ -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()
|
51
config/munin/mongodb_replset_lag
Executable 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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()
|
32
config/munin/pgbouncer_pools_cl_
Executable 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()
|
||||
|
50
config/munin/pgbouncer_pools_sv_
Executable 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()
|
||||
|
32
config/munin/pgbouncer_stats_avg_bytes_
Executable 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()
|
||||
|
26
config/munin/pgbouncer_stats_avg_query_
Executable 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()
|
||||
|
26
config/munin/pgbouncer_stats_avg_req_
Executable 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()
|
||||
|
66
config/munin/postgres_block_read_
Executable 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
|
@ -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()
|
34
config/munin/postgres_connections
Executable 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
|
@ -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
|
@ -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
|
@ -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()
|
26
config/munin/postgres_table_sizes
Executable 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()
|
||||
|
19
config/munin/redis_active_connections
Executable 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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():
|
||||
|
|
|
@ -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 =
|
||||
# ===========
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
BIN
media/img/iphone/Default.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
media/img/iphone/skeleton.png
Normal file
After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
BIN
media/img/iphone/v1.2 - iPhone Screenshot 1.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
media/img/iphone/v1.2 - iPhone Screenshot 2.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
media/img/iphone/v1.2 - iPhone Screenshot 3.png
Normal file
After Width: | Height: | Size: 236 KiB |
BIN
media/img/iphone/v1.2 - iPhone Screenshot 4.png
Normal file
After Width: | Height: | Size: 264 KiB |
BIN
media/img/iphone/v1.2 - iPhone Screenshot 5.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
media/img/originals/Download.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
media/img/reader/download.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
media/img/reader/iphone_skeleton.png
Normal file
After Width: | Height: | Size: 110 KiB |
BIN
media/img/reader/stripe_background.png
Normal file
After Width: | Height: | Size: 16 KiB |
|
@ -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
|
|
@ -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);
|
|
@ -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>
|
|
@ -15,6 +15,6 @@
|
|||
- (void) clearFinishedRequests;
|
||||
- (void) cancelRequests;
|
||||
|
||||
- (void)informError:(NSError *)error;
|
||||
- (void)informError:(id)error;
|
||||
|
||||
@end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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];
|
||||
|
|
43
media/iphone/Classes/MoveSiteViewController.h
Normal 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
|
284
media/iphone/Classes/MoveSiteViewController.m
Normal 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
|
5700
media/iphone/Classes/MoveSiteViewController.xib
Normal 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|