diff --git a/app/assets/javascripts/discourse/views/modal/avatar_selector_view.js b/app/assets/javascripts/discourse/views/modal/avatar_selector_view.js
index 54f8e8eb3a9..94d73a8ab93 100644
--- a/app/assets/javascripts/discourse/views/modal/avatar_selector_view.js
+++ b/app/assets/javascripts/discourse/views/modal/avatar_selector_view.js
@@ -33,9 +33,10 @@ Discourse.AvatarSelectorView = Discourse.ModalBodyView.extend({
// define the upload endpoint
$upload.fileupload({
- url: Discourse.getURL("/users/" + this.get("controller.username") + "/preferences/avatar"),
+ url: Discourse.getURL("/users/" + this.get("controller.username") + "/preferences/user_image"),
dataType: "json",
- fileInput: $upload
+ fileInput: $upload,
+ formData: { user_image_type: "avatar" }
});
// when a file has been selected
diff --git a/app/assets/javascripts/discourse/views/user/preferences_view.js b/app/assets/javascripts/discourse/views/user/preferences_view.js
index 1652af24c06..96d9062f324 100644
--- a/app/assets/javascripts/discourse/views/user/preferences_view.js
+++ b/app/assets/javascripts/discourse/views/user/preferences_view.js
@@ -8,7 +8,48 @@
**/
Discourse.PreferencesView = Discourse.View.extend({
templateName: 'user/preferences',
- classNames: ['user-preferences']
+ classNames: ['user-preferences'],
+
+ uploading: false,
+ uploadProgress: 0,
+
+ didInsertElement: function() {
+ var self = this;
+ var $upload = $("#profile-background-input");
+
+ this._super();
+
+ $upload.fileupload({
+ url: Discourse.getURL("/users/" + this.get('controller.model.username') + "/preferences/user_image"),
+ dataType: "json",
+ fileInput: $upload,
+ formData: { user_image_type: "profile_background" }
+ });
+
+ $upload.on("fileuploadadd", function() {
+ self.set("uploading", true);
+ });
+ $upload.on("fileuploadprogressall", function(e, data) {
+ var progress = parseInt(data.loaded / data.total * 100, 10);
+ self.set("uploadProgress", progress);
+ });
+ $upload.on("fileuploaddone", function(e, data) {
+ if(data.result.url) {
+ self.set("controller.model.profile_background", data.result.url);
+ } else {
+ bootbox.alert(I18n.t('post.errors.upload'));
+ }
+ });
+ $upload.on("fileuploadfail", function(e, data) {
+ Discourse.Utilities.displayErrorForUpload(data);
+ });
+ $upload.on("fileuploadalways", function() {
+ self.setProperties({ uploading: false, uploadProgress: 0});
+ });
+ },
+ willDestroyElement: function() {
+ $("#profile-background-input").fileupload("destroy");
+ }
});
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index 4cee7f51cd9..7d55f3aa7d1 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -40,6 +40,19 @@
width: 440px;
}
}
+
+ #profile-background-preview {
+ height: 270px;
+
+ background-position: center center;
+ background-size: cover;
+
+ background-color: $secondary_background_color;
+ }
+
+ #profile-background-controls {
+ padding: 10px;
+ }
.static {
color: $primary_text_color;
@@ -174,6 +187,9 @@
.about {
background-color: $secondary_background_color;
+ background-size: cover;
+ background-position: center center;
+
margin-bottom: 10px;
overflow: hidden;
color: $secondary_text_color;
diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss
index cf7fbc601a5..d3da6e2eb12 100644
--- a/app/assets/stylesheets/mobile/user.scss
+++ b/app/assets/stylesheets/mobile/user.scss
@@ -64,6 +64,13 @@
width: 100%;
padding: 5px 8px;
}
+
+ #profile-background-preview {
+ height: 150px;
+ background-position: center center;
+ background-size: cover;
+ background-color: $secondary_background_color;
+ }
}
#about-me {
@@ -120,6 +127,8 @@
.about {
background-color: $secondary_background_color;
+ background-position: center center;
+ background-size: cover;
margin-bottom: 10px;
overflow: hidden;
color: $primary_text_color;
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 05b23328e9f..7b42a4ece66 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -7,7 +7,7 @@ class UsersController < ApplicationController
skip_before_filter :authorize_mini_profiler, only: [:avatar]
skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :activate_account, :authorize_email, :user_preferences_redirect, :avatar]
- before_filter :ensure_logged_in, only: [:username, :update, :change_email, :user_preferences_redirect, :upload_avatar, :toggle_avatar, :destroy]
+ before_filter :ensure_logged_in, only: [:username, :update, :change_email, :user_preferences_redirect, :upload_user_image, :toggle_avatar, :clear_profile_background, :destroy]
before_filter :respond_to_suspicious_request, only: [:create]
# we need to allow account creation with bad CSRF tokens, if people are caching, the CSRF token on the
@@ -297,8 +297,16 @@ class UsersController < ApplicationController
size = 128 if size > 128
size
end
-
+
def upload_avatar
+ params[:user_image_type] = "avatar"
+
+ upload_user_image
+
+ end
+
+ def upload_user_image
+ params.require(:user_image_type)
user = fetch_user_from_params
guardian.ensure_can_edit!(user)
@@ -308,17 +316,26 @@ class UsersController < ApplicationController
# TODO: Does not protect from huge uploads
# https://github.com/discourse/discourse/pull/1512
# check the file size (note: this might also be done in the web server)
- avatar = build_avatar_from(file)
- avatar_policy = AvatarUploadPolicy.new(avatar)
+ img = build_user_image_from(file)
+ upload_policy = AvatarUploadPolicy.new(img)
- if avatar_policy.too_big?
+ if upload_policy.too_big?
return render status: 413, text: I18n.t("upload.images.too_large",
- max_size_kb: avatar_policy.max_size_kb)
+ max_size_kb: upload_policy.max_size_kb)
end
- raise FastImage::UnknownImageType unless SiteSetting.authorized_image?(avatar.file)
-
- upload_avatar_for(user, avatar)
+ raise FastImage::UnknownImageType unless SiteSetting.authorized_image?(img.file)
+
+ upload_type = params[:user_image_type]
+
+ if upload_type == "avatar"
+ upload_avatar_for(user, img)
+ elsif upload_type == "profile_background"
+ upload_profile_background_for(user, img)
+ else
+ render status: 422, text: ""
+ end
+
rescue Discourse::InvalidParameters
render status: 422, text: I18n.t("upload.images.unknown_image_type")
@@ -340,7 +357,17 @@ class UsersController < ApplicationController
render nothing: true
end
-
+
+ def clear_profile_background
+ user = fetch_user_from_params
+ guardian.ensure_can_edit!(user)
+
+ user.profile_background = ""
+ user.save!
+
+ render nothing: true
+ end
+
def destroy
@user = fetch_user_from_params
guardian.ensure_can_delete_user!(@user)
@@ -364,7 +391,7 @@ class UsersController < ApplicationController
challenge
end
- def build_avatar_from(file)
+ def build_user_image_from(file)
source = if file.is_a?(String)
is_api? ? :url : (raise FastImage::UnknownImageType)
else
@@ -380,7 +407,17 @@ class UsersController < ApplicationController
Jobs.enqueue(:generate_avatars, user_id: user.id, upload_id: upload.id)
render json: { url: upload.url, width: upload.width, height: upload.height }
end
-
+
+ def upload_profile_background_for(user, background)
+ upload = Upload.create_for(user.id, background.file, background.filesize)
+ user.profile_background = upload.url
+ user.save!
+
+ # TODO: maybe add a resize job here
+
+ render json: { url: upload.url, width: upload.width, height: upload.height }
+ end
+
def respond_to_suspicious_request
if suspicious?(params)
render(
diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb
index 219492fa932..503662b0418 100644
--- a/app/jobs/scheduled/clean_up_uploads.rb
+++ b/app/jobs/scheduled/clean_up_uploads.rb
@@ -8,11 +8,13 @@ module Jobs
uploads_used_in_posts = PostUpload.uniq.pluck(:upload_id)
uploads_used_as_avatars = User.uniq.where('uploaded_avatar_id IS NOT NULL').pluck(:uploaded_avatar_id)
-
+ uploads_used_as_profile_backgrounds = User.uniq.where("profile_background IS NOT NULL AND profile_background != ''").pluck(:profile_background)
+
grace_period = [SiteSetting.clean_orphan_uploads_grace_period_hours, 1].max
Upload.where("created_at < ?", grace_period.hour.ago)
.where("id NOT IN (?)", uploads_used_in_posts + uploads_used_as_avatars)
+ .where("url NOT IN (?)", uploads_used_as_profile_backgrounds)
.find_each do |upload|
upload.destroy
end
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 4b55a2fd9e4..1d47e991685 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -8,6 +8,7 @@ class UserSerializer < BasicUserSerializer
:bio_cooked,
:created_at,
:website,
+ :profile_background,
:can_edit,
:can_edit_username,
:can_edit_email,
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 14dc7e1300b..76f5166ec45 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -297,6 +297,9 @@ en:
uploaded_avatar_empty: "Add a custom picture"
upload_title: "Upload your picture"
image_is_not_a_square: "Warning: we've cropped your image; it is not square."
+
+ change_profile_background:
+ title: "Change Profile Background"
email:
title: "Email"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index fe50842f556..949b19a626e 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -809,7 +809,9 @@ en:
detect_custom_avatars: "Whether or not to check that users have uploaded custom avatars"
max_daily_gravatar_crawls: "The maximum amount of times Discourse will check gravatar for custom avatars in a day"
-
+
+ allow_profile_backgrounds: "Allows users to upload profile backgrounds"
+
sequential_replies_threshold: "The amount of posts a user has to make in a row in a topic before being notified"
enable_mobile_theme: "Mobile devices use a mobile-friendly theme, with the ability to switch to the full site. Disable this if you want to use a custom stylesheet that is fully responsive."
diff --git a/config/routes.rb b/config/routes.rb
index d42072a6e37..bdcec4a48f7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -181,7 +181,9 @@ Discourse::Application.routes.draw do
put "users/:username/preferences/username" => "users#username", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/avatar(/:size)" => "users#avatar", constraints: {username: USERNAME_ROUTE_FORMAT} # LEGACY ROUTE
post "users/:username/preferences/avatar" => "users#upload_avatar", constraints: {username: USERNAME_ROUTE_FORMAT}
+ post "users/:username/preferences/user_image" => "users#upload_user_image", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/avatar/toggle" => "users#toggle_avatar", constraints: {username: USERNAME_ROUTE_FORMAT}
+ put "users/:username/preferences/profile_background/clear" => "users#clear_profile_background", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/invited" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT}
post "users/:username/send_activation_email" => "users#send_activation_email", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
diff --git a/config/site_settings.yml b/config/site_settings.yml
index ef00962fb4a..c2abbb74053 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -270,6 +270,9 @@ files:
default: ''
enum: 'S3RegionSiteSetting'
s3_upload_bucket: ''
+ allow_profile_backgrounds:
+ client: true
+ default: true
allow_uploaded_avatars:
client: true
default: true
diff --git a/db/migrate/20140224232712_add_profile_background_to_user.rb b/db/migrate/20140224232712_add_profile_background_to_user.rb
new file mode 100644
index 00000000000..edf46f5932f
--- /dev/null
+++ b/db/migrate/20140224232712_add_profile_background_to_user.rb
@@ -0,0 +1,5 @@
+class AddProfileBackgroundToUser < ActiveRecord::Migration
+ def change
+ add_column :users, :profile_background, :string, limit: 255
+ end
+end
\ No newline at end of file
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 2751ed39995..09a3589d555 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -1092,55 +1092,65 @@ describe UsersController do
end
end
- describe '.upload_avatar' do
+ describe '.upload_user_image' do
it 'raises an error when not logged in' do
- lambda { xhr :put, :upload_avatar, username: 'asdf' }.should raise_error(Discourse::NotLoggedIn)
+ lambda { xhr :put, :upload_user_image, username: 'asdf' }.should raise_error(Discourse::NotLoggedIn)
end
+
context 'while logged in' do
let!(:user) { log_in }
- let(:avatar) do
+ let(:user_image) do
ActionDispatch::Http::UploadedFile.new({
filename: 'logo.png',
tempfile: File.new("#{Rails.root}/spec/fixtures/images/logo.png")
})
end
+
+ it 'raises an error without a user_image_type param' do
+ lambda { xhr :put, :upload_user_image, username: user.username }.should raise_error(ActionController::ParameterMissing)
+ end
describe "with uploaded file" do
-
- it 'raises an error when you don\'t have permission to upload an avatar' do
+
+ it 'raises an error when you don\'t have permission to upload an user image' do
Guardian.any_instance.expects(:can_edit?).with(user).returns(false)
- xhr :post, :upload_avatar, username: user.username
+ xhr :post, :upload_user_image, username: user.username, user_image_type: "avatar"
response.should be_forbidden
end
it 'rejects large images' do
AvatarUploadPolicy.any_instance.stubs(:too_big?).returns(true)
- xhr :post, :upload_avatar, username: user.username, file: avatar
+ xhr :post, :upload_user_image, username: user.username, file: user_image, user_image_type: "avatar"
response.status.should eq 413
end
it 'rejects unauthorized images' do
SiteSetting.stubs(:authorized_image?).returns(false)
- xhr :post, :upload_avatar, username: user.username, file: avatar
+ xhr :post, :upload_user_image, username: user.username, file: user_image, user_image_type: "avatar"
+ response.status.should eq 422
+ end
+
+ it 'rejects requests with unknown user_image_type' do
+ xhr :post, :upload_user_image, username: user.username, file: user_image, user_image_type: "asdf"
response.status.should eq 422
end
- it 'is successful' do
+ it 'is successful for avatars' do
upload = Fabricate(:upload)
Upload.expects(:create_for).returns(upload)
- # enqueues the avatar generator job
+ # enqueues the user_image generator job
Jobs.expects(:enqueue).with(:generate_avatars, { user_id: user.id, upload_id: upload.id })
- xhr :post, :upload_avatar, username: user.username, file: avatar
+ xhr :post, :upload_user_image, username: user.username, file: user_image, user_image_type: "avatar"
user.reload
# erase the previous template
user.uploaded_avatar_template.should == nil
# link to the right upload
user.uploaded_avatar.id.should == upload.id
- # automatically set "use_uploaded_avatar"
+ # automatically set "use_uploaded_user_image"
user.use_uploaded_avatar.should == true
# returns the url, width and height of the uploaded image
json = JSON.parse(response.body)
@@ -1148,10 +1158,26 @@ describe UsersController do
json['width'].should == 100
json['height'].should == 200
end
+
+ it 'is successful for profile backgrounds' do
+ upload = Fabricate(:upload)
+ Upload.expects(:create_for).returns(upload)
+ xhr :post, :upload_user_image, username: user.username, file: user_image, user_image_type: "profile_background"
+ user.reload
+
+ user.profile_background.should == "/uploads/default/1/1234567890123456.jpg"
+
+ # returns the url, width and height of the uploaded image
+ json = JSON.parse(response.body)
+ json['url'].should == "/uploads/default/1/1234567890123456.jpg"
+ json['width'].should == 100
+ json['height'].should == 200
+ end
+
end
describe "with url" do
- let(:avatar_url) { "http://cdn.discourse.org/assets/logo.png" }
+ let(:user_image_url) { "http://cdn.discourse.org/assets/logo.png" }
before :each do
UsersController.any_instance.stubs(:is_api?).returns(true)
@@ -1161,40 +1187,63 @@ describe UsersController do
before :each do
UriAdapter.any_instance.stubs(:open).returns StringIO.new(fixture_file("images/logo.png"))
end
-
+
it 'rejects large images' do
AvatarUploadPolicy.any_instance.stubs(:too_big?).returns(true)
- xhr :post, :upload_avatar, username: user.username, file: avatar_url
+ xhr :post, :upload_user_image, username: user.username, file: user_image_url, user_image_type: "profile_background"
response.status.should eq 413
end
it 'rejects unauthorized images' do
SiteSetting.stubs(:authorized_image?).returns(false)
- xhr :post, :upload_avatar, username: user.username, file: avatar_url
+ xhr :post, :upload_user_image, username: user.username, file: user_image_url, user_image_type: "profile_background"
+ response.status.should eq 422
+ end
+
+ it 'rejects requests with unknown user_image_type' do
+ xhr :post, :upload_user_image, username: user.username, file: user_image_url, user_image_type: "asdf"
response.status.should eq 422
end
- it 'is successful' do
+ it 'is successful for avatars' do
upload = Fabricate(:upload)
Upload.expects(:create_for).returns(upload)
- # enqueues the avatar generator job
+ # enqueues the user_image generator job
Jobs.expects(:enqueue).with(:generate_avatars, { user_id: user.id, upload_id: upload.id })
- xhr :post, :upload_avatar, username: user.username, file: avatar_url
+ xhr :post, :upload_avatar, username: user.username, file: user_image_url, user_image_type: "avatar"
user.reload
+ # erase the previous template
user.uploaded_avatar_template.should == nil
+ # link to the right upload
user.uploaded_avatar.id.should == upload.id
+ # automatically set "use_uploaded_user_image"
user.use_uploaded_avatar.should == true
-
# returns the url, width and height of the uploaded image
json = JSON.parse(response.body)
json['url'].should == "/uploads/default/1/1234567890123456.jpg"
json['width'].should == 100
json['height'].should == 200
end
+
+ it 'is successful for profile backgrounds' do
+ upload = Fabricate(:upload)
+ Upload.expects(:create_for).returns(upload)
+ xhr :post, :upload_user_image, username: user.username, file: user_image_url, user_image_type: "profile_background"
+ user.reload
+
+ user.profile_background.should == "/uploads/default/1/1234567890123456.jpg"
+
+ # returns the url, width and height of the uploaded image
+ json = JSON.parse(response.body)
+ json['url'].should == "/uploads/default/1/1234567890123456.jpg"
+ json['width'].should == 100
+ json['height'].should == 200
+ end
+
end
it "should handle malformed urls" do
- xhr :post, :upload_avatar, username: user.username, file: "foobar"
+ xhr :post, :upload_user_image, username: user.username, file: "foobar", user_image_type: "profile_background"
response.status.should eq 422
end
@@ -1233,6 +1282,32 @@ describe UsersController do
end
end
+
+ describe '.clear_profile_background' do
+
+ it 'raises an error when not logged in' do
+ lambda { xhr :put, :clear_profile_background, username: 'asdf' }.should raise_error(Discourse::NotLoggedIn)
+ end
+
+ context 'while logged in' do
+
+ let!(:user) { log_in }
+
+ it 'raises an error when you don\'t have permission to clear the profile background' do
+ Guardian.any_instance.expects(:can_edit?).with(user).returns(false)
+ xhr :put, :clear_profile_background, username: user.username
+ response.should be_forbidden
+ end
+
+ it 'it successful' do
+ xhr :put, :clear_profile_background, username: user.username
+ user.reload.profile_background.should == ""
+ response.should be_success
+ end
+
+ end
+
+ end
describe '.destroy' do
it 'raises an error when not logged in' do