mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-05 16:58:59 +00:00
Adding avatar photo uploading.
This commit is contained in:
parent
62597c61e8
commit
4dd1d10d5a
14 changed files with 262 additions and 11 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
NEWSBLUR.ReaderAccount = function(options) {
|
||||
var defaults = {
|
||||
'width': 700,
|
||||
'animate_email': false,
|
||||
'change_password': false,
|
||||
'onOpen': _.bind(function() {
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
70
utils/image_functions.py
Normal 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
|
|
@ -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')
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue