diff --git a/apps/profile/models.py b/apps/profile/models.py index eca12bf6f..add7a31d6 100644 --- a/apps/profile/models.py +++ b/apps/profile/models.py @@ -17,6 +17,7 @@ from apps.rss_feeds.models import Feed from apps.rss_feeds.tasks import NewFeeds from utils import log as logging from utils.user_functions import generate_secret_token +from utils.feed_functions import relative_timesince from vendor.timezones.fields import TimeZoneField from vendor.paypal.standard.ipn.signals import subscription_signup from zebra.signals import zebra_webhook_customer_subscription_created @@ -206,54 +207,6 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()} }) + ('?' + next + '=1' if next else '') -class MInteraction(mongo.Document): - user_id = mongo.IntField() - date = mongo.DateTimeField(default=datetime.datetime.now) - category = mongo.StringField() - title = mongo.StringField() - content = mongo.StringField() - with_user_id = mongo.IntField() - feed_id = mongo.IntField() - content_id = mongo.StringField() - - meta = { - 'collection': 'interactions', - 'indexes': [('user_id', 'date'), 'category'], - 'allow_inheritance': False, - 'index_drop_dups': True, - 'ordering': ['-date'], - } - - def __unicode__(self): - user = User.objects.get(pk=self.user_id) - with_user = self.with_user_id and User.objects.get(pk=self.with_user_id) - return "<%s> %s on %s: %s - %s" % (user.username, with_user and with_user.username, self.date, - self.category, self.content and self.content[:20]) - - @classmethod - def new_follow(cls, follower_user_id, followee_user_id): - cls.objects.create(user_id=followee_user_id, - with_user_id=follower_user_id, - category='follow') - - @classmethod - def new_comment_reply(cls, user_id, reply_user_id, reply_content, social_feed_id, story_id): - cls.objects.create(user_id=user_id, - with_user_id=reply_user_id, - category='comment_reply', - content=reply_content, - feed_id=social_feed_id, - content_id=story_id) - - @classmethod - def new_reply_reply(cls, user_id, reply_user_id, reply_content, social_feed_id, story_id): - cls.objects.create(user_id=user_id, - with_user_id=reply_user_id, - category='reply_reply', - content=reply_content, - feed_id=social_feed_id, - content_id=story_id) - class MActivity(mongo.Document): user_id = mongo.IntField() @@ -276,6 +229,22 @@ class MActivity(mongo.Document): user = User.objects.get(pk=self.user_id) return "<%s> %s - %s" % (user.username, self.category, self.content and self.content[:20]) + @classmethod + def user(cls, user, page=1): + page = max(1, page) + limit = 5 + offset = (page-1) * limit + + activities_db = cls.objects.filter(user_id=user.pk)[offset:offset+limit+1] + + activities = [] + for activity_db in activities_db: + activity = activity_db.to_mongo() + activity['date'] = relative_timesince(activity_db.date) + activities.append(activity) + + return activities + @classmethod def new_starred_story(cls, user_id, story_title, story_feed_id, story_id): cls.objects.create(user_id=user_id, diff --git a/apps/profile/urls.py b/apps/profile/urls.py index 18f5fe293..5b7cd7ade 100644 --- a/apps/profile/urls.py +++ b/apps/profile/urls.py @@ -13,4 +13,5 @@ urlpatterns = patterns('', url(r'^is_premium/?', views.profile_is_premium, name='profile-is-premium'), url(r'^paypal_ipn/?', include('paypal.standard.ipn.urls'), name='paypal-ipn'), url(r'^stripe_form/?', views.stripe_form, name='stripe-form'), + url(r'^activities/?', views.load_activities, name='profile-activities'), ) diff --git a/apps/profile/views.py b/apps/profile/views.py index 293b1322e..16b02bf08 100644 --- a/apps/profile/views.py +++ b/apps/profile/views.py @@ -9,12 +9,14 @@ from django.template import RequestContext from django.shortcuts import render_to_response from django.core.mail import mail_admins from django.conf import settings -from apps.profile.models import Profile, change_password +from apps.profile.models import Profile, change_password, MActivity from apps.reader.models import UserSubscription from apps.profile.forms import StripePlusPaymentForm, PLANS from apps.social.models import MSocialServices from utils import json_functions as json from utils.user_functions import ajax_login_required +from utils.view_functions import render_to +from utils.user_functions import get_user from vendor.paypal.standard.forms import PayPalPaymentsForm SINGLE_FIELD_PREFS = ('timezone','feed_pane_size','hide_mobile','send_emails', @@ -247,3 +249,14 @@ def stripe_form(request): }, context_instance=RequestContext(request) ) + +@render_to('reader/activities_module.xhtml') +def load_activities(request): + user = get_user(request) + page = max(1, int(request.REQUEST.get('page', 1))) + activities = MActivity.user(user, page=page) + + return { + 'activities': activities, + 'page': page, + } \ No newline at end of file diff --git a/apps/social/models.py b/apps/social/models.py index 28a832e94..b7b4e5880 100644 --- a/apps/social/models.py +++ b/apps/social/models.py @@ -14,7 +14,6 @@ from apps.reader.models import UserSubscription, MUserStory from apps.analyzer.models import MClassifierFeed, MClassifierAuthor, MClassifierTag, MClassifierTitle from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds, apply_classifier_authors, apply_classifier_tags from apps.rss_feeds.models import Feed, MStory -from apps.profile.models import MInteraction from vendor import facebook from vendor import tweepy from utils import log as logging @@ -1042,3 +1041,72 @@ class MSocialServices(mongo.Document): hashlib.md5(user.email).hexdigest() profile.save() return profile + + +class MInteraction(mongo.Document): + user_id = mongo.IntField() + date = mongo.DateTimeField(default=datetime.datetime.now) + category = mongo.StringField() + title = mongo.StringField() + content = mongo.StringField() + with_user_id = mongo.IntField() + feed_id = mongo.IntField() + content_id = mongo.StringField() + + meta = { + 'collection': 'interactions', + 'indexes': [('user_id', 'date'), 'category'], + 'allow_inheritance': False, + 'index_drop_dups': True, + 'ordering': ['-date'], + } + + def __unicode__(self): + user = User.objects.get(pk=self.user_id) + with_user = self.with_user_id and User.objects.get(pk=self.with_user_id) + return "<%s> %s on %s: %s - %s" % (user.username, with_user and with_user.username, self.date, + self.category, self.content and self.content[:20]) + + @classmethod + def user(cls, user, page=1): + page = max(1, page) + limit = 5 + offset = (page-1) * limit + interactions_db = cls.objects.filter(user_id=user.pk)[offset:offset+limit+1] + with_user_ids = [i.with_user_id for i in interactions_db if i.with_user_id] + social_profiles = dict((p.user_id, p) for p in MSocialProfile.objects.filter(user_id__in=with_user_ids)) + + interactions = [] + for interaction_db in interactions_db: + interaction = interaction_db.to_mongo() + interaction['photo_url'] = getattr(social_profiles.get(interaction_db.with_user_id), 'photo_url', None) + interaction['with_user'] = social_profiles.get(interaction_db.with_user_id) + interaction['date'] = relative_timesince(interaction_db.date) + interactions.append(interaction) + + print len(interactions), len(interactions_db) + return interactions + + @classmethod + def new_follow(cls, follower_user_id, followee_user_id): + cls.objects.create(user_id=followee_user_id, + with_user_id=follower_user_id, + category='follow') + + @classmethod + def new_comment_reply(cls, user_id, reply_user_id, reply_content, social_feed_id, story_id): + cls.objects.create(user_id=user_id, + with_user_id=reply_user_id, + category='comment_reply', + content=reply_content, + feed_id=social_feed_id, + content_id=story_id) + + @classmethod + def new_reply_reply(cls, user_id, reply_user_id, reply_content, social_feed_id, story_id): + cls.objects.create(user_id=user_id, + with_user_id=reply_user_id, + category='reply_reply', + content=reply_content, + feed_id=social_feed_id, + content_id=story_id) \ No newline at end of file diff --git a/apps/social/urls.py b/apps/social/urls.py index 36af40f09..661571678 100644 --- a/apps/social/urls.py +++ b/apps/social/urls.py @@ -8,6 +8,7 @@ urlpatterns = patterns('', url(r'^profile/?$', views.profile, name='profile'), url(r'^load_user_profile/?$', views.load_user_profile, name='load-user-profile'), url(r'^save_user_profile/?$', views.save_user_profile, name='save-user-profile'), + url(r'^interactions/?$', views.load_interactions, name='social-interactions'), url(r'^follow/?$', views.follow, name='social-follow'), url(r'^unfollow/?$', views.unfollow, name='social-unfollow'), url(r'^feed_trainer', views.social_feed_trainer, name='social-feed-trainer'), diff --git a/apps/social/views.py b/apps/social/views.py index b9c9d441d..82b5a929c 100644 --- a/apps/social/views.py +++ b/apps/social/views.py @@ -7,12 +7,11 @@ from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.conf import settings from apps.rss_feeds.models import MStory, Feed, MStarredStory from apps.social.models import MSharedStory, MSocialServices, MSocialProfile, MSocialSubscription, MCommentReply -from apps.social.models import MRequestInvite +from apps.social.models import MRequestInvite, MInteraction from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds, apply_classifier_authors, apply_classifier_tags from apps.analyzer.models import get_classifiers_for_user from apps.reader.models import MUserStory, UserSubscription -from apps.profile.models import MInteraction from utils import json_functions as json from utils import log as logging from utils import PyRSS2Gen as RSS @@ -121,12 +120,14 @@ def load_social_stories(request, user_id, username=None): story['starred_date'] = format_story_link_date__long(starred_date, now) if story['id'] in shared_stories: story['shared'] = True - shared_date = localtime_for_timezone(shared_stories[story['id']]['shared_date'], user.profile.timezone) + shared_date = localtime_for_timezone(shared_stories[story['id']]['shared_date'], + user.profile.timezone) story['shared_date'] = format_story_link_date__long(shared_date, now) story['shared_comments'] = shared_stories[story['id']]['comments'] story['intelligence'] = { - 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id'], social_user_id=social_user_id), + 'feed': apply_classifier_feeds(classifier_feeds, story['story_feed_id'], + social_user_id=social_user_id), 'author': apply_classifier_authors(classifier_authors, story), 'tags': apply_classifier_tags(classifier_tags, story), 'title': apply_classifier_titles(classifier_titles, story), @@ -180,7 +181,9 @@ def story_comments(request): feed_id = int(request.REQUEST['feed_id']) story_id = request.REQUEST['story_id'] - shared_stories = MSharedStory.objects.filter(story_feed_id=feed_id, story_guid=story_id, has_comments=True) + shared_stories = MSharedStory.objects.filter(story_feed_id=feed_id, + story_guid=story_id, + has_comments=True) comments = [s.comments_with_author() for s in shared_stories] profile_user_ids = set([c['user_id'] for c in comments]) @@ -202,7 +205,9 @@ def mark_story_as_shared(request): if not story: return {'code': -1, 'message': 'Story not found.'} - shared_story = MSharedStory.objects.filter(user_id=request.user.pk, story_feed_id=feed_id, story_guid=story_id) + shared_story = MSharedStory.objects.filter(user_id=request.user.pk, + story_feed_id=feed_id, + story_guid=story_id) if not shared_story: story_db = dict([(k, v) for k, v in story._data.items() if k is not None and v is not None]) @@ -219,7 +224,8 @@ def mark_story_as_shared(request): shared_story.comments = comments shared_story.has_comments = bool(comments) shared_story.save() - logging.user(request, "~FCUpdating shared story: ~SB~FM%s (~FB%s~FM)" % (story.story_title[:50], comments[:100])) + logging.user(request, "~FCUpdating shared story: ~SB~FM%s (~FB%s~FM)" % ( + story.story_title[:50], comments[:100])) story.count_comments() shared_story.publish_update_to_subscribers() @@ -246,7 +252,9 @@ def save_comment_reply(request): if not story: return {'code': -1, 'message': 'Story not found.'} - shared_story = MSharedStory.objects.get(user_id=comment_user_id, story_feed_id=feed_id, story_guid=story_id) + shared_story = MSharedStory.objects.get(user_id=comment_user_id, + story_feed_id=feed_id, + story_guid=story_id) reply = MCommentReply() reply.user_id = request.user.pk reply.publish_date = datetime.datetime.now() @@ -254,7 +262,8 @@ def save_comment_reply(request): shared_story.replies.append(reply) shared_story.save() - logging.user(request, "~FCReplying to comment in: ~SB~FM%s (~FB%s~FM)" % (story.story_title[:50], reply_comments[:100])) + logging.user(request, "~FCReplying to comment in: ~SB~FM%s (~FB%s~FM)" % ( + story.story_title[:50], reply_comments[:100])) comment = shared_story.comments_with_author() profile_user_ids = set([comment['user_id']]) @@ -483,7 +492,8 @@ def social_feed_trainer(request): classifier['feed_tags'] = [] classifier['feed_authors'] = [] - logging.user(user, "~FGLoading social trainer on ~SB%s: %s" % (social_user.username, social_profile.title)) + logging.user(user, "~FGLoading social trainer on ~SB%s: %s" % ( + social_user.username, social_profile.title)) return [classifier] @@ -506,7 +516,8 @@ def load_social_statistics(request, social_user_id, username=None): # Classifier counts stats['classifier_counts'] = social_profile.feed_classifier_counts - logging.user(request, "~FBStatistics social: ~SB%s ~FG(%s subs)" % (social_profile.user_id, social_profile.follower_count)) + logging.user(request, "~FBStatistics social: ~SB%s ~FG(%s subs)" % ( + social_profile.user_id, social_profile.follower_count)) return stats @@ -515,3 +526,14 @@ def load_social_settings(request, social_user_id, username=None): social_profile = MSocialProfile.objects.get(user_id=social_user_id) return social_profile.to_json() + +@render_to('reader/interactions_module.xhtml') +def load_interactions(request): + user = get_user(request) + page = max(1, int(request.REQUEST.get('page', 1))) + interactions = MInteraction.user(user, page=page) + + return { + 'interactions': interactions, + 'page': page, + } \ No newline at end of file diff --git a/media/css/reader.css b/media/css/reader.css index 25bba4cbf..24405746c 100644 --- a/media/css/reader.css +++ b/media/css/reader.css @@ -3961,7 +3961,8 @@ background: transparent; opacity: .6; display: none; } -.NB-module.NB-loading .NB-spinner { +.NB-module.NB-loading .NB-spinner, +.NB-module-item.NB-loading .NB-spinner { display: block; } .NB-module .NB-module-content-header { @@ -4098,7 +4099,7 @@ background: transparent; margin-top: 0; overflow: hidden; } -.NB-module .NB-module-activity .NB-module-content-header { +.NB-module .NB-module-activities .NB-module-content-header { margin-left: 12px; padding-right: 12px; } diff --git a/media/js/newsblur/common/assetmodel.js b/media/js/newsblur/common/assetmodel.js index 8c3328db0..3c3f5f0bb 100644 --- a/media/js/newsblur/common/assetmodel.js +++ b/media/js/newsblur/common/assetmodel.js @@ -965,6 +965,18 @@ NEWSBLUR.AssetModel.Reader.prototype = { }, callback, error_callback, {request_type: 'GET'}); }, + load_interactions_page: function(page, callback, error_callback) { + this.make_request('/social/interactions', { + 'page': page + }, callback, error_callback, {request_type: 'GET'}); + }, + + load_activities_page: function(page, callback, error_callback) { + this.make_request('/profile/activities', { + 'page': page + }, callback, error_callback, {request_type: 'GET'}); + }, + approve_feed_in_moderation_queue: function(feed_id, date, callback) { this.make_request('/recommendations/approve_feed', { 'feed_id' : feed_id, diff --git a/media/js/newsblur/reader/reader.js b/media/js/newsblur/reader/reader.js index c13b6d884..73def5726 100644 --- a/media/js/newsblur/reader/reader.js +++ b/media/js/newsblur/reader/reader.js @@ -46,7 +46,9 @@ 'fetched_feeds': 0, 'page_fill_outs': 0, 'recommended_feed_page': 0, - 'feed_view_positions_timer': 0 + 'feed_view_positions_timer': 0, + 'interactions_page': 1, + 'activities_page': 1 }; this.cache = { 'iframe_stories': {}, @@ -6542,6 +6544,53 @@ } }, + // =============================== + // = Interactions and Activities = + // =============================== + + load_interactions_page: function(direction) { + var self = this; + var $module = $('.NB-module-interactions'); + + $module.addClass('NB-loading'); + direction = direction || 0; + console.log(["loading page", self.counts['interactions_page']+direction]); + this.model.load_interactions_page(this.counts['interactions_page']+direction, + function(resp) { + $module.removeClass('NB-loading'); + if (!resp) return; + $module.replaceWith(resp); + $module = $('.NB-module-interactions'); + var page = $module[0].className.match(/NB-page-(\d+)/)[1]; + console.log(["now on page", page]); + self.counts['interactions_page'] = parseInt(page, 10); + self.load_javascript_elements_on_page(); + }, function() { + $module.removeClass('NB-loading'); + }); + }, + + load_activities_page: function(direction) { + var self = this; + var $module = $('.NB-module-activities'); + + $module.addClass('NB-loading'); + direction = direction || 0; + + this.model.load_activities_page(this.counts['activities_page']+direction, + function(resp) { + $module.removeClass('NB-loading'); + if (!resp) return; + $module.replaceWith(resp); + $module = $('.NB-module-activities'); + var page = $module[0].className.match(/NB-page-(\d+)/)[1]; + self.counts['activities_page'] = parseInt(page, 10); + self.load_javascript_elements_on_page(); + }, function() { + $module.removeClass('NB-loading'); + }); + }, + // ======== // = FTUX = // ======== @@ -6833,13 +6882,14 @@ this.model.load_recommended_feed(this.counts['recommended_feed_page']+direction, !!refresh, unmoderated, function(resp) { - if (!resp) return; - self.counts['recommended_feed_page'] += direction; - $module.removeClass('NB-loading'); + if (!resp) return; $module.replaceWith(resp); + self.counts['recommended_feed_page'] += direction; self.load_javascript_elements_on_page(); - }, $.noop); + }, function() { + $module.removeClass('NB-loading'); + }); }, // ==================== @@ -6864,11 +6914,13 @@ $module.addClass('NB-loading'); this.model.load_dashboard_graphs(function(resp) { - if (!resp) return; $module.removeClass('NB-loading'); + if (!resp) return; $module.replaceWith(resp); self.load_javascript_elements_on_page(); - }, $.noop); + }, function() { + $module.removeClass('NB-loading'); + }); }, @@ -7706,6 +7758,30 @@ var page = $t.prevAll('.NB-module-page-indicator').length; self.load_howitworks_page(page); }); + $.targetIs(e, { tagSelector: '.NB-module-next-page', childOf: '.NB-module-interactions' }, function($t, $p){ + e.preventDefault(); + if (!$t.hasClass('NB-disabled')) { + self.load_interactions_page(1); + } + }); + $.targetIs(e, { tagSelector: '.NB-module-previous-page', childOf: '.NB-module-interactions' }, function($t, $p){ + e.preventDefault(); + if (!$t.hasClass('NB-disabled')) { + self.load_interactions_page(-1); + } + }); + $.targetIs(e, { tagSelector: '.NB-module-next-page', childOf: '.NB-module-activities' }, function($t, $p){ + e.preventDefault(); + if (!$t.hasClass('NB-disabled')) { + self.load_activities_page(1); + } + }); + $.targetIs(e, { tagSelector: '.NB-module-previous-page', childOf: '.NB-module-activities' }, function($t, $p){ + e.preventDefault(); + if (!$t.hasClass('NB-disabled')) { + self.load_activities_page(-1); + } + }); $.targetIs(e, { tagSelector: '.NB-splash-meta-about' }, function($t, $p){ e.preventDefault(); NEWSBLUR.about = new NEWSBLUR.About(); diff --git a/node/favicons.coffee b/node/favicons.coffee index dd1a113e5..ab385d1b5 100644 --- a/node/favicons.coffee +++ b/node/favicons.coffee @@ -1,7 +1,7 @@ express = require 'express' mongo = require 'mongodb' -MONGODB_SERVER = if process.env.NODE_ENV == 'dev' then 'localhost' else 'db04' +MONGODB_SERVER = if process.env.NODE_ENV == 'development' then 'localhost' else 'db04' server = new mongo.Server(MONGODB_SERVER, 27017, auto_reconnect: true poolSize: 12) diff --git a/node/favicons.js b/node/favicons.js index f5868dde8..1da220921 100644 --- a/node/favicons.js +++ b/node/favicons.js @@ -6,7 +6,7 @@ mongo = require('mongodb'); - MONGODB_SERVER = process.env.NODE_ENV === 'dev' ? 'localhost' : 'db04'; + MONGODB_SERVER = process.env.NODE_ENV === 'development' ? 'localhost' : 'db04'; server = new mongo.Server(MONGODB_SERVER, 27017, { auto_reconnect: true, diff --git a/templates/reader/activities_module.xhtml b/templates/reader/activities_module.xhtml index 2e6e7f20d..32241fa90 100644 --- a/templates/reader/activities_module.xhtml +++ b/templates/reader/activities_module.xhtml @@ -2,13 +2,13 @@ {% if activities %} -