From 42bac7f9f27b0e8fb113e452fc2106882262172d Mon Sep 17 00:00:00 2001 From: Steven Thonus Date: Sat, 25 Jan 2014 18:15:44 +0100 Subject: [PATCH] adding avatar to project settings page added avatar removal show project avatar on dashboard, projects page, project page added rspec and feature tests added project avatar from repository new default project icon added added copying af avatar to forking of project added generated icon fixed avatar fork hound fix style fix test fix --- app/assets/images/no_project_icon.png | Bin 0 -> 3387 bytes app/assets/javascripts/project.js.coffee | 10 ++++ app/assets/stylesheets/generic/avatar.scss | 13 +++++ .../stylesheets/sections/dashboard.scss | 6 +++ .../projects/avatars_controller.rb | 29 +++++++++++ app/helpers/application_helper.rb | 25 ++++++++++ app/models/project.rb | 25 ++++++++++ app/services/projects/fork_service.rb | 3 ++ app/views/dashboard/_project.html.haml | 2 + app/views/dashboard/projects.html.haml | 2 + app/views/projects/_home_panel.html.haml | 1 + app/views/projects/edit.html.haml | 28 ++++++++++- config/routes.rb | 2 + .../20140125162722_add_avatar_to_projects.rb | 5 ++ db/schema.rb | 1 + features/project/project.feature | 13 +++++ features/steps/project/project.rb | 47 ++++++++++++++++-- spec/helpers/application_helper_spec.rb | 22 ++++++++ spec/models/project_spec.rb | 15 ++++++ spec/routing/project_routing_spec.rb | 7 +++ 20 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 app/assets/images/no_project_icon.png create mode 100644 app/controllers/projects/avatars_controller.rb create mode 100644 db/migrate/20140125162722_add_avatar_to_projects.rb diff --git a/app/assets/images/no_project_icon.png b/app/assets/images/no_project_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8e9529c67ec3a7167c59f2c52bdb4b009b9803cf GIT binary patch literal 3387 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 + form = $(this).closest("form") + form.find(".js-project-avatar-input").click() + + $('.js-project-avatar-input').bind "change", -> + form = $(this).closest("form") + filename = $(this).val().replace(/^.*[\\\/]/, '') + form.find(".js-avatar-filename").text(filename) diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index 80514615559..f04848ae6dc 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -23,3 +23,16 @@ &.s90 { width: 90px; height: 90px; margin-right: 15px; } &.s160 { width: 160px; height: 160px; margin-right: 20px; } } + +.identicon { + text-align: center; + vertical-align: top; + + &.s16 { font-size: 12px; line-height: 1.33; } + &.s24 { font-size: 18px; line-height: 1.33; } + &.s26 { font-size: 20px; line-height: 1.33; } + &.s32 { font-size: 24px; line-height: 1.33; } + &.s60 { font-size: 45px; line-height: 1.33; } + &.s90 { font-size: 68px; line-height: 1.33; } + &.s160 { font-size: 120px; line-height: 1.33; } +} \ No newline at end of file diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index 824f136d300..3135056db58 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -75,6 +75,9 @@ } } } +.project-avatar { + float: left; +} .project-description { overflow: hidden; @@ -92,6 +95,9 @@ } } +.dash-project-avatar { + float: left; +} .dash-project-access-icon { float: left; margin-right: 3px; diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb new file mode 100644 index 00000000000..a482b90880d --- /dev/null +++ b/app/controllers/projects/avatars_controller.rb @@ -0,0 +1,29 @@ +class Projects::AvatarsController < Projects::ApplicationController + layout 'project' + + before_filter :project + + def show + @blob = @project.repository.blob_at_branch('master', @project.avatar_in_git) + if @blob + headers['X-Content-Type-Options'] = 'nosniff' + send_data( + @blob.data, + type: @blob.mime_type, + disposition: 'inline', + filename: @blob.name + ) + else + not_found! + end + end + + def destroy + @project.remove_avatar! + + @project.save + @project.reset_events_cache + + redirect_to edit_project_path(@project) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f65e04af205..772400d55ec 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -50,6 +50,31 @@ module ApplicationHelper args.any? { |v| v.to_s.downcase == action_name } end + def project_icon(project_id, options = {}) + project = Project.find_with_namespace(project_id) + if project.avatar.present? + image_tag project.avatar.url, options + elsif options[:only_uploaded] + image_tag '/assets/no_project_icon.png', options + elsif project.avatar_in_git + image_tag project_avatar_path(project), options + else # generated icon + project_identicon(project, options) + end + end + + def project_identicon(project, options = {}) + options[:class] ||= '' + options[:class] << ' identicon' + bg_color = Digest::MD5.hexdigest(project.name)[0, 6] + brightness = bg_color[0, 2].hex + bg_color[2, 2].hex + bg_color[4, 2].hex + text_color = (brightness > 375) ? '#000' : '#fff' + content_tag(:div, class: options[:class], + style: "background-color: ##{ bg_color }; color: #{ text_color }") do + project.name[0, 1].upcase + end + end + def group_icon(group_path) group = Group.find_by(path: group_path) if group && group.avatar.present? diff --git a/app/models/project.rb b/app/models/project.rb index f102c477404..7160e704aa1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -26,6 +26,7 @@ # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) +# avatar :string(255) # class Project < ActiveRecord::Base @@ -119,6 +120,11 @@ class Project < ActiveRecord::Base if: :import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create + validate :avatar_type, + if: ->(project) { project.avatar && project.avatar_changed? } + validates :avatar, file_size: { maximum: 100.kilobytes.to_i } + + mount_uploader :avatar, AttachmentUploader # Scopes scope :without_user, ->(user) { where("projects.id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } @@ -338,6 +344,24 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.select(&:activated?).first end + def avatar_type + unless avatar.image? + errors.add :avatar, 'only images allowed' + end + end + + def avatar_in_git + @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png') + @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg') + @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif') + @avatar_file + end + + # For compatibility with old code + def code + path + end + def items_for(entity) case entity when 'issue' then @@ -529,6 +553,7 @@ class Project < ActiveRecord::Base # Since we do cache @event we need to reset cache in special cases: # * when project was moved # * when project was renamed + # * when the project avatar changes # Events cache stored like events/23-20130109142513. # The cache key includes updated_at timestamp. # Thus it will automatically generate a new fragment diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 4930660055a..8bb0fcf9474 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -14,6 +14,9 @@ module Projects project.name = @from_project.name project.path = @from_project.path project.creator = @current_user + if @from_project.avatar && @from_project.avatar.image? + project.avatar = @from_project.avatar + end if namespace = @params[:namespace] project.namespace = namespace diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml index 89ed5102754..7f19fb5a81c 100644 --- a/app/views/dashboard/_project.html.haml +++ b/app/views/dashboard/_project.html.haml @@ -1,4 +1,6 @@ = link_to project_path(project), class: dom_class(project) do + .dash-project-avatar + = project_icon(project.to_param, alt: '', class: 'avatar s24') .dash-project-access-icon = visibility_level_icon(project.visibility_level) %span.str-truncated diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index 944441669e7..f60bcc72e1d 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -11,6 +11,8 @@ - @projects.each do |project| %li.my-project-row %h4.project-title + .project-avatar + = project_icon(project.to_param, alt: '', class: 'avatar s60') .project-access-icon = visibility_level_icon(project.visibility_level) = link_to project_path(project), class: dom_class(project) do diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 30d063c7a36..05910c6038c 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -2,6 +2,7 @@ .project-home-panel{:class => ("empty-project" if empty_repo)} .project-home-row .project-home-desc + = project_icon(@project.to_param, alt: '', class: 'avatar s32') - if @project.description.present? = escaped_autolink(@project.description) - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f2bb56b5664..fc6499de3e2 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -7,7 +7,8 @@ %p.light Some settings, such as "Transfer Project", are hidden inside the danger area below. %hr .panel-body - = form_for @project, remote: true, html: { class: "edit_project form-horizontal" } do |f| + = form_for @project, remote: true, html: { multipart: true, class: "edit_project form-horizontal" }, authenticity_token: true do |f| + %fieldset .form-group.project_name_holder = f.label :name, class: 'control-label' do @@ -80,6 +81,31 @@ = f.check_box :snippets_enabled %span.descr Share code pastes with others out of git repository + %fieldset.features + %legend + Project avatar: + .form-group + .col-sm-2 + .col-sm-10 + = project_icon(@project.to_param, alt: '', class: 'avatar s160', only_uploaded: true) + %p.light + - if @project.avatar_in_git + Project avatar in repository: #{ @project.avatar_in_git } + %p.light + - if @project.avatar? + You can change your project avatar here + - else + You can upload an project avatar here + %a.choose-btn.btn.btn-small.js-choose-project-avatar-button + %i.icon-paper-clip + %span Choose File ... +   + %span.file_name.js-avatar-filename File name... + = f.file_field :avatar, class: "js-project-avatar-input hidden" + .light The maximum file size allowed is 100KB. + - if @project.avatar? + %hr + = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" .form-actions = f.submit 'Save changes', class: "btn btn-save" diff --git a/config/routes.rb b/config/routes.rb index ef3c5aedfcb..32378665c94 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -353,6 +353,8 @@ Gitlab::Application.routes.draw do delete :delete_attachment end end + + resource :avatar, only: [:show, :destroy] end end diff --git a/db/migrate/20140125162722_add_avatar_to_projects.rb b/db/migrate/20140125162722_add_avatar_to_projects.rb new file mode 100644 index 00000000000..9523ac722f2 --- /dev/null +++ b/db/migrate/20140125162722_add_avatar_to_projects.rb @@ -0,0 +1,5 @@ +class AddAvatarToProjects < ActiveRecord::Migration + def change + add_column :projects, :avatar, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index b453164d712..29466f048eb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -327,6 +327,7 @@ ActiveRecord::Schema.define(version: 20150116234544) do t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" + t.string "avatar" end add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree diff --git a/features/project/project.feature b/features/project/project.feature index 7bb24e013a9..3e1fd54bee8 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -5,6 +5,19 @@ Feature: Project And project "Shop" has push event And I visit project "Shop" page + Scenario: I edit the project avatar + Given I visit edit project "Shop" page + When I change the project avatar + And I should see new project avatar + And I should see the "Remove avatar" button + + Scenario: I remove the project avatar + Given I visit edit project "Shop" page + And I have an project avatar + When I remove my project avatar + Then I should see the default project avatar + And I should not see the "Remove avatar" button + @javascript Scenario: I should see project activity When I visit project "Shop" page diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 5e7312d90ff..455539d7471 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -17,12 +17,53 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'change project path settings' do - fill_in "project_path", with: "new-path" - click_button "Rename" + fill_in 'project_path', with: 'new-path' + click_button 'Rename' end step 'I should see project with new path settings' do - project.path.should == "new-path" + project.path.should == 'new-path' + end + + step 'I change the project avatar' do + attach_file( + :project_avatar, + File.join(Rails.root, 'public', 'gitlab_logo.png') + ) + click_button 'Save changes' + @project.reload + end + + step 'I should see new project avatar' do + @project.avatar.should be_instance_of AttachmentUploader + url = @project.avatar.url + url.should == "/uploads/project/avatar/#{ @project.id }/gitlab_logo.png" + end + + step 'I should see the "Remove avatar" button' do + page.should have_link('Remove avatar') + end + + step 'I have an project avatar' do + attach_file( + :project_avatar, + File.join(Rails.root, 'public', 'gitlab_logo.png') + ) + click_button 'Save changes' + @project.reload + end + + step 'I remove my project avatar' do + click_link 'Remove avatar' + @project.reload + end + + step 'I should see the default project avatar' do + @project.avatar?.should be_false + end + + step 'I should not see the "Remove avatar" button' do + page.should_not have_link('Remove avatar') end step 'I should see project "Shop" version' do diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 1738f3443cf..ed50bf7c75d 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -56,6 +56,28 @@ describe ApplicationHelper do end end + describe 'project_icon' do + avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') + + it 'should return an url for the avatar' do + project = create(:project) + project.avatar = File.open(avatar_file_path) + project.save! + project_icon(project.to_param).to_s.should == + "/uploads/project/avatar/#{ project.id }/gitlab_logo.png" + end + + it "should give uploaded icon when present" do + project = create(:project) + project.save! + + Project.any_instance.stub(:avatar_in_git).and_return(true) + + project_icon(project.to_param).to_s.should match( + image_tag(project_avatar_path(project))) + end + end + describe "avatar_icon" do avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2a278176371..87d26f98b44 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -26,6 +26,7 @@ # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) +# avatar :string(255) # require 'spec_helper' @@ -310,4 +311,18 @@ describe Project do expect(project.star_count).to eq(0) end end + + describe :avatar_type do + let(:project) { create(:project) } + + it 'should be true if avatar is image' do + project.update_attribute(:avatar, 'uploads/avatar.png') + project.avatar_type.should be_true + end + + it 'should be false if avatar is html page' do + project.update_attribute(:avatar, 'uploads/avatar.html') + project.avatar_type.should == ['only images allowed'] + end + end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index f149f3f62a9..67705c6cb42 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -489,4 +489,11 @@ describe Projects::ForksController, "routing" do it "to #create" do post("/gitlab/gitlabhq/fork").should route_to("projects/forks#create", project_id: 'gitlab/gitlabhq') end + +# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy +describe Projects::AvatarsController, 'routing' do + it 'to #destroy' do + delete('/gitlab/gitlabhq/avatar').should route_to( + 'projects/avatars#destroy', project_id: 'gitlab/gitlabhq') + end end