diff --git a/apps/reader/views.py b/apps/reader/views.py index 75bcc05c2..ef63562a1 100644 --- a/apps/reader/views.py +++ b/apps/reader/views.py @@ -34,7 +34,8 @@ try: from apps.rss_feeds.models import Feed, MFeedPage, DuplicateFeed, MStory, MStarredStory, FeedLoadtime except: pass -from apps.social.models import MSharedStory, MSocialProfile, MSocialSubscription, MActivity +from apps.social.models import MSharedStory, MSocialProfile, MSocialServices +from apps.social.models import MSocialSubscription, MActivity from apps.social.views import load_social_page from utils import json_functions as json from utils.user_functions import get_user, ajax_login_required @@ -223,6 +224,7 @@ def load_feeds(request): } social_feeds = MSocialSubscription.feeds(**social_params) social_profile = MSocialProfile.profile(user.pk) + social_services = MSocialServices.profile(user.pk) user.profile.dashboard_date = datetime.datetime.now() user.profile.save() @@ -231,6 +233,7 @@ def load_feeds(request): 'feeds': feeds.values() if version == 2 else feeds, 'social_feeds': social_feeds, 'social_profile': social_profile, + 'social_services': social_services, 'folders': json.decode(folders.folders), 'starred_count': starred_count, } diff --git a/apps/social/models.py b/apps/social/models.py index 7719f2a8a..867a35f12 100644 --- a/apps/social/models.py +++ b/apps/social/models.py @@ -24,6 +24,7 @@ from vendor import facebook from vendor import tweepy from utils import log as logging from utils.feed_functions import relative_timesince +from utils.story_functions import truncate_chars from utils import json_functions as json RECOMMENDATIONS_LIMIT = 5 @@ -271,6 +272,15 @@ class MSocialProfile(mongo.Document): if self.photo_url: return self.photo_url return settings.MEDIA_URL + 'img/reader/default_profile_photo.png' + + @property + def email_photo_url(self): + if self.photo_url: + if self.photo_url.startswith('//'): + self.photo_url = 'http:' + self.photo_url + return self.photo_url + domain = Site.objects.get_current().domain + return 'http://' + domain + settings.MEDIA_URL + 'img/reader/default_profile_photo.png' def to_json(self, compact=False, include_follows=False, common_follows_with_user=None): # domain = Site.objects.get_current().domain @@ -375,6 +385,9 @@ class MSocialProfile(mongo.Document): subscription_user_id=user_id) socialsub.needs_unread_recalc = True socialsub.save() + + from apps.social.tasks import EmailNewFollower + EmailNewFollower.delay(follower_user_id=self.user_id, followee_user_id=user_id) def is_following_user(self, user_id): return user_id in self.following_user_ids @@ -425,6 +438,43 @@ class MSocialProfile(mongo.Document): follows_diff.remove(user_id) return follows_inter, follows_diff + + def send_email_for_new_follower(self, follower_user_id): + user = User.objects.get(pk=self.user_id) + if not user.email or not user.profile.send_emails: + return + + follower_profile = MSocialProfile.objects.get(user_id=follower_user_id) + photo_url = follower_profile.profile_photo_url + if 'graph.facebook.com' in photo_url: + follower_profile.photo_url = photo_url + '?type=large' + elif 'twimg' in photo_url: + follower_profile.photo_url = photo_url.replace('_normal', '') + + common_followers, _ = self.common_follows(follower_user_id, direction='followers') + common_followings, _ = self.common_follows(follower_user_id, direction='following') + common_followers.remove(self.user_id) + common_followings.remove(self.user_id) + common_followers = MSocialProfile.profiles(common_followers) + common_followings = MSocialProfile.profiles(common_followings) + + data = { + 'user': user, + 'follower_profile': follower_profile, + 'common_followers': common_followers, + 'common_followings': common_followings, + } + + text = render_to_string('mail/email_new_follower.txt', data) + html = render_to_string('mail/email_new_follower.xhtml', data) + subject = "%s is now following your Blurblog on NewsBlur!" % follower_profile.username + msg = EmailMultiAlternatives(subject, text, + from_email='NewsBlur <%s>' % settings.HELLO_EMAIL, + to=['%s <%s>' % (user.username, user.email)]) + msg.attach_alternative(html, "text/html") + msg.send() + + logging.user(user, "~BB~FM~SBSending email for new follower: %s" % follower_profile.username) def save_feed_story_history_statistics(self): """ @@ -870,6 +920,7 @@ class MSharedStory(mongo.Document): story_permalink = mongo.StringField() story_guid = mongo.StringField(unique_with=('user_id',)) story_tags = mongo.ListField(mongo.StringField(max_length=250)) + posted_to_services = mongo.ListField(mongo.StringField(max_length=20)) meta = { 'collection': 'shared_stories', @@ -1129,6 +1180,40 @@ class MSharedStory(mongo.Document): profiles = [profile.to_json(compact=True) for profile in profiles] return stories, profiles + + def blurblog_permalink(self): + profile = MSocialProfile.objects.get(user_id=self.user_id) + return "http://%s.%s/story/%s" % ( + profile.username_slug, + Site.objects.get_current().domain.replace('www', 'dev'), + self.guid_hash[:6] + ) + + def generate_post_to_service_message(self): + message = self.comments + if not message or len(message) < 1: + message = self.story_title + + message = truncate_chars(message, 116) + message += " " + self.blurblog_permalink() + print message + + return message + + def post_to_service(self, service): + if service in self.posted_to_services: + return + + message = self.generate_post_to_service_message() + social_service = MSocialServices.objects.get(user_id=self.user_id) + if service == 'twitter': + posted = social_service.post_to_twitter(message) + elif service == 'facebook': + posted = social_service.post_to_facebook(message) + + if posted: + self.posted_to_services.append(service) + self.save() class MSocialServices(mongo.Document): @@ -1179,6 +1264,14 @@ class MSocialServices(mongo.Document): } } + @classmethod + def profile(cls, user_id): + try: + profile = cls.objects.get(user_id=user_id) + except cls.DoesNotExist: + return {} + return profile.to_json() + def twitter_api(self): twitter_consumer_key = settings.TWITTER_CONSUMER_KEY twitter_consumer_secret = settings.TWITTER_CONSUMER_SECRET @@ -1325,7 +1418,26 @@ class MSocialServices(mongo.Document): hashlib.md5(user.email).hexdigest() profile.save() return profile + + def post_to_twitter(self, message): + try: + api = self.twitter_api() + api.update_status(status=message) + except tweepy.TweepError, e: + print e + return + return True + + def post_to_facebook(self, message): + try: + api = self.facebook_api() + api.put_wall_post(message=message) + except facebook.GraphAPIError, e: + print e + return + + return True class MInteraction(mongo.Document): user_id = mongo.IntField() diff --git a/apps/social/views.py b/apps/social/views.py index 84f35f9d8..4ecc6fd60 100644 --- a/apps/social/views.py +++ b/apps/social/views.py @@ -10,6 +10,7 @@ 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, MInteraction, MActivity +from apps.social.tasks import PostToService 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, sort_classifiers_by_feed @@ -239,6 +240,7 @@ def mark_story_as_shared(request): story_id = request.POST['story_id'] comments = request.POST.get('comments', '') source_user_id = request.POST.get('source_user_id') + post_to_services = request.POST.getlist('post_to_services') story = MStory.objects(story_feed_id=feed_id, story_guid=story_id).limit(1).first() if not story: @@ -279,6 +281,11 @@ def mark_story_as_shared(request): story = stories[0] story['shared_comments'] = shared_story['comments'] or "" + if post_to_services: + for service in post_to_services: + if service not in shared_story.posted_to_services: + PostToService.delay(shared_story_id=shared_story.id, service=service) + return {'code': code, 'story': story, 'user_profiles': profiles} @ajax_login_required diff --git a/media/css/reader.css b/media/css/reader.css index e2f475313..7b6e8d845 100644 --- a/media/css/reader.css +++ b/media/css/reader.css @@ -2428,12 +2428,34 @@ background: transparent; text-shadow: 0 1px 0 #F6F6F6; color: #202020; } -.NB-sideoption-share .NB-sideoption-share-optional { - text-transform: uppercase; +.NB-sideoption-share .NB-sideoption-share-crosspost { + margin-right: -4px; +} +.NB-sideoption-share .NB-sideoption-share-crosspost-twitter, +.NB-sideoption-share .NB-sideoption-share-crosspost-facebook { float: right; - color: #808080; - font-size: 10px; - text-shadow: 0 1px 0 #F6F6F6; + width: 16px; + height: 16px; + margin: 0 0 0 6px; + opacity: .4; + cursor: pointer; + -webkit-filter: grayscale(100%); +} +.NB-sideoption-share .NB-sideoption-share-crosspost-twitter:hover, +.NB-sideoption-share .NB-sideoption-share-crosspost-facebook:hover { + opacity: .7; + -webkit-filter: none; +} +.NB-sideoption-share .NB-sideoption-share-crosspost-twitter.NB-active, +.NB-sideoption-share .NB-sideoption-share-crosspost-facebook.NB-active { + opacity: 1; + -webkit-filter: none; +} +.NB-sideoption-share .NB-sideoption-share-crosspost-twitter { + background: transparent url('/media/embed/reader/twitter_icon.png') no-repeat 0 0; +} +.NB-sideoption-share .NB-sideoption-share-crosspost-facebook { + background: transparent url('/media/embed/reader/facebook_icon.png') no-repeat 0 0; } .NB-sideoption-share .NB-sideoption-share-comments { width: 100%; diff --git a/media/js/newsblur/common/assetmodel.js b/media/js/newsblur/common/assetmodel.js index f9520ed62..dc6916e43 100644 --- a/media/js/newsblur/common/assetmodel.js +++ b/media/js/newsblur/common/assetmodel.js @@ -19,6 +19,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ this.friends = {}; this.profile = {}; this.user_profile = new NEWSBLUR.Models.User(); + this.social_services = {}; this.user_profiles = new NEWSBLUR.Collections.Users(); this.follower_profiles = new NEWSBLUR.Collections.Users(); this.following_profiles = new NEWSBLUR.Collections.Users(); @@ -216,7 +217,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ }, callback); }, - mark_story_as_shared: function(story_id, feed_id, comments, source_user_id, callback, error_callback) { + mark_story_as_shared: function(story_id, feed_id, comments, source_user_id, post_to_services, + callback, error_callback) { var pre_callback = _.bind(function(data) { if (data.user_profiles) { this.add_user_profiles(data.user_profiles); @@ -231,7 +233,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ story_id: story_id, feed_id: feed_id, comments: comments, - source_user_id: source_user_id + source_user_id: source_user_id, + post_to_services: post_to_services }, pre_callback, error_callback); } else { error_callback(); @@ -292,6 +295,7 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({ self.starred_count = subscriptions.starred_count; self.social_feeds.reset(subscriptions.social_feeds); self.user_profile.set(subscriptions.social_profile); + self.social_services = subscriptions.social_services; if (!_.isEqual(self.favicons, {})) { self.feeds.each(function(feed) { diff --git a/media/js/newsblur/views/story_detail_view.js b/media/js/newsblur/views/story_detail_view.js index fc097e198..0a221124c 100644 --- a/media/js/newsblur/views/story_detail_view.js +++ b/media/js/newsblur/views/story_detail_view.js @@ -47,7 +47,8 @@ NEWSBLUR.Views.StoryDetailView = Backbone.View.extend({ model: this.model, el: this.el }).template({ - story: this.model + story: this.model, + social_services: NEWSBLUR.assets.social_services }); this.$el.html(this.template(params)); this.toggle_classes(); diff --git a/media/js/newsblur/views/story_share_view.js b/media/js/newsblur/views/story_share_view.js index ed1f673e3..6c2aa43aa 100644 --- a/media/js/newsblur/views/story_share_view.js +++ b/media/js/newsblur/views/story_share_view.js @@ -4,6 +4,8 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ "click .NB-feed-story-share" : "toggle_feed_story_share_dialog", "click .NB-sideoption-share-save" : "mark_story_as_shared", "click .NB-sideoption-share-unshare" : "mark_story_as_unshared", + "click .NB-sideoption-share-crosspost-twitter" : "toggle_twitter", + "click .NB-sideoption-share-crosspost-facebook" : "toggle_facebook", "keyup .NB-sideoption-share-comments" : "update_share_button_label" }, @@ -13,7 +15,8 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ render: function() { this.$el.html(this.template({ - story: this.model + story: this.model, + social_services: NEWSBLUR.assets.social_services })); return this; @@ -23,7 +26,14 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({
\
\
\ -
Optional
\ +
\ + <% if (social_services.twitter.twitter_uid) { %>\ +
\ + <% } %>\ + <% if (social_services.facebook.facebook_uid) { %>\ +
\ + <% } %>\ +
\
Comments:
\ \
Share
\ @@ -41,6 +51,8 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ var $comment_input = this.$('.NB-sideoption-share-comments'); var $story_comments = this.$('.NB-feed-story-comments'); var $unshare_button = this.$('.NB-sideoption-share-unshare'); + var $twitter_button = this.$('.NB-sideoption-share-crosspost-twitter'); + var $facebook_button = this.$('.NB-sideoption-share-crosspost-facebook'); if (options.close || ($sideoption.hasClass('NB-active') && !options.resize_open)) { @@ -71,8 +83,11 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ } } else { // Open/resize + this.$('.NB-error').remove(); $sideoption.addClass('NB-active'); $unshare_button.toggleClass('NB-hidden', !this.model.get("shared")); + $twitter_button.toggleClass('NB-active', !!NEWSBLUR.assets.preference('post_to_twitter')); + $facebook_button.toggleClass('NB-active', !!NEWSBLUR.assets.preference('post_to_facebook')); var $share_clone = $share.clone(); var full_height = $share_clone.css({ 'height': 'auto', @@ -135,10 +150,14 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ var comments = _.string.trim((options.source == 'menu' ? $comments_menu : $comments_sideoptions).val()); var feed = NEWSBLUR.assets.get_feed(NEWSBLUR.reader.active_feed); var source_user_id = feed && feed.get('user_id'); + var post_to_services = _.compact([ + NEWSBLUR.assets.preference('post_to_twitter') && 'twitter', + NEWSBLUR.assets.preference('post_to_facebook') && 'facebook' + ]); $share_button.addClass('NB-saving').addClass('NB-disabled').text('Sharing...'); $share_button_menu.addClass('NB-saving').addClass('NB-disabled').text('Sharing...'); - NEWSBLUR.assets.mark_story_as_shared(this.model.id, this.model.get('story_feed_id'), comments, source_user_id, _.bind(this.post_share_story, this, true), _.bind(function(data) { + NEWSBLUR.assets.mark_story_as_shared(this.model.id, this.model.get('story_feed_id'), comments, source_user_id, post_to_services, _.bind(this.post_share_story, this, true), _.bind(function(data) { this.post_share_error(data, true); }, this)); @@ -242,6 +261,30 @@ NEWSBLUR.Views.StoryShareView = Backbone.View.extend({ count_selected_words_when_sharing_story: function($feed_story) { var $wordcount = $('.NB-sideoption-share-wordcount', $feed_story); + }, + + toggle_twitter: function() { + var $twitter_button = this.$('.NB-sideoption-share-crosspost-twitter'); + + if (NEWSBLUR.assets.preference('post_to_twitter')) { + NEWSBLUR.assets.preference('post_to_twitter', false); + } else { + NEWSBLUR.assets.preference('post_to_twitter', true); + } + + $twitter_button.toggleClass('NB-active', NEWSBLUR.assets.preference('post_to_twitter')); + }, + + toggle_facebook: function() { + var $facebook_button = this.$('.NB-sideoption-share-crosspost-facebook'); + + if (NEWSBLUR.assets.preference('post_to_facebook')) { + NEWSBLUR.assets.preference('post_to_facebook', false); + } else { + NEWSBLUR.assets.preference('post_to_facebook', true); + } + + $facebook_button.toggleClass('NB-active', NEWSBLUR.assets.preference('post_to_facebook')); } }); \ No newline at end of file diff --git a/templates/mail/email_base.txt b/templates/mail/email_base.txt index 32b72825d..82b0959cd 100644 --- a/templates/mail/email_base.txt +++ b/templates/mail/email_base.txt @@ -1,16 +1,17 @@ {% block body %}{% endblock body %} -- Samuel Clay, @samuelclay +- Samuel & Roy ----------------------------------------------------------------------------- -Stay up to date and in touch with me, yr. developer, in a few different ways: +NewsBlur is your social news reader with intelligence. A new sound of an old instrument. + +Stay up to date and in touch with us, yr. developers, in a few different ways: - * Follow @samuelclay on Twitter: http://twitter.com/samuelclay/ * Follow @newsblur on Twitter: http://twitter.com/newsblur/ * Follow @samuelclay on GitHub: http://github.com/samuelclay/ -{% block resources_header %}There are a few resources you can use if you end up loving NewsBlur:{% endblock resources_header %} +{% block resources_header %}To get the most out of NewsBlur, here are a few resources:{% endblock resources_header %} * Read the NewsBlur Blog: http://blog.newsblur.com * Get support on NewsBlur's Get Satisfaction: http://getsatisfaction.com/newsblur/ diff --git a/templates/mail/email_base.xhtml b/templates/mail/email_base.xhtml index 6e02eede6..5fa86ecec 100644 --- a/templates/mail/email_base.xhtml +++ b/templates/mail/email_base.xhtml @@ -20,7 +20,7 @@ {% block body %}{% endblock %} -

- Samuel Clay, @samuelclay

+

- Samuel & Roy

@@ -28,22 +28,22 @@
-

Stay up to date and in touch with me, yr. developer, in a few different ways:

+

NewsBlur is your social news reader with intelligence. A new sound of an old instrument.

+

Stay up to date and in touch with us, yr. developers, in a few different ways:

-

{% block resources_header %}There are a couple resources you can use if you end up loving NewsBlur:{% endblock resources_header %}

+

{% block resources_header %}To get the most out of NewsBlur, here are a few resources:{% endblock resources_header %}

-

There's plenty of ways to use NewsBlur beyond the website:

+

There's plenty of ways to use NewsBlur beyond the web:

  • Download the free iPhone App.
  • diff --git a/templates/mail/email_new_account.xhtml b/templates/mail/email_new_account.xhtml index a0ac85029..0f0a85758 100644 --- a/templates/mail/email_new_account.xhtml +++ b/templates/mail/email_new_account.xhtml @@ -6,3 +6,5 @@

    Spend a few days trying out NewsBlur. I hope you end up loving it.

    If you really do love using NewsBlur you should purchase a fancy premium account for only $12/year (nights and weekends and all major public holidays included). You get to support an independent developer, help feed his dog, and contribute to the long-term health of NewsBlur.

    {% endblock %} + +{% block resources_header %}There are a couple resources you can use if you end up loving NewsBlur:{% endblock resources_header %} \ No newline at end of file diff --git a/templates/static/api.yml b/templates/static/api.yml index 6adf106e7..0906e848a 100644 --- a/templates/static/api.yml +++ b/templates/static/api.yml @@ -337,6 +337,10 @@ user_id of the original sharer. optional: true example: "128" + - key: post_to_services + desc: "List of services to cross-post to. Can be 'twitter' and/or 'facebook'." + optional: true + example: "['twitter', 'facebook']" - url: /social/unshare_story method: POST short_desc: "Remove a shared story from user's blurblog." diff --git a/utils/story_functions.py b/utils/story_functions.py index 656533a0e..3f1690c4d 100644 --- a/utils/story_functions.py +++ b/utils/story_functions.py @@ -146,7 +146,6 @@ class bunch(dict): else: self.__setitem__(item, value) - class MLStripper(HTMLParser): def __init__(self): self.reset() @@ -159,4 +158,16 @@ class MLStripper(HTMLParser): def strip_tags(html): s = MLStripper() s.feed(html) - return s.get_data() \ No newline at end of file + return s.get_data() + +def truncate_chars(value, max_length): + if len(value) <= max_length: + return value + + truncd_val = value[:max_length] + if value[max_length] != " ": + rightmost_space = truncd_val.rfind(" ") + if rightmost_space != -1: + truncd_val = truncd_val[:rightmost_space] + + return truncd_val + "..." \ No newline at end of file