Adding avatar photo uploading.

This commit is contained in:
Samuel Clay 2013-01-08 14:11:59 -08:00
parent 62597c61e8
commit 4dd1d10d5a
14 changed files with 262 additions and 11 deletions

View file

@ -351,7 +351,7 @@ class UserSubscription(models.Model):
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

View file

@ -1401,7 +1401,7 @@ class MFeedPage(mongo.Document):
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()
@ -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())
@ -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
@ -1449,6 +1454,7 @@ class MStory(mongo.Document):
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()

View file

@ -32,6 +32,7 @@ from utils import json_functions as json
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 = [
@ -244,6 +245,8 @@ class MSocialProfile(mongo.Document):
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
@ -1991,6 +1994,22 @@ class MSocialServices(mongo.Document):
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

View file

@ -10,6 +10,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'^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'),

View file

@ -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):

View file

@ -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;
}

View file

@ -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>

View file

@ -1,5 +1,6 @@
NEWSBLUR.ReaderAccount = function(options) {
var defaults = {
'width': 700,
'animate_email': false,
'change_password': false,
'onOpen': _.bind(function() {

View file

@ -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');

View file

@ -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();
},
@ -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 }),
@ -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')
@ -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);
@ -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));

View file

@ -452,6 +452,7 @@ PROXY_S3_PAGES = True
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 =

View file

@ -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 %}",

70
utils/image_functions.py Normal file
View file

@ -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

View file

@ -1,7 +1,10 @@
from boto.s3.connection import S3Connection
from boto.s3.key import Key
import os
import sys
import time
import mimetypes
from boto.s3.connection import S3Connection
from boto.s3.key import Key
from utils.image_functions import ImageOps
if '/home/sclay/newsblur' not in ' '.join(sys.path):
sys.path.append("/home/sclay/newsblur")
@ -60,3 +63,62 @@ if __name__ == '__main__':
delete_all_backups()
else:
print 'Usage: %s <get/set/list/delete> <backup_filename>' % (sys.argv[0])
class S3Store:
def __init__(self, bucket_name=settings.S3_AVATARS_BUCKET_NAME):
self.s3 = S3Connection(ACCESS_KEY, SECRET)
self.bucket = self.create_bucket(bucket_name)
def create_bucket(self, bucket_name):
return self.s3.create_bucket(bucket_name)
def save_profile_picture(self, user_id, filename, image_body):
mimetype, extension = self._extract_mimetype(filename)
if not mimetype or not extension:
return
image_name = 'profile_%s.%s' % (int(time.time()), extension)
image = ImageOps.resize_image(image_body, 'fullsize', fit_to_size=False)
if image:
key = 'avatars/%s/large_%s' % (user_id, image_name)
self._save_object(key, image, mimetype=mimetype)
image = ImageOps.resize_image(image_body, 'thumbnail', fit_to_size=True)
if image:
key = 'avatars/%s/thumbnail_%s' % (user_id, image_name)
self._save_object(key, image, mimetype=mimetype)
return image and image_name
def _extract_mimetype(self, filename):
mimetype = mimetypes.guess_type(filename)[0]
extension = None
if mimetype == 'image/jpeg':
extension = 'jpg'
elif mimetype == 'image/png':
extension = 'png'
elif mimetype == 'image/gif':
extension = 'gif'
return mimetype, extension
def _make_key(self):
return Key(bucket=self.bucket)
def _save_object(self, key, file_object, mimetype=None):
k = self._make_key()
k.key = key
file_object.seek(0)
if mimetype:
k.set_contents_from_file(file_object, headers={
'Content-Type': mimetype,
})
else:
k.set_contents_from_file(file_object)
k.set_acl('public-read')