diff --git a/app/assets/javascripts/discourse/components/image-uploader.js.es6 b/app/assets/javascripts/discourse/components/image-uploader.js.es6 index 52b1112ee..4173886eb 100644 --- a/app/assets/javascripts/discourse/components/image-uploader.js.es6 +++ b/app/assets/javascripts/discourse/components/image-uploader.js.es6 @@ -13,7 +13,19 @@ export default Em.Component.extend(UploadMixin, { this.set('imageUrl', data.result.url); }, - deleteDone: function() { - this.set('imageUrl', null); + actions: { + trash: function() { + this.set('imageUrl', null); + + // Do we want to signal the delete to the server right away? + if (this.get('instantDelete')) { + Discourse.ajax(this.get('uploadUrl'), { + type: 'DELETE', + data: { image_type: this.get('type') } + }).then(null, function() { + bootbox.alert(I18n.t('generic_error')); + }); + } + } } }); diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 index eef1108be..30006d67a 100644 --- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 @@ -9,6 +9,7 @@ **/ export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, { foregroundColors: ['FFFFFF', '000000'], + categoryUploadUrl: '/category/uploads', parentCategories: function() { return Discourse.Category.list().filter(function (c) { diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6 index 9560c4ff7..dc612dd89 100644 --- a/app/assets/javascripts/discourse/mixins/upload.js.es6 +++ b/app/assets/javascripts/discourse/mixins/upload.js.es6 @@ -53,18 +53,6 @@ export default Em.Mixin.create({ actions: { selectFile: function() { this.$('input[type=file]').click(); - }, - - trash: function() { - var self = this; - Discourse.ajax(this.get('uploadUrl'), { - type: 'DELETE', - data: { image_type: this.get('type') } - }).then(function() { - self.deleteDone(); - }).catch(function() { - bootbox.alert(I18n.t('generic_error')); - }); } } }); diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js index 9687dfb85..b0382abd7 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js @@ -68,7 +68,9 @@ Discourse.Category = Discourse.Model.extend({ position: this.get('position'), email_in: this.get('email_in'), email_in_allow_strangers: this.get('email_in_allow_strangers'), - parent_category_id: this.get('parent_category_id') + parent_category_id: this.get('parent_category_id'), + logo_url: this.get('logo_url'), + background_url: this.get('background_url') }, type: this.get('id') ? 'PUT' : 'POST' }); diff --git a/app/assets/javascripts/discourse/templates/modal/edit-category-images.js.handlebars b/app/assets/javascripts/discourse/templates/modal/edit-category-images.js.handlebars new file mode 100644 index 000000000..73fd9cf8f --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/edit-category-images.js.handlebars @@ -0,0 +1,9 @@ +
+ + {{image-uploader uploadUrl=categoryUploadUrl imageUrl=logo_url type="logo"}} +
+ +
+ + {{image-uploader uploadUrl=categoryUploadUrl imageUrl=background_url type="background"}} +
diff --git a/app/assets/javascripts/discourse/templates/user/preferences.js.handlebars b/app/assets/javascripts/discourse/templates/user/preferences.js.handlebars index ff2d172c4..a81ee7b3e 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/preferences.js.handlebars @@ -94,6 +94,7 @@
{{image-uploader uploadUrl=imageUploadUrl imageUrl=profile_background + instantDelete="true" type="profile_background"}}
diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss index 3ac8fc3b6..b59182870 100644 --- a/app/assets/stylesheets/desktop/modal.scss +++ b/app/assets/stylesheets/desktop/modal.scss @@ -186,3 +186,9 @@ animation: modal .25s; .uploaded-avatar { margin-top: 20px; } + +.uploaded-image-preview { + width: 400px; + max-height: 150px; + margin-bottom: 10px; +} diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 088645367..a168729d9 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -26,6 +26,19 @@ class CategoriesController < ApplicationController end end + def upload + params.require(:image_type) + guardian.ensure_can_create!(Category) + + file = params[:file] || params[:files].first + upload = Upload.create_for(current_user.id, file.tempfile, file.original_filename, File.size(file.tempfile)) + if upload.errors.blank? + render json: { url: upload.url, width: upload.width, height: upload.height } + else + render status: 422, text: upload.errors.full_messages + end + end + def move guardian.ensure_can_create!(Category) @@ -105,7 +118,7 @@ class CategoriesController < ApplicationController end end - params.permit(*required_param_keys, :position, :email_in, :email_in_allow_strangers, :parent_category_id, :auto_close_hours, :permissions => [*p.try(:keys)]) + params.permit(*required_param_keys, :position, :email_in, :email_in_allow_strangers, :parent_category_id, :auto_close_hours, :logo_url, :background_url, :permissions => [*p.try(:keys)]) end end diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb index 719656077..b04fda1f8 100644 --- a/app/jobs/scheduled/clean_up_uploads.rb +++ b/app/jobs/scheduled/clean_up_uploads.rb @@ -6,7 +6,11 @@ module Jobs def execute(args) return unless SiteSetting.clean_up_uploads? - uploads_used_as_profile_backgrounds = UserProfile.uniq.where("profile_background IS NOT NULL AND profile_background != ''").pluck(:profile_background) + ignore_urls = [] + ignore_urls << UserProfile.uniq.where("profile_background IS NOT NULL AND profile_background != ''").pluck(:profile_background) + ignore_urls << Category.uniq.where("logo_url IS NOT NULL AND logo_url != ''").pluck(:logo_url) + ignore_urls << Category.uniq.where("background_url IS NOT NULL AND background_url != ''").pluck(:background_url) + ignore_urls.flatten! grace_period = [SiteSetting.clean_orphan_uploads_grace_period_hours, 1].max @@ -14,7 +18,7 @@ module Jobs .where("id NOT IN (SELECT upload_id from post_uploads)") .where("id NOT IN (SELECT custom_upload_id from user_avatars)") .where("id NOT IN (SELECT gravatar_upload_id from user_avatars)") - .where("url NOT IN (?)", uploads_used_as_profile_backgrounds) + .where("url NOT IN (?)", ignore_urls) .find_each do |upload| upload.destroy end diff --git a/app/serializers/basic_category_serializer.rb b/app/serializers/basic_category_serializer.rb index 166f8d59b..3ca2ffa13 100644 --- a/app/serializers/basic_category_serializer.rb +++ b/app/serializers/basic_category_serializer.rb @@ -12,7 +12,9 @@ class BasicCategorySerializer < ApplicationSerializer :read_restricted, :permission, :parent_category_id, - :notification_level + :notification_level, + :logo_url, + :background_url def include_parent_category_id? parent_category_id diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 214826e66..547fa6b7e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1146,6 +1146,8 @@ en: name: "Category Name" description: "Description" topic: "category topic" + logo: "Category Logo Image" + background_image: "Category Background Image" badge_colors: "Badge colors" background_color: "Background color" foreground_color: "Foreground color" diff --git a/config/routes.rb b/config/routes.rb index 333cfb922..ef282eabb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -275,6 +275,7 @@ Discourse::Application.routes.draw do resources :categories, :except => :show get "category/:id/show" => "categories#show" + post "category/uploads" => "categories#upload" post "category/:category_id/move" => "categories#move" get "category/:category.rss" => "list#category_feed", format: :rss get "category/:parent_category/:category.rss" => "list#category_feed", format: :rss diff --git a/db/migrate/20140627193814_add_images_to_categories.rb b/db/migrate/20140627193814_add_images_to_categories.rb new file mode 100644 index 000000000..58e4cce73 --- /dev/null +++ b/db/migrate/20140627193814_add_images_to_categories.rb @@ -0,0 +1,6 @@ +class AddImagesToCategories < ActiveRecord::Migration + def change + add_column :categories, :logo_url, :string + add_column :categories, :background_url, :string + end +end diff --git a/spec/controllers/categories_controller_spec.rb b/spec/controllers/categories_controller_spec.rb index 3ccc461b8..030c7049f 100644 --- a/spec/controllers/categories_controller_spec.rb +++ b/spec/controllers/categories_controller_spec.rb @@ -95,6 +95,37 @@ describe CategoriesController do end + describe "upload" do + it "requires the user to be logged in" do + lambda { xhr :post, :upload, image_type: 'logo'}.should raise_error(Discourse::NotLoggedIn) + end + + describe "logged in" do + let!(:user) { log_in(:admin) } + + let(:logo) { File.new("#{Rails.root}/spec/fixtures/images/logo.png") } + let(:upload) do + ActionDispatch::Http::UploadedFile.new({ filename: 'logo.png', tempfile: logo }) + end + + it "raises an error when you don't have permission to upload" do + Guardian.any_instance.expects(:can_create?).with(Category).returns(false) + xhr :post, :upload, image_type: 'logo', file: upload + response.should be_forbidden + end + + it "requires the `image_type` param" do + -> { xhr :post, :upload }.should raise_error(ActionController::ParameterMissing) + end + + it "calls Upload.create_for" do + Upload.expects(:create_for).returns(Upload.new) + xhr :post, :upload, image_type: 'logo', file: upload + response.should be_success + end + end + end + describe "update" do it "requires the user to be logged in" do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 3e73192c1..4c6e9d49d 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -1272,7 +1272,7 @@ describe UsersController do describe '.destroy_user_image' do it 'raises an error when not logged in' do - lambda { xhr :put, :destroy_user_image, type: 'profile_background', username: 'asdf' }.should raise_error(Discourse::NotLoggedIn) + lambda { xhr :delete, :destroy_user_image, type: 'profile_background', username: 'asdf' }.should raise_error(Discourse::NotLoggedIn) end context 'while logged in' do @@ -1281,20 +1281,20 @@ describe UsersController do 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, :destroy_user_image, username: user.username, image_type: 'profile_background' + xhr :delete, :destroy_user_image, username: user.username, image_type: 'profile_background' response.should be_forbidden end it "requires the `image_type` param" do - -> { xhr :put, :destroy_user_image, username: user.username }.should raise_error(ActionController::ParameterMissing) + -> { xhr :delete, :destroy_user_image, username: user.username }.should raise_error(ActionController::ParameterMissing) end it "only allows certain `image_types`" do - -> { xhr :put, :destroy_user_image, username: user.username, image_type: 'wat' }.should raise_error(Discourse::InvalidParameters) + -> { xhr :delete, :destroy_user_image, username: user.username, image_type: 'wat' }.should raise_error(Discourse::InvalidParameters) end it 'can clear the profile background' do - xhr :put, :destroy_user_image, image_type: 'profile_background', username: user.username + xhr :delete, :destroy_user_image, image_type: 'profile_background', username: user.username user.reload.user_profile.profile_background.should == "" response.should be_success end