Commit 714f7201 authored by Ciro Santilli's avatar Ciro Santilli

Add project stars.

parent b634d280
...@@ -24,6 +24,7 @@ v 7.1.0 ...@@ -24,6 +24,7 @@ v 7.1.0
- Increased default git max_size value from 5MB to 20MB in gitlab.yml. Please update your configs! - Increased default git max_size value from 5MB to 20MB in gitlab.yml. Please update your configs!
- Show error message in case of timeout in satellite when create MR - Show error message in case of timeout in satellite when create MR
- Show first 100 files for huge diff instead of hiding all - Show first 100 files for huge diff instead of hiding all
- Add project stars (Ciro Santilli)
v 7.0.0 v 7.0.0
- The CPU no longer overheats when you hold down the spacebar - The CPU no longer overheats when you hold down the spacebar
......
...@@ -4,3 +4,9 @@ ...@@ -4,3 +4,9 @@
.js-details-container .content.hide { display: block; } .js-details-container .content.hide { display: block; }
.js-details-container.open .content { display: block; } .js-details-container.open .content { display: block; }
.js-details-container.open .content.hide { display: none; } .js-details-container.open .content.hide { display: none; }
// Toggle between two states.
.js-toggler-container .turn-on { display: inline-block; }
.js-toggler-container .turn-off { display: none; }
.js-toggler-container.on .turn-on { display: none; }
.js-toggler-container.on .turn-off { display: inline-block; }
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
vertical-align: middle; vertical-align: middle;
cursor: pointer; cursor: pointer;
background-image: none; background-image: none;
border: 1px solid transparent; border: $btn-border;
white-space: nowrap; white-space: nowrap;
padding: 6px 12px; padding: 6px 12px;
font-size: 13px; font-size: 13px;
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
user-select: none; user-select: none;
color: #444444; color: #444444;
background-color: #fff; background-color: #fff;
border-color: #ccc;
text-shadow: none; text-shadow: none;
&.hover, &.hover,
......
...@@ -10,6 +10,8 @@ $hover: #D9EDF7; ...@@ -10,6 +10,8 @@ $hover: #D9EDF7;
$link_color: #446e9b; $link_color: #446e9b;
$link_hover_color: #2FA0BB; $link_hover_color: #2FA0BB;
$btn-border: 1px solid #ccc;
/* /*
* Success colors (green) * Success colors (green)
*/ */
......
...@@ -50,6 +50,22 @@ ...@@ -50,6 +50,22 @@
margin-left: 10px; margin-left: 10px;
font-weight: 500; font-weight: 500;
} }
.star .btn {
font-weight: bold;
line-height: 22px;
padding: 0px;
$margin-x: 6px;
.toggle {
display: inline-block;
padding: 0px $margin-x;
}
.count {
border-left: $btn-border;
display: inline-block;
padding: 0px $margin-x;
}
}
} }
} }
......
...@@ -167,6 +167,11 @@ class ProjectsController < ApplicationController ...@@ -167,6 +167,11 @@ class ProjectsController < ApplicationController
end end
end end
def toggle_star
current_user.toggle_star(@project)
render json: { star_count: @project.star_count }
end
private private
def upload_path def upload_path
......
...@@ -81,6 +81,8 @@ class Project < ActiveRecord::Base ...@@ -81,6 +81,8 @@ class Project < ActiveRecord::Base
has_many :users, through: :users_projects has_many :users, through: :users_projects
has_many :deploy_keys_projects, dependent: :destroy has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
has_many :starrers, through: :users_star_projects, source: :user
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
...@@ -575,4 +577,8 @@ class Project < ActiveRecord::Base ...@@ -575,4 +577,8 @@ class Project < ActiveRecord::Base
def update_repository_size def update_repository_size
update_attribute(:repository_size, repository.size) update_attribute(:repository_size, repository.size)
end end
def star_count
starrers.count
end
end end
...@@ -91,6 +91,8 @@ class User < ActiveRecord::Base ...@@ -91,6 +91,8 @@ class User < ActiveRecord::Base
has_many :personal_projects, through: :namespace, source: :projects has_many :personal_projects, through: :namespace, source: :projects
has_many :projects, through: :users_projects has_many :projects, through: :users_projects
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet" has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
has_many :users_projects, dependent: :destroy has_many :users_projects, dependent: :destroy
...@@ -508,4 +510,17 @@ class User < ActiveRecord::Base ...@@ -508,4 +510,17 @@ class User < ActiveRecord::Base
def system_hook_service def system_hook_service
SystemHooksService.new SystemHooksService.new
end end
def starred?(project)
starred_projects.exists?(project)
end
def toggle_star(project)
user_star_project = users_star_projects.where(project: project).take
if user_star_project
user_star_project.destroy
else
UsersStarProject.create!(project: project, user: self)
end
end
end end
# == Schema Information
#
# Table name: users_star_projects
#
# id :integer not null, primary key
# starrer_id :integer not null
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
#
class UsersStarProject < ActiveRecord::Base
belongs_to :project
belongs_to :user
validates :user, presence: true
validates :user_id, uniqueness: { scope: [:project_id] }
validates :project, presence: true
end
...@@ -27,10 +27,30 @@ ...@@ -27,10 +27,30 @@
= link_to project_blob_path(@project, tree_join(@repository.root_ref, readme.name)) do = link_to project_blob_path(@project, tree_join(@repository.root_ref, readme.name)) do
= readme.name = readme.name
- unless empty_repo .col-md-5
.col-md-5 .project-home-links
.project-home-links - unless empty_repo
= link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), project_commits_path(@project, @ref || @repository.root_ref) = link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), project_commits_path(@project, @ref || @repository.root_ref)
= link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), project_branches_path(@project) = link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), project_branches_path(@project)
= link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), project_tags_path(@project) = link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), project_tags_path(@project)
%span.light.prepend-left-20= repository_size %span.light.prepend-left-20= repository_size
%span.star.js-toggler-container.on
- if current_user
= render 'link_to_toggle_star',
title: 'Star this project.',
starred: false,
signed_in: true
= render 'link_to_toggle_star',
title: 'Unstar this project.',
starred: true,
signed_in: true
:coffeescript
$('.star').on 'ajax:success', (e, data, status, xhr) ->
$(@).toggleClass('on').find('.count').html(data.star_count)
.on 'ajax:error', (e, xhr, status, error) ->
new Flash('Star toggle failed. Try again later.', 'alert')
- else
= render 'link_to_toggle_star',
title: 'You must sign in to star a project.',
starred: false,
signed_in: false
- cls = 'btn'
- cls += ' disabled' unless signed_in
%span{class: starred ? 'turn-on' : 'turn-off'}
= link_to toggle_star_project_path(@project), title: title,
class: cls, method: :post, remote: true, data: {type: 'json'} do
%span.toggle<>
%i.icon-star
- if starred
Unstar
- else
Star
%span.count<>
= @project.star_count
...@@ -175,6 +175,7 @@ Gitlab::Application.routes.draw do ...@@ -175,6 +175,7 @@ Gitlab::Application.routes.draw do
post :archive post :archive
post :unarchive post :unarchive
post :upload_image post :upload_image
post :toggle_star
get :autocomplete_sources get :autocomplete_sources
get :import get :import
put :retry_import put :retry_import
......
class CreateUsersStarProjects < ActiveRecord::Migration
def change
create_table :users_star_projects do |t|
t.integer :project_id, null: false
t.integer :user_id, null: false
t.timestamps
end
add_index :users_star_projects, :user_id
add_index :users_star_projects, :project_id
add_index :users_star_projects, [:user_id, :project_id], unique: true
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20140611135229) do ActiveRecord::Schema.define(version: 20140625115202) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -369,6 +369,17 @@ ActiveRecord::Schema.define(version: 20140611135229) do ...@@ -369,6 +369,17 @@ ActiveRecord::Schema.define(version: 20140611135229) do
add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree
add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree
create_table "users_star_projects", force: true do |t|
t.integer "project_id", null: false
t.integer "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "users_star_projects", ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree
add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree
add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
create_table "web_hooks", force: true do |t| create_table "web_hooks", force: true do |t|
t.string "url" t.string "url"
t.integer "project_id" t.integer "project_id"
......
Feature: Project Star
Scenario: New projects have 0 stars
Given public project "Community"
When I visit project "Community" page
Then The project has 0 stars
Scenario: Empty projects show star count
Given public empty project "Empty Public Project"
When I visit empty project page
Then The project has 0 stars
Scenario: Signed off users can't star projects
Given public project "Community"
And I visit project "Community" page
When I click on the star toggle button
Then The project has 0 stars
@javascript
Scenario: Signed in users can toggle star
Given I sign in as "John Doe"
And public project "Community"
And I visit project "Community" page
When I click on the star toggle button
Then The project has 1 star
When I click on the star toggle button
Then The project has 0 stars
@javascript
Scenario: Star count sums stars
Given I sign in as "John Doe"
And public project "Community"
And I visit project "Community" page
And I click on the star toggle button
And I logout
And I sign in as "Mary Jane"
And I visit project "Community" page
When I click on the star toggle button
Then The project has 2 stars
@javascript
Scenario: If an user deletes his account his stars are destroyed
Given I sign in as "John Doe"
And public project "Community"
And I visit project "Community" page
And I click on the star toggle button
And I delete my account
When I visit project "Community" page
Then The project has 0 stars
class Spinach::Features::ProjectStar < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
include SharedUser
step "The project has 0 stars" do
has_n_stars(0)
end
step "The project has 1 star" do
has_n_stars(1)
end
step "The project has 2 stars" do
has_n_stars(2)
end
# Requires @javascript
step "I click on the star toggle button" do
page.find(".star .toggle", visible: true).click
end
protected
def has_n_stars(n)
expect(page).to have_css(".star .count", text: /^#{n}$/, visible: true)
end
end
...@@ -3,10 +3,6 @@ class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps ...@@ -3,10 +3,6 @@ class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps
include SharedPaths include SharedPaths
include SharedProject include SharedProject
step 'public empty project "Empty Public Project"' do
create :empty_project, :public, name: 'Empty Public Project'
end
step 'I should see project "Empty Public Project"' do step 'I should see project "Empty Public Project"' do
page.should have_content "Empty Public Project" page.should have_content "Empty Public Project"
end end
...@@ -20,16 +16,6 @@ class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps ...@@ -20,16 +16,6 @@ class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps
page.should have_content 'README.md' page.should have_content 'README.md'
end end
step 'I visit empty project page' do
project = Project.find_by(name: 'Empty Public Project')
visit project_path(project)
end
step 'I visit project "Community" page' do
project = Project.find_by(name: 'Community')
visit project_path(project)
end
step 'I should see empty public project details' do step 'I should see empty public project details' do
page.should have_content 'Git global setup' page.should have_content 'Git global setup'
end end
...@@ -48,22 +34,12 @@ class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps ...@@ -48,22 +34,12 @@ class Spinach::Features::PublicProjectsFeature < Spinach::FeatureSteps
end end
end end
step 'I visit project "Enterprise" page' do
project = Project.find_by(name: 'Enterprise')
visit project_path(project)
end
step 'I should see project "Community" home page' do step 'I should see project "Community" home page' do
within '.project-home-title' do within '.project-home-title' do
page.should have_content 'Community' page.should have_content 'Community'
end end
end end
step 'I visit project "Internal" page' do
project = Project.find_by(name: 'Internal')
visit project_path(project)
end
step 'I should see project "Internal" home page' do step 'I should see project "Internal" home page' do
within '.project-home-title' do within '.project-home-title' do
page.should have_content 'Internal' page.should have_content 'Internal'
......
...@@ -24,6 +24,10 @@ module SharedAuthentication ...@@ -24,6 +24,10 @@ module SharedAuthentication
current_path.should == new_user_session_path current_path.should == new_user_session_path
end end
step "I logout" do
logout
end
def current_user def current_user
@user || User.first @user || User.first
end end
......
...@@ -319,6 +319,34 @@ module SharedPaths ...@@ -319,6 +319,34 @@ module SharedPaths
visit project_wiki_path(@project, :home) visit project_wiki_path(@project, :home)
end end
# ----------------------------------------
# Visibility Projects
# ----------------------------------------
step 'I visit project "Community" page' do
project = Project.find_by(name: "Community")
visit project_path(project)
end
step 'I visit project "Internal" page' do
project = Project.find_by(name: "Internal")
visit project_path(project)
end
step 'I visit project "Enterprise" page' do
project = Project.find_by(name: "Enterprise")
visit project_path(project)
end
# ----------------------------------------
# Empty Projects
# ----------------------------------------
step "I visit empty project page" do
project = Project.find_by(name: "Empty Public Project")
visit project_path(project)
end
# ---------------------------------------- # ----------------------------------------
# Public Projects # Public Projects
# ---------------------------------------- # ----------------------------------------
...@@ -327,10 +355,6 @@ module SharedPaths ...@@ -327,10 +355,6 @@ module SharedPaths
visit public_root_path visit public_root_path
end end
step 'I visit public page for "Community" project' do
visit public_project_path(Project.find_by(name: "Community"))
end
# ---------------------------------------- # ----------------------------------------
# Snippets # Snippets
# ---------------------------------------- # ----------------------------------------
......
...@@ -122,4 +122,12 @@ module SharedProject ...@@ -122,4 +122,12 @@ module SharedProject
project ||= create :empty_project, :public, name: 'Community', namespace: user.namespace project ||= create :empty_project, :public, name: 'Community', namespace: user.namespace
project.team << [user, :master] project.team << [user, :master]
end end
# ----------------------------------------
# Empty projects
# ----------------------------------------
step 'public empty project "Empty Public Project"' do
create :empty_project, :public, name: "Empty Public Project"
end
end end
...@@ -9,6 +9,11 @@ module SharedUser ...@@ -9,6 +9,11 @@ module SharedUser
user_exists("Mary Jane", {username: "mary_jane"}) user_exists("Mary Jane", {username: "mary_jane"})
end end
step "I delete my account" do
visit profile_account_path
click_link "Delete account"
end
protected protected
def user_exists(name, options = {}) def user_exists(name, options = {})
......
...@@ -2,6 +2,7 @@ require('spec_helper') ...@@ -2,6 +2,7 @@ require('spec_helper')
describe ProjectsController do describe ProjectsController do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:public_project) { create(:project, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
...@@ -40,4 +41,17 @@ describe ProjectsController do ...@@ -40,4 +41,17 @@ describe ProjectsController do
end end
end end
end end
describe "POST #toggle_star" do
it "increases star count if user is signed in" do
sign_in(user)
post :toggle_star, id: public_project.to_param
expect(public_project.star_count).to eq(1)
end
it "does nothing if user is not signed in" do
post :toggle_star, id: public_project.to_param
expect(public_project.star_count).to eq(0)
end
end
end end
...@@ -240,4 +240,22 @@ describe Project do ...@@ -240,4 +240,22 @@ describe Project do
it { project.open_branches.map(&:name).should include('bootstrap') } it { project.open_branches.map(&:name).should include('bootstrap') }
it { project.open_branches.map(&:name).should_not include('master') } it { project.open_branches.map(&:name).should_not include('master') }
end end
describe "#count_star" do
it "counts stars" do
user1 = create :user
user2 = create :user
project = create :project, :public
expect(project.star_count).to eq(0)
user1.toggle_star(project)
expect(project.star_count).to eq(1)
user2.toggle_star(project)
expect(project.star_count).to eq(2)
user1.toggle_star(project)
expect(project.star_count).to eq(1)
user2.toggle_star(project)
expect(project.star_count).to eq(0)
end
end
end end
...@@ -355,4 +355,17 @@ describe User do ...@@ -355,4 +355,17 @@ describe User do
expect(user.short_website_url).to eq 'test.com' expect(user.short_website_url).to eq 'test.com'
end end
end end
describe "#toggle_star" do
it "toggles stars" do
user = create :user
project = create :project, :public
expect(user.starred?(project)).to be_false
user.toggle_star(project)
expect(user.starred?(project)).to be_true
user.toggle_star(project)
expect(user.starred?(project)).to be_false
end
end
end end
...@@ -20,6 +20,6 @@ module LoginHelpers ...@@ -20,6 +20,6 @@ module LoginHelpers
end end
def logout def logout
click_link "Logout" rescue nil page.find(:css, ".icon-signout").click rescue nil
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment