Commit aa1f1eb680e4328c2cf619a770f1e90f74c41987
Exists in
master
and in
4 other branches
Merge pull request #2746 from gitlabhq/features/teams
New feature: Teams
Showing
118 changed files
with
2862 additions
and
322 deletions
Show diff stats
Too many changes.
To preserve performance only 100 of 118 files displayed.
app/assets/javascripts/dashboard.js.coffee
| @@ -4,11 +4,11 @@ window.dashboardPage = -> | @@ -4,11 +4,11 @@ window.dashboardPage = -> | ||
| 4 | event.preventDefault() | 4 | event.preventDefault() |
| 5 | toggleFilter $(this) | 5 | toggleFilter $(this) |
| 6 | reloadActivities() | 6 | reloadActivities() |
| 7 | - | 7 | + |
| 8 | reloadActivities = -> | 8 | reloadActivities = -> |
| 9 | $(".content_list").html '' | 9 | $(".content_list").html '' |
| 10 | Pager.init 20, true | 10 | Pager.init 20, true |
| 11 | - | 11 | + |
| 12 | toggleFilter = (sender) -> | 12 | toggleFilter = (sender) -> |
| 13 | sender.parent().toggleClass "inactive" | 13 | sender.parent().toggleClass "inactive" |
| 14 | event_filters = $.cookie("event_filter") | 14 | event_filters = $.cookie("event_filter") |
| @@ -17,11 +17,11 @@ toggleFilter = (sender) -> | @@ -17,11 +17,11 @@ toggleFilter = (sender) -> | ||
| 17 | event_filters = event_filters.split(",") | 17 | event_filters = event_filters.split(",") |
| 18 | else | 18 | else |
| 19 | event_filters = new Array() | 19 | event_filters = new Array() |
| 20 | - | 20 | + |
| 21 | index = event_filters.indexOf(filter) | 21 | index = event_filters.indexOf(filter) |
| 22 | if index is -1 | 22 | if index is -1 |
| 23 | event_filters.push filter | 23 | event_filters.push filter |
| 24 | else | 24 | else |
| 25 | event_filters.splice index, 1 | 25 | event_filters.splice index, 1 |
| 26 | - | 26 | + |
| 27 | $.cookie "event_filter", event_filters.join(",") | 27 | $.cookie "event_filter", event_filters.join(",") |
app/assets/javascripts/merge_requests.js.coffee
| 1 | # | 1 | # |
| 2 | # * Filter merge requests | 2 | # * Filter merge requests |
| 3 | -# | 3 | +# |
| 4 | @merge_requestsPage = -> | 4 | @merge_requestsPage = -> |
| 5 | $('#assignee_id').chosen() | 5 | $('#assignee_id').chosen() |
| 6 | $('#milestone_id').chosen() | 6 | $('#milestone_id').chosen() |
| @@ -8,16 +8,16 @@ | @@ -8,16 +8,16 @@ | ||
| 8 | $(this).closest('form').submit() | 8 | $(this).closest('form').submit() |
| 9 | 9 | ||
| 10 | class MergeRequest | 10 | class MergeRequest |
| 11 | - | 11 | + |
| 12 | constructor: (@opts) -> | 12 | constructor: (@opts) -> |
| 13 | this.$el = $('.merge-request') | 13 | this.$el = $('.merge-request') |
| 14 | @diffs_loaded = false | 14 | @diffs_loaded = false |
| 15 | @commits_loaded = false | 15 | @commits_loaded = false |
| 16 | - | 16 | + |
| 17 | this.activateTab(@opts.action) | 17 | this.activateTab(@opts.action) |
| 18 | - | 18 | + |
| 19 | this.bindEvents() | 19 | this.bindEvents() |
| 20 | - | 20 | + |
| 21 | this.initMergeWidget() | 21 | this.initMergeWidget() |
| 22 | this.$('.show-all-commits').on 'click', => | 22 | this.$('.show-all-commits').on 'click', => |
| 23 | this.showAllCommits() | 23 | this.showAllCommits() |
| @@ -28,7 +28,7 @@ class MergeRequest | @@ -28,7 +28,7 @@ class MergeRequest | ||
| 28 | 28 | ||
| 29 | initMergeWidget: -> | 29 | initMergeWidget: -> |
| 30 | this.showState( @opts.current_state ) | 30 | this.showState( @opts.current_state ) |
| 31 | - | 31 | + |
| 32 | if this.$('.automerge_widget').length and @opts.check_enable | 32 | if this.$('.automerge_widget').length and @opts.check_enable |
| 33 | $.get @opts.url_to_automerge_check, (data) => | 33 | $.get @opts.url_to_automerge_check, (data) => |
| 34 | this.showState( data.state ) | 34 | this.showState( data.state ) |
| @@ -42,12 +42,12 @@ class MergeRequest | @@ -42,12 +42,12 @@ class MergeRequest | ||
| 42 | bindEvents: -> | 42 | bindEvents: -> |
| 43 | this.$('.nav-tabs').on 'click', 'a', (event) => | 43 | this.$('.nav-tabs').on 'click', 'a', (event) => |
| 44 | a = $(event.currentTarget) | 44 | a = $(event.currentTarget) |
| 45 | - | 45 | + |
| 46 | href = a.attr('href') | 46 | href = a.attr('href') |
| 47 | History.replaceState {path: href}, document.title, href | 47 | History.replaceState {path: href}, document.title, href |
| 48 | - | 48 | + |
| 49 | event.preventDefault() | 49 | event.preventDefault() |
| 50 | - | 50 | + |
| 51 | this.$('.nav-tabs').on 'click', 'li', (event) => | 51 | this.$('.nav-tabs').on 'click', 'li', (event) => |
| 52 | this.activateTab($(event.currentTarget).data('action')) | 52 | this.activateTab($(event.currentTarget).data('action')) |
| 53 | 53 |
app/assets/stylesheets/sections/projects.scss
| @@ -0,0 +1,11 @@ | @@ -0,0 +1,11 @@ | ||
| 1 | +# Provides a base class for Admin controllers to subclass | ||
| 2 | +# | ||
| 3 | +# Automatically sets the layout and ensures an administrator is logged in | ||
| 4 | +class Admin::ApplicationController < ApplicationController | ||
| 5 | + layout 'admin' | ||
| 6 | + before_filter :authenticate_admin! | ||
| 7 | + | ||
| 8 | + def authenticate_admin! | ||
| 9 | + return render_404 unless current_user.is_admin? | ||
| 10 | + end | ||
| 11 | +end |
app/controllers/admin/dashboard_controller.rb
| 1 | -class Admin::DashboardController < AdminController | 1 | +class Admin::DashboardController < Admin::ApplicationController |
| 2 | def index | 2 | def index |
| 3 | @projects = Project.order("created_at DESC").limit(10) | 3 | @projects = Project.order("created_at DESC").limit(10) |
| 4 | @users = User.order("created_at DESC").limit(10) | 4 | @users = User.order("created_at DESC").limit(10) |
app/controllers/admin/groups_controller.rb
| 1 | -class Admin::GroupsController < AdminController | 1 | +class Admin::GroupsController < Admin::ApplicationController |
| 2 | before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update] | 2 | before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update] |
| 3 | 3 | ||
| 4 | def index | 4 | def index |
app/controllers/admin/hooks_controller.rb
app/controllers/admin/logs_controller.rb
app/controllers/admin/projects/application_controller.rb
0 → 100644
| @@ -0,0 +1,11 @@ | @@ -0,0 +1,11 @@ | ||
| 1 | +# Provides a base class for Admin controllers to subclass | ||
| 2 | +# | ||
| 3 | +# Automatically sets the layout and ensures an administrator is logged in | ||
| 4 | +class Admin::Projects::ApplicationController < Admin::ApplicationController | ||
| 5 | + | ||
| 6 | + protected | ||
| 7 | + | ||
| 8 | + def project | ||
| 9 | + @project ||= Project.find_by_path(params[:project_id]) | ||
| 10 | + end | ||
| 11 | +end |
| @@ -0,0 +1,32 @@ | @@ -0,0 +1,32 @@ | ||
| 1 | +class Admin::Projects::MembersController < Admin::Projects::ApplicationController | ||
| 2 | + def edit | ||
| 3 | + @member = team_member | ||
| 4 | + @project = project | ||
| 5 | + @team_member_relation = team_member_relation | ||
| 6 | + end | ||
| 7 | + | ||
| 8 | + def update | ||
| 9 | + if team_member_relation.update_attributes(params[:team_member]) | ||
| 10 | + redirect_to [:admin, project], notice: 'Project Access was successfully updated.' | ||
| 11 | + else | ||
| 12 | + render action: "edit" | ||
| 13 | + end | ||
| 14 | + end | ||
| 15 | + | ||
| 16 | + def destroy | ||
| 17 | + team_member_relation.destroy | ||
| 18 | + | ||
| 19 | + redirect_to :back | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | + private | ||
| 23 | + | ||
| 24 | + def team_member | ||
| 25 | + @member ||= project.users.find(params[:id]) | ||
| 26 | + end | ||
| 27 | + | ||
| 28 | + def team_member_relation | ||
| 29 | + team_member.users_projects.find_by_project_id(project) | ||
| 30 | + end | ||
| 31 | + | ||
| 32 | +end |
app/controllers/admin/projects_controller.rb
| 1 | -class Admin::ProjectsController < AdminController | 1 | +class Admin::ProjectsController < Admin::ApplicationController |
| 2 | before_filter :project, only: [:edit, :show, :update, :destroy, :team_update] | 2 | before_filter :project, only: [:edit, :show, :update, :destroy, :team_update] |
| 3 | 3 | ||
| 4 | def index | 4 | def index |
| @@ -29,7 +29,9 @@ class Admin::ProjectsController < AdminController | @@ -29,7 +29,9 @@ class Admin::ProjectsController < AdminController | ||
| 29 | end | 29 | end |
| 30 | 30 | ||
| 31 | def update | 31 | def update |
| 32 | - status = Projects::UpdateContext.new(project, current_user, params).execute(:admin) | 32 | + project.creator = current_user unless project.creator |
| 33 | + | ||
| 34 | + status = ::Projects::UpdateContext.new(project, current_user, params).execute(:admin) | ||
| 33 | 35 | ||
| 34 | if status | 36 | if status |
| 35 | redirect_to [:admin, @project], notice: 'Project was successfully updated.' | 37 | redirect_to [:admin, @project], notice: 'Project was successfully updated.' |
app/controllers/admin/resque_controller.rb
app/controllers/admin/team_members_controller.rb
| @@ -1,22 +0,0 @@ | @@ -1,22 +0,0 @@ | ||
| 1 | -class Admin::TeamMembersController < AdminController | ||
| 2 | - def edit | ||
| 3 | - @admin_team_member = UsersProject.find(params[:id]) | ||
| 4 | - end | ||
| 5 | - | ||
| 6 | - def update | ||
| 7 | - @admin_team_member = UsersProject.find(params[:id]) | ||
| 8 | - | ||
| 9 | - if @admin_team_member.update_attributes(params[:team_member]) | ||
| 10 | - redirect_to [:admin, @admin_team_member.project], notice: 'Project Access was successfully updated.' | ||
| 11 | - else | ||
| 12 | - render action: "edit" | ||
| 13 | - end | ||
| 14 | - end | ||
| 15 | - | ||
| 16 | - def destroy | ||
| 17 | - @admin_team_member = UsersProject.find(params[:id]) | ||
| 18 | - @admin_team_member.destroy | ||
| 19 | - | ||
| 20 | - redirect_to :back | ||
| 21 | - end | ||
| 22 | -end |
| @@ -0,0 +1,11 @@ | @@ -0,0 +1,11 @@ | ||
| 1 | +# Provides a base class for Admin controllers to subclass | ||
| 2 | +# | ||
| 3 | +# Automatically sets the layout and ensures an administrator is logged in | ||
| 4 | +class Admin::Teams::ApplicationController < Admin::ApplicationController | ||
| 5 | + | ||
| 6 | + private | ||
| 7 | + | ||
| 8 | + def user_team | ||
| 9 | + @team = UserTeam.find_by_path(params[:team_id]) | ||
| 10 | + end | ||
| 11 | +end |
| @@ -0,0 +1,41 @@ | @@ -0,0 +1,41 @@ | ||
| 1 | +class Admin::Teams::MembersController < Admin::Teams::ApplicationController | ||
| 2 | + def new | ||
| 3 | + @users = User.potential_team_members(user_team) | ||
| 4 | + @users = UserDecorator.decorate @users | ||
| 5 | + end | ||
| 6 | + | ||
| 7 | + def create | ||
| 8 | + unless params[:user_ids].blank? | ||
| 9 | + user_ids = params[:user_ids] | ||
| 10 | + access = params[:default_project_access] | ||
| 11 | + is_admin = params[:group_admin] | ||
| 12 | + user_team.add_members(user_ids, access, is_admin) | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + redirect_to admin_team_path(user_team), notice: 'Members was successfully added into Team of users.' | ||
| 16 | + end | ||
| 17 | + | ||
| 18 | + def edit | ||
| 19 | + team_member | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | + def update | ||
| 23 | + options = {default_projects_access: params[:default_project_access], group_admin: params[:group_admin]} | ||
| 24 | + if user_team.update_membership(team_member, options) | ||
| 25 | + redirect_to admin_team_path(user_team), notice: "Membership for #{team_member.name} was successfully updated in Team of users." | ||
| 26 | + else | ||
| 27 | + render :edit | ||
| 28 | + end | ||
| 29 | + end | ||
| 30 | + | ||
| 31 | + def destroy | ||
| 32 | + user_team.remove_member(team_member) | ||
| 33 | + redirect_to admin_team_path(user_team), notice: "Member #{team_member.name} was successfully removed from Team of users." | ||
| 34 | + end | ||
| 35 | + | ||
| 36 | + protected | ||
| 37 | + | ||
| 38 | + def team_member | ||
| 39 | + @member ||= user_team.members.find(params[:id]) | ||
| 40 | + end | ||
| 41 | +end |
| @@ -0,0 +1,41 @@ | @@ -0,0 +1,41 @@ | ||
| 1 | +class Admin::Teams::ProjectsController < Admin::Teams::ApplicationController | ||
| 2 | + def new | ||
| 3 | + @projects = Project.scoped | ||
| 4 | + @projects = @projects.without_team(user_team) if user_team.projects.any? | ||
| 5 | + #@projects.reject!(&:empty_repo?) | ||
| 6 | + end | ||
| 7 | + | ||
| 8 | + def create | ||
| 9 | + unless params[:project_ids].blank? | ||
| 10 | + project_ids = params[:project_ids] | ||
| 11 | + access = params[:greatest_project_access] | ||
| 12 | + user_team.assign_to_projects(project_ids, access) | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully assgned to projects.' | ||
| 16 | + end | ||
| 17 | + | ||
| 18 | + def edit | ||
| 19 | + team_project | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | + def update | ||
| 23 | + if user_team.update_project_access(team_project, params[:greatest_project_access]) | ||
| 24 | + redirect_to admin_team_path(user_team), notice: 'Access was successfully updated.' | ||
| 25 | + else | ||
| 26 | + render :edit | ||
| 27 | + end | ||
| 28 | + end | ||
| 29 | + | ||
| 30 | + def destroy | ||
| 31 | + user_team.resign_from_project(team_project) | ||
| 32 | + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully reassigned from project.' | ||
| 33 | + end | ||
| 34 | + | ||
| 35 | + protected | ||
| 36 | + | ||
| 37 | + def team_project | ||
| 38 | + @project ||= user_team.projects.find_with_namespace(params[:id]) | ||
| 39 | + end | ||
| 40 | + | ||
| 41 | +end |
| @@ -0,0 +1,59 @@ | @@ -0,0 +1,59 @@ | ||
| 1 | +class Admin::TeamsController < Admin::ApplicationController | ||
| 2 | + def index | ||
| 3 | + @teams = UserTeam.order('name ASC') | ||
| 4 | + @teams = @teams.search(params[:name]) if params[:name].present? | ||
| 5 | + @teams = @teams.page(params[:page]).per(20) | ||
| 6 | + end | ||
| 7 | + | ||
| 8 | + def show | ||
| 9 | + user_team | ||
| 10 | + end | ||
| 11 | + | ||
| 12 | + def new | ||
| 13 | + @team = UserTeam.new | ||
| 14 | + end | ||
| 15 | + | ||
| 16 | + def edit | ||
| 17 | + user_team | ||
| 18 | + end | ||
| 19 | + | ||
| 20 | + def create | ||
| 21 | + @team = UserTeam.new(params[:user_team]) | ||
| 22 | + @team.path = @team.name.dup.parameterize if @team.name | ||
| 23 | + @team.owner = current_user | ||
| 24 | + | ||
| 25 | + if @team.save | ||
| 26 | + redirect_to admin_team_path(@team), notice: 'Team of users was successfully created.' | ||
| 27 | + else | ||
| 28 | + render action: "new" | ||
| 29 | + end | ||
| 30 | + end | ||
| 31 | + | ||
| 32 | + def update | ||
| 33 | + user_team_params = params[:user_team].dup | ||
| 34 | + owner_id = user_team_params.delete(:owner_id) | ||
| 35 | + | ||
| 36 | + if owner_id | ||
| 37 | + user_team.owner = User.find(owner_id) | ||
| 38 | + end | ||
| 39 | + | ||
| 40 | + if user_team.update_attributes(user_team_params) | ||
| 41 | + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully updated.' | ||
| 42 | + else | ||
| 43 | + render action: "edit" | ||
| 44 | + end | ||
| 45 | + end | ||
| 46 | + | ||
| 47 | + def destroy | ||
| 48 | + user_team.destroy | ||
| 49 | + | ||
| 50 | + redirect_to admin_user_teams_path, notice: 'Team of users was successfully deleted.' | ||
| 51 | + end | ||
| 52 | + | ||
| 53 | + protected | ||
| 54 | + | ||
| 55 | + def user_team | ||
| 56 | + @team ||= UserTeam.find_by_path(params[:id]) | ||
| 57 | + end | ||
| 58 | + | ||
| 59 | +end |
app/controllers/admin/users_controller.rb
| 1 | -class Admin::UsersController < AdminController | 1 | +class Admin::UsersController < Admin::ApplicationController |
| 2 | def index | 2 | def index |
| 3 | @admin_users = User.scoped | 3 | @admin_users = User.scoped |
| 4 | @admin_users = @admin_users.filter(params[:filter]) | 4 | @admin_users = @admin_users.filter(params[:filter]) |
app/controllers/admin_controller.rb
| @@ -1,11 +0,0 @@ | @@ -1,11 +0,0 @@ | ||
| 1 | -# Provides a base class for Admin controllers to subclass | ||
| 2 | -# | ||
| 3 | -# Automatically sets the layout and ensures an administrator is logged in | ||
| 4 | -class AdminController < ApplicationController | ||
| 5 | - layout 'admin' | ||
| 6 | - before_filter :authenticate_admin! | ||
| 7 | - | ||
| 8 | - def authenticate_admin! | ||
| 9 | - return render_404 unless current_user.is_admin? | ||
| 10 | - end | ||
| 11 | -end |
app/controllers/application_controller.rb
| @@ -94,6 +94,14 @@ class ApplicationController < ActionController::Base | @@ -94,6 +94,14 @@ class ApplicationController < ActionController::Base | ||
| 94 | return access_denied! unless can?(current_user, :download_code, project) | 94 | return access_denied! unless can?(current_user, :download_code, project) |
| 95 | end | 95 | end |
| 96 | 96 | ||
| 97 | + def authorize_manage_user_team! | ||
| 98 | + return access_denied! unless user_team.present? && can?(current_user, :manage_user_team, user_team) | ||
| 99 | + end | ||
| 100 | + | ||
| 101 | + def authorize_admin_user_team! | ||
| 102 | + return access_denied! unless user_team.present? && can?(current_user, :admin_user_team, user_team) | ||
| 103 | + end | ||
| 104 | + | ||
| 97 | def access_denied! | 105 | def access_denied! |
| 98 | render "errors/access_denied", layout: "errors", status: 404 | 106 | render "errors/access_denied", layout: "errors", status: 404 |
| 99 | end | 107 | end |
| @@ -135,4 +143,5 @@ class ApplicationController < ActionController::Base | @@ -135,4 +143,5 @@ class ApplicationController < ActionController::Base | ||
| 135 | def dev_tools | 143 | def dev_tools |
| 136 | Rack::MiniProfiler.authorize_request | 144 | Rack::MiniProfiler.authorize_request |
| 137 | end | 145 | end |
| 146 | + | ||
| 138 | end | 147 | end |
app/controllers/dashboard_controller.rb
| @@ -18,6 +18,8 @@ class DashboardController < ApplicationController | @@ -18,6 +18,8 @@ class DashboardController < ApplicationController | ||
| 18 | @projects | 18 | @projects |
| 19 | end | 19 | end |
| 20 | 20 | ||
| 21 | + @teams = (UserTeam.with_member(current_user) + UserTeam.created_by(current_user)).uniq | ||
| 22 | + | ||
| 21 | @projects = @projects.page(params[:page]).per(30) | 23 | @projects = @projects.page(params[:page]).per(30) |
| 22 | 24 | ||
| 23 | @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) | 25 | @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) |
| @@ -0,0 +1,27 @@ | @@ -0,0 +1,27 @@ | ||
| 1 | +class Projects::TeamsController < Projects::ApplicationController | ||
| 2 | + | ||
| 3 | + def available | ||
| 4 | + @teams = current_user.is_admin? ? UserTeam.scoped : current_user.user_teams | ||
| 5 | + @teams = @teams.without_project(project) | ||
| 6 | + unless @teams.any? | ||
| 7 | + redirect_to project_team_index_path(project), notice: "No avaliable teams for assigment." | ||
| 8 | + end | ||
| 9 | + end | ||
| 10 | + | ||
| 11 | + def assign | ||
| 12 | + unless params[:team_id].blank? | ||
| 13 | + team = UserTeam.find(params[:team_id]) | ||
| 14 | + access = params[:greatest_project_access] | ||
| 15 | + team.assign_to_project(project, access) | ||
| 16 | + end | ||
| 17 | + redirect_to project_team_index_path(project) | ||
| 18 | + end | ||
| 19 | + | ||
| 20 | + def resign | ||
| 21 | + team = project.user_teams.find_by_path(params[:id]) | ||
| 22 | + team.resign_from_project(project) | ||
| 23 | + | ||
| 24 | + redirect_to project_team_index_path(project) | ||
| 25 | + end | ||
| 26 | + | ||
| 27 | +end |
app/controllers/projects_controller.rb
| @@ -19,7 +19,7 @@ class ProjectsController < ProjectResourceController | @@ -19,7 +19,7 @@ class ProjectsController < ProjectResourceController | ||
| 19 | end | 19 | end |
| 20 | 20 | ||
| 21 | def create | 21 | def create |
| 22 | - @project = Projects::CreateContext.new(current_user, params[:project]).execute | 22 | + @project = ::Projects::CreateContext.new(current_user, params[:project]).execute |
| 23 | 23 | ||
| 24 | respond_to do |format| | 24 | respond_to do |format| |
| 25 | flash[:notice] = 'Project was successfully created.' if @project.saved? | 25 | flash[:notice] = 'Project was successfully created.' if @project.saved? |
| @@ -35,7 +35,7 @@ class ProjectsController < ProjectResourceController | @@ -35,7 +35,7 @@ class ProjectsController < ProjectResourceController | ||
| 35 | end | 35 | end |
| 36 | 36 | ||
| 37 | def update | 37 | def update |
| 38 | - status = Projects::UpdateContext.new(project, current_user, params).execute | 38 | + status = ::Projects::UpdateContext.new(project, current_user, params).execute |
| 39 | 39 | ||
| 40 | respond_to do |format| | 40 | respond_to do |format| |
| 41 | if status | 41 | if status |
app/controllers/team_members_controller.rb
| @@ -4,15 +4,16 @@ class TeamMembersController < ProjectResourceController | @@ -4,15 +4,16 @@ class TeamMembersController < ProjectResourceController | ||
| 4 | before_filter :authorize_admin_project!, except: [:index, :show] | 4 | before_filter :authorize_admin_project!, except: [:index, :show] |
| 5 | 5 | ||
| 6 | def index | 6 | def index |
| 7 | + @teams = UserTeam.scoped | ||
| 7 | end | 8 | end |
| 8 | 9 | ||
| 9 | def show | 10 | def show |
| 10 | - @team_member = project.users_projects.find(params[:id]) | ||
| 11 | - @events = @team_member.user.recent_events.where(:project_id => @project.id).limit(7) | 11 | + @user_project_relation = project.users_projects.find_by_user_id(member) |
| 12 | + @events = member.recent_events.in_projects(project).limit(7) | ||
| 12 | end | 13 | end |
| 13 | 14 | ||
| 14 | def new | 15 | def new |
| 15 | - @team_member = project.users_projects.new | 16 | + @user_project_relation = project.users_projects.new |
| 16 | end | 17 | end |
| 17 | 18 | ||
| 18 | def create | 19 | def create |
| @@ -28,18 +29,18 @@ class TeamMembersController < ProjectResourceController | @@ -28,18 +29,18 @@ class TeamMembersController < ProjectResourceController | ||
| 28 | end | 29 | end |
| 29 | 30 | ||
| 30 | def update | 31 | def update |
| 31 | - @team_member = project.users_projects.find(params[:id]) | ||
| 32 | - @team_member.update_attributes(params[:team_member]) | 32 | + @user_project_relation = project.users_projects.find_by_user_id(member) |
| 33 | + @user_project_relation.update_attributes(params[:team_member]) | ||
| 33 | 34 | ||
| 34 | - unless @team_member.valid? | 35 | + unless @user_project_relation.valid? |
| 35 | flash[:alert] = "User should have at least one role" | 36 | flash[:alert] = "User should have at least one role" |
| 36 | end | 37 | end |
| 37 | redirect_to project_team_index_path(@project) | 38 | redirect_to project_team_index_path(@project) |
| 38 | end | 39 | end |
| 39 | 40 | ||
| 40 | def destroy | 41 | def destroy |
| 41 | - @team_member = project.users_projects.find(params[:id]) | ||
| 42 | - @team_member.destroy | 42 | + @user_project_relation = project.users_projects.find_by_user_id(params[:id]) |
| 43 | + @user_project_relation.destroy | ||
| 43 | 44 | ||
| 44 | respond_to do |format| | 45 | respond_to do |format| |
| 45 | format.html { redirect_to project_team_index_path(@project) } | 46 | format.html { redirect_to project_team_index_path(@project) } |
| @@ -54,4 +55,10 @@ class TeamMembersController < ProjectResourceController | @@ -54,4 +55,10 @@ class TeamMembersController < ProjectResourceController | ||
| 54 | 55 | ||
| 55 | redirect_to project_team_members_path(project), notice: notice | 56 | redirect_to project_team_members_path(project), notice: notice |
| 56 | end | 57 | end |
| 58 | + | ||
| 59 | + protected | ||
| 60 | + | ||
| 61 | + def member | ||
| 62 | + @member ||= User.find(params[:id]) | ||
| 63 | + end | ||
| 57 | end | 64 | end |
| @@ -0,0 +1,49 @@ | @@ -0,0 +1,49 @@ | ||
| 1 | +class Teams::MembersController < Teams::ApplicationController | ||
| 2 | + | ||
| 3 | + skip_before_filter :authorize_manage_user_team!, only: [:index] | ||
| 4 | + | ||
| 5 | + def index | ||
| 6 | + @members = user_team.members | ||
| 7 | + end | ||
| 8 | + | ||
| 9 | + def new | ||
| 10 | + @users = User.potential_team_members(user_team) | ||
| 11 | + @users = UserDecorator.decorate @users | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + def create | ||
| 15 | + unless params[:user_ids].blank? | ||
| 16 | + user_ids = params[:user_ids] | ||
| 17 | + access = params[:default_project_access] | ||
| 18 | + is_admin = params[:group_admin] | ||
| 19 | + user_team.add_members(user_ids, access, is_admin) | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | + redirect_to team_members_path(user_team), notice: 'Members was successfully added into Team of users.' | ||
| 23 | + end | ||
| 24 | + | ||
| 25 | + def edit | ||
| 26 | + team_member | ||
| 27 | + end | ||
| 28 | + | ||
| 29 | + def update | ||
| 30 | + options = {default_projects_access: params[:default_project_access], group_admin: params[:group_admin]} | ||
| 31 | + if user_team.update_membership(team_member, options) | ||
| 32 | + redirect_to team_members_path(user_team), notice: "Membership for #{team_member.name} was successfully updated in Team of users." | ||
| 33 | + else | ||
| 34 | + render :edit | ||
| 35 | + end | ||
| 36 | + end | ||
| 37 | + | ||
| 38 | + def destroy | ||
| 39 | + user_team.remove_member(team_member) | ||
| 40 | + redirect_to team_path(user_team), notice: "Member #{team_member.name} was successfully removed from Team of users." | ||
| 41 | + end | ||
| 42 | + | ||
| 43 | + protected | ||
| 44 | + | ||
| 45 | + def team_member | ||
| 46 | + @member ||= user_team.members.find(params[:id]) | ||
| 47 | + end | ||
| 48 | + | ||
| 49 | +end |
| @@ -0,0 +1,57 @@ | @@ -0,0 +1,57 @@ | ||
| 1 | +class Teams::ProjectsController < Teams::ApplicationController | ||
| 2 | + | ||
| 3 | + skip_before_filter :authorize_manage_user_team!, only: [:index] | ||
| 4 | + | ||
| 5 | + def index | ||
| 6 | + @projects = user_team.projects | ||
| 7 | + @avaliable_projects = current_user.admin? ? Project.without_team(user_team) : current_user.owned_projects.without_team(user_team) | ||
| 8 | + end | ||
| 9 | + | ||
| 10 | + def new | ||
| 11 | + user_team | ||
| 12 | + @avaliable_projects = current_user.owned_projects.scoped | ||
| 13 | + @avaliable_projects = @avaliable_projects.without_team(user_team) if user_team.projects.any? | ||
| 14 | + | ||
| 15 | + redirect_to team_projects_path(user_team), notice: "No avalible projects." unless @avaliable_projects.any? | ||
| 16 | + end | ||
| 17 | + | ||
| 18 | + def create | ||
| 19 | + redirect_to :back if params[:project_ids].blank? | ||
| 20 | + | ||
| 21 | + project_ids = params[:project_ids] | ||
| 22 | + access = params[:greatest_project_access] | ||
| 23 | + | ||
| 24 | + # Reject non-allowed projects | ||
| 25 | + allowed_project_ids = current_user.owned_projects.map(&:id) | ||
| 26 | + project_ids.select! { |id| allowed_project_ids.include?(id) } | ||
| 27 | + | ||
| 28 | + # Assign projects to team | ||
| 29 | + user_team.assign_to_projects(project_ids, access) | ||
| 30 | + | ||
| 31 | + redirect_to team_projects_path(user_team), notice: 'Team of users was successfully assigned to projects.' | ||
| 32 | + end | ||
| 33 | + | ||
| 34 | + def edit | ||
| 35 | + team_project | ||
| 36 | + end | ||
| 37 | + | ||
| 38 | + def update | ||
| 39 | + if user_team.update_project_access(team_project, params[:greatest_project_access]) | ||
| 40 | + redirect_to team_projects_path(user_team), notice: 'Access was successfully updated.' | ||
| 41 | + else | ||
| 42 | + render :edit | ||
| 43 | + end | ||
| 44 | + end | ||
| 45 | + | ||
| 46 | + def destroy | ||
| 47 | + user_team.resign_from_project(team_project) | ||
| 48 | + redirect_to team_projects_path(user_team), notice: 'Team of users was successfully reassigned from project.' | ||
| 49 | + end | ||
| 50 | + | ||
| 51 | + private | ||
| 52 | + | ||
| 53 | + def team_project | ||
| 54 | + @project ||= user_team.projects.find_with_namespace(params[:id]) | ||
| 55 | + end | ||
| 56 | + | ||
| 57 | +end |
| @@ -0,0 +1,92 @@ | @@ -0,0 +1,92 @@ | ||
| 1 | +class TeamsController < ApplicationController | ||
| 2 | + # Authorize | ||
| 3 | + before_filter :authorize_manage_user_team! | ||
| 4 | + before_filter :authorize_admin_user_team! | ||
| 5 | + | ||
| 6 | + # Skip access control on public section | ||
| 7 | + skip_before_filter :authorize_manage_user_team!, only: [:index, :show, :new, :destroy, :create, :search, :issues, :merge_requests] | ||
| 8 | + skip_before_filter :authorize_admin_user_team!, only: [:index, :show, :new, :create, :search, :issues, :merge_requests] | ||
| 9 | + | ||
| 10 | + layout 'user_team', only: [:show, :edit, :update, :destroy, :issues, :merge_requests, :search] | ||
| 11 | + | ||
| 12 | + def index | ||
| 13 | + @teams = current_user.user_teams.order('name ASC') | ||
| 14 | + end | ||
| 15 | + | ||
| 16 | + def show | ||
| 17 | + user_team | ||
| 18 | + projects | ||
| 19 | + @events = Event.in_projects(user_team.project_ids).limit(20).offset(params[:offset] || 0) | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | + def edit | ||
| 23 | + user_team | ||
| 24 | + end | ||
| 25 | + | ||
| 26 | + def update | ||
| 27 | + if user_team.update_attributes(params[:user_team]) | ||
| 28 | + redirect_to team_path(user_team) | ||
| 29 | + else | ||
| 30 | + render action: :edit | ||
| 31 | + end | ||
| 32 | + end | ||
| 33 | + | ||
| 34 | + def destroy | ||
| 35 | + user_team.destroy | ||
| 36 | + redirect_to teams_path | ||
| 37 | + end | ||
| 38 | + | ||
| 39 | + def new | ||
| 40 | + @team = UserTeam.new | ||
| 41 | + end | ||
| 42 | + | ||
| 43 | + def create | ||
| 44 | + @team = UserTeam.new(params[:user_team]) | ||
| 45 | + @team.owner = current_user unless params[:owner] | ||
| 46 | + @team.path = @team.name.dup.parameterize if @team.name | ||
| 47 | + | ||
| 48 | + if @team.save | ||
| 49 | + redirect_to team_path(@team) | ||
| 50 | + else | ||
| 51 | + render action: :new | ||
| 52 | + end | ||
| 53 | + end | ||
| 54 | + | ||
| 55 | + # Get authored or assigned open merge requests | ||
| 56 | + def merge_requests | ||
| 57 | + projects | ||
| 58 | + @merge_requests = MergeRequest.of_user_team(user_team) | ||
| 59 | + @merge_requests = FilterContext.new(@merge_requests, params).execute | ||
| 60 | + @merge_requests = @merge_requests.recent.page(params[:page]).per(20) | ||
| 61 | + end | ||
| 62 | + | ||
| 63 | + # Get only assigned issues | ||
| 64 | + def issues | ||
| 65 | + projects | ||
| 66 | + @issues = Issue.of_user_team(user_team) | ||
| 67 | + @issues = FilterContext.new(@issues, params).execute | ||
| 68 | + @issues = @issues.recent.page(params[:page]).per(20) | ||
| 69 | + @issues = @issues.includes(:author, :project) | ||
| 70 | + end | ||
| 71 | + | ||
| 72 | + def search | ||
| 73 | + result = SearchContext.new(user_team.project_ids, params).execute | ||
| 74 | + | ||
| 75 | + @projects = result[:projects] | ||
| 76 | + @merge_requests = result[:merge_requests] | ||
| 77 | + @issues = result[:issues] | ||
| 78 | + @wiki_pages = result[:wiki_pages] | ||
| 79 | + @teams = result[:teams] | ||
| 80 | + end | ||
| 81 | + | ||
| 82 | + protected | ||
| 83 | + | ||
| 84 | + def projects | ||
| 85 | + @projects ||= user_team.projects.sorted_by_activity | ||
| 86 | + end | ||
| 87 | + | ||
| 88 | + def user_team | ||
| 89 | + @team ||= UserTeam.find_by_path(params[:id]) | ||
| 90 | + end | ||
| 91 | + | ||
| 92 | +end |
app/decorators/user_decorator.rb
| @@ -8,4 +8,8 @@ class UserDecorator < ApplicationDecorator | @@ -8,4 +8,8 @@ class UserDecorator < ApplicationDecorator | ||
| 8 | def tm_of(project) | 8 | def tm_of(project) |
| 9 | project.team_member_by_id(self.id) | 9 | project.team_member_by_id(self.id) |
| 10 | end | 10 | end |
| 11 | + | ||
| 12 | + def name_with_email | ||
| 13 | + "#{name} (#{email})" | ||
| 14 | + end | ||
| 11 | end | 15 | end |
app/helpers/projects_helper.rb
| @@ -3,8 +3,12 @@ module ProjectsHelper | @@ -3,8 +3,12 @@ module ProjectsHelper | ||
| 3 | @project.users_projects.sort_by(&:project_access).reverse.group_by(&:project_access) | 3 | @project.users_projects.sort_by(&:project_access).reverse.group_by(&:project_access) |
| 4 | end | 4 | end |
| 5 | 5 | ||
| 6 | - def remove_from_team_message(project, member) | ||
| 7 | - "You are going to remove #{member.user_name} from #{project.name}. Are you sure?" | 6 | + def grouper_project_teams(project) |
| 7 | + @project.user_team_project_relationships.sort_by(&:greatest_access).reverse.group_by(&:greatest_access) | ||
| 8 | + end | ||
| 9 | + | ||
| 10 | + def remove_from_project_team_message(project, user) | ||
| 11 | + "You are going to remove #{user.name} from #{project.name} project team. Are you sure?" | ||
| 8 | end | 12 | end |
| 9 | 13 | ||
| 10 | def link_to_project project | 14 | def link_to_project project |
| @@ -0,0 +1,26 @@ | @@ -0,0 +1,26 @@ | ||
| 1 | +module UserTeamsHelper | ||
| 2 | + def team_filter_path(entity, options={}) | ||
| 3 | + exist_opts = { | ||
| 4 | + status: params[:status], | ||
| 5 | + project_id: params[:project_id], | ||
| 6 | + } | ||
| 7 | + | ||
| 8 | + options = exist_opts.merge(options) | ||
| 9 | + | ||
| 10 | + case entity | ||
| 11 | + when 'issue' then | ||
| 12 | + issues_team_path(@team, options) | ||
| 13 | + when 'merge_request' | ||
| 14 | + merge_requests_team_path(@team, options) | ||
| 15 | + end | ||
| 16 | + end | ||
| 17 | + | ||
| 18 | + def grouped_user_team_members(team) | ||
| 19 | + team.user_team_user_relationships.sort_by(&:permission).reverse.group_by(&:permission) | ||
| 20 | + end | ||
| 21 | + | ||
| 22 | + def remove_from_user_team_message(team, member) | ||
| 23 | + "You are going to remove #{member.name} from #{team.name}. Are you sure?" | ||
| 24 | + end | ||
| 25 | + | ||
| 26 | +end |
app/models/ability.rb
| @@ -8,6 +8,7 @@ class Ability | @@ -8,6 +8,7 @@ class Ability | ||
| 8 | when "Snippet" then snippet_abilities(object, subject) | 8 | when "Snippet" then snippet_abilities(object, subject) |
| 9 | when "MergeRequest" then merge_request_abilities(object, subject) | 9 | when "MergeRequest" then merge_request_abilities(object, subject) |
| 10 | when "Group", "Namespace" then group_abilities(object, subject) | 10 | when "Group", "Namespace" then group_abilities(object, subject) |
| 11 | + when "UserTeam" then user_team_abilities(object, subject) | ||
| 11 | else [] | 12 | else [] |
| 12 | end | 13 | end |
| 13 | end | 14 | end |
| @@ -110,6 +111,22 @@ class Ability | @@ -110,6 +111,22 @@ class Ability | ||
| 110 | rules.flatten | 111 | rules.flatten |
| 111 | end | 112 | end |
| 112 | 113 | ||
| 114 | + def user_team_abilities user, team | ||
| 115 | + rules = [] | ||
| 116 | + | ||
| 117 | + # Only group owner and administrators can manage group | ||
| 118 | + if team.owner == user || team.admin?(user) || user.admin? | ||
| 119 | + rules << [ :manage_user_team ] | ||
| 120 | + end | ||
| 121 | + | ||
| 122 | + if team.owner == user || user.admin? | ||
| 123 | + rules << [ :admin_user_team ] | ||
| 124 | + end | ||
| 125 | + | ||
| 126 | + rules.flatten | ||
| 127 | + end | ||
| 128 | + | ||
| 129 | + | ||
| 113 | [:issue, :note, :snippet, :merge_request].each do |name| | 130 | [:issue, :note, :snippet, :merge_request].each do |name| |
| 114 | define_method "#{name}_abilities" do |user, subject| | 131 | define_method "#{name}_abilities" do |user, subject| |
| 115 | if subject.author == user | 132 | if subject.author == user |
app/models/concerns/issuable.rb
| @@ -22,6 +22,7 @@ module Issuable | @@ -22,6 +22,7 @@ module Issuable | ||
| 22 | scope :opened, where(closed: false) | 22 | scope :opened, where(closed: false) |
| 23 | scope :closed, where(closed: true) | 23 | scope :closed, where(closed: true) |
| 24 | scope :of_group, ->(group) { where(project_id: group.project_ids) } | 24 | scope :of_group, ->(group) { where(project_id: group.project_ids) } |
| 25 | + scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) } | ||
| 25 | scope :assigned, ->(u) { where(assignee_id: u.id)} | 26 | scope :assigned, ->(u) { where(assignee_id: u.id)} |
| 26 | scope :recent, order("created_at DESC") | 27 | scope :recent, order("created_at DESC") |
| 27 | 28 |
app/models/project.rb
| @@ -33,28 +33,31 @@ class Project < ActiveRecord::Base | @@ -33,28 +33,31 @@ class Project < ActiveRecord::Base | ||
| 33 | attr_accessor :error_code | 33 | attr_accessor :error_code |
| 34 | 34 | ||
| 35 | # Relations | 35 | # Relations |
| 36 | - belongs_to :group, foreign_key: "namespace_id", conditions: "type = 'Group'" | 36 | + belongs_to :creator, foreign_key: "creator_id", class_name: "User" |
| 37 | + belongs_to :group, foreign_key: "namespace_id", conditions: "type = 'Group'" | ||
| 37 | belongs_to :namespace | 38 | belongs_to :namespace |
| 38 | 39 | ||
| 39 | - belongs_to :creator, | ||
| 40 | - class_name: "User", | ||
| 41 | - foreign_key: "creator_id" | ||
| 42 | - | ||
| 43 | - has_many :users, through: :users_projects | ||
| 44 | - has_many :events, dependent: :destroy | ||
| 45 | - has_many :merge_requests, dependent: :destroy | ||
| 46 | - has_many :issues, dependent: :destroy, order: "closed, created_at DESC" | ||
| 47 | - has_many :milestones, dependent: :destroy | ||
| 48 | - has_many :users_projects, dependent: :destroy | ||
| 49 | - has_many :notes, dependent: :destroy | ||
| 50 | - has_many :snippets, dependent: :destroy | ||
| 51 | - has_many :deploy_keys, dependent: :destroy, foreign_key: "project_id", class_name: "Key" | ||
| 52 | - has_many :hooks, dependent: :destroy, class_name: "ProjectHook" | ||
| 53 | - has_many :wikis, dependent: :destroy | ||
| 54 | - has_many :protected_branches, dependent: :destroy | ||
| 55 | has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id' | 40 | has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id' |
| 56 | has_one :gitlab_ci_service, dependent: :destroy | 41 | has_one :gitlab_ci_service, dependent: :destroy |
| 57 | 42 | ||
| 43 | + has_many :events, dependent: :destroy | ||
| 44 | + has_many :merge_requests, dependent: :destroy | ||
| 45 | + has_many :issues, dependent: :destroy, order: "closed, created_at DESC" | ||
| 46 | + has_many :milestones, dependent: :destroy | ||
| 47 | + has_many :users_projects, dependent: :destroy | ||
| 48 | + has_many :notes, dependent: :destroy | ||
| 49 | + has_many :snippets, dependent: :destroy | ||
| 50 | + has_many :deploy_keys, dependent: :destroy, class_name: "Key", foreign_key: "project_id" | ||
| 51 | + has_many :hooks, dependent: :destroy, class_name: "ProjectHook" | ||
| 52 | + has_many :wikis, dependent: :destroy | ||
| 53 | + has_many :protected_branches, dependent: :destroy | ||
| 54 | + has_many :user_team_project_relationships, dependent: :destroy | ||
| 55 | + | ||
| 56 | + has_many :users, through: :users_projects | ||
| 57 | + has_many :user_teams, through: :user_team_project_relationships | ||
| 58 | + has_many :user_team_user_relationships, through: :user_teams | ||
| 59 | + has_many :user_teams_members, through: :user_team_user_relationships | ||
| 60 | + | ||
| 58 | delegate :name, to: :owner, allow_nil: true, prefix: true | 61 | delegate :name, to: :owner, allow_nil: true, prefix: true |
| 59 | 62 | ||
| 60 | # Validations | 63 | # Validations |
| @@ -77,6 +80,8 @@ class Project < ActiveRecord::Base | @@ -77,6 +80,8 @@ class Project < ActiveRecord::Base | ||
| 77 | # Scopes | 80 | # Scopes |
| 78 | scope :without_user, ->(user) { where("id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } | 81 | scope :without_user, ->(user) { where("id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } |
| 79 | scope :not_in_group, ->(group) { where("id NOT IN (:ids)", ids: group.project_ids ) } | 82 | scope :not_in_group, ->(group) { where("id NOT IN (:ids)", ids: group.project_ids ) } |
| 83 | + scope :without_team, ->(team) { where("id NOT IN (:ids)", ids: team.projects.map(&:id)) } | ||
| 84 | + scope :in_team, ->(team) { where("id IN (:ids)", ids: team.projects.map(&:id)) } | ||
| 80 | scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } | 85 | scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } |
| 81 | scope :sorted_by_activity, ->() { order("(SELECT max(events.created_at) FROM events WHERE events.project_id = projects.id) DESC") } | 86 | scope :sorted_by_activity, ->() { order("(SELECT max(events.created_at) FROM events WHERE events.project_id = projects.id) DESC") } |
| 82 | scope :personal, ->(user) { where(namespace_id: user.namespace_id) } | 87 | scope :personal, ->(user) { where(namespace_id: user.namespace_id) } |
| @@ -122,7 +127,7 @@ class Project < ActiveRecord::Base | @@ -122,7 +127,7 @@ class Project < ActiveRecord::Base | ||
| 122 | end | 127 | end |
| 123 | 128 | ||
| 124 | def team | 129 | def team |
| 125 | - @team ||= Team.new(self) | 130 | + @team ||= ProjectTeam.new(self) |
| 126 | end | 131 | end |
| 127 | 132 | ||
| 128 | def repository | 133 | def repository |
| @@ -489,6 +494,11 @@ class Project < ActiveRecord::Base | @@ -489,6 +494,11 @@ class Project < ActiveRecord::Base | ||
| 489 | http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') | 494 | http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') |
| 490 | end | 495 | end |
| 491 | 496 | ||
| 497 | + def project_access_human(member) | ||
| 498 | + project_user_relation = self.users_projects.find_by_user_id(member.id) | ||
| 499 | + self.class.access_options.key(project_user_relation.project_access) | ||
| 500 | + end | ||
| 501 | + | ||
| 492 | # Check if current branch name is marked as protected in the system | 502 | # Check if current branch name is marked as protected in the system |
| 493 | def protected_branch? branch_name | 503 | def protected_branch? branch_name |
| 494 | protected_branches.map(&:name).include?(branch_name) | 504 | protected_branches.map(&:name).include?(branch_name) |
| @@ -0,0 +1,122 @@ | @@ -0,0 +1,122 @@ | ||
| 1 | +class ProjectTeam | ||
| 2 | + attr_accessor :project | ||
| 3 | + | ||
| 4 | + def initialize(project) | ||
| 5 | + @project = project | ||
| 6 | + end | ||
| 7 | + | ||
| 8 | + # Shortcut to add users | ||
| 9 | + # | ||
| 10 | + # Use: | ||
| 11 | + # @team << [@user, :master] | ||
| 12 | + # @team << [@users, :master] | ||
| 13 | + # | ||
| 14 | + def << args | ||
| 15 | + users = args.first | ||
| 16 | + | ||
| 17 | + if users.respond_to?(:each) | ||
| 18 | + add_users(users, args.second) | ||
| 19 | + else | ||
| 20 | + add_user(users, args.second) | ||
| 21 | + end | ||
| 22 | + end | ||
| 23 | + | ||
| 24 | + def get_tm user_id | ||
| 25 | + project.users_projects.find_by_user_id(user_id) | ||
| 26 | + end | ||
| 27 | + | ||
| 28 | + def add_user(user, access) | ||
| 29 | + add_users_ids([user.id], access) | ||
| 30 | + end | ||
| 31 | + | ||
| 32 | + def add_users(users, access) | ||
| 33 | + add_users_ids(users.map(&:id), access) | ||
| 34 | + end | ||
| 35 | + | ||
| 36 | + def add_users_ids(user_ids, access) | ||
| 37 | + UsersProject.add_users_into_projects( | ||
| 38 | + [project.id], | ||
| 39 | + user_ids, | ||
| 40 | + access | ||
| 41 | + ) | ||
| 42 | + end | ||
| 43 | + | ||
| 44 | + # Remove all users from project team | ||
| 45 | + def truncate | ||
| 46 | + UsersProject.truncate_team(project) | ||
| 47 | + end | ||
| 48 | + | ||
| 49 | + def members | ||
| 50 | + project.users_projects | ||
| 51 | + end | ||
| 52 | + | ||
| 53 | + def guests | ||
| 54 | + members.guests.map(&:user) | ||
| 55 | + end | ||
| 56 | + | ||
| 57 | + def reporters | ||
| 58 | + members.reporters.map(&:user) | ||
| 59 | + end | ||
| 60 | + | ||
| 61 | + def developers | ||
| 62 | + members.developers.map(&:user) | ||
| 63 | + end | ||
| 64 | + | ||
| 65 | + def masters | ||
| 66 | + members.masters.map(&:user) | ||
| 67 | + end | ||
| 68 | + | ||
| 69 | + def repository_readers | ||
| 70 | + repository_members[UsersProject::REPORTER] | ||
| 71 | + end | ||
| 72 | + | ||
| 73 | + def repository_writers | ||
| 74 | + repository_members[UsersProject::DEVELOPER] | ||
| 75 | + end | ||
| 76 | + | ||
| 77 | + def repository_masters | ||
| 78 | + repository_members[UsersProject::MASTER] | ||
| 79 | + end | ||
| 80 | + | ||
| 81 | + def repository_members | ||
| 82 | + keys = Hash.new {|h,k| h[k] = [] } | ||
| 83 | + UsersProject.select("keys.identifier, project_access"). | ||
| 84 | + joins(user: :keys).where(project_id: project.id). | ||
| 85 | + each {|row| keys[row.project_access] << [row.identifier] } | ||
| 86 | + | ||
| 87 | + keys[UsersProject::REPORTER] += project.deploy_keys.pluck(:identifier) | ||
| 88 | + keys | ||
| 89 | + end | ||
| 90 | + | ||
| 91 | + def import(source_project) | ||
| 92 | + target_project = project | ||
| 93 | + | ||
| 94 | + source_team = source_project.users_projects.all | ||
| 95 | + target_team = target_project.users_projects.all | ||
| 96 | + target_user_ids = target_team.map(&:user_id) | ||
| 97 | + | ||
| 98 | + source_team.reject! do |tm| | ||
| 99 | + # Skip if user already present in team | ||
| 100 | + target_user_ids.include?(tm.user_id) | ||
| 101 | + end | ||
| 102 | + | ||
| 103 | + source_team.map! do |tm| | ||
| 104 | + new_tm = tm.dup | ||
| 105 | + new_tm.id = nil | ||
| 106 | + new_tm.project_id = target_project.id | ||
| 107 | + new_tm.skip_git = true | ||
| 108 | + new_tm | ||
| 109 | + end | ||
| 110 | + | ||
| 111 | + UsersProject.transaction do | ||
| 112 | + source_team.each do |tm| | ||
| 113 | + tm.save | ||
| 114 | + end | ||
| 115 | + target_project.update_repository | ||
| 116 | + end | ||
| 117 | + | ||
| 118 | + true | ||
| 119 | + rescue | ||
| 120 | + false | ||
| 121 | + end | ||
| 122 | +end |
app/models/team.rb
| @@ -1,122 +0,0 @@ | @@ -1,122 +0,0 @@ | ||
| 1 | -class Team | ||
| 2 | - attr_accessor :project | ||
| 3 | - | ||
| 4 | - def initialize(project) | ||
| 5 | - @project = project | ||
| 6 | - end | ||
| 7 | - | ||
| 8 | - # Shortcut to add users | ||
| 9 | - # | ||
| 10 | - # Use: | ||
| 11 | - # @team << [@user, :master] | ||
| 12 | - # @team << [@users, :master] | ||
| 13 | - # | ||
| 14 | - def << args | ||
| 15 | - users = args.first | ||
| 16 | - | ||
| 17 | - if users.respond_to?(:each) | ||
| 18 | - add_users(users, args.second) | ||
| 19 | - else | ||
| 20 | - add_user(users, args.second) | ||
| 21 | - end | ||
| 22 | - end | ||
| 23 | - | ||
| 24 | - def get_tm user_id | ||
| 25 | - project.users_projects.find_by_user_id(user_id) | ||
| 26 | - end | ||
| 27 | - | ||
| 28 | - def add_user(user, access) | ||
| 29 | - add_users_ids([user.id], access) | ||
| 30 | - end | ||
| 31 | - | ||
| 32 | - def add_users(users, access) | ||
| 33 | - add_users_ids(users.map(&:id), access) | ||
| 34 | - end | ||
| 35 | - | ||
| 36 | - def add_users_ids(user_ids, access) | ||
| 37 | - UsersProject.add_users_into_projects( | ||
| 38 | - [project.id], | ||
| 39 | - user_ids, | ||
| 40 | - access | ||
| 41 | - ) | ||
| 42 | - end | ||
| 43 | - | ||
| 44 | - # Remove all users from project team | ||
| 45 | - def truncate | ||
| 46 | - UsersProject.truncate_team(project) | ||
| 47 | - end | ||
| 48 | - | ||
| 49 | - def members | ||
| 50 | - project.users_projects | ||
| 51 | - end | ||
| 52 | - | ||
| 53 | - def guests | ||
| 54 | - members.guests.map(&:user) | ||
| 55 | - end | ||
| 56 | - | ||
| 57 | - def reporters | ||
| 58 | - members.reporters.map(&:user) | ||
| 59 | - end | ||
| 60 | - | ||
| 61 | - def developers | ||
| 62 | - members.developers.map(&:user) | ||
| 63 | - end | ||
| 64 | - | ||
| 65 | - def masters | ||
| 66 | - members.masters.map(&:user) | ||
| 67 | - end | ||
| 68 | - | ||
| 69 | - def repository_readers | ||
| 70 | - repository_members[UsersProject::REPORTER] | ||
| 71 | - end | ||
| 72 | - | ||
| 73 | - def repository_writers | ||
| 74 | - repository_members[UsersProject::DEVELOPER] | ||
| 75 | - end | ||
| 76 | - | ||
| 77 | - def repository_masters | ||
| 78 | - repository_members[UsersProject::MASTER] | ||
| 79 | - end | ||
| 80 | - | ||
| 81 | - def repository_members | ||
| 82 | - keys = Hash.new {|h,k| h[k] = [] } | ||
| 83 | - UsersProject.select("keys.identifier, project_access"). | ||
| 84 | - joins(user: :keys).where(project_id: project.id). | ||
| 85 | - each {|row| keys[row.project_access] << [row.identifier] } | ||
| 86 | - | ||
| 87 | - keys[UsersProject::REPORTER] += project.deploy_keys.pluck(:identifier) | ||
| 88 | - keys | ||
| 89 | - end | ||
| 90 | - | ||
| 91 | - def import(source_project) | ||
| 92 | - target_project = project | ||
| 93 | - | ||
| 94 | - source_team = source_project.users_projects.all | ||
| 95 | - target_team = target_project.users_projects.all | ||
| 96 | - target_user_ids = target_team.map(&:user_id) | ||
| 97 | - | ||
| 98 | - source_team.reject! do |tm| | ||
| 99 | - # Skip if user already present in team | ||
| 100 | - target_user_ids.include?(tm.user_id) | ||
| 101 | - end | ||
| 102 | - | ||
| 103 | - source_team.map! do |tm| | ||
| 104 | - new_tm = tm.dup | ||
| 105 | - new_tm.id = nil | ||
| 106 | - new_tm.project_id = target_project.id | ||
| 107 | - new_tm.skip_git = true | ||
| 108 | - new_tm | ||
| 109 | - end | ||
| 110 | - | ||
| 111 | - UsersProject.transaction do | ||
| 112 | - source_team.each do |tm| | ||
| 113 | - tm.save | ||
| 114 | - end | ||
| 115 | - target_project.update_repository | ||
| 116 | - end | ||
| 117 | - | ||
| 118 | - true | ||
| 119 | - rescue | ||
| 120 | - false | ||
| 121 | - end | ||
| 122 | -end |
app/models/user.rb
| @@ -45,18 +45,27 @@ class User < ActiveRecord::Base | @@ -45,18 +45,27 @@ class User < ActiveRecord::Base | ||
| 45 | attr_accessor :force_random_password | 45 | attr_accessor :force_random_password |
| 46 | 46 | ||
| 47 | # Namespace for personal projects | 47 | # Namespace for personal projects |
| 48 | - has_one :namespace, class_name: "Namespace", foreign_key: :owner_id, conditions: 'type IS NULL', dependent: :destroy | ||
| 49 | - has_many :groups, class_name: "Group", foreign_key: :owner_id | ||
| 50 | - | ||
| 51 | - has_many :keys, dependent: :destroy | ||
| 52 | - has_many :users_projects, dependent: :destroy | ||
| 53 | - has_many :issues, foreign_key: :author_id, dependent: :destroy | ||
| 54 | - has_many :notes, foreign_key: :author_id, dependent: :destroy | ||
| 55 | - has_many :merge_requests, foreign_key: :author_id, dependent: :destroy | ||
| 56 | - has_many :events, class_name: "Event", foreign_key: :author_id, dependent: :destroy | ||
| 57 | - has_many :recent_events, class_name: "Event", foreign_key: :author_id, order: "id DESC" | ||
| 58 | - has_many :assigned_issues, class_name: "Issue", foreign_key: :assignee_id, dependent: :destroy | ||
| 59 | - has_many :assigned_merge_requests, class_name: "MergeRequest", foreign_key: :assignee_id, dependent: :destroy | 48 | + has_one :namespace, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace", conditions: 'type IS NULL' |
| 49 | + | ||
| 50 | + has_many :keys, dependent: :destroy | ||
| 51 | + has_many :users_projects, dependent: :destroy | ||
| 52 | + has_many :issues, dependent: :destroy, foreign_key: :author_id | ||
| 53 | + has_many :notes, dependent: :destroy, foreign_key: :author_id | ||
| 54 | + has_many :merge_requests, dependent: :destroy, foreign_key: :author_id | ||
| 55 | + has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event" | ||
| 56 | + has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" | ||
| 57 | + has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" | ||
| 58 | + | ||
| 59 | + has_many :groups, class_name: "Group", foreign_key: :owner_id | ||
| 60 | + has_many :recent_events, class_name: "Event", foreign_key: :author_id, order: "id DESC" | ||
| 61 | + | ||
| 62 | + has_many :projects, through: :users_projects | ||
| 63 | + | ||
| 64 | + has_many :user_team_user_relationships, dependent: :destroy | ||
| 65 | + | ||
| 66 | + has_many :user_teams, through: :user_team_user_relationships | ||
| 67 | + has_many :user_team_project_relationships, through: :user_teams | ||
| 68 | + has_many :team_projects, through: :user_team_project_relationships | ||
| 60 | 69 | ||
| 61 | validates :name, presence: true | 70 | validates :name, presence: true |
| 62 | validates :bio, length: { within: 0..255 } | 71 | validates :bio, length: { within: 0..255 } |
| @@ -80,6 +89,9 @@ class User < ActiveRecord::Base | @@ -80,6 +89,9 @@ class User < ActiveRecord::Base | ||
| 80 | scope :blocked, where(blocked: true) | 89 | scope :blocked, where(blocked: true) |
| 81 | scope :active, where(blocked: false) | 90 | scope :active, where(blocked: false) |
| 82 | scope :alphabetically, order('name ASC') | 91 | scope :alphabetically, order('name ASC') |
| 92 | + scope :in_team, ->(team){ where(id: team.member_ids) } | ||
| 93 | + scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) } | ||
| 94 | + scope :potential_team_members, ->(team) { team.members.any? ? active : active.not_in_team(team) } | ||
| 83 | 95 | ||
| 84 | # | 96 | # |
| 85 | # Class methods | 97 | # Class methods |
| @@ -0,0 +1,97 @@ | @@ -0,0 +1,97 @@ | ||
| 1 | +class UserTeam < ActiveRecord::Base | ||
| 2 | + attr_accessible :name, :owner_id, :path | ||
| 3 | + | ||
| 4 | + belongs_to :owner, class_name: User | ||
| 5 | + | ||
| 6 | + has_many :user_team_project_relationships, dependent: :destroy | ||
| 7 | + has_many :user_team_user_relationships, dependent: :destroy | ||
| 8 | + | ||
| 9 | + has_many :projects, through: :user_team_project_relationships | ||
| 10 | + has_many :members, through: :user_team_user_relationships, source: :user | ||
| 11 | + | ||
| 12 | + validates :name, presence: true, uniqueness: true | ||
| 13 | + validates :owner, presence: true | ||
| 14 | + validates :path, uniqueness: true, presence: true, length: { within: 1..255 }, | ||
| 15 | + format: { with: Gitlab::Regex.path_regex, | ||
| 16 | + message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" } | ||
| 17 | + | ||
| 18 | + scope :with_member, ->(user){ joins(:user_team_user_relationships).where(user_team_user_relationships: {user_id: user.id}) } | ||
| 19 | + scope :with_project, ->(project){ joins(:user_team_project_relationships).where(user_team_project_relationships: {project_id: project})} | ||
| 20 | + scope :without_project, ->(project){ where("user_teams.id NOT IN (:ids)", ids: (a = with_project(project); a.blank? ? 0 : a))} | ||
| 21 | + scope :created_by, ->(user){ where(owner_id: user) } | ||
| 22 | + | ||
| 23 | + class << self | ||
| 24 | + def search query | ||
| 25 | + where("name LIKE :query OR path LIKE :query", query: "%#{query}%") | ||
| 26 | + end | ||
| 27 | + | ||
| 28 | + def global_id | ||
| 29 | + 'GLN' | ||
| 30 | + end | ||
| 31 | + | ||
| 32 | + def access_roles | ||
| 33 | + UsersProject.access_roles | ||
| 34 | + end | ||
| 35 | + end | ||
| 36 | + | ||
| 37 | + def to_param | ||
| 38 | + path | ||
| 39 | + end | ||
| 40 | + | ||
| 41 | + def assign_to_projects(projects, access) | ||
| 42 | + projects.each do |project| | ||
| 43 | + assign_to_project(project, access) | ||
| 44 | + end | ||
| 45 | + end | ||
| 46 | + | ||
| 47 | + def assign_to_project(project, access) | ||
| 48 | + Gitlab::UserTeamManager.assign(self, project, access) | ||
| 49 | + end | ||
| 50 | + | ||
| 51 | + def resign_from_project(project) | ||
| 52 | + Gitlab::UserTeamManager.resign(self, project) | ||
| 53 | + end | ||
| 54 | + | ||
| 55 | + def add_members(users, access, group_admin) | ||
| 56 | + users.each do |user| | ||
| 57 | + add_member(user, access, group_admin) | ||
| 58 | + end | ||
| 59 | + end | ||
| 60 | + | ||
| 61 | + def add_member(user, access, group_admin) | ||
| 62 | + Gitlab::UserTeamManager.add_member_into_team(self, user, access, group_admin) | ||
| 63 | + end | ||
| 64 | + | ||
| 65 | + def remove_member(user) | ||
| 66 | + Gitlab::UserTeamManager.remove_member_from_team(self, user) | ||
| 67 | + end | ||
| 68 | + | ||
| 69 | + def update_membership(user, options) | ||
| 70 | + Gitlab::UserTeamManager.update_team_user_membership(self, user, options) | ||
| 71 | + end | ||
| 72 | + | ||
| 73 | + def update_project_access(project, permission) | ||
| 74 | + Gitlab::UserTeamManager.update_project_greates_access(self, project, permission) | ||
| 75 | + end | ||
| 76 | + | ||
| 77 | + def max_project_access(project) | ||
| 78 | + user_team_project_relationships.find_by_project_id(project).greatest_access | ||
| 79 | + end | ||
| 80 | + | ||
| 81 | + def human_max_project_access(project) | ||
| 82 | + self.class.access_roles.invert[max_project_access(project)] | ||
| 83 | + end | ||
| 84 | + | ||
| 85 | + def default_projects_access(member) | ||
| 86 | + user_team_user_relationships.find_by_user_id(member).permission | ||
| 87 | + end | ||
| 88 | + | ||
| 89 | + def human_default_projects_access(member) | ||
| 90 | + self.class.access_roles.invert[default_projects_access(member)] | ||
| 91 | + end | ||
| 92 | + | ||
| 93 | + def admin?(member) | ||
| 94 | + user_team_user_relationships.with_user(member).first.group_admin? | ||
| 95 | + end | ||
| 96 | + | ||
| 97 | +end |
| @@ -0,0 +1,28 @@ | @@ -0,0 +1,28 @@ | ||
| 1 | +class UserTeamProjectRelationship < ActiveRecord::Base | ||
| 2 | + attr_accessible :greatest_access, :project_id, :user_team_id | ||
| 3 | + | ||
| 4 | + belongs_to :user_team | ||
| 5 | + belongs_to :project | ||
| 6 | + | ||
| 7 | + validates :project, presence: true | ||
| 8 | + validates :user_team, presence: true | ||
| 9 | + validate :check_greatest_access | ||
| 10 | + | ||
| 11 | + scope :with_project, ->(project){ where(project_id: project.id) } | ||
| 12 | + | ||
| 13 | + def team_name | ||
| 14 | + user_team.name | ||
| 15 | + end | ||
| 16 | + | ||
| 17 | + private | ||
| 18 | + | ||
| 19 | + def check_greatest_access | ||
| 20 | + errors.add(:base, :incorrect_access_code) unless correct_access? | ||
| 21 | + end | ||
| 22 | + | ||
| 23 | + def correct_access? | ||
| 24 | + return false if greatest_access.blank? | ||
| 25 | + return true if UsersProject.access_roles.has_value?(greatest_access) | ||
| 26 | + false | ||
| 27 | + end | ||
| 28 | +end |
| @@ -0,0 +1,19 @@ | @@ -0,0 +1,19 @@ | ||
| 1 | +class UserTeamUserRelationship < ActiveRecord::Base | ||
| 2 | + attr_accessible :group_admin, :permission, :user_id, :user_team_id | ||
| 3 | + | ||
| 4 | + belongs_to :user_team | ||
| 5 | + belongs_to :user | ||
| 6 | + | ||
| 7 | + validates :user_team, presence: true | ||
| 8 | + validates :user, presence: true | ||
| 9 | + | ||
| 10 | + scope :with_user, ->(user) { where(user_id: user.id) } | ||
| 11 | + | ||
| 12 | + def user_name | ||
| 13 | + user.name | ||
| 14 | + end | ||
| 15 | + | ||
| 16 | + def access_human | ||
| 17 | + UsersProject.access_roles.invert[permission] | ||
| 18 | + end | ||
| 19 | +end |
app/models/users_project.rb
| @@ -39,7 +39,10 @@ class UsersProject < ActiveRecord::Base | @@ -39,7 +39,10 @@ class UsersProject < ActiveRecord::Base | ||
| 39 | scope :reporters, where(project_access: REPORTER) | 39 | scope :reporters, where(project_access: REPORTER) |
| 40 | scope :developers, where(project_access: DEVELOPER) | 40 | scope :developers, where(project_access: DEVELOPER) |
| 41 | scope :masters, where(project_access: MASTER) | 41 | scope :masters, where(project_access: MASTER) |
| 42 | + | ||
| 42 | scope :in_project, ->(project) { where(project_id: project.id) } | 43 | scope :in_project, ->(project) { where(project_id: project.id) } |
| 44 | + scope :in_projects, ->(projects) { where(project_id: project_ids) } | ||
| 45 | + scope :with_user, ->(user) { where(user_id: user.id) } | ||
| 43 | 46 | ||
| 44 | class << self | 47 | class << self |
| 45 | 48 |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | += form_for @team_member_relation, as: :team_member, url: admin_project_member_path(@project, @member) do |f| | ||
| 2 | + -if @team_member_relation.errors.any? | ||
| 3 | + .alert-message.block-message.error | ||
| 4 | + %ul | ||
| 5 | + - @team_member_relation.errors.full_messages.each do |msg| | ||
| 6 | + %li= msg | ||
| 7 | + | ||
| 8 | + .clearfix | ||
| 9 | + %label Project Access: | ||
| 10 | + .input | ||
| 11 | + = f.select :project_access, options_for_select(Project.access_options, @team_member_relation.project_access), {}, class: "project-access-select chosen span3" | ||
| 12 | + | ||
| 13 | + %br | ||
| 14 | + .actions | ||
| 15 | + = f.submit 'Save', class: "btn primary" | ||
| 16 | + = link_to 'Cancel', :back, class: "btn" |
app/views/admin/projects/show.html.haml
| @@ -114,9 +114,9 @@ | @@ -114,9 +114,9 @@ | ||
| 114 | %h5 | 114 | %h5 |
| 115 | Team | 115 | Team |
| 116 | %small | 116 | %small |
| 117 | - (#{@project.users_projects.count}) | 117 | + (#{@project.users.count}) |
| 118 | %br | 118 | %br |
| 119 | -%table.zebra-striped | 119 | +%table.zebra-striped.team_members |
| 120 | %thead | 120 | %thead |
| 121 | %tr | 121 | %tr |
| 122 | %th Name | 122 | %th Name |
| @@ -124,13 +124,13 @@ | @@ -124,13 +124,13 @@ | ||
| 124 | %th Repository Access | 124 | %th Repository Access |
| 125 | %th | 125 | %th |
| 126 | 126 | ||
| 127 | - - @project.users_projects.each do |tm| | 127 | + - @project.users.each do |tm| |
| 128 | %tr | 128 | %tr |
| 129 | %td | 129 | %td |
| 130 | - = link_to tm.user_name, admin_user_path(tm.user) | ||
| 131 | - %td= tm.project_access_human | ||
| 132 | - %td= link_to 'Edit Access', edit_admin_team_member_path(tm), class: "btn small" | ||
| 133 | - %td= link_to 'Remove from team', admin_team_member_path(tm), confirm: 'Are you sure?', method: :delete, class: "btn danger small" | 130 | + = link_to tm.name, admin_user_path(tm) |
| 131 | + %td= @project.project_access_human(tm) | ||
| 132 | + %td= link_to 'Edit Access', edit_admin_project_member_path(@project, tm), class: "btn small" | ||
| 133 | + %td= link_to 'Remove from team', admin_project_member_path(@project, tm), confirm: 'Are you sure?', method: :delete, class: "btn danger small" | ||
| 134 | 134 | ||
| 135 | %br | 135 | %br |
| 136 | %h5 Add new team member | 136 | %h5 Add new team member |
app/views/admin/team_members/_form.html.haml
| @@ -1,16 +0,0 @@ | @@ -1,16 +0,0 @@ | ||
| 1 | -= form_for @admin_team_member, as: :team_member, url: admin_team_member_path(@admin_team_member) do |f| | ||
| 2 | - -if @admin_team_member.errors.any? | ||
| 3 | - .alert-message.block-message.error | ||
| 4 | - %ul | ||
| 5 | - - @admin_team_member.errors.full_messages.each do |msg| | ||
| 6 | - %li= msg | ||
| 7 | - | ||
| 8 | - .clearfix | ||
| 9 | - %label Project Access: | ||
| 10 | - .input | ||
| 11 | - = f.select :project_access, options_for_select(Project.access_options, @admin_team_member.project_access), {}, class: "project-access-select chosen span3" | ||
| 12 | - | ||
| 13 | - %br | ||
| 14 | - .actions | ||
| 15 | - = f.submit 'Save', class: "btn primary" | ||
| 16 | - = link_to 'Cancel', :back, class: "btn" |
app/views/admin/team_members/edit.html.haml
| @@ -0,0 +1,23 @@ | @@ -0,0 +1,23 @@ | ||
| 1 | +%h3.page_title Rename Team | ||
| 2 | +%hr | ||
| 3 | += form_for @team, url: admin_team_path(@team), method: :put do |f| | ||
| 4 | + - if @team.errors.any? | ||
| 5 | + .alert-message.block-message.error | ||
| 6 | + %span= @team.errors.full_messages.first | ||
| 7 | + .clearfix.team_name_holder | ||
| 8 | + = f.label :name do | ||
| 9 | + Team name is | ||
| 10 | + .input | ||
| 11 | + = f.text_field :name, placeholder: "Example Team", class: "xxlarge" | ||
| 12 | + | ||
| 13 | + .clearfix.team_name_holder | ||
| 14 | + = f.label :path do | ||
| 15 | + %span.cred Team path is | ||
| 16 | + .input | ||
| 17 | + = f.text_field :path, placeholder: "example-team", class: "xxlarge danger" | ||
| 18 | + %ul.cred | ||
| 19 | + %li It will change web url for access team and team projects. | ||
| 20 | + | ||
| 21 | + .form-actions | ||
| 22 | + = f.submit 'Rename team', class: "btn danger" | ||
| 23 | + = link_to 'Cancel', admin_teams_path, class: "btn cancel-btn" |
| @@ -0,0 +1,38 @@ | @@ -0,0 +1,38 @@ | ||
| 1 | +%h3.page_title | ||
| 2 | + Teams | ||
| 3 | + %small | ||
| 4 | + simple Teams description | ||
| 5 | + | ||
| 6 | + = link_to 'New Team', new_admin_team_path, class: "btn small right" | ||
| 7 | + %br | ||
| 8 | + | ||
| 9 | += form_tag admin_teams_path, method: :get, class: 'form-inline' do | ||
| 10 | + = text_field_tag :name, params[:name], class: "xlarge" | ||
| 11 | + = submit_tag "Search", class: "btn submit primary" | ||
| 12 | + | ||
| 13 | +%table | ||
| 14 | + %thead | ||
| 15 | + %tr | ||
| 16 | + %th | ||
| 17 | + Name | ||
| 18 | + %i.icon-sort-down | ||
| 19 | + %th Path | ||
| 20 | + %th Projects | ||
| 21 | + %th Members | ||
| 22 | + %th Owner | ||
| 23 | + %th.cred Danger Zone! | ||
| 24 | + | ||
| 25 | + - @teams.each do |team| | ||
| 26 | + %tr | ||
| 27 | + %td | ||
| 28 | + %strong= link_to team.name, admin_team_path(team) | ||
| 29 | + %td= team.path | ||
| 30 | + %td= team.projects.count | ||
| 31 | + %td= team.members.count | ||
| 32 | + %td | ||
| 33 | + = link_to team.owner.name, admin_user_path(team.owner_id) | ||
| 34 | + %td.bgred | ||
| 35 | + = link_to 'Rename', edit_admin_team_path(team), id: "edit_#{dom_id(team)}", class: "btn small" | ||
| 36 | + = link_to 'Destroy', admin_team_path(team), confirm: "REMOVE #{team.name}? Are you sure?", method: :delete, class: "btn small danger" | ||
| 37 | + | ||
| 38 | += paginate @teams, theme: "admin" |
| @@ -0,0 +1,20 @@ | @@ -0,0 +1,20 @@ | ||
| 1 | += form_tag admin_team_member_path(@team, @member), method: :put do | ||
| 2 | + -if @member.errors.any? | ||
| 3 | + .alert-message.block-message.error | ||
| 4 | + %ul | ||
| 5 | + - @member.errors.full_messages.each do |msg| | ||
| 6 | + %li= msg | ||
| 7 | + | ||
| 8 | + .clearfix | ||
| 9 | + %label Default access for Team projects: | ||
| 10 | + .input | ||
| 11 | + = select_tag :default_project_access, options_for_select(UserTeam.access_roles, @team.default_projects_access(@member)), class: "project-access-select chosen span3" | ||
| 12 | + .clearfix | ||
| 13 | + %label Team admin? | ||
| 14 | + .input | ||
| 15 | + = check_box_tag :group_admin, true, @team.admin?(@member) | ||
| 16 | + | ||
| 17 | + %br | ||
| 18 | + .actions | ||
| 19 | + = submit_tag 'Save', class: "btn primary" | ||
| 20 | + = link_to 'Cancel', :back, class: "btn" |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | +%h3 | ||
| 2 | + Edit access #{@member.name} in #{@team.name} team | ||
| 3 | + | ||
| 4 | +%hr | ||
| 5 | +%table.zebra-striped | ||
| 6 | + %tr | ||
| 7 | + %td User: | ||
| 8 | + %td= @member.name | ||
| 9 | + %tr | ||
| 10 | + %td Team: | ||
| 11 | + %td= @team.name | ||
| 12 | + %tr | ||
| 13 | + %td Since: | ||
| 14 | + %td= member_since(@team, @member).stamp("Nov 11, 2010") | ||
| 15 | + | ||
| 16 | += render 'form' |
| @@ -0,0 +1,29 @@ | @@ -0,0 +1,29 @@ | ||
| 1 | +%h3.page_title | ||
| 2 | + Team: #{@team.name} | ||
| 3 | + | ||
| 4 | +%fieldset | ||
| 5 | + %legend Members (#{@team.members.count}) | ||
| 6 | + = form_tag admin_team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do | ||
| 7 | + %table#members_list | ||
| 8 | + %thead | ||
| 9 | + %tr | ||
| 10 | + %th User name | ||
| 11 | + %th Default project access | ||
| 12 | + %th Team access | ||
| 13 | + %th | ||
| 14 | + - @team.members.each do |member| | ||
| 15 | + %tr.member | ||
| 16 | + %td | ||
| 17 | + = link_to [:admin, member] do | ||
| 18 | + = member.name | ||
| 19 | + %small= "(#{member.email})" | ||
| 20 | + %td= @team.human_default_projects_access(member) | ||
| 21 | + %td= @team.admin?(member) ? "Admin" : "Member" | ||
| 22 | + %td | ||
| 23 | + %tr | ||
| 24 | + %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_email), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' | ||
| 25 | + %td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } | ||
| 26 | + %td | ||
| 27 | + %span= check_box_tag :group_admin | ||
| 28 | + %span Admin? | ||
| 29 | + %td= submit_tag 'Add', class: "btn primary", id: :add_members_to_team |
| @@ -0,0 +1,19 @@ | @@ -0,0 +1,19 @@ | ||
| 1 | +%h3.page_title New Team | ||
| 2 | +%hr | ||
| 3 | += form_for @team, url: admin_teams_path do |f| | ||
| 4 | + - if @team.errors.any? | ||
| 5 | + .alert-message.block-message.error | ||
| 6 | + %span= @team.errors.full_messages.first | ||
| 7 | + .clearfix | ||
| 8 | + = f.label :name do | ||
| 9 | + Team name is | ||
| 10 | + .input | ||
| 11 | + = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" | ||
| 12 | + | ||
| 13 | + = f.submit 'Create team', class: "btn primary" | ||
| 14 | + %hr | ||
| 15 | + .padded | ||
| 16 | + %ul | ||
| 17 | + %li All created teams are public (users can view who enter into team and which project are assigned for this team) | ||
| 18 | + %li People within a team see only projects they have access to | ||
| 19 | + %li You will be able to assign existing projects for team |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | += form_tag admin_team_project_path(@team, @project), method: :put do | ||
| 2 | + -if @project.errors.any? | ||
| 3 | + .alert-message.block-message.error | ||
| 4 | + %ul | ||
| 5 | + - @project.errors.full_messages.each do |msg| | ||
| 6 | + %li= msg | ||
| 7 | + | ||
| 8 | + .clearfix | ||
| 9 | + %label Max access for Team members: | ||
| 10 | + .input | ||
| 11 | + = select_tag :greatest_project_access, options_for_select(UserTeam.access_roles, @team.max_project_access(@project)), class: "project-access-select chosen span3" | ||
| 12 | + | ||
| 13 | + %br | ||
| 14 | + .actions | ||
| 15 | + = submit_tag 'Save', class: "btn primary" | ||
| 16 | + = link_to 'Cancel', :back, class: "btn" |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | +%h3 | ||
| 2 | + Edit max access in #{@project.name} for #{@team.name} team | ||
| 3 | + | ||
| 4 | +%hr | ||
| 5 | +%table.zebra-striped | ||
| 6 | + %tr | ||
| 7 | + %td Project: | ||
| 8 | + %td= @project.name | ||
| 9 | + %tr | ||
| 10 | + %td Team: | ||
| 11 | + %td= @team.name | ||
| 12 | + %tr | ||
| 13 | + %td Since: | ||
| 14 | + %td= assigned_since(@team, @project).stamp("Nov 11, 2010") | ||
| 15 | + | ||
| 16 | += render 'form' |
| @@ -0,0 +1,23 @@ | @@ -0,0 +1,23 @@ | ||
| 1 | +%h3.page_title | ||
| 2 | + Team: #{@team.name} | ||
| 3 | + | ||
| 4 | +%fieldset | ||
| 5 | + %legend Projects (#{@team.projects.count}) | ||
| 6 | + = form_tag admin_team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do | ||
| 7 | + %table#projects_list | ||
| 8 | + %thead | ||
| 9 | + %tr | ||
| 10 | + %th Project name | ||
| 11 | + %th Max access | ||
| 12 | + %th | ||
| 13 | + - @team.projects.each do |project| | ||
| 14 | + %tr.project | ||
| 15 | + %td | ||
| 16 | + = link_to project.name_with_namespace, [:admin, project] | ||
| 17 | + %td | ||
| 18 | + %span= @team.human_max_project_access(project) | ||
| 19 | + %td | ||
| 20 | + %tr | ||
| 21 | + %td= select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' | ||
| 22 | + %td= select_tag :greatest_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } | ||
| 23 | + %td= submit_tag 'Add', class: "btn primary", id: :assign_projects_to_team |
| @@ -0,0 +1,101 @@ | @@ -0,0 +1,101 @@ | ||
| 1 | +%h3.page_title | ||
| 2 | + Team: #{@team.name} | ||
| 3 | + | ||
| 4 | +%br | ||
| 5 | +%table.zebra-striped | ||
| 6 | + %thead | ||
| 7 | + %tr | ||
| 8 | + %th Team | ||
| 9 | + %th | ||
| 10 | + %tr | ||
| 11 | + %td | ||
| 12 | + %b | ||
| 13 | + Name: | ||
| 14 | + %td | ||
| 15 | + = @team.name | ||
| 16 | + | ||
| 17 | + = link_to edit_admin_team_path(@team), class: "btn btn-small right" do | ||
| 18 | + %i.icon-edit | ||
| 19 | + Rename | ||
| 20 | + %tr | ||
| 21 | + %td | ||
| 22 | + %b | ||
| 23 | + Owner: | ||
| 24 | + %td | ||
| 25 | + = @team.owner.name | ||
| 26 | + .right | ||
| 27 | + = link_to "#", class: "btn btn-small change-owner-link" do | ||
| 28 | + %i.icon-edit | ||
| 29 | + Change owner | ||
| 30 | + | ||
| 31 | + %tr.change-owner-holder.hide | ||
| 32 | + %td.bgred | ||
| 33 | + %b.cred | ||
| 34 | + New Owner: | ||
| 35 | + %td.bgred | ||
| 36 | + = form_for @team, url: admin_team_path(@team) do |f| | ||
| 37 | + = f.select :owner_id, User.all.map { |user| [user.name, user.id] }, {}, {class: 'chosen'} | ||
| 38 | + %div | ||
| 39 | + = f.submit 'Change Owner', class: "btn danger" | ||
| 40 | + = link_to "Cancel", "#", class: "btn change-owner-cancel-link" | ||
| 41 | + | ||
| 42 | +%fieldset | ||
| 43 | + %legend | ||
| 44 | + Members (#{@team.members.count}) | ||
| 45 | + %span= link_to 'Add members', new_admin_team_member_path(@team), class: "btn success small right", id: :add_members_to_team | ||
| 46 | + - if @team.members.any? | ||
| 47 | + %table#members_list | ||
| 48 | + %thead | ||
| 49 | + %tr | ||
| 50 | + %th User name | ||
| 51 | + %th Default project access | ||
| 52 | + %th Team access | ||
| 53 | + %th.cred.span3 Danger Zone! | ||
| 54 | + - @team.members.each do |member| | ||
| 55 | + %tr.member{ class: "user_#{member.id}"} | ||
| 56 | + %td | ||
| 57 | + = link_to [:admin, member] do | ||
| 58 | + = member.name | ||
| 59 | + %small= "(#{member.email})" | ||
| 60 | + %td= @team.human_default_projects_access(member) | ||
| 61 | + %td= @team.admin?(member) ? "Admin" : "Member" | ||
| 62 | + %td.bgred | ||
| 63 | + = link_to 'Edit', edit_admin_team_member_path(@team, member), class: "btn small" | ||
| 64 | + | ||
| 65 | + = link_to 'Remove', admin_team_member_path(@team, member), confirm: 'Remove member from team. Are you sure?', method: :delete, class: "btn danger small", id: "remove_member_#{member.id}" | ||
| 66 | + | ||
| 67 | +%fieldset | ||
| 68 | + %legend | ||
| 69 | + Projects (#{@team.projects.count}) | ||
| 70 | + %span= link_to 'Add projects', new_admin_team_project_path(@team), class: "btn success small right", id: :assign_projects_to_team | ||
| 71 | + - if @team.projects.any? | ||
| 72 | + %table#projects_list | ||
| 73 | + %thead | ||
| 74 | + %tr | ||
| 75 | + %th Project name | ||
| 76 | + %th Max access | ||
| 77 | + %th.cred.span3 Danger Zone! | ||
| 78 | + - @team.projects.each do |project| | ||
| 79 | + %tr.project | ||
| 80 | + %td | ||
| 81 | + = link_to project.name_with_namespace, [:admin, project] | ||
| 82 | + %td | ||
| 83 | + %span= @team.human_max_project_access(project) | ||
| 84 | + %td.bgred | ||
| 85 | + = link_to 'Edit', edit_admin_team_project_path(@team, project), class: "btn small" | ||
| 86 | + | ||
| 87 | + = link_to 'Relegate', admin_team_project_path(@team, project), confirm: 'Remove project from team. Are you sure?', method: :delete, class: "btn danger small", id: "relegate_project_#{project.id}" | ||
| 88 | + | ||
| 89 | +:javascript | ||
| 90 | + $(function(){ | ||
| 91 | + var modal = $('.change-owner-holder'); | ||
| 92 | + $('.change-owner-link').bind("click", function(){ | ||
| 93 | + $(this).hide(); | ||
| 94 | + modal.show(); | ||
| 95 | + }); | ||
| 96 | + $('.change-owner-cancel-link').bind("click", function(){ | ||
| 97 | + modal.hide(); | ||
| 98 | + $('.change-owner-link').show(); | ||
| 99 | + }) | ||
| 100 | + }) | ||
| 101 | + |
app/views/dashboard/_groups.html.haml
| 1 | -.groups_box | 1 | +.ui-box |
| 2 | %h5.title | 2 | %h5.title |
| 3 | Groups | 3 | Groups |
| 4 | %small | 4 | %small |
| @@ -13,8 +13,6 @@ | @@ -13,8 +13,6 @@ | ||
| 13 | %li | 13 | %li |
| 14 | = link_to group_path(id: group.path), class: dom_class(group) do | 14 | = link_to group_path(id: group.path), class: dom_class(group) do |
| 15 | %strong.well-title= truncate(group.name, length: 35) | 15 | %strong.well-title= truncate(group.name, length: 35) |
| 16 | - %span.arrow | ||
| 17 | - → | ||
| 18 | - %span.last_activity | ||
| 19 | - %strong Projects: | ||
| 20 | - %span= current_user.authorized_projects.where(namespace_id: group.id).count | 16 | + %span.right.light |
| 17 | + - if group.owner == current_user | ||
| 18 | + %i.icon-wrench |
app/views/dashboard/_sidebar.html.haml
| @@ -0,0 +1,20 @@ | @@ -0,0 +1,20 @@ | ||
| 1 | +.ui-box | ||
| 2 | + %h5.title | ||
| 3 | + Teams | ||
| 4 | + %small | ||
| 5 | + (#{@teams.count}) | ||
| 6 | + %span.right | ||
| 7 | + = link_to new_team_path, class: "btn very_small info" do | ||
| 8 | + %i.icon-plus | ||
| 9 | + New Team | ||
| 10 | + %ul.well-list | ||
| 11 | + - @teams.each do |team| | ||
| 12 | + %li | ||
| 13 | + = link_to team_path(id: team.path), class: dom_class(team) do | ||
| 14 | + %strong.well-title= truncate(team.name, length: 35) | ||
| 15 | + %span.right.light | ||
| 16 | + - if team.owner == current_user | ||
| 17 | + %i.icon-wrench | ||
| 18 | + - tm = current_user.user_team_user_relationships.find_by_user_team_id(team.id) | ||
| 19 | + - if tm | ||
| 20 | + = tm.access_human |
app/views/layouts/admin.html.haml
| @@ -10,6 +10,8 @@ | @@ -10,6 +10,8 @@ | ||
| 10 | = link_to "Stats", admin_root_path | 10 | = link_to "Stats", admin_root_path |
| 11 | = nav_link(controller: :projects) do | 11 | = nav_link(controller: :projects) do |
| 12 | = link_to "Projects", admin_projects_path | 12 | = link_to "Projects", admin_projects_path |
| 13 | + = nav_link(controller: :teams) do | ||
| 14 | + = link_to "Teams", admin_teams_path | ||
| 13 | = nav_link(controller: :groups) do | 15 | = nav_link(controller: :groups) do |
| 14 | = link_to "Groups", admin_groups_path | 16 | = link_to "Groups", admin_groups_path |
| 15 | = nav_link(controller: :users) do | 17 | = nav_link(controller: :users) do |
app/views/layouts/group.html.haml
| @@ -3,7 +3,7 @@ | @@ -3,7 +3,7 @@ | ||
| 3 | = render "layouts/head", title: "#{@group.name}" | 3 | = render "layouts/head", title: "#{@group.name}" |
| 4 | %body{class: "#{app_theme} application"} | 4 | %body{class: "#{app_theme} application"} |
| 5 | = render "layouts/flash" | 5 | = render "layouts/flash" |
| 6 | - = render "layouts/head_panel", title: "#{@group.name}" | 6 | + = render "layouts/head_panel", title: "group: #{@group.name}" |
| 7 | .container | 7 | .container |
| 8 | %ul.main_menu | 8 | %ul.main_menu |
| 9 | = nav_link(path: 'groups#show', html_options: {class: 'home'}) do | 9 | = nav_link(path: 'groups#show', html_options: {class: 'home'}) do |
| @@ -0,0 +1,40 @@ | @@ -0,0 +1,40 @@ | ||
| 1 | +!!! 5 | ||
| 2 | +%html{ lang: "en"} | ||
| 3 | + = render "layouts/head", title: "#{@team.name}" | ||
| 4 | + %body{class: "#{app_theme} application"} | ||
| 5 | + = render "layouts/flash" | ||
| 6 | + = render "layouts/head_panel", title: "team: #{@team.name}" | ||
| 7 | + .container | ||
| 8 | + %ul.main_menu | ||
| 9 | + = nav_link(path: 'teams#show', html_options: {class: 'home'}) do | ||
| 10 | + = link_to "Home", team_path(@team), title: "Home" | ||
| 11 | + | ||
| 12 | + = nav_link(path: 'teams#issues') do | ||
| 13 | + = link_to issues_team_path(@team) do | ||
| 14 | + Issues | ||
| 15 | + %span.count= Issue.opened.of_user_team(@team).count | ||
| 16 | + | ||
| 17 | + = nav_link(path: 'teams#merge_requests') do | ||
| 18 | + = link_to merge_requests_team_path(@team) do | ||
| 19 | + Merge Requests | ||
| 20 | + %span.count= MergeRequest.opened.of_user_team(@team).count | ||
| 21 | + | ||
| 22 | + = nav_link(path: 'teams#search') do | ||
| 23 | + = link_to "Search", search_team_path(@team) | ||
| 24 | + | ||
| 25 | + = nav_link(controller: [:members]) do | ||
| 26 | + = link_to team_members_path(@team), class: "team-tab tab" do | ||
| 27 | + Members | ||
| 28 | + | ||
| 29 | + - if can? current_user, :admin_user_team, @team | ||
| 30 | + = nav_link(controller: [:projects]) do | ||
| 31 | + = link_to team_projects_path(@team), class: "team-tab tab" do | ||
| 32 | + %i.icon-briefcase | ||
| 33 | + Projects | ||
| 34 | + | ||
| 35 | + = nav_link(path: 'teams#edit') do | ||
| 36 | + = link_to edit_team_path(@team), class: "stat-tab tab " do | ||
| 37 | + %i.icon-edit | ||
| 38 | + Edit Team | ||
| 39 | + | ||
| 40 | + .content= yield |
app/views/projects/_project_head.html.haml
| @@ -3,7 +3,7 @@ | @@ -3,7 +3,7 @@ | ||
| 3 | = link_to project_path(@project), class: "activities-tab tab" do | 3 | = link_to project_path(@project), class: "activities-tab tab" do |
| 4 | %i.icon-home | 4 | %i.icon-home |
| 5 | Show | 5 | Show |
| 6 | - = nav_link(controller: :team_members) do | 6 | + = nav_link(controller: [:team_members, :teams]) do |
| 7 | = link_to project_team_index_path(@project), class: "team-tab tab" do | 7 | = link_to project_team_index_path(@project), class: "team-tab tab" do |
| 8 | %i.icon-user | 8 | %i.icon-user |
| 9 | Team | 9 | Team |
| @@ -0,0 +1,22 @@ | @@ -0,0 +1,22 @@ | ||
| 1 | += render "projects/project_head" | ||
| 2 | + | ||
| 3 | +%h3.page_title | ||
| 4 | + = "Assign project to team of users" | ||
| 5 | +%hr | ||
| 6 | +%p.slead | ||
| 7 | + Read more about assign to team of users #{link_to "here", '#', class: 'vlink'}. | ||
| 8 | += form_tag assign_project_teams_path(@project), method: 'post' do | ||
| 9 | + %p.slead Choose Team of users you want to assign: | ||
| 10 | + .padded | ||
| 11 | + = label_tag :team_id, "Team" | ||
| 12 | + .input= select_tag(:team_id, options_from_collection_for_select(@teams, :id, :name), prompt: "Select team", class: "chosen xxlarge", required: true) | ||
| 13 | + %p.slead Choose greatest user acces in team you want to assign: | ||
| 14 | + .padded | ||
| 15 | + = label_tag :team_ids, "Permission" | ||
| 16 | + .input= select_tag :greatest_project_access, options_for_select(UserTeam.access_roles), {class: "project-access-select chosen span3" } | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + .actions | ||
| 20 | + = submit_tag 'Assign', class: "btn save-btn" | ||
| 21 | + = link_to "Cancel", project_team_index_path(@project), class: "btn cancel-btn" | ||
| 22 | + |
app/views/team_members/_form.html.haml
| 1 | %h3.page_title | 1 | %h3.page_title |
| 2 | = "New Team member(s)" | 2 | = "New Team member(s)" |
| 3 | %hr | 3 | %hr |
| 4 | -= form_for @team_member, as: :team_member, url: project_team_members_path(@project, @team_member) do |f| | ||
| 5 | - -if @team_member.errors.any? | 4 | += form_for @user_project_relation, as: :team_member, url: project_team_members_path(@project) do |f| |
| 5 | + -if @user_project_relation.errors.any? | ||
| 6 | .alert-message.block-message.error | 6 | .alert-message.block-message.error |
| 7 | %ul | 7 | %ul |
| 8 | - - @team_member.errors.full_messages.each do |msg| | 8 | + - @user_project_relation.errors.full_messages.each do |msg| |
| 9 | %li= msg | 9 | %li= msg |
| 10 | 10 | ||
| 11 | %h6 1. Choose people you want in the team | 11 | %h6 1. Choose people you want in the team |
| @@ -16,7 +16,7 @@ | @@ -16,7 +16,7 @@ | ||
| 16 | %h6 2. Set access level for them | 16 | %h6 2. Set access level for them |
| 17 | .clearfix | 17 | .clearfix |
| 18 | = f.label :project_access, "Project Access" | 18 | = f.label :project_access, "Project Access" |
| 19 | - .input= select_tag :project_access, options_for_select(Project.access_options, @team_member.project_access), class: "project-access-select chosen" | 19 | + .input= select_tag :project_access, options_for_select(Project.access_options, @user_project_relation.project_access), class: "project-access-select chosen" |
| 20 | 20 | ||
| 21 | .actions | 21 | .actions |
| 22 | = f.submit 'Save', class: "btn save-btn" | 22 | = f.submit 'Save', class: "btn save-btn" |
app/views/team_members/_show.html.haml
| 1 | - user = member.user | 1 | - user = member.user |
| 2 | - allow_admin = can? current_user, :admin_project, @project | 2 | - allow_admin = can? current_user, :admin_project, @project |
| 3 | -%li{id: dom_id(member), class: "team_member_row user_#{user.id}"} | 3 | +%li{id: dom_id(user), class: "team_member_row user_#{user.id}"} |
| 4 | .row | 4 | .row |
| 5 | .span6 | 5 | .span6 |
| 6 | - = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do | 6 | + = link_to project_team_member_path(@project, user), title: user.name, class: "dark" do |
| 7 | = image_tag gravatar_icon(user.email, 40), class: "avatar s32" | 7 | = image_tag gravatar_icon(user.email, 40), class: "avatar s32" |
| 8 | - = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do | 8 | + = link_to project_team_member_path(@project, user), title: user.name, class: "dark" do |
| 9 | %strong= truncate(user.name, lenght: 40) | 9 | %strong= truncate(user.name, lenght: 40) |
| 10 | %br | 10 | %br |
| 11 | %small.cgray= user.email | 11 | %small.cgray= user.email |
| @@ -13,7 +13,7 @@ | @@ -13,7 +13,7 @@ | ||
| 13 | .span5.right | 13 | .span5.right |
| 14 | - if allow_admin | 14 | - if allow_admin |
| 15 | .left | 15 | .left |
| 16 | - = form_for(member, as: :team_member, url: project_team_member_path(@project, member)) do |f| | 16 | + = form_for(member, as: :team_member, url: project_team_member_path(@project, member.user)) do |f| |
| 17 | = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2" | 17 | = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2" |
| 18 | .right | 18 | .right |
| 19 | - if current_user == user | 19 | - if current_user == user |
| @@ -23,6 +23,6 @@ | @@ -23,6 +23,6 @@ | ||
| 23 | - elsif user.blocked | 23 | - elsif user.blocked |
| 24 | %span.btn.disabled.blocked Blocked | 24 | %span.btn.disabled.blocked Blocked |
| 25 | - elsif allow_admin | 25 | - elsif allow_admin |
| 26 | - = link_to project_team_member_path(project_id: @project, id: member.id), confirm: remove_from_team_message(@project, member), method: :delete, class: "very_small btn danger" do | 26 | + = link_to project_team_member_path(@project, user), confirm: remove_from_project_team_message(@project, user), method: :delete, class: "very_small btn danger" do |
| 27 | %i.icon-minus.icon-white | 27 | %i.icon-minus.icon-white |
| 28 | 28 |
| @@ -0,0 +1,15 @@ | @@ -0,0 +1,15 @@ | ||
| 1 | +- team = team_rel.user_team | ||
| 2 | +- allow_admin = can? current_user, :admin_team_member, @project | ||
| 3 | +%li{id: dom_id(team), class: "user_team_row team_#{team.id}"} | ||
| 4 | + .row | ||
| 5 | + .span6 | ||
| 6 | + %strong= link_to team.name, team_path(team), title: team.name, class: "dark" | ||
| 7 | + %br | ||
| 8 | + %small.cgray Members: #{team.members.count} | ||
| 9 | + | ||
| 10 | + .span5.right | ||
| 11 | + .right | ||
| 12 | + - if allow_admin | ||
| 13 | + .left | ||
| 14 | + = link_to resign_project_team_path(@project, team), method: :delete, confirm: "Are you shure?", class: "btn danger small" do | ||
| 15 | + %i.icon-minus.icon-white |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | +- grouper_project_teams(@project).each do |access, teams| | ||
| 2 | + .ui-box | ||
| 3 | + %h5.title | ||
| 4 | + = UserTeam.access_roles.key(access).pluralize | ||
| 5 | + %small= teams.size | ||
| 6 | + %ul.well-list | ||
| 7 | + - teams.sort_by(&:team_name).each do |tofr| | ||
| 8 | + = render(partial: 'team_members/show_team', locals: {team_rel: tofr}) | ||
| 9 | + | ||
| 10 | + | ||
| 11 | +:javascript | ||
| 12 | + $(function(){ | ||
| 13 | + $('.repo-access-select, .project-access-select').live("change", function() { | ||
| 14 | + $(this.form).submit(); | ||
| 15 | + }); | ||
| 16 | + }) |
app/views/team_members/create.js.haml
| 1 | -- if @team_member.valid? | 1 | +- if @user_project_relation.valid? |
| 2 | :plain | 2 | :plain |
| 3 | $("#new_team_member").hide("slide", { direction: "right" }, 150, function(){ | 3 | $("#new_team_member").hide("slide", { direction: "right" }, 150, function(){ |
| 4 | $("#team-table").show("slide", { direction: "left" }, 150, function() { | 4 | $("#team-table").show("slide", { direction: "left" }, 150, function() { |
app/views/team_members/import.html.haml
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | = "Import team from another project" | 4 | = "Import team from another project" |
| 5 | %hr | 5 | %hr |
| 6 | %p.slead | 6 | %p.slead |
| 7 | - Read more about team import #{link_to "here", '#', class: 'vlink'}. | 7 | + Read more about project team import #{link_to "here", '#', class: 'vlink'}. |
| 8 | = form_tag apply_import_project_team_members_path(@project), method: 'post' do | 8 | = form_tag apply_import_project_team_members_path(@project), method: 'post' do |
| 9 | %p.slead Choose project you want to use as team source: | 9 | %p.slead Choose project you want to use as team source: |
| 10 | .padded | 10 | .padded |
app/views/team_members/index.html.haml
| 1 | = render "projects/project_head" | 1 | = render "projects/project_head" |
| 2 | %h3.page_title | 2 | %h3.page_title |
| 3 | Team Members | 3 | Team Members |
| 4 | - (#{@project.users_projects.count}) | 4 | + (#{@project.users.count}) |
| 5 | %small | 5 | %small |
| 6 | Read more about project permissions | 6 | Read more about project permissions |
| 7 | %strong= link_to "here", help_permissions_path, class: "vlink" | 7 | %strong= link_to "here", help_permissions_path, class: "vlink" |
| @@ -10,11 +10,24 @@ | @@ -10,11 +10,24 @@ | ||
| 10 | %span.right | 10 | %span.right |
| 11 | = link_to import_project_team_members_path(@project), class: "btn small grouped", title: "Import team from another project" do | 11 | = link_to import_project_team_members_path(@project), class: "btn small grouped", title: "Import team from another project" do |
| 12 | Import team from another project | 12 | Import team from another project |
| 13 | + = link_to available_project_teams_path(@project), class: "btn small grouped", title: "Assign project to team of users" do | ||
| 14 | + Assign project to Team of users | ||
| 13 | = link_to new_project_team_member_path(@project), class: "btn success small grouped", title: "New Team Member" do | 15 | = link_to new_project_team_member_path(@project), class: "btn success small grouped", title: "New Team Member" do |
| 14 | New Team Member | 16 | New Team Member |
| 15 | -%hr | ||
| 16 | 17 | ||
| 18 | +%hr | ||
| 17 | 19 | ||
| 18 | .clearfix | 20 | .clearfix |
| 19 | %div.team-table | 21 | %div.team-table |
| 20 | = render partial: "team_members/team", locals: {project: @project} | 22 | = render partial: "team_members/team", locals: {project: @project} |
| 23 | + | ||
| 24 | + | ||
| 25 | +%h3.page_title | ||
| 26 | + Assigned teams | ||
| 27 | + (#{@project.user_teams.count}) | ||
| 28 | + | ||
| 29 | +%hr | ||
| 30 | + | ||
| 31 | +.clearfix | ||
| 32 | +%div.team-table | ||
| 33 | + = render partial: "team_members/teams", locals: {project: @project} |
app/views/team_members/show.html.haml
| 1 | - allow_admin = can? current_user, :admin_project, @project | 1 | - allow_admin = can? current_user, :admin_project, @project |
| 2 | -- user = @team_member.user | ||
| 3 | 2 | ||
| 4 | .team_member_show | 3 | .team_member_show |
| 5 | - if can? current_user, :admin_project, @project | 4 | - if can? current_user, :admin_project, @project |
| 6 | - = link_to 'Remove from team', project_team_member_path(project_id: @project, id: @team_member.id), confirm: 'Are you sure?', method: :delete, class: "right btn danger" | 5 | + = link_to 'Remove from team', project_team_member_path(@project, @member), confirm: 'Are you sure?', method: :delete, class: "right btn danger" |
| 7 | .profile_avatar_holder | 6 | .profile_avatar_holder |
| 8 | - = image_tag gravatar_icon(user.email, 60), class: "borders" | 7 | + = image_tag gravatar_icon(@member.email, 60), class: "borders" |
| 9 | %h3.page_title | 8 | %h3.page_title |
| 10 | - = user.name | ||
| 11 | - %small (@#{user.username}) | 9 | + = @member.name |
| 10 | + %small (@#{@member.username}) | ||
| 12 | 11 | ||
| 13 | %hr | 12 | %hr |
| 14 | .back_link | 13 | .back_link |
| @@ -21,34 +20,34 @@ | @@ -21,34 +20,34 @@ | ||
| 21 | %table.lite | 20 | %table.lite |
| 22 | %tr | 21 | %tr |
| 23 | %td Email | 22 | %td Email |
| 24 | - %td= mail_to user.email | 23 | + %td= mail_to @member.email |
| 25 | %tr | 24 | %tr |
| 26 | %td Skype | 25 | %td Skype |
| 27 | - %td= user.skype | ||
| 28 | - - unless user.linkedin.blank? | 26 | + %td= @member.skype |
| 27 | + - unless @member.linkedin.blank? | ||
| 29 | %tr | 28 | %tr |
| 30 | %td LinkedIn | 29 | %td LinkedIn |
| 31 | - %td= user.linkedin | ||
| 32 | - - unless user.twitter.blank? | 30 | + %td= @member.linkedin |
| 31 | + - unless @member.twitter.blank? | ||
| 33 | %tr | 32 | %tr |
| 34 | %td Twitter | 33 | %td Twitter |
| 35 | - %td= user.twitter | ||
| 36 | - - unless user.bio.blank? | 34 | + %td= @member.twitter |
| 35 | + - unless @member.bio.blank? | ||
| 37 | %tr | 36 | %tr |
| 38 | %td Bio | 37 | %td Bio |
| 39 | - %td= user.bio | 38 | + %td= @member.bio |
| 40 | .span6 | 39 | .span6 |
| 41 | %table.lite | 40 | %table.lite |
| 42 | %tr | 41 | %tr |
| 43 | %td Member since | 42 | %td Member since |
| 44 | - %td= @team_member.created_at.stamp("Aug 21, 2011") | 43 | + %td= @user_project_relation.created_at.stamp("Aug 21, 2011") |
| 45 | %tr | 44 | %tr |
| 46 | %td | 45 | %td |
| 47 | Project Access: | 46 | Project Access: |
| 48 | %small (#{link_to "read more", help_permissions_path, class: "vlink"}) | 47 | %small (#{link_to "read more", help_permissions_path, class: "vlink"}) |
| 49 | %td | 48 | %td |
| 50 | - = form_for(@team_member, as: :team_member, url: project_team_member_path(@project, @team_member)) do |f| | ||
| 51 | - = f.select :project_access, options_for_select(Project.access_options, @team_member.project_access), {}, class: "project-access-select", disabled: !allow_admin | 49 | + = form_for(@user_project_relation, as: :team_member, url: project_team_member_path(@project, @member)) do |f| |
| 50 | + = f.select :project_access, options_for_select(Project.access_options, @user_project_relation.project_access), {}, class: "project-access-select", disabled: !allow_admin | ||
| 52 | %hr | 51 | %hr |
| 53 | = render @events | 52 | = render @events |
| 54 | :javascript | 53 | :javascript |
app/views/team_members/update.js.haml
| 1 | -- if @team_member.valid? | 1 | +- if @user_project_relation.valid? |
| 2 | :plain | 2 | :plain |
| 3 | - $("##{dom_id(@team_member)}").effect("highlight", {color: "#529214"}, 1000);; | 3 | + $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#529214"}, 1000);; |
| 4 | - else | 4 | - else |
| 5 | :plain | 5 | :plain |
| 6 | - $("##{dom_id(@team_member)}").effect("highlight", {color: "#D12F19"}, 1000);; | 6 | + $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#D12F19"}, 1000);; |
| @@ -0,0 +1,33 @@ | @@ -0,0 +1,33 @@ | ||
| 1 | += form_tag team_filter_path(entity), method: 'get' do | ||
| 2 | + %fieldset.dashboard-search-filter | ||
| 3 | + = search_field_tag "search", params[:search], { placeholder: 'Search', class: 'search-text-input' } | ||
| 4 | + = button_tag type: 'submit', class: 'btn' do | ||
| 5 | + %i.icon-search | ||
| 6 | + | ||
| 7 | + %fieldset | ||
| 8 | + %legend Status: | ||
| 9 | + %ul.nav.nav-pills.nav-stacked | ||
| 10 | + %li{class: ("active" if !params[:status])} | ||
| 11 | + = link_to team_filter_path(entity, status: nil) do | ||
| 12 | + Open | ||
| 13 | + %li{class: ("active" if params[:status] == 'closed')} | ||
| 14 | + = link_to team_filter_path(entity, status: 'closed') do | ||
| 15 | + Closed | ||
| 16 | + %li{class: ("active" if params[:status] == 'all')} | ||
| 17 | + = link_to team_filter_path(entity, status: 'all') do | ||
| 18 | + All | ||
| 19 | + | ||
| 20 | + %fieldset | ||
| 21 | + %legend Projects: | ||
| 22 | + %ul.nav.nav-pills.nav-stacked | ||
| 23 | + - @projects.each do |project| | ||
| 24 | + - unless entities_per_project(project, entity).zero? | ||
| 25 | + %li{class: ("active" if params[:project_id] == project.id.to_s)} | ||
| 26 | + = link_to team_filter_path(entity, project_id: project.id) do | ||
| 27 | + = project.name_with_namespace | ||
| 28 | + %small.right= entities_per_project(project, entity) | ||
| 29 | + | ||
| 30 | + %fieldset | ||
| 31 | + %hr | ||
| 32 | + = link_to "Reset", team_filter_path(entity), class: 'btn right' | ||
| 33 | + |
| @@ -0,0 +1,22 @@ | @@ -0,0 +1,22 @@ | ||
| 1 | +.projects_box | ||
| 2 | + %h5.title | ||
| 3 | + Projects | ||
| 4 | + %small | ||
| 5 | + (#{projects.count}) | ||
| 6 | + - if can? current_user, :manage_group, @group | ||
| 7 | + %span.right | ||
| 8 | + = link_to new_project_path(namespace_id: @group.id), class: "btn very_small info" do | ||
| 9 | + %i.icon-plus | ||
| 10 | + New Project | ||
| 11 | + %ul.well-list | ||
| 12 | + - if projects.blank? | ||
| 13 | + %p.nothing_here_message This team has no projects yet | ||
| 14 | + - projects.each do |project| | ||
| 15 | + %li | ||
| 16 | + = link_to project_path(project), class: dom_class(project) do | ||
| 17 | + %strong.well-title= truncate(project.name, length: 25) | ||
| 18 | + %span.arrow | ||
| 19 | + → | ||
| 20 | + %span.last_activity | ||
| 21 | + %strong Last activity: | ||
| 22 | + %span= project_last_activity(project) |
| @@ -0,0 +1,24 @@ | @@ -0,0 +1,24 @@ | ||
| 1 | += render "team_head" | ||
| 2 | + | ||
| 3 | +%h3.page_title= "Edit Team #{@team.name}" | ||
| 4 | +%hr | ||
| 5 | += form_for @team, url: teams_path do |f| | ||
| 6 | + - if @team.errors.any? | ||
| 7 | + .alert-message.block-message.error | ||
| 8 | + %span= @team.errors.full_messages.first | ||
| 9 | + .clearfix | ||
| 10 | + = f.label :name do | ||
| 11 | + Team name is | ||
| 12 | + .input | ||
| 13 | + = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" | ||
| 14 | + | ||
| 15 | + .clearfix | ||
| 16 | + = f.label :path do | ||
| 17 | + Team path is | ||
| 18 | + .input | ||
| 19 | + = f.text_field :path, placeholder: "opensource", class: "xxlarge left" | ||
| 20 | + .clearfix | ||
| 21 | + .input.span3.center | ||
| 22 | + = f.submit 'Save team changes', class: "btn primary" | ||
| 23 | + .input.span3.center | ||
| 24 | + = link_to 'Delete team', team_path(@team), method: :delete, confirm: "You are shure?", class: "btn danger" |
| @@ -0,0 +1,38 @@ | @@ -0,0 +1,38 @@ | ||
| 1 | +%h3.page_title | ||
| 2 | + Teams | ||
| 3 | + %small | ||
| 4 | + list of all teams | ||
| 5 | + | ||
| 6 | + = link_to 'New Team', new_team_path, class: "btn success small right" | ||
| 7 | + %br | ||
| 8 | + | ||
| 9 | += form_tag search_teams_path, method: :get, class: 'form-inline' do | ||
| 10 | + = text_field_tag :name, params[:name], class: "xlarge" | ||
| 11 | + = submit_tag "Search", class: "btn submit primary" | ||
| 12 | + | ||
| 13 | +%table.teams_list | ||
| 14 | + %thead | ||
| 15 | + %tr | ||
| 16 | + %th | ||
| 17 | + Name | ||
| 18 | + %i.icon-sort-down | ||
| 19 | + %th Path | ||
| 20 | + %th Projects | ||
| 21 | + %th Members | ||
| 22 | + %th Owner | ||
| 23 | + %th.cred Danger Zone! | ||
| 24 | + | ||
| 25 | + - @teams.each do |team| | ||
| 26 | + %tr | ||
| 27 | + %td | ||
| 28 | + %strong= link_to team.name, team_path(team) | ||
| 29 | + %td= team.path | ||
| 30 | + %td= link_to team.projects.count, team_projects_path(team) | ||
| 31 | + %td= link_to team.members.count, team_members_path(team) | ||
| 32 | + %td= link_to team.owner.name, team_member_path(team, team.owner) | ||
| 33 | + %td.bgred | ||
| 34 | + - if current_user.can?(:manage_user_team, team) | ||
| 35 | + = link_to "Edit", edit_team_path(team), class: "btn small" | ||
| 36 | + - if current_user.can?(:admin_user_team, team) | ||
| 37 | + = link_to "Destroy", team_path(team), method: :delete, confirm: "You are shure?", class: "danger btn small" | ||
| 38 | + |
| @@ -0,0 +1,25 @@ | @@ -0,0 +1,25 @@ | ||
| 1 | += render "team_head" | ||
| 2 | + | ||
| 3 | +%h3.page_title | ||
| 4 | + Issues | ||
| 5 | + %small (in Team projects assigned to Team members) | ||
| 6 | + %small.right #{@issues.total_count} issues | ||
| 7 | + | ||
| 8 | +%hr | ||
| 9 | +.row | ||
| 10 | + .span3 | ||
| 11 | + = render 'filter', entity: 'issue' | ||
| 12 | + .span9 | ||
| 13 | + - if @issues.any? | ||
| 14 | + - @issues.group_by(&:project).each do |group| | ||
| 15 | + %div.ui-box | ||
| 16 | + - @project = group[0] | ||
| 17 | + %h5.title | ||
| 18 | + = link_to_project @project | ||
| 19 | + %ul.well-list.issues_table | ||
| 20 | + - group[1].each do |issue| | ||
| 21 | + = render(partial: 'issues/show', locals: {issue: issue}) | ||
| 22 | + %hr | ||
| 23 | + = paginate @issues, theme: "gitlab" | ||
| 24 | + - else | ||
| 25 | + %p.nothing_here_message Nothing to show here |
| @@ -0,0 +1,20 @@ | @@ -0,0 +1,20 @@ | ||
| 1 | += form_tag admin_team_member_path(@team, @member), method: :put do | ||
| 2 | + -if @member.errors.any? | ||
| 3 | + .alert-message.block-message.error | ||
| 4 | + %ul | ||
| 5 | + - @member.errors.full_messages.each do |msg| | ||
| 6 | + %li= msg | ||
| 7 | + | ||
| 8 | + .clearfix | ||
| 9 | + %label Default access for Team projects: | ||
| 10 | + .input | ||
| 11 | + = select_tag :default_project_access, options_for_select(UserTeam.access_roles, @team.default_projects_access(@member)), class: "project-access-select chosen span3" | ||
| 12 | + .clearfix | ||
| 13 | + %label Team admin? | ||
| 14 | + .input | ||
| 15 | + = check_box_tag :group_admin, true, @team.admin?(@member) | ||
| 16 | + | ||
| 17 | + %br | ||
| 18 | + .actions | ||
| 19 | + = submit_tag 'Save', class: "btn primary" | ||
| 20 | + = link_to 'Cancel', :back, class: "btn" |
| @@ -0,0 +1,31 @@ | @@ -0,0 +1,31 @@ | ||
| 1 | +- user = member.user | ||
| 2 | +- allow_admin = can? current_user, :manage_user_team, @team | ||
| 3 | +%li{id: dom_id(member), class: "team_member_row user_#{user.id}"} | ||
| 4 | + .row | ||
| 5 | + .span5 | ||
| 6 | + = link_to user_path(user.username), title: user.name, class: "dark" do | ||
| 7 | + = image_tag gravatar_icon(user.email, 40), class: "avatar s32" | ||
| 8 | + = link_to user_path(user.username), title: user.name, class: "dark" do | ||
| 9 | + %strong= truncate(user.name, lenght: 40) | ||
| 10 | + %br | ||
| 11 | + %small.cgray= user.email | ||
| 12 | + | ||
| 13 | + .span6.right | ||
| 14 | + - if allow_admin | ||
| 15 | + .left.span2 | ||
| 16 | + = form_for(member, as: :team_member, url: team_member_path(@team, user)) do |f| | ||
| 17 | + = f.select :permission, options_for_select(UsersProject.access_roles, @team.default_projects_access(user)), {}, class: "medium project-access-select span2" | ||
| 18 | + .left.span2 | ||
| 19 | + %span | ||
| 20 | + Admin access | ||
| 21 | + = check_box_tag :group_admin | ||
| 22 | + .right | ||
| 23 | + - if current_user == user | ||
| 24 | + %span.btn.disabled This is you! | ||
| 25 | + - if @team.owner == user | ||
| 26 | + %span.btn.disabled.success Owner | ||
| 27 | + - elsif user.blocked | ||
| 28 | + %span.btn.disabled.blocked Blocked | ||
| 29 | + - elsif allow_admin | ||
| 30 | + = link_to team_member_path(@team, user), confirm: remove_from_user_team_message(@team, user), method: :delete, class: "very_small btn danger" do | ||
| 31 | + %i.icon-minus.icon-white |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | +- grouped_user_team_members(@team).each do |access, members| | ||
| 2 | + .ui-box | ||
| 3 | + %h5.title | ||
| 4 | + = Project.access_options.key(access).pluralize | ||
| 5 | + %small= members.size | ||
| 6 | + %ul.well-list | ||
| 7 | + - members.sort_by(&:user_name).each do |up| | ||
| 8 | + = render(partial: 'teams/members/show', locals: {member: up}) | ||
| 9 | + | ||
| 10 | + | ||
| 11 | +:javascript | ||
| 12 | + $(function(){ | ||
| 13 | + $('.repo-access-select, .project-access-select').live("change", function() { | ||
| 14 | + $(this.form).submit(); | ||
| 15 | + }); | ||
| 16 | + }) |
| @@ -0,0 +1,18 @@ | @@ -0,0 +1,18 @@ | ||
| 1 | += render "teams/team_head" | ||
| 2 | + | ||
| 3 | +%h3 | ||
| 4 | + Edit access #{@member.name} in #{@team.name} team | ||
| 5 | + | ||
| 6 | +%hr | ||
| 7 | +%table.zebra-striped | ||
| 8 | + %tr | ||
| 9 | + %td User: | ||
| 10 | + %td= @member.name | ||
| 11 | + %tr | ||
| 12 | + %td Team: | ||
| 13 | + %td= @team.name | ||
| 14 | + %tr | ||
| 15 | + %td Since: | ||
| 16 | + %td= member_since(@team, @member).stamp("Nov 11, 2010") | ||
| 17 | + | ||
| 18 | += render 'form' |
| @@ -0,0 +1,19 @@ | @@ -0,0 +1,19 @@ | ||
| 1 | += render "teams/team_head" | ||
| 2 | + | ||
| 3 | +%h3.page_title | ||
| 4 | + Team Members | ||
| 5 | + (#{@members.count}) | ||
| 6 | + %small | ||
| 7 | + Read more about project permissions | ||
| 8 | + %strong= link_to "here", help_permissions_path, class: "vlink" | ||
| 9 | + | ||
| 10 | + - if can? current_user, :manage_user_team, @team | ||
| 11 | + %span.right | ||
| 12 | + = link_to new_team_member_path(@team), class: "btn success small grouped", title: "New Team Member" do | ||
| 13 | + New Team Member | ||
| 14 | +%hr | ||
| 15 | + | ||
| 16 | + | ||
| 17 | +.clearfix | ||
| 18 | +%div.team-table | ||
| 19 | + = render partial: "teams/members/team", locals: {project: @team} |
| @@ -0,0 +1,30 @@ | @@ -0,0 +1,30 @@ | ||
| 1 | += render "teams/team_head" | ||
| 2 | + | ||
| 3 | +%h3.page_title | ||
| 4 | + Team: #{@team.name} | ||
| 5 | + | ||
| 6 | +%fieldset | ||
| 7 | + %legend Members (#{@team.members.count}) | ||
| 8 | + = form_tag team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do | ||
| 9 | + %table#members_list | ||
| 10 | + %thead | ||
| 11 | + %tr | ||
| 12 | + %th User name | ||
| 13 | + %th Default project access | ||
| 14 | + %th Team access | ||
| 15 | + %th | ||
| 16 | + - @team.members.each do |member| | ||
| 17 | + %tr.member | ||
| 18 | + %td | ||
| 19 | + = member.name | ||
| 20 | + %small= "(#{member.email})" | ||
| 21 | + %td= @team.human_default_projects_access(member) | ||
| 22 | + %td= @team.admin?(member) ? "Admin" : "Member" | ||
| 23 | + %td | ||
| 24 | + %tr | ||
| 25 | + %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_email), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' | ||
| 26 | + %td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } | ||
| 27 | + %td | ||
| 28 | + %span= check_box_tag :group_admin | ||
| 29 | + %span Admin? | ||
| 30 | + %td= submit_tag 'Add', class: "btn primary", id: :add_members_to_team |
| @@ -0,0 +1,62 @@ | @@ -0,0 +1,62 @@ | ||
| 1 | += render "teams/team_head" | ||
| 2 | + | ||
| 3 | +- allow_admin = can? current_user, :admin_project, @project | ||
| 4 | +- user = @team_member.user | ||
| 5 | + | ||
| 6 | +.team_member_show | ||
| 7 | + - if can? current_user, :admin_project, @project | ||
| 8 | + = link_to 'Remove from team', project_team_member_path(project_id: @project, id: @team_member.id), confirm: 'Are you sure?', method: :delete, class: "right btn danger" | ||
| 9 | + .profile_avatar_holder | ||
| 10 | + = image_tag gravatar_icon(user.email, 60), class: "borders" | ||
| 11 | + %h3.page_title | ||
| 12 | + = user.name | ||
| 13 | + %small (@#{user.username}) | ||
| 14 | + | ||
| 15 | + %hr | ||
| 16 | + .back_link | ||
| 17 | + %br | ||
| 18 | + = link_to project_team_index_path(@project), class: "" do | ||
| 19 | + ← To team list | ||
| 20 | + %br | ||
| 21 | + .row | ||
| 22 | + .span6 | ||
| 23 | + %table.lite | ||
| 24 | + %tr | ||
| 25 | + %td Email | ||
| 26 | + %td= mail_to user.email | ||
| 27 | + %tr | ||
| 28 | + %td Skype | ||
| 29 | + %td= user.skype | ||
| 30 | + - unless user.linkedin.blank? | ||
| 31 | + %tr | ||
| 32 | + %td LinkedIn | ||
| 33 | + %td= user.linkedin | ||
| 34 | + - unless user.twitter.blank? | ||
| 35 | + %tr | ||
| 36 | + %td Twitter | ||
| 37 | + %td= user.twitter | ||
| 38 | + - unless user.bio.blank? | ||
| 39 | + %tr | ||
| 40 | + %td Bio | ||
| 41 | + %td= user.bio | ||
| 42 | + .span6 | ||
| 43 | + %table.lite | ||
| 44 | + %tr | ||
| 45 | + %td Member since | ||
| 46 | + %td= @team_member.created_at.stamp("Aug 21, 2011") | ||
| 47 | + %tr | ||
| 48 | + %td | ||
| 49 | + Project Access: | ||
| 50 | + %small (#{link_to "read more", help_permissions_path, class: "vlink"}) | ||
| 51 | + %td | ||
| 52 | + = form_for(@team_member, as: :team_member, url: project_team_member_path(@project, @team_member)) do |f| | ||
| 53 | + = f.select :project_access, options_for_select(Project.access_options, @team_member.project_access), {}, class: "project-access-select", disabled: !allow_admin | ||
| 54 | + %hr | ||
| 55 | + = render @events | ||
| 56 | +:javascript | ||
| 57 | + $(function(){ | ||
| 58 | + $('.repo-access-select, .project-access-select').live("change", function() { | ||
| 59 | + $(this.form).submit(); | ||
| 60 | + }); | ||
| 61 | + }) | ||
| 62 | + |
| @@ -0,0 +1,26 @@ | @@ -0,0 +1,26 @@ | ||
| 1 | += render "team_head" | ||
| 2 | + | ||
| 3 | +%h3.page_title | ||
| 4 | + Merge Requests | ||
| 5 | + %small (authored by or assigned to Team members) | ||
| 6 | + %small.right #{@merge_requests.total_count} merge requests | ||
| 7 | + | ||
| 8 | +%hr | ||
| 9 | +.row | ||
| 10 | + .span3 | ||
| 11 | + = render 'filter', entity: 'merge_request' | ||
| 12 | + .span9 | ||
| 13 | + - if @merge_requests.any? | ||
| 14 | + - @merge_requests.group_by(&:project).each do |group| | ||
| 15 | + .ui-box | ||
| 16 | + - @project = group[0] | ||
| 17 | + %h5.title | ||
| 18 | + = link_to_project @project | ||
| 19 | + %ul.well-list | ||
| 20 | + - group[1].each do |merge_request| | ||
| 21 | + = render(partial: 'merge_requests/merge_request', locals: {merge_request: merge_request}) | ||
| 22 | + %hr | ||
| 23 | + = paginate @merge_requests, theme: "gitlab" | ||
| 24 | + | ||
| 25 | + - else | ||
| 26 | + %h3.nothing_here_message Nothing to show here |
| @@ -0,0 +1,19 @@ | @@ -0,0 +1,19 @@ | ||
| 1 | +%h3.page_title New Team | ||
| 2 | +%hr | ||
| 3 | += form_for @team, url: teams_path do |f| | ||
| 4 | + - if @team.errors.any? | ||
| 5 | + .alert-message.block-message.error | ||
| 6 | + %span= @team.errors.full_messages.first | ||
| 7 | + .clearfix | ||
| 8 | + = f.label :name do | ||
| 9 | + Team name is | ||
| 10 | + .input | ||
| 11 | + = f.text_field :name, placeholder: "Ex. Ruby Developers", class: "xxlarge left" | ||
| 12 | + | ||
| 13 | + = f.submit 'Create team', class: "btn primary" | ||
| 14 | + %hr | ||
| 15 | + .padded | ||
| 16 | + %ul | ||
| 17 | + %li All created teams are public (users can view who enter into team and which project are assigned for this team) | ||
| 18 | + %li People within a team see only projects they have access to | ||
| 19 | + %li You will be able to assign existing projects for team |
| @@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
| 1 | += form_tag team_project_path(@team, @project), method: :put do | ||
| 2 | + -if @project.errors.any? | ||
| 3 | + .alert-message.block-message.error | ||
| 4 | + %ul | ||
| 5 | + - @project.errors.full_messages.each do |msg| | ||
| 6 | + %li= msg | ||
| 7 | + | ||
| 8 | + .clearfix | ||
| 9 | + %label Max access for Team members: | ||
| 10 | + .input | ||
| 11 | + = select_tag :greatest_project_access, options_for_select(UserTeam.access_roles, @team.max_project_access(@project)), class: "project-access-select chosen span3" | ||
| 12 | + | ||
| 13 | + %br | ||
| 14 | + .actions | ||
| 15 | + = submit_tag 'Save', class: "btn primary" | ||
| 16 | + = link_to 'Cancel', :back, class: "btn" |
| @@ -0,0 +1,18 @@ | @@ -0,0 +1,18 @@ | ||
| 1 | += render "teams/team_head" | ||
| 2 | + | ||
| 3 | +%h3 | ||
| 4 | + Edit max access in #{@project.name} for #{@team.name} team | ||
| 5 | + | ||
| 6 | +%hr | ||
| 7 | +%table.zebra-striped | ||
| 8 | + %tr | ||
| 9 | + %td Project: | ||
| 10 | + %td= @project.name | ||
| 11 | + %tr | ||
| 12 | + %td Team: | ||
| 13 | + %td= @team.name | ||
| 14 | + %tr | ||
| 15 | + %td Since: | ||
| 16 | + %td= assigned_since(@team, @project).stamp("Nov 11, 2010") | ||
| 17 | + | ||
| 18 | += render 'form' |
| @@ -0,0 +1,34 @@ | @@ -0,0 +1,34 @@ | ||
| 1 | += render "teams/team_head" | ||
| 2 | + | ||
| 3 | +%h3.page_title | ||
| 4 | + Assigned projects (#{@team.projects.count}) | ||
| 5 | + %small | ||
| 6 | + Read more about project permissions | ||
| 7 | + %strong= link_to "here", help_permissions_path, class: "vlink" | ||
| 8 | + | ||
| 9 | + - if current_user.can?(:manage_user_team, @team) && @avaliable_projects.any? | ||
| 10 | + %span.right | ||
| 11 | + = link_to new_team_project_path(@team), class: "btn success small grouped", title: "New Team Member" do | ||
| 12 | + Assign project to Team | ||
| 13 | + | ||
| 14 | +%hr | ||
| 15 | + | ||
| 16 | +%table.projects-table | ||
| 17 | + %thead | ||
| 18 | + %tr | ||
| 19 | + %th Project name | ||
| 20 | + %th Max access | ||
| 21 | + - if current_user.can?(:admin_user_team, @team) | ||
| 22 | + %th.span3 | ||
| 23 | + | ||
| 24 | + - @team.projects.each do |project| | ||
| 25 | + %tr.project | ||
| 26 | + %td | ||
| 27 | + = link_to project.name_with_namespace, project_path(project) | ||
| 28 | + %td | ||
| 29 | + %span= @team.human_max_project_access(project) | ||
| 30 | + | ||
| 31 | + - if current_user.can?(:admin_user_team, @team) | ||
| 32 | + %td.bgred | ||
| 33 | + = link_to 'Edit max access', edit_team_project_path(@team, project), class: "btn small" | ||
| 34 | + = link_to 'Relegate', team_project_path(@team, project), confirm: 'Remove project from team and move to global namespace. Are you sure?', method: :delete, class: "btn danger small" |
| @@ -0,0 +1,25 @@ | @@ -0,0 +1,25 @@ | ||
| 1 | += render "teams/team_head" | ||
| 2 | + | ||
| 3 | +%h3.page_title | ||
| 4 | + Team: #{@team.name} | ||
| 5 | + | ||
| 6 | +%fieldset | ||
| 7 | + %legend Projects (#{@team.projects.count}) | ||
| 8 | + = form_tag team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do | ||
| 9 | + %table#projects_list | ||
| 10 | + %thead | ||
| 11 | + %tr | ||
| 12 | + %th Project name | ||
| 13 | + %th Max access | ||
| 14 | + %th | ||
| 15 | + - @team.projects.each do |project| | ||
| 16 | + %tr.project | ||
| 17 | + %td | ||
| 18 | + = link_to project.name_with_namespace, team_project_path(@team, project) | ||
| 19 | + %td | ||
| 20 | + %span= @team.human_max_project_access(project) | ||
| 21 | + %td | ||
| 22 | + %tr | ||
| 23 | + %td= select_tag :project_ids, options_from_collection_for_select(@avaliable_projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' | ||
| 24 | + %td= select_tag :greatest_project_access, options_for_select(UserTeam.access_roles), {class: "project-access-select chosen span3" } | ||
| 25 | + %td= submit_tag 'Add', class: "btn primary", id: :assign_projects_to_team |
| @@ -0,0 +1,11 @@ | @@ -0,0 +1,11 @@ | ||
| 1 | += render "team_head" | ||
| 2 | + | ||
| 3 | += form_tag search_team_path(@team), method: :get, class: 'form-inline' do |f| | ||
| 4 | + .padded | ||
| 5 | + = label_tag :search do | ||
| 6 | + %strong Looking for | ||
| 7 | + .input | ||
| 8 | + = search_field_tag :search, params[:search], placeholder: "issue 143", class: "input-xxlarge search-text-input", id: "dashboard_search" | ||
| 9 | + = submit_tag 'Search', class: "btn primary wide" | ||
| 10 | +- if params[:search].present? | ||
| 11 | + = render 'search/result' |
| @@ -0,0 +1,30 @@ | @@ -0,0 +1,30 @@ | ||
| 1 | += render "team_head" | ||
| 2 | + | ||
| 3 | +.projects | ||
| 4 | + .activities.span8 | ||
| 5 | + = link_to dashboard_path, class: 'btn very_small' do | ||
| 6 | + ← To dashboard | ||
| 7 | + | ||
| 8 | + %span.cgray Events and projects are filtered in scope of team | ||
| 9 | + %hr | ||
| 10 | + - if @events.any? | ||
| 11 | + .content_list | ||
| 12 | + - else | ||
| 13 | + %p.nothing_here_message Projects activity will be displayed here | ||
| 14 | + .loading.hide | ||
| 15 | + .side.span4 | ||
| 16 | + = render "projects", projects: @projects | ||
| 17 | + %div | ||
| 18 | + %span.rss-icon | ||
| 19 | + = link_to dashboard_path(:atom, { private_token: current_user.private_token }) do | ||
| 20 | + = image_tag "rss_ui.png", title: "feed" | ||
| 21 | + %strong News Feed | ||
| 22 | + | ||
| 23 | + %hr | ||
| 24 | + .gitlab-promo | ||
| 25 | + = link_to "Homepage", "http://gitlabhq.com" | ||
| 26 | + = link_to "Blog", "http://blog.gitlabhq.com" | ||
| 27 | + = link_to "@gitlabhq", "https://twitter.com/gitlabhq" | ||
| 28 | + | ||
| 29 | +:javascript | ||
| 30 | + $(function(){ Pager.init(20, true); }); |
config/routes.rb
| @@ -21,7 +21,7 @@ Gitlab::Application.routes.draw do | @@ -21,7 +21,7 @@ Gitlab::Application.routes.draw do | ||
| 21 | project_root: Gitlab.config.gitolite.repos_path, | 21 | project_root: Gitlab.config.gitolite.repos_path, |
| 22 | upload_pack: Gitlab.config.gitolite.upload_pack, | 22 | upload_pack: Gitlab.config.gitolite.upload_pack, |
| 23 | receive_pack: Gitlab.config.gitolite.receive_pack | 23 | receive_pack: Gitlab.config.gitolite.receive_pack |
| 24 | - }), at: '/', constraints: lambda { |request| /[-\/\w\.-]+\.git\//.match(request.path_info) } | 24 | + }), at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) } |
| 25 | 25 | ||
| 26 | # | 26 | # |
| 27 | # Help | 27 | # Help |
| @@ -56,6 +56,7 @@ Gitlab::Application.routes.draw do | @@ -56,6 +56,7 @@ Gitlab::Application.routes.draw do | ||
| 56 | put :unblock | 56 | put :unblock |
| 57 | end | 57 | end |
| 58 | end | 58 | end |
| 59 | + | ||
| 59 | resources :groups, constraints: { id: /[^\/]+/ } do | 60 | resources :groups, constraints: { id: /[^\/]+/ } do |
| 60 | member do | 61 | member do |
| 61 | put :project_update | 62 | put :project_update |
| @@ -63,18 +64,31 @@ Gitlab::Application.routes.draw do | @@ -63,18 +64,31 @@ Gitlab::Application.routes.draw do | ||
| 63 | delete :remove_project | 64 | delete :remove_project |
| 64 | end | 65 | end |
| 65 | end | 66 | end |
| 66 | - resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, except: [:new, :create] do | ||
| 67 | - member do | ||
| 68 | - get :team | ||
| 69 | - put :team_update | 67 | + |
| 68 | + resources :teams, constraints: { id: /[^\/]+/ } do | ||
| 69 | + scope module: :teams do | ||
| 70 | + resources :members, only: [:edit, :update, :destroy, :new, :create] | ||
| 71 | + resources :projects, only: [:edit, :update, :destroy, :new, :create], constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } | ||
| 70 | end | 72 | end |
| 71 | end | 73 | end |
| 72 | - resources :team_members, only: [:edit, :update, :destroy] | 74 | + |
| 73 | resources :hooks, only: [:index, :create, :destroy] do | 75 | resources :hooks, only: [:index, :create, :destroy] do |
| 74 | get :test | 76 | get :test |
| 75 | end | 77 | end |
| 78 | + | ||
| 76 | resource :logs, only: [:show] | 79 | resource :logs, only: [:show] |
| 77 | resource :resque, controller: 'resque', only: [:show] | 80 | resource :resque, controller: 'resque', only: [:show] |
| 81 | + | ||
| 82 | + resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, except: [:new, :create] do | ||
| 83 | + member do | ||
| 84 | + get :team | ||
| 85 | + put :team_update | ||
| 86 | + end | ||
| 87 | + scope module: :projects, constraints: { id: /[^\/]+/ } do | ||
| 88 | + resources :members, only: [:edit, :update, :destroy] | ||
| 89 | + end | ||
| 90 | + end | ||
| 91 | + | ||
| 78 | root to: "dashboard#index" | 92 | root to: "dashboard#index" |
| 79 | end | 93 | end |
| 80 | 94 | ||
| @@ -108,7 +122,6 @@ Gitlab::Application.routes.draw do | @@ -108,7 +122,6 @@ Gitlab::Application.routes.draw do | ||
| 108 | get "dashboard/issues" => "dashboard#issues" | 122 | get "dashboard/issues" => "dashboard#issues" |
| 109 | get "dashboard/merge_requests" => "dashboard#merge_requests" | 123 | get "dashboard/merge_requests" => "dashboard#merge_requests" |
| 110 | 124 | ||
| 111 | - | ||
| 112 | # | 125 | # |
| 113 | # Groups Area | 126 | # Groups Area |
| 114 | # | 127 | # |
| @@ -122,6 +135,24 @@ Gitlab::Application.routes.draw do | @@ -122,6 +135,24 @@ Gitlab::Application.routes.draw do | ||
| 122 | end | 135 | end |
| 123 | end | 136 | end |
| 124 | 137 | ||
| 138 | + # | ||
| 139 | + # Teams Area | ||
| 140 | + # | ||
| 141 | + resources :teams, constraints: { id: /[^\/]+/ } do | ||
| 142 | + member do | ||
| 143 | + get :issues | ||
| 144 | + get :merge_requests | ||
| 145 | + get :search | ||
| 146 | + end | ||
| 147 | + scope module: :teams do | ||
| 148 | + resources :members, only: [:index, :new, :create, :edit, :update, :destroy] | ||
| 149 | + resources :projects, only: [:index, :new, :create, :edit, :update, :destroy], constraints: { id: /[a-zA-Z.0-9_\-\/]+/ } | ||
| 150 | + end | ||
| 151 | + collection do | ||
| 152 | + get :search | ||
| 153 | + end | ||
| 154 | + end | ||
| 155 | + | ||
| 125 | resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create] | 156 | resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create] |
| 126 | 157 | ||
| 127 | devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations } | 158 | devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations } |
| @@ -238,6 +269,18 @@ Gitlab::Application.routes.draw do | @@ -238,6 +269,18 @@ Gitlab::Application.routes.draw do | ||
| 238 | end | 269 | end |
| 239 | end | 270 | end |
| 240 | 271 | ||
| 272 | + scope module: :projects do | ||
| 273 | + resources :teams, only: [] do | ||
| 274 | + collection do | ||
| 275 | + get :available | ||
| 276 | + post :assign | ||
| 277 | + end | ||
| 278 | + member do | ||
| 279 | + delete :resign | ||
| 280 | + end | ||
| 281 | + end | ||
| 282 | + end | ||
| 283 | + | ||
| 241 | resources :notes, only: [:index, :create, :destroy] do | 284 | resources :notes, only: [:index, :create, :destroy] do |
| 242 | collection do | 285 | collection do |
| 243 | post :preview | 286 | post :preview |
db/migrate/20121220064104_create_user_team_project_relationships.rb
0 → 100644
| @@ -0,0 +1,11 @@ | @@ -0,0 +1,11 @@ | ||
| 1 | +class CreateUserTeamProjectRelationships < ActiveRecord::Migration | ||
| 2 | + def change | ||
| 3 | + create_table :user_team_project_relationships do |t| | ||
| 4 | + t.integer :project_id | ||
| 5 | + t.integer :user_team_id | ||
| 6 | + t.integer :greatest_access | ||
| 7 | + | ||
| 8 | + t.timestamps | ||
| 9 | + end | ||
| 10 | + end | ||
| 11 | +end |