diff --git a/Gemfile b/Gemfile index 002fbce94..b1597899b 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,7 @@ gem "crono" gem "net-ldap" gem "redcarpet" gem "font-awesome-rails" +gem "bootstrap-typeahead-rails" # This is already a Rails dependency, but we use it to run portusctl gem "thor" diff --git a/Gemfile.lock b/Gemfile.lock index 8cb73f7a4..bc384c15f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,6 +56,8 @@ GEM bootstrap-sass (3.3.5) autoprefixer-rails (>= 5.0.0.1) sass (>= 3.2.19) + bootstrap-typeahead-rails (0.10.5.1) + railties (>= 3.0) builder (3.2.2) byebug (5.0.0) columnize (= 0.9.0) @@ -319,6 +321,7 @@ DEPENDENCIES awesome_print base32 bootstrap-sass (~> 3.3.4) + bootstrap-typeahead-rails byebug capybara codeclimate-test-reporter @@ -365,6 +368,3 @@ DEPENDENCIES webmock wirb wirble - -BUNDLED WITH - 1.10.5 diff --git a/app/assets/javascripts/application.coffee b/app/assets/javascripts/application.coffee index 68a61c11f..2766a1d32 100644 --- a/app/assets/javascripts/application.coffee +++ b/app/assets/javascripts/application.coffee @@ -5,3 +5,4 @@ #= require bootstrap-sprockets #= require lifeitup_layout #= require_tree . +#= require bootstrap-typeahead-rails diff --git a/app/assets/javascripts/teams.coffee b/app/assets/javascripts/teams.coffee index 80ffae612..124d73caa 100644 --- a/app/assets/javascripts/teams.coffee +++ b/app/assets/javascripts/teams.coffee @@ -34,6 +34,22 @@ $(document).on "page:change", -> $('#namespace_description').focus() ) + searchSelektor = $('.remote .typeahead') + teamID = $('.remote').attr('id') + bloodhound = new Bloodhound( + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('username'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: + cache: false, + url: teamID + '/typeahead/%QUERY', + wildcard: '%QUERY' + ) + bloodhound.initialize() + + $('.remote .typeahead').typeahead null, + displayKey: 'username', + source: bloodhound.ttAdapter() + $('#add_namespace_btn').unbind('click').on 'click', (event) -> $('#namespace_namespace').val('') diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 49a3d9c4d..c0d560d6f 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,3 +1,8 @@ +// Typeahead +/* +*= require bootstrap-typeahead-rails +*/ + // Vendored fonts and styles. @import "font-awesome"; @import "pacifico"; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 41a1c9c80..d368724ae 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base include Pundit rescue_from Pundit::NotAuthorizedError, with: :deny_access - respond_to :html + respond_to :html, :json # Two things can happen when signing in. # 1. The current user has no email: this happens on LDAP registration. In @@ -83,6 +83,7 @@ def protected_controllers?(*controllers) # Render the 401 page. def deny_access @status = 401 + render template: "errors/401", status: 401, layout: "errors" end end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index a8d43d7a8..739d330be 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -1,7 +1,7 @@ class TeamsController < ApplicationController include ChangeDescription - before_action :set_team, only: [:show, :update] + before_action :set_team, only: [:show, :update, :typeahead] after_action :verify_policy_scoped, only: :index respond_to :js, :html @@ -39,6 +39,17 @@ def update change_description(@team, :team) end + # GET /teams/1/typeahead/%QUERY + def typeahead + authorize @team + @query = params[:query] + matches = User.search_from_query(@team.member_ids, + "#{@query}%").map { |user| { username: user } } + respond_to do |format| + format.json { render json: matches.to_json } + end + end + private def set_team diff --git a/app/models/team.rb b/app/models/team.rb index 2b8ad9f07..e495295c2 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -21,4 +21,9 @@ def self.all_non_special # Right now, all the special namespaces are simply marked as hidden. Team.where(hidden: false) end + + # Returns all the member-IDs + def member_ids + team_users.pluck(:user_id) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 3fbd0b03f..c794e25d5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -107,6 +107,11 @@ def inactive_message "Sorry, this account has been disabled." end + # Returns all users who match the query. + def self.search_from_query(members, query) + User.enabled.where.not(id: members).where(arel_table[:username].matches(query)).pluck(:username) + end + protected # Returns whether the given user can be disabled or not. The following rules diff --git a/app/policies/team_policy.rb b/app/policies/team_policy.rb index 939e55f4d..456c1227e 100644 --- a/app/policies/team_policy.rb +++ b/app/policies/team_policy.rb @@ -15,8 +15,9 @@ def owner? user.admin? || @team.owners.exists?(user.id) end - alias_method :update?, :owner? - alias_method :show?, :member? + alias_method :update?, :owner? + alias_method :show?, :member? + alias_method :typeahead?, :owner? class Scope attr_reader :user, :scope diff --git a/app/policies/team_user_policy.rb b/app/policies/team_user_policy.rb index f167aa364..8d353b5d3 100644 --- a/app/policies/team_user_policy.rb +++ b/app/policies/team_user_policy.rb @@ -15,7 +15,7 @@ def owner? user.admin? || @team_user.team.owners.exists?(user.id) end - alias_method :destroy?, :owner? - alias_method :update?, :owner? - alias_method :create?, :owner? + alias_method :destroy?, :owner? + alias_method :update?, :owner? + alias_method :create?, :owner? end diff --git a/app/views/errors/401.json.slim b/app/views/errors/401.json.slim new file mode 100644 index 000000000..c12cc9848 --- /dev/null +++ b/app/views/errors/401.json.slim @@ -0,0 +1 @@ +'#{@status} diff --git a/app/views/teams/show.html.slim b/app/views/teams/show.html.slim index beee41e3a..07e789108 100644 --- a/app/views/teams/show.html.slim +++ b/app/views/teams/show.html.slim @@ -48,7 +48,8 @@ .form-group = f.label :user, {class: 'control-label col-md-2'} .col-md-7 - = f.text_field(:user, class: 'form-control', placeholder: 'Name', required: true) + .remote id="#{@team.id}" + = f.text_field(:user, class: 'form-control typeahead', placeholder: 'Name', required: true) .form-group .col-md-offset-2.col-md-7 = f.submit('Add', class: 'btn btn-primary') diff --git a/config/routes.rb b/config/routes.rb index 56a6f73ef..5b412a67e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,12 @@ Rails.application.routes.draw do resources :errors, only: [:show] - resources :teams, only: [:index, :show, :create, :update] + resources :teams, only: [:index, :show, :create, :update] do + member do + get "typeahead/:query" => "teams#typeahead", :defaults => { format: "json" } + end + end resources :team_users, only: [:create, :destroy, :update] + # get "teams/typeahead/:id/:query" => "teams#typeahead", :defaults => { format: "json" } resources :namespaces, only: [:create, :index, :show, :update] do put "toggle_public", on: :member end diff --git a/spec/controllers/teams_controller_spec.rb b/spec/controllers/teams_controller_spec.rb index 3bf4324ea..8e96058bc 100644 --- a/spec/controllers/teams_controller_spec.rb +++ b/spec/controllers/teams_controller_spec.rb @@ -1,6 +1,8 @@ require "rails_helper" RSpec.describe TeamsController, type: :controller do + render_views + let(:valid_attributes) do { name: "qa team", description: "short test description" } end @@ -123,6 +125,32 @@ end end + describe "typeahead" do + it "does allow to search for valid users by owners" do + sign_in owner + get :typeahead, id: team.id, query: "user", format: "json" + expect(response.status).to eq(200) + user1 = create(:user) + create(:user, username: "user2") + TeamUser.create(team: team, user: user1, role: TeamUser.roles["viewer"]) + get :typeahead, id: team.id, query: "user", format: "json" + usernames = JSON.parse(response.body) + expect(usernames.length).to eq(1) + expect(usernames[0]["username"]).to eq("user2") + end + + it "does not allow to search by contributers or viewers" do + disallowed_roles = ["viewer", "contributer"] + disallowed_roles.each do |role| + user = create(:user) + TeamUser.create(team: team, user: user, role: TeamUser.roles[role]) + sign_in user + get :typeahead, id: team.id, query: "user", format: "js" + expect(response.status).to eq(401) + end + end + end + describe "activity tracking" do before :each do sign_in owner