Skip to content

Commit

Permalink
Adding avatar photo uploading.
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelclay committed Jan 8, 2013
1 parent 62597c6 commit 4dd1d10
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 11 deletions.
2 changes: 1 addition & 1 deletion apps/reader/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ def mark_story_ids_as_read(self, story_ids, request=None):

for story_id in set(story_ids):
try:
story = MStory.objects.get(story_feed_id=self.feed_id, story_guid=story_id)
story = MStory.get_story(story_feed_id=self.feed_id, story_guid=story_id)
except MStory.DoesNotExist:
# Story has been deleted, probably by feed_fetcher.
continue
Expand Down
8 changes: 7 additions & 1 deletion apps/rss_feeds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1401,7 +1401,7 @@ def get_data(cls, feed_id):

class MStory(mongo.Document):
'''A feed item'''
story_feed_id = mongo.IntField(unique_with='story_guid')
story_feed_id = mongo.IntField()
story_date = mongo.DateTimeField()
story_title = mongo.StringField(max_length=1024)
story_content = mongo.StringField()
Expand All @@ -1414,6 +1414,7 @@ class MStory(mongo.Document):
story_author_name = mongo.StringField()
story_permalink = mongo.StringField()
story_guid = mongo.StringField()
story_hash = mongo.StringField()
story_tags = mongo.ListField(mongo.StringField(max_length=250))
comment_count = mongo.IntField()
comment_user_ids = mongo.ListField(mongo.IntField())
Expand All @@ -1432,6 +1433,10 @@ class MStory(mongo.Document):
@property
def guid_hash(self):
return hashlib.sha1(self.story_guid).hexdigest()[:6]

@property
def feed_guid_hash(self):
return hashlib.sha1("%s:%s" % (self.story_feed_id, self.story_guid)).hexdigest()[:6]

def save(self, *args, **kwargs):
story_title_max = MStory._fields['story_title'].max_length
Expand All @@ -1449,6 +1454,7 @@ def save(self, *args, **kwargs):
self.story_title = self.story_title[:story_title_max]
if self.story_content_type and len(self.story_content_type) > story_content_type_max:
self.story_content_type = self.story_content_type[:story_content_type_max]

super(MStory, self).save(*args, **kwargs)

self.sync_redis()
Expand Down
19 changes: 19 additions & 0 deletions apps/social/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from utils.feed_functions import relative_timesince
from utils.story_functions import truncate_chars, strip_tags, linkify, image_size
from utils.scrubber import SelectiveScriptScrubber
from utils import s3_utils

RECOMMENDATIONS_LIMIT = 5
IGNORE_IMAGE_SOURCES = [
Expand Down Expand Up @@ -244,6 +245,8 @@ def large_photo_url(self):
return photo_url + '?type=large'
elif 'twimg' in photo_url:
return photo_url.replace('_normal', '')
elif '/avatars/' in photo_url:
return photo_url.replace('thumbnail_', 'large_')
return photo_url

@property
Expand Down Expand Up @@ -1991,6 +1994,22 @@ def get_user(cls, user_id):
def profile(cls, user_id):
profile = cls.get_user(user_id=user_id)
return profile.to_json()

def save_uploaded_photo(self, photo):
photo_body = photo.read()
filename = photo.name

s3 = s3_utils.S3Store()
image_name = s3.save_profile_picture(self.user_id, filename, photo_body)
if image_name:
self.upload_picture_url = "https://s3.amazonaws.com/%s/avatars/%s/thumbnail_%s" % (
settings.S3_AVATARS_BUCKET_NAME,
self.user_id,
image_name,
)
self.save()

return image_name and self.upload_picture_url

def twitter_api(self):
twitter_consumer_key = settings.TWITTER_CONSUMER_KEY
Expand Down
1 change: 1 addition & 0 deletions apps/social/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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'^upload_avatar/?', views.upload_avatar, name='upload-avatar'),
url(r'^save_blurblog_settings/?$', views.save_blurblog_settings, name='save-blurblog-settings'),
url(r'^interactions/?$', views.load_interactions, name='social-interactions'),
url(r'^activities/?$', views.load_activities, name='social-activities'),
Expand Down
19 changes: 18 additions & 1 deletion apps/social/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,24 @@ def save_user_profile(request):

return dict(code=1, user_profile=profile.to_json(include_follows=True))



@ajax_login_required
@json.json_view
def upload_avatar(request):
photo = request.FILES['photo']
profile = MSocialProfile.get_user(request.user.pk)
social_services = MSocialServices.objects.get(user_id=request.user.pk)
image_url = social_services.save_uploaded_photo(photo)
if image_url:
profile = social_services.set_photo('upload')

return {
"code": 1 if image_url else -1,
"uploaded": image_url,
"services": social_services,
"user_profile": profile.to_json(include_follows=True),
}

@ajax_login_required
@json.json_view
def save_blurblog_settings(request):
Expand Down
2 changes: 1 addition & 1 deletion media/css/reader.css
Original file line number Diff line number Diff line change
Expand Up @@ -7197,7 +7197,7 @@ form.opml_import_form input {

.NB-modal-preferences .NB-preferences-scroll {
overflow: auto;
max-height: 500px;
max-height: 600px;
width: 100%;
padding-right: 12px;
}
Expand Down
2 changes: 1 addition & 1 deletion media/ios/Entitlements.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<key>application-identifier</key>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
<key>get-task-allow</key>
<false/>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
Expand Down
1 change: 1 addition & 0 deletions media/js/newsblur/reader/reader_account.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
NEWSBLUR.ReaderAccount = function(options) {
var defaults = {
'width': 700,
'animate_email': false,
'change_password': false,
'onOpen': _.bind(function() {
Expand Down
2 changes: 1 addition & 1 deletion media/js/newsblur/reader/reader_preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ _.extend(NEWSBLUR.ReaderPreferences.prototype, {
},

resize_modal: function() {
var $scroll = $('.NB-preferences-scroll', this.$modal);
var $scroll = $('.NB-tab.NB-active', this.$modal);
var $modal = this.$modal;
var $modal_container = $modal.closest('.simplemodal-container');

Expand Down
79 changes: 76 additions & 3 deletions media/js/newsblur/reader/reader_profile_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
this.fetch_user_profile();

this.$modal.bind('click', $.rescope(this.handle_click, this));
this.$modal.bind('change', $.rescope(this.handle_change, this));
this.handle_profile_counts();
this.delegate_change();
},
Expand Down Expand Up @@ -217,7 +218,9 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
make_profile_photo_chooser: function() {
var $profiles = $('.NB-friends-profilephoto', this.$modal).empty();

_.each(['nothing', 'twitter', 'facebook', 'gravatar'], _.bind(function(service) {
$profiles.append($.make('div', { className: "NB-photo-upload-error NB-error" }));

_.each(['nothing', 'upload', 'twitter', 'facebook', 'gravatar'], _.bind(function(service) {
var $profile = $.make('div', { className: 'NB-friends-profile-photo-group NB-friends-photo-'+service }, [
$.make('div', { className: 'NB-friends-photo-title' }, [
$.make('input', { type: 'radio', name: 'profile_photo_service', value: service, id: 'NB-profile-photo-service-'+service }),
Expand All @@ -233,8 +236,10 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
])
]),
(service == 'upload' && $.make('div', { className: 'NB-photo-link' }, [
$.make('a', { href: '#', className: 'NB-photo-upload-link NB-splash-link' }, 'Upload picture'),
$.make('input', { type: 'file', name: 'photo' })
$.make('form', { method: 'post', enctype: 'multipart/form-data', encoding: 'multipart/form-data' }, [
$.make('a', { href: '#', className: 'NB-photo-upload-link NB-splash-link' }, 'upload picture'),
$.make('input', { type: 'file', name: 'photo', id: "NB-photo-upload-file", className: 'NB-photo-upload-file' })
])
])),
(service == 'gravatar' && $.make('div', { className: 'NB-gravatar-link' }, [
$.make('a', { href: 'http://www.gravatar.com', className: 'NB-splash-link', target: '_blank' }, 'gravatar.com')
Expand Down Expand Up @@ -463,6 +468,15 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
});
},

handle_change: function(elem, e) {
var self = this;
$.targetIs(e, { tagSelector: '.NB-photo-upload-file' }, function($t, $p) {
e.preventDefault();

self.handle_photo_upload();
});
},

handle_cancel: function() {
var $cancel = $('.NB-modal-cancel', this.$modal);

Expand Down Expand Up @@ -492,6 +506,65 @@ _.extend(NEWSBLUR.ReaderProfileEditor.prototype, {
});
},


handle_photo_upload: function() {
var self = this;
var $loading = $('.NB-modal-loading', this.$modal);
var $error = $('.NB-photo-upload-error', this.$modal);
var $file = $('.NB-photo-upload-file', this.$modal);

$error.slideUp(300);
$loading.addClass('NB-active');

var params = {
url: NEWSBLUR.URLs['upload-avatar'],
type: 'POST',
dataType: 'json',
success: _.bind(function(data, status) {
if (data.code < 0) {
this.error_uploading_photo();
} else {
$loading.removeClass('NB-active');
console.log(["success uploading", data, status, this]);
NEWSBLUR.assets.user_profile.set(data.user_profile);
this.services = data.services;
this.make_profile_section();
this.make_profile_photo_chooser();
}
}, this),
error: _.bind(this.error_uploading_photo, this),
cache: false,
contentType: false,
processData: false
};
if (window.FormData) {
var formData = new FormData($file.closest('form')[0]);
params['data'] = formData;

$.ajax(params);
} else {
// IE9 has no FormData
params['secureuri'] = false;
params['fileElementId'] = 'NB-photo-upload-file';
params['dataType'] = 'json';

$.ajaxFileUpload(params);
}

$file.replaceWith($file.clone());

return false;
},

error_uploading_photo: function() {
var $loading = $('.NB-modal-loading', this.$modal);
var $error = $('.NB-photo-upload-error', this.$modal);

$loading.removeClass('NB-active');
$error.text("There was a problem uploading your photo.");
$error.slideDown(300);
},

delegate_change: function() {
$('.NB-tab-profile', this.$modal).delegate('input[type=radio],input[type=checkbox],select', 'change', _.bind(this.enable_save_profile, this));
$('.NB-tab-profile', this.$modal).delegate('input[type=text]', 'keydown', _.bind(this.enable_save_profile, this));
Expand Down
1 change: 1 addition & 0 deletions settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ def allow_syncdb(self, db, model):
S3_BACKUP_BUCKET = 'newsblur_backups'
S3_PAGES_BUCKET_NAME = 'pages.newsblur.com'
S3_ICONS_BUCKET_NAME = 'icons.newsblur.com'
S3_AVATARS_BUCKET_NAME = 'avatars.newsblur.com'

# ==================
# = Configurations =
Expand Down
1 change: 1 addition & 0 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
};
NEWSBLUR.URLs = {
'google-reader-authorize' : "{% url google-reader-authorize %}",
'upload-avatar' : "{% url upload-avatar %}",
'opml-upload' : "{% url opml-upload %}",
'opml-export' : "{% url opml-export %}",
'domain' : "{% current_domain %}",
Expand Down
70 changes: 70 additions & 0 deletions utils/image_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Operations for images through the PIL."""

import Image
import ImageOps as PILOps
from ExifTags import TAGS
from StringIO import StringIO

PROFILE_PICTURE_SIZES = {
'fullsize': (256, 256),
'thumbnail': (64, 64)
}

class ImageOps:
"""Module that holds all image operations. Since there's no state,
everything is a classmethod."""

@classmethod
def resize_image(cls, image_body, size, fit_to_size=False):
"""Takes a raw image (in image_body) and resizes it to fit given
dimensions. Returns a file-like object in the form of a StringIO.
This must happen in this function because PIL is transforming the
original as it works."""

image_file = StringIO(image_body)
try:
image = Image.open(image_file)
except IOError:
# Invalid image file
return False

# Get the image format early, as we lose it after perform a `thumbnail` or `fit`.
format = image.format

# Check for rotation
image = cls.adjust_image_orientation(image)

if not fit_to_size:
image.thumbnail(PROFILE_PICTURE_SIZES[size], Image.ANTIALIAS)
else:
image = PILOps.fit(image, PROFILE_PICTURE_SIZES[size],
method=Image.ANTIALIAS,
centering=(0.5, 0.5))

output = StringIO()
if format.lower() == 'jpg':
format = 'jpeg'
image.save(output, format=format, quality=95)

return output

@classmethod
def adjust_image_orientation(cls, image):
"""Since the iPhone will store an image on its side but with EXIF
data stating that it should be rotated, we need to find that
EXIF data and correctly rotate the image before storage."""

if hasattr(image, '_getexif'):
exif = image._getexif()
if exif:
for tag, value in exif.items():
decoded = TAGS.get(tag, tag)
if decoded == 'Orientation':
if value == 6:
image = image.rotate(-90)
if value == 8:
image = image.rotate(90)
if value == 3:
image = image.rotate(180)
break
return image
Loading

0 comments on commit 4dd1d10

Please sign in to comment.