diff --git a/Procfile b/Procfile index 21dfade1..28a97dda 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ web: bundle exec unicorn_rails -p $PORT -worker: bundle exec sidekiq -q post_receive,mailer,system_hook,common,default +worker: bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,common,default diff --git a/app/assets/images/onion_skin_sprites.gif b/app/assets/images/onion_skin_sprites.gif new file mode 100644 index 00000000..85d20260 Binary files /dev/null and b/app/assets/images/onion_skin_sprites.gif differ diff --git a/app/assets/images/swipemode_sprites.gif b/app/assets/images/swipemode_sprites.gif new file mode 100644 index 00000000..327b3c31 Binary files /dev/null and b/app/assets/images/swipemode_sprites.gif differ diff --git a/app/assets/javascripts/commit/file.js.coffee b/app/assets/javascripts/commit/file.js.coffee new file mode 100644 index 00000000..a45ee58d --- /dev/null +++ b/app/assets/javascripts/commit/file.js.coffee @@ -0,0 +1,7 @@ +class CommitFile + + constructor: (file) -> + if $('.image', file).length + new ImageFile(file) + +this.CommitFile = CommitFile \ No newline at end of file diff --git a/app/assets/javascripts/commit/image-file.js.coffee b/app/assets/javascripts/commit/image-file.js.coffee new file mode 100644 index 00000000..f901a9e2 --- /dev/null +++ b/app/assets/javascripts/commit/image-file.js.coffee @@ -0,0 +1,128 @@ +class ImageFile + + # Width where images must fits in, for 2-up this gets divided by 2 + @availWidth = 900 + @viewModes = ['two-up', 'swipe'] + + constructor: (@file) -> + # Determine if old and new file has same dimensions, if not show 'two-up' view + this.requestImageInfo $('.two-up.view .frame.deleted img', @file), (deletedWidth, deletedHeight) => + this.requestImageInfo $('.two-up.view .frame.added img', @file), (width, height) => + if width == deletedWidth && height == deletedHeight + this.initViewModes() + else + this.initView('two-up') + + initViewModes: -> + viewMode = ImageFile.viewModes[0] + + $('.view-modes', @file).removeClass 'hide' + $('.view-modes-menu', @file).on 'click', 'li', (event) => + unless $(event.currentTarget).hasClass('active') + this.activateViewMode(event.currentTarget.className) + + this.activateViewMode(viewMode) + + activateViewMode: (viewMode) -> + $('.view-modes-menu li', @file) + .removeClass('active') + .filter(".#{viewMode}").addClass 'active' + $(".view:visible:not(.#{viewMode})", @file).fadeOut 200, => + $(".view.#{viewMode}", @file).fadeIn(200) + this.initView viewMode + + initView: (viewMode) -> + this.views[viewMode].call(this) + + prepareFrames = (view) -> + maxWidth = 0 + maxHeight = 0 + $('.frame', view).each (index, frame) => + width = $(frame).width() + height = $(frame).height() + maxWidth = if width > maxWidth then width else maxWidth + maxHeight = if height > maxHeight then height else maxHeight + .css + width: maxWidth + height: maxHeight + + [maxWidth, maxHeight] + + views: + 'two-up': -> + $('.two-up.view .wrap', @file).each (index, wrap) => + $('img', wrap).each -> + currentWidth = $(this).width() + if currentWidth > ImageFile.availWidth / 2 + $(this).width ImageFile.availWidth / 2 + + this.requestImageInfo $('img', wrap), (width, height) -> + $('.image-info .meta-width', wrap).text "#{width}px" + $('.image-info .meta-height', wrap).text "#{height}px" + $('.image-info', wrap).removeClass('hide') + + 'swipe': -> + maxWidth = 0 + maxHeight = 0 + + $('.swipe.view', @file).each (index, view) => + + [maxWidth, maxHeight] = prepareFrames(view) + + $('.swipe-frame', view).css + width: maxWidth + 16 + height: maxHeight + 28 + + $('.swipe-wrap', view).css + width: maxWidth + 1 + height: maxHeight + 2 + + $('.swipe-bar', view).css + left: 0 + .draggable + axis: 'x' + containment: 'parent' + drag: (event) -> + $('.swipe-wrap', view).width (maxWidth + 1) - $(this).position().left + stop: (event) -> + $('.swipe-wrap', view).width (maxWidth + 1) - $(this).position().left + + 'onion-skin': -> + maxWidth = 0 + maxHeight = 0 + + dragTrackWidth = $('.drag-track', @file).width() - $('.dragger', @file).width() + + $('.onion-skin.view', @file).each (index, view) => + + [maxWidth, maxHeight] = prepareFrames(view) + + $('.onion-skin-frame', view).css + width: maxWidth + 16 + height: maxHeight + 28 + + $('.swipe-wrap', view).css + width: maxWidth + 1 + height: maxHeight + 2 + + $('.dragger', view).css + left: dragTrackWidth + .draggable + axis: 'x' + containment: 'parent' + drag: (event) -> + $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth) + stop: (event) -> + $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth) + + + + requestImageInfo: (img, callback) -> + domImg = img.get(0) + if domImg.complete + callback.call(this, domImg.naturalWidth, domImg.naturalHeight) + else + img.on 'load', => + callback.call(this, domImg.naturalWidth, domImg.naturalHeight) + +this.ImageFile = ImageFile \ No newline at end of file diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js deleted file mode 100644 index b31fe485..00000000 --- a/app/assets/javascripts/commits.js +++ /dev/null @@ -1,59 +0,0 @@ -var CommitsList = { - ref:null, - limit:0, - offset:0, - disable:false, - - init: - function(ref, limit) { - $(".day-commits-table li.commit").live('click', function(e){ - if(e.target.nodeName != "A") { - location.href = $(this).attr("url"); - e.stopPropagation(); - return false; - } - }); - - this.ref=ref; - this.limit=limit; - this.offset=limit; - this.initLoadMore(); - $('.loading').show(); - }, - - getOld: - function() { - $('.loading').show(); - $.ajax({ - type: "GET", - url: location.href, - data: "limit=" + this.limit + "&offset=" + this.offset + "&ref=" + this.ref, - complete: function(){ $('.loading').hide()}, - dataType: "script"}); - }, - - append: - function(count, html) { - $("#commits_list").append(html); - if(count > 0) { - this.offset += count; - } else { - this.disable = true; - } - }, - - initLoadMore: - function() { - $(document).endlessScroll({ - bottomPixels: 400, - fireDelay: 1000, - fireOnce:true, - ceaseFire: function() { - return CommitsList.disable; - }, - callback: function(i) { - CommitsList.getOld(); - } - }); - } -} diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee new file mode 100644 index 00000000..47d6fcf8 --- /dev/null +++ b/app/assets/javascripts/commits.js.coffee @@ -0,0 +1,54 @@ +class CommitsList + @data = + ref: null + limit: 0 + offset: 0 + @disable = false + + @showProgress: -> + $('.loading').show() + + @hideProgress: -> + $('.loading').hide() + + @init: (ref, limit) -> + $(".day-commits-table li.commit").live 'click', (event) -> + if event.target.nodeName != "A" + location.href = $(this).attr("url") + e.stopPropagation() + return false + + @data.ref = ref + @data.limit = limit + @data.offset = limit + + this.initLoadMore() + this.showProgress(); + + @getOld: -> + this.showProgress() + $.ajax + type: "GET" + url: location.href + data: @data + complete: this.hideProgress + dataType: "script" + + @append: (count, html) -> + $("#commits-list").append(html) + if count > 0 + @data.offset += count + else + @disable = true + + @initLoadMore: -> + $(document).endlessScroll + bottomPixels: 400 + fireDelay: 1000 + fireOnce: true + ceaseFire: => + @disable + callback: => + this.getOld() + +this.CommitsList = CommitsList \ No newline at end of file diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index f15d09dd..6171e0d5 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -4,11 +4,11 @@ window.dashboardPage = -> event.preventDefault() toggleFilter $(this) reloadActivities() - + reloadActivities = -> $(".content_list").html '' Pager.init 20, true - + toggleFilter = (sender) -> sender.parent().toggleClass "inactive" event_filters = $.cookie("event_filter") @@ -17,11 +17,11 @@ toggleFilter = (sender) -> event_filters = event_filters.split(",") else event_filters = new Array() - + index = event_filters.indexOf(filter) if index is -1 event_filters.push filter else event_filters.splice index, 1 - + $.cookie "event_filter", event_filters.join(",") diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee index 9c9cc613..65ed817c 100644 --- a/app/assets/javascripts/merge_requests.js.coffee +++ b/app/assets/javascripts/merge_requests.js.coffee @@ -1,6 +1,6 @@ # # * Filter merge requests -# +# @merge_requestsPage = -> $('#assignee_id').chosen() $('#milestone_id').chosen() @@ -8,16 +8,16 @@ $(this).closest('form').submit() class MergeRequest - + constructor: (@opts) -> this.$el = $('.merge-request') @diffs_loaded = false @commits_loaded = false - + this.activateTab(@opts.action) - + this.bindEvents() - + this.initMergeWidget() this.$('.show-all-commits').on 'click', => this.showAllCommits() @@ -28,7 +28,7 @@ class MergeRequest initMergeWidget: -> this.showState( @opts.current_state ) - + if this.$('.automerge_widget').length and @opts.check_enable $.get @opts.url_to_automerge_check, (data) => this.showState( data.state ) @@ -42,12 +42,12 @@ class MergeRequest bindEvents: -> this.$('.nav-tabs').on 'click', 'a', (event) => a = $(event.currentTarget) - + href = a.attr('href') History.replaceState {path: href}, document.title, href - + event.preventDefault() - + this.$('.nav-tabs').on 'click', 'li', (event) => this.activateTab($(event.currentTarget).data('action')) diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js deleted file mode 100644 index 7edd6bd6..00000000 --- a/app/assets/javascripts/pager.js +++ /dev/null @@ -1,56 +0,0 @@ -var Pager = { - limit:0, - offset:0, - disable:false, - - init: - function(limit, preload) { - this.limit=limit; - - if(preload) { - this.offset = 0; - this.getOld(); - } else { - this.offset = limit; - } - - this.initLoadMore(); - }, - - getOld: - function() { - $('.loading').show(); - $.ajax({ - type: "GET", - url: location.href, - data: "limit=" + this.limit + "&offset=" + this.offset, - complete: function(){ $('.loading').hide()}, - dataType: "script"}); - }, - - append: - function(count, html) { - $(".content_list").append(html); - if(count > 0) { - this.offset += count; - } else { - this.disable = true; - } - }, - - initLoadMore: - function() { - $(document).endlessScroll({ - bottomPixels: 400, - fireDelay: 1000, - fireOnce:true, - ceaseFire: function() { - return Pager.disable; - }, - callback: function(i) { - $('.loading').show(); - Pager.getOld(); - } - }); - } -} diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee new file mode 100644 index 00000000..5f606acd --- /dev/null +++ b/app/assets/javascripts/pager.js.coffee @@ -0,0 +1,42 @@ +@Pager = + limit: 0 + offset: 0 + disable: false + init: (limit, preload) -> + @limit = limit + if preload + @offset = 0 + @getOld() + else + @offset = limit + @initLoadMore() + + getOld: -> + $(".loading").show() + $.ajax + type: "GET" + url: location.href + data: "limit=" + @limit + "&offset=" + @offset + complete: -> + $(".loading").hide() + + dataType: "script" + + append: (count, html) -> + $(".content_list").append html + if count > 0 + @offset += count + else + @disable = true + + initLoadMore: -> + $(document).endlessScroll + bottomPixels: 400 + fireDelay: 1000 + fireOnce: true + ceaseFire: -> + Pager.disable + + callback: (i) -> + $(".loading").show() + Pager.getOld() diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index db077048..1884c39b 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -1,5 +1,5 @@ html { - overflow-y: scroll; + overflow-y: scroll; } /** LAYOUT **/ @@ -277,8 +277,20 @@ p.time { } } +.search-holder { + label, input { + height: 30px; + padding: 0; + font-size: 14px; + } + label { + line-height: 30px; + color: #666; + } +} + .highlight_word { - background: #EEDC94; + border-bottom: 2px solid #F90; } .status_info { @@ -326,7 +338,7 @@ li.note { li { border-bottom:none !important; } - .file { + .attachment { padding-left: 20px; background:url("icon-attachment.png") no-repeat left center; } diff --git a/app/assets/stylesheets/gitlab_bootstrap/files.scss b/app/assets/stylesheets/gitlab_bootstrap/files.scss index 865a10e1..279cfcd2 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/files.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/files.scss @@ -135,7 +135,7 @@ pre { border: none; border-radius: 0; - font-family: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono','lucida console',monospace; + font-family: $monospace_font; font-size: 12px !important; line-height: 16px !important; margin: 0; diff --git a/app/assets/stylesheets/gitlab_bootstrap/fonts.scss b/app/assets/stylesheets/gitlab_bootstrap/fonts.scss index b4217fa9..a0c9a6c7 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/fonts.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/fonts.scss @@ -4,4 +4,4 @@ } /** Typo **/ -$monospace: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono', 'lucida console', monospace; +$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono', 'lucida console', monospace; diff --git a/app/assets/stylesheets/gitlab_bootstrap/typography.scss b/app/assets/stylesheets/gitlab_bootstrap/typography.scss index e74a0de1..781577c2 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/typography.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/typography.scss @@ -21,7 +21,7 @@ h6 { /** CODE **/ pre { - font-family:'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono','lucida console',monospace; + font-family: $monospace_font; &.dark { background: #333; @@ -79,7 +79,7 @@ a:focus { } .monospace { - font-family: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono','lucida console',monospace; + font-family: $monospace_font; } /** diff --git a/app/assets/stylesheets/gitlab_bootstrap/variables.scss b/app/assets/stylesheets/gitlab_bootstrap/variables.scss index 869eb168..aeabe7ad 100644 --- a/app/assets/stylesheets/gitlab_bootstrap/variables.scss +++ b/app/assets/stylesheets/gitlab_bootstrap/variables.scss @@ -1,5 +1,13 @@ -/** Colors **/ +/** + * General Colors + */ $primary_color: #2FA0BB; $link_color: #3A89A3; $style_color: #474D57; $hover: #D9EDF7; + +/** + * Commit Diff Colors + */ +$added: #63c363; +$deleted: #f77; diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index c60aae49..8b93287e 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -1,7 +1,5 @@ /** - * - * COMMIT SHOw - * + * Commit file */ .commit-committer-link, .commit-author-link { @@ -12,11 +10,11 @@ } } -.diff_file { +.file { border: 1px solid #CCC; margin-bottom: 1em; - .diff_file_header { + .header { @extend .clearfix; padding: 5px 5px 5px 10px; color: #555; @@ -28,32 +26,35 @@ background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); + a{ + color: $style_color; + } + > span { - font-family: $monospace; + font-family: $monospace_font; font-size: 14px; line-height: 30px; } - a.view-commit{ + a.view-file{ font-weight: bold; } .commit-short-id{ - font-family: $monospace; + font-family: $monospace_font; font-size: smaller; } .file-mode{ - font-family: $monospace; + font-family: $monospace_font; } } - .diff_file_content { + .content { overflow: auto; overflow-y: hidden; - background: #fff; + background: #FFF; color: #333; font-size: 12px; - font-family: $monospace; .old{ span.idiff{ background-color: #FAA; @@ -66,114 +67,274 @@ } table { + font-family: $monospace_font; + border: none; + margin: 0px; + padding: 0px; td { line-height: 18px; - } - } - } - .diff_file_content_image { - background: #eee; - text-align: center; - .image { - display: inline-block; - margin: 50px; - max-width: 400px; - - img{ - background: url('trans_bg.gif'); - } - - &.diff_removed { - img{ - border: 1px solid #C00; - } - } - - &.diff_added { - img{ - border: 1px solid #0C0; - } - } - - .image-info{ - margin: 5px 0 0 0; - } - } - - &.img_compared { - .image { - max-width: 300px; - } - } - } -} - -.diff_file_content{ - table { - border: none; - margin: 0px; - padding: 0px; - tr { - td { font-size: 12px; } } - } - .new_line, - .old_line, - .notes_line { - margin:0px; - padding:0px; - border:none; - background:#EEE; - color:#666; - padding: 0px 5px; - border-right: 1px solid #ccc; - text-align: right; - min-width: 35px; - max-width: 35px; - width: 35px; - moz-user-select: none; - -khtml-user-select: none; - user-select: none; - - a { - float: left; - width: 35px; - font-weight: normal; + .old_line, .new_line { + margin: 0px; + padding: 0px; + border: none; + background: #EEE; color: #666; - &:hover { - text-decoration: underline; + padding: 0px 5px; + border-right: 1px solid #ccc; + text-align: right; + min-width: 35px; + max-width: 35px; + width: 35px; + @include user-select(none); + a { + float: left; + width: 35px; + font-weight: normal; + color: #666; + &:hover { + text-decoration: underline; + } + } + } + .line_content { + white-space: pre; + height: 14px; + margin: 0px; + padding: 0px; + border: none; + &.new { + background: #CFD; + } + &.old { + background: #FDD; + } + &.matched { + color: #ccc; + background: #fafafa; } } } - .line_content { - white-space: pre; - height: 14px; - margin: 0px; - padding: 0px; - border: none; - &.new { - background: #CFD; + .image { + background: #ddd; + text-align: center; + padding: 30px; + .wrap{ + display: inline-block; } - &.old { - background: #FDD; + + .frame { + display: inline-block; + background-color: #fff; + line-height: 0; + img{ + border: 1px solid #FFF; + background: url('trans_bg.gif'); + } + &.deleted { + border: 1px solid $deleted; + } + + &.added { + border: 1px solid $added; + } } - &.matched { - color: #ccc; - background: #fafafa; + .image-info{ + font-size: 12px; + margin: 5px 0 0 0; + color: grey; + } + + .view.swipe{ + position: relative; + + .swipe-frame{ + display: block; + margin: auto; + position: relative; + } + .swipe-wrap{ + overflow: hidden; + border-left: 1px solid #999; + position: absolute; + display: block; + top: 13px; + right: 7px; + } + .frame{ + top: 0; + right: 0; + position: absolute; + &.deleted{ + margin: 0; + display: block; + top: 13px; + right: 7px; + } + } + .swipe-bar{ + display: block; + height: 100%; + width: 15px; + z-index: 100; + position: absolute; + cursor: pointer; + &:hover{ + .top-handle{ + background-position: -15px 3px; + } + .bottom-handle{ + background-position: -15px -11px; + } + }; + .top-handle{ + display: block; + height: 14px; + width: 15px; + position: absolute; + top: 0px; + background: url('swipemode_sprites.gif') 0 3px no-repeat; + } + .bottom-handle{ + display: block; + height: 14px; + width: 15px; + position: absolute; + bottom: 0px; + background: url('swipemode_sprites.gif') 0 -11px no-repeat; + } + } + } //.view.swipe + .view.onion-skin{ + .onion-skin-frame{ + display: block; + margin: auto; + position: relative; + } + .frame.added, .frame.deleted { + position: absolute; + display: block; + top: 0px; + left: 0px; + } + .controls{ + display: block; + height: 14px; + width: 300px; + z-index: 100; + position: absolute; + bottom: 0px; + left: 50%; + margin-left: -150px; + + .drag-track{ + display: block; + position: absolute; + left: 12px; + height: 10px; + width: 276px; + background: url('onion_skin_sprites.gif') -4px -20px repeat-x; + } + + .dragger { + display: block; + position: absolute; + left: 0px; + top: 0px; + height: 14px; + width: 14px; + background: url('onion_skin_sprites.gif') 0px -34px repeat-x; + cursor: pointer; + } + + .transparent { + display: block; + position: absolute; + top: 2px; + right: 0px; + height: 10px; + width: 10px; + background: url('onion_skin_sprites.gif') -2px 0px no-repeat; + } + + .opaque { + display: block; + position: absolute; + top: 2px; + left: 0px; + height: 10px; + width: 10px; + background: url('onion_skin_sprites.gif') -2px -10px no-repeat; + } + } + } //.view.onion-skin + } + .view-modes{ + + padding: 10px; + text-align: center; + + background-image: -webkit-gradient(linear, 0 0, 0 30, color-stop(0.066, #eee), to(#dfdfdf)); + background-image: -webkit-linear-gradient(#eee 6.6%, #dfdfdf); + background-image: -moz-linear-gradient(#eee 6.6%, #dfdfdf); + background-image: -o-linear-gradient(#eee 6.6%, #dfdfdf); + + ul, li{ + list-style: none; + margin: 0; + padding: 0; + display: inline-block; + } + + li{ + color: grey; + border-left: 1px solid #c1c1c1; + padding: 0 12px 0 16px; + cursor: pointer; + &:first-child{ + border-left: none; + } + &:hover{ + text-decoration: underline; + } + &.active{ + &:hover{ + text-decoration: none; + } + cursor: default; + color: #333; + } + &.disabled{ + display: none; + } } } } /** COMMIT BLOCK **/ -.commit-title{display: block;} -.commit-title{margin-bottom: 10px} -.commit-author, .commit-committer{display: block;color: #999; font-weight: normal; font-style: italic;} -.commit-author strong, .commit-committer strong{font-weight: bold; font-style: normal;} +.commit-title{ + display: block; +} +.commit-title{ + margin-bottom: 10px; +} +.commit-author, .commit-committer{ + display: block; + color: #999; + font-weight: normal; + font-style: italic; +} +.commit-author strong, .commit-committer strong{ + font-weight: bold; + font-style: normal; +} -/** COMMIT ROW **/ +/** + * COMMIT ROW + */ .commit { .browse_code_link_holder { @extend .span2; @@ -199,11 +360,10 @@ float: left; @extend .lined; min-width: 65px; - font-family: $monospace; + font-family: $monospace_font; } } -.diff_file_header a, .file-stats a { color: $style_color; } @@ -237,7 +397,7 @@ font-size: 13px; background: #474D57; color: #fff; - font-family: $monospace; + font-family: $monospace_font; } diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 5225a242..ff715c0f 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -77,7 +77,7 @@ li.merge_request { font-size: 14px; background: #474D57; color: #fff; - font-family: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono','lucida console',monospace; + font-family: $monospace_font; } .mr_source_commit, diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index a7fadd4f..7a1cc444 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -40,13 +40,13 @@ ul.notes { .discussion-body { margin-left: 50px; - .diff_file, + .file, .discussion-hidden, .notes { @extend .borders; background-color: #F9F9F9; } - .diff_file .notes { + .file .notes { /* reset */ background: inherit; border: none; @@ -109,7 +109,7 @@ ul.notes { } } -.diff_file .notes_holder { +.file .notes_holder { font-family: $sansFontFamily; font-size: 13px; line-height: 18px; @@ -134,8 +134,6 @@ ul.notes { } } - - /** * Actions for Discussions/Notes */ @@ -171,7 +169,7 @@ ul.notes { } } } -.diff_file .note .note-actions { +.file .note .note-actions { right: 0; top: 0; } @@ -182,7 +180,7 @@ ul.notes { * Line note button on the side of diffs */ -.diff_file tr.line_holder { +.file tr.line_holder { .add-diff-note { background: url("diff_note_add.png") no-repeat left 0; height: 22px; @@ -212,8 +210,6 @@ ul.notes { } } - - /** * Note Form */ @@ -222,7 +218,12 @@ ul.notes { .reply-btn { @extend .save-btn; } -.diff_file, +.file .content tr.line_holder:hover > td { background: $hover !important; } +.file .content tr.line_holder:hover > td .line_note_link { + opacity: 1.0; + filter: alpha(opacity=100); +} +.file, .discussion { .new_note { margin: 8px 5px 8px 0; diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index 4bdc56d2..b6db65ad 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -6,7 +6,6 @@ .side { @extend .right; - .groups_box, .projects_box { > .title { padding: 2px 15px; diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin/application_controller.rb similarity index 82% rename from app/controllers/admin_controller.rb rename to app/controllers/admin/application_controller.rb index bce9f692..6a8f20f6 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -1,7 +1,7 @@ # Provides a base class for Admin controllers to subclass # # Automatically sets the layout and ensures an administrator is logged in -class AdminController < ApplicationController +class Admin::ApplicationController < ApplicationController layout 'admin' before_filter :authenticate_admin! diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index f97c56b0..3c27b861 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -1,4 +1,4 @@ -class Admin::DashboardController < AdminController +class Admin::DashboardController < Admin::ApplicationController def index @projects = Project.order("created_at DESC").limit(10) @users = User.order("created_at DESC").limit(10) diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 90dbda3e..f552fb59 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,4 +1,4 @@ -class Admin::GroupsController < AdminController +class Admin::GroupsController < Admin::ApplicationController before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update] def index diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 91a1d633..c5bf76f8 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -1,4 +1,4 @@ -class Admin::HooksController < AdminController +class Admin::HooksController < Admin::ApplicationController def index @hooks = SystemHook.all @hook = SystemHook.new diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb index 28c321a9..b999018d 100644 --- a/app/controllers/admin/logs_controller.rb +++ b/app/controllers/admin/logs_controller.rb @@ -1,2 +1,2 @@ -class Admin::LogsController < AdminController +class Admin::LogsController < Admin::ApplicationController end diff --git a/app/controllers/admin/projects/application_controller.rb b/app/controllers/admin/projects/application_controller.rb new file mode 100644 index 00000000..b3f1539f --- /dev/null +++ b/app/controllers/admin/projects/application_controller.rb @@ -0,0 +1,11 @@ +# Provides a base class for Admin controllers to subclass +# +# Automatically sets the layout and ensures an administrator is logged in +class Admin::Projects::ApplicationController < Admin::ApplicationController + + protected + + def project + @project ||= Project.find_with_namespace(params[:project_id]) + end +end diff --git a/app/controllers/admin/projects/members_controller.rb b/app/controllers/admin/projects/members_controller.rb new file mode 100644 index 00000000..d9c0d572 --- /dev/null +++ b/app/controllers/admin/projects/members_controller.rb @@ -0,0 +1,32 @@ +class Admin::Projects::MembersController < Admin::Projects::ApplicationController + def edit + @member = team_member + @project = project + @team_member_relation = team_member_relation + end + + def update + if team_member_relation.update_attributes(params[:team_member]) + redirect_to [:admin, project], notice: 'Project Access was successfully updated.' + else + render action: "edit" + end + end + + def destroy + team_member_relation.destroy + + redirect_to :back + end + + private + + def team_member + @member ||= project.users.find_by_username(params[:id]) + end + + def team_member_relation + team_member.users_projects.find_by_project_id(project) + end + +end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index fc2793a7..71181739 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,4 +1,4 @@ -class Admin::ProjectsController < AdminController +class Admin::ProjectsController < Admin::ApplicationController before_filter :project, only: [:edit, :show, :update, :destroy, :team_update] def index @@ -29,7 +29,9 @@ class Admin::ProjectsController < AdminController end def update - status = Projects::UpdateContext.new(project, current_user, params).execute(:admin) + project.creator = current_user unless project.creator + + status = ::Projects::UpdateContext.new(project, current_user, params).execute(:admin) if status redirect_to [:admin, @project], notice: 'Project was successfully updated.' diff --git a/app/controllers/admin/resque_controller.rb b/app/controllers/admin/resque_controller.rb index 9d8e7e30..7d489ab4 100644 --- a/app/controllers/admin/resque_controller.rb +++ b/app/controllers/admin/resque_controller.rb @@ -1,4 +1,4 @@ -class Admin::ResqueController < AdminController +class Admin::ResqueController < Admin::ApplicationController def show end end diff --git a/app/controllers/admin/team_members_controller.rb b/app/controllers/admin/team_members_controller.rb deleted file mode 100644 index 07320805..00000000 --- a/app/controllers/admin/team_members_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -class Admin::TeamMembersController < AdminController - def edit - @admin_team_member = UsersProject.find(params[:id]) - end - - def update - @admin_team_member = UsersProject.find(params[:id]) - - if @admin_team_member.update_attributes(params[:team_member]) - redirect_to [:admin, @admin_team_member.project], notice: 'Project Access was successfully updated.' - else - render action: "edit" - end - end - - def destroy - @admin_team_member = UsersProject.find(params[:id]) - @admin_team_member.destroy - - redirect_to :back - end -end diff --git a/app/controllers/admin/teams/application_controller.rb b/app/controllers/admin/teams/application_controller.rb new file mode 100644 index 00000000..87108214 --- /dev/null +++ b/app/controllers/admin/teams/application_controller.rb @@ -0,0 +1,11 @@ +# Provides a base class for Admin controllers to subclass +# +# Automatically sets the layout and ensures an administrator is logged in +class Admin::Teams::ApplicationController < Admin::ApplicationController + + private + + def user_team + @team = UserTeam.find_by_path(params[:team_id]) + end +end diff --git a/app/controllers/admin/teams/members_controller.rb b/app/controllers/admin/teams/members_controller.rb new file mode 100644 index 00000000..e7dbcad5 --- /dev/null +++ b/app/controllers/admin/teams/members_controller.rb @@ -0,0 +1,41 @@ +class Admin::Teams::MembersController < Admin::Teams::ApplicationController + def new + @users = User.potential_team_members(user_team) + @users = UserDecorator.decorate @users + end + + def create + unless params[:user_ids].blank? + user_ids = params[:user_ids] + access = params[:default_project_access] + is_admin = params[:group_admin] + user_team.add_members(user_ids, access, is_admin) + end + + redirect_to admin_team_path(user_team), notice: 'Members was successfully added into Team of users.' + end + + def edit + team_member + end + + def update + options = {default_projects_access: params[:default_project_access], group_admin: params[:group_admin]} + if user_team.update_membership(team_member, options) + redirect_to admin_team_path(user_team), notice: "Membership for #{team_member.name} was successfully updated in Team of users." + else + render :edit + end + end + + def destroy + user_team.remove_member(team_member) + redirect_to admin_team_path(user_team), notice: "Member #{team_member.name} was successfully removed from Team of users." + end + + protected + + def team_member + @member ||= user_team.members.find_by_username(params[:id]) + end +end diff --git a/app/controllers/admin/teams/projects_controller.rb b/app/controllers/admin/teams/projects_controller.rb new file mode 100644 index 00000000..8584a188 --- /dev/null +++ b/app/controllers/admin/teams/projects_controller.rb @@ -0,0 +1,41 @@ +class Admin::Teams::ProjectsController < Admin::Teams::ApplicationController + def new + @projects = Project.scoped + @projects = @projects.without_team(user_team) if user_team.projects.any? + #@projects.reject!(&:empty_repo?) + end + + def create + unless params[:project_ids].blank? + project_ids = params[:project_ids] + access = params[:greatest_project_access] + user_team.assign_to_projects(project_ids, access) + end + + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully assgned to projects.' + end + + def edit + team_project + end + + def update + if user_team.update_project_access(team_project, params[:greatest_project_access]) + redirect_to admin_team_path(user_team), notice: 'Access was successfully updated.' + else + render :edit + end + end + + def destroy + user_team.resign_from_project(team_project) + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully reassigned from project.' + end + + protected + + def team_project + @project ||= user_team.projects.find_with_namespace(params[:id]) + end + +end diff --git a/app/controllers/admin/teams_controller.rb b/app/controllers/admin/teams_controller.rb new file mode 100644 index 00000000..786957cb --- /dev/null +++ b/app/controllers/admin/teams_controller.rb @@ -0,0 +1,59 @@ +class Admin::TeamsController < Admin::ApplicationController + def index + @teams = UserTeam.order('name ASC') + @teams = @teams.search(params[:name]) if params[:name].present? + @teams = @teams.page(params[:page]).per(20) + end + + def show + user_team + end + + def new + @team = UserTeam.new + end + + def edit + user_team + end + + def create + @team = UserTeam.new(params[:user_team]) + @team.path = @team.name.dup.parameterize if @team.name + @team.owner = current_user + + if @team.save + redirect_to admin_team_path(@team), notice: 'Team of users was successfully created.' + else + render action: "new" + end + end + + def update + user_team_params = params[:user_team].dup + owner_id = user_team_params.delete(:owner_id) + + if owner_id + user_team.owner = User.find(owner_id) + end + + if user_team.update_attributes(user_team_params) + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully updated.' + else + render action: "edit" + end + end + + def destroy + user_team.destroy + + redirect_to admin_teams_path, notice: 'Team of users was successfully deleted.' + end + + protected + + def user_team + @team ||= UserTeam.find_by_path(params[:id]) + end + +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 8669f5d1..400e44e0 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,4 +1,6 @@ -class Admin::UsersController < AdminController +class Admin::UsersController < Admin::ApplicationController + before_filter :admin_user, only: [:show, :edit, :update, :destroy] + def index @admin_users = User.scoped @admin_users = @admin_users.filter(params[:filter]) @@ -7,25 +9,18 @@ class Admin::UsersController < AdminController end def show - @admin_user = User.find(params[:id]) - - @projects = if @admin_user.authorized_projects.empty? - Project - else - Project.without_user(@admin_user) - end.all + @projects = Project.scoped + @projects = @projects.without_user(admin_user) if admin_user.authorized_projects.present? end def team_update - @admin_user = User.find(params[:id]) - UsersProject.add_users_into_projects( params[:project_ids], - [@admin_user.id], + [admin_user.id], params[:project_access] ) - redirect_to [:admin, @admin_user], notice: 'Teams were successfully updated.' + redirect_to [:admin, admin_user], notice: 'Teams were successfully updated.' end @@ -34,13 +29,11 @@ class Admin::UsersController < AdminController end def edit - @admin_user = User.find(params[:id]) + admin_user end def block - @admin_user = User.find(params[:id]) - - if @admin_user.block + if admin_user.block redirect_to :back, alert: "Successfully blocked" else redirect_to :back, alert: "Error occured. User was not blocked" @@ -48,9 +41,7 @@ class Admin::UsersController < AdminController end def unblock - @admin_user = User.find(params[:id]) - - if @admin_user.update_attribute(:blocked, false) + if admin_user.update_attribute(:blocked, false) redirect_to :back, alert: "Successfully unblocked" else redirect_to :back, alert: "Error occured. User was not unblocked" @@ -82,30 +73,34 @@ class Admin::UsersController < AdminController params[:user].delete(:password_confirmation) end - @admin_user = User.find(params[:id]) - @admin_user.admin = (admin && admin.to_i > 0) + admin_user.admin = (admin && admin.to_i > 0) respond_to do |format| - if @admin_user.update_attributes(params[:user], as: :admin) - format.html { redirect_to [:admin, @admin_user], notice: 'User was successfully updated.' } + if admin_user.update_attributes(params[:user], as: :admin) + format.html { redirect_to [:admin, admin_user], notice: 'User was successfully updated.' } format.json { head :ok } else format.html { render action: "edit" } - format.json { render json: @admin_user.errors, status: :unprocessable_entity } + format.json { render json: admin_user.errors, status: :unprocessable_entity } end end end def destroy - @admin_user = User.find(params[:id]) - if @admin_user.personal_projects.count > 0 + if admin_user.personal_projects.count > 0 redirect_to admin_users_path, alert: "User is a project owner and can't be removed." and return end - @admin_user.destroy + admin_user.destroy respond_to do |format| - format.html { redirect_to admin_users_url } + format.html { redirect_to admin_users_path } format.json { head :ok } end end + + protected + + def admin_user + @admin_user ||= User.find_by_username!(params[:id]) + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3457a1ab..74125e33 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -94,6 +94,18 @@ class ApplicationController < ActionController::Base return access_denied! unless can?(current_user, :download_code, project) end + def authorize_create_team! + return access_denied! unless can?(current_user, :create_team, nil) + end + + def authorize_manage_user_team! + return access_denied! unless user_team.present? && can?(current_user, :manage_user_team, user_team) + end + + def authorize_admin_user_team! + return access_denied! unless user_team.present? && can?(current_user, :admin_user_team, user_team) + end + def access_denied! render "errors/access_denied", layout: "errors", status: 404 end @@ -135,4 +147,5 @@ class ApplicationController < ActionController::Base def dev_tools Rack::MiniProfiler.authorize_request end + end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index c0ec4708..f320e819 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,24 +1,15 @@ class DashboardController < ApplicationController respond_to :html - before_filter :projects - before_filter :event_filter, only: :index + before_filter :load_projects + before_filter :event_filter, only: :show - def index + def show @groups = current_user.authorized_groups - @has_authorized_projects = @projects.count > 0 - - @projects = case params[:scope] - when 'personal' then - @projects.personal(current_user) - when 'joined' then - @projects.joined(current_user) - else - @projects - end - - @projects = @projects.page(params[:page]).per(30) + @teams = current_user.authorized_teams + @projects_count = @projects.count + @projects = @projects.limit(20) @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) @events = @event_filter.apply_filter(@events) @@ -33,6 +24,19 @@ class DashboardController < ApplicationController end end + def projects + @projects = case params[:scope] + when 'personal' then + @projects.personal(current_user) + when 'joined' then + @projects.joined(current_user) + else + @projects + end + + @projects = @projects.page(params[:page]).per(30) + end + # Get authored or assigned open merge requests def merge_requests @merge_requests = current_user.cared_merge_requests @@ -55,7 +59,7 @@ class DashboardController < ApplicationController protected - def projects + def load_projects @projects = current_user.authorized_projects.sorted_by_activity end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index c25fc32a..72df170f 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,12 +1,31 @@ class GroupsController < ApplicationController respond_to :html - layout 'group' + layout 'group', except: [:new, :create] - before_filter :group - before_filter :projects + before_filter :group, except: [:new, :create] # Authorize - before_filter :authorize_read_group! + before_filter :authorize_read_group!, except: [:new, :create] + before_filter :authorize_create_group!, only: [:new, :create] + + # Load group projects + before_filter :projects, except: [:new, :create] + + def new + @group = Group.new + end + + def create + @group = Group.new(params[:group]) + @group.path = @group.name.dup.parameterize if @group.name + @group.owner = current_user + + if @group.save + redirect_to @group, notice: 'Group was successfully created.' + else + render action: "new" + end + end def show @events = Event.in_projects(project_ids).limit(20).offset(params[:offset] || 0) @@ -85,4 +104,8 @@ class GroupsController < ApplicationController return render_404 end end + + def authorize_create_group! + can?(current_user, :create_group, nil) + end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb new file mode 100644 index 00000000..7e4776d2 --- /dev/null +++ b/app/controllers/projects/application_controller.rb @@ -0,0 +1,11 @@ +class Projects::ApplicationController < ApplicationController + + before_filter :authorize_admin_team_member! + + protected + + def user_team + @team ||= UserTeam.find_by_path(params[:id]) + end + +end diff --git a/app/controllers/projects/teams_controller.rb b/app/controllers/projects/teams_controller.rb new file mode 100644 index 00000000..3ca724aa --- /dev/null +++ b/app/controllers/projects/teams_controller.rb @@ -0,0 +1,27 @@ +class Projects::TeamsController < Projects::ApplicationController + + def available + @teams = current_user.is_admin? ? UserTeam.scoped : current_user.user_teams + @teams = @teams.without_project(project) + unless @teams.any? + redirect_to project_team_index_path(project), notice: "No avaliable teams for assigment." + end + end + + def assign + unless params[:team_id].blank? + team = UserTeam.find(params[:team_id]) + access = params[:greatest_project_access] + team.assign_to_project(project, access) + end + redirect_to project_team_index_path(project) + end + + def resign + team = project.user_teams.find_by_path(params[:id]) + team.resign_from_project(project) + + redirect_to project_team_index_path(project) + end + +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 368737d1..6e5e1f91 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -19,7 +19,7 @@ class ProjectsController < ProjectResourceController end def create - @project = Projects::CreateContext.new(current_user, params[:project]).execute + @project = ::Projects::CreateContext.new(current_user, params[:project]).execute respond_to do |format| flash[:notice] = 'Project was successfully created.' if @project.saved? @@ -35,7 +35,7 @@ class ProjectsController < ProjectResourceController end def update - status = Projects::UpdateContext.new(project, current_user, params).execute + status = ::Projects::UpdateContext.new(project, current_user, params).execute respond_to do |format| if status diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index a2329239..bbd67df6 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,6 +1,18 @@ class SearchController < ApplicationController def show - result = SearchContext.new(current_user.authorized_projects.map(&:id), params).execute + project_id = params[:project_id] + group_id = params[:group_id] + + project_ids = current_user.authorized_projects.map(&:id) + + if group_id.present? + group_project_ids = Group.find(group_id).projects.map(&:id) + project_ids.select! { |id| group_project_ids.include?(id)} + elsif project_id.present? + project_ids.select! { |id| id == project_id.to_i} + end + + result = SearchContext.new(project_ids, params).execute @projects = result[:projects] @merge_requests = result[:merge_requests] diff --git a/app/controllers/team_members_controller.rb b/app/controllers/team_members_controller.rb index 8378a845..18d4ae3a 100644 --- a/app/controllers/team_members_controller.rb +++ b/app/controllers/team_members_controller.rb @@ -4,15 +4,16 @@ class TeamMembersController < ProjectResourceController before_filter :authorize_admin_project!, except: [:index, :show] def index + @teams = UserTeam.scoped end def show - @team_member = project.users_projects.find(params[:id]) - @events = @team_member.user.recent_events.where(:project_id => @project.id).limit(7) + @user_project_relation = project.users_projects.find_by_user_id(member) + @events = member.recent_events.in_projects(project).limit(7) end def new - @team_member = project.users_projects.new + @user_project_relation = project.users_projects.new end def create @@ -28,18 +29,18 @@ class TeamMembersController < ProjectResourceController end def update - @team_member = project.users_projects.find(params[:id]) - @team_member.update_attributes(params[:team_member]) + @user_project_relation = project.users_projects.find_by_user_id(member) + @user_project_relation.update_attributes(params[:team_member]) - unless @team_member.valid? + unless @user_project_relation.valid? flash[:alert] = "User should have at least one role" end redirect_to project_team_index_path(@project) end def destroy - @team_member = project.users_projects.find(params[:id]) - @team_member.destroy + @user_project_relation = project.users_projects.find_by_user_id(member) + @user_project_relation.destroy respond_to do |format| format.html { redirect_to project_team_index_path(@project) } @@ -54,4 +55,10 @@ class TeamMembersController < ProjectResourceController redirect_to project_team_members_path(project), notice: notice end + + protected + + def member + @member ||= User.find_by_username(params[:id]) + end end diff --git a/app/controllers/teams/application_controller.rb b/app/controllers/teams/application_controller.rb new file mode 100644 index 00000000..fc232026 --- /dev/null +++ b/app/controllers/teams/application_controller.rb @@ -0,0 +1,13 @@ +class Teams::ApplicationController < ApplicationController + + layout 'user_team' + + before_filter :authorize_manage_user_team! + + protected + + def user_team + @team ||= UserTeam.find_by_path(params[:team_id]) + end + +end diff --git a/app/controllers/teams/members_controller.rb b/app/controllers/teams/members_controller.rb new file mode 100644 index 00000000..db218b8c --- /dev/null +++ b/app/controllers/teams/members_controller.rb @@ -0,0 +1,49 @@ +class Teams::MembersController < Teams::ApplicationController + + skip_before_filter :authorize_manage_user_team!, only: [:index] + + def index + @members = user_team.members + end + + def new + @users = User.potential_team_members(user_team) + @users = UserDecorator.decorate @users + end + + def create + unless params[:user_ids].blank? + user_ids = params[:user_ids] + access = params[:default_project_access] + is_admin = params[:group_admin] + user_team.add_members(user_ids, access, is_admin) + end + + redirect_to team_members_path(user_team), notice: 'Members was successfully added into Team of users.' + end + + def edit + team_member + end + + def update + options = {default_projects_access: params[:default_project_access], group_admin: params[:group_admin]} + if user_team.update_membership(team_member, options) + redirect_to team_members_path(user_team), notice: "Membership for #{team_member.name} was successfully updated in Team of users." + else + render :edit + end + end + + def destroy + user_team.remove_member(team_member) + redirect_to team_path(user_team), notice: "Member #{team_member.name} was successfully removed from Team of users." + end + + protected + + def team_member + @member ||= user_team.members.find_by_username(params[:id]) + end + +end diff --git a/app/controllers/teams/projects_controller.rb b/app/controllers/teams/projects_controller.rb new file mode 100644 index 00000000..e87889b4 --- /dev/null +++ b/app/controllers/teams/projects_controller.rb @@ -0,0 +1,57 @@ +class Teams::ProjectsController < Teams::ApplicationController + + skip_before_filter :authorize_manage_user_team!, only: [:index] + + def index + @projects = user_team.projects + @avaliable_projects = current_user.admin? ? Project.without_team(user_team) : current_user.owned_projects.without_team(user_team) + end + + def new + user_team + @avaliable_projects = current_user.owned_projects.scoped + @avaliable_projects = @avaliable_projects.without_team(user_team) if user_team.projects.any? + + redirect_to team_projects_path(user_team), notice: "No avalible projects." unless @avaliable_projects.any? + end + + def create + redirect_to :back if params[:project_ids].blank? + + project_ids = params[:project_ids] + access = params[:greatest_project_access] + + # Reject non-allowed projects + allowed_project_ids = current_user.owned_projects.map(&:id) + project_ids.select! { |id| allowed_project_ids.include?(id.to_i) } + + # Assign projects to team + user_team.assign_to_projects(project_ids, access) + + redirect_to team_projects_path(user_team), notice: 'Team of users was successfully assigned to projects.' + end + + def edit + team_project + end + + def update + if user_team.update_project_access(team_project, params[:greatest_project_access]) + redirect_to team_projects_path(user_team), notice: 'Access was successfully updated.' + else + render :edit + end + end + + def destroy + user_team.resign_from_project(team_project) + redirect_to team_projects_path(user_team), notice: 'Team of users was successfully reassigned from project.' + end + + private + + def team_project + @project ||= user_team.projects.find_with_namespace(params[:id]) + end + +end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb new file mode 100644 index 00000000..ef66b77e --- /dev/null +++ b/app/controllers/teams_controller.rb @@ -0,0 +1,76 @@ +class TeamsController < ApplicationController + # Authorize + before_filter :authorize_create_team!, only: [:new, :create] + before_filter :authorize_manage_user_team!, only: [:edit, :update] + before_filter :authorize_admin_user_team!, only: [:destroy] + + before_filter :user_team, except: [:new, :create] + + layout 'user_team', except: [:new, :create] + + def show + user_team + projects + @events = Event.in_projects(user_team.project_ids).limit(20).offset(params[:offset] || 0) + end + + def edit + user_team + end + + def update + if user_team.update_attributes(params[:user_team]) + redirect_to team_path(user_team) + else + render action: :edit + end + end + + def destroy + user_team.destroy + redirect_to dashboard_path + end + + def new + @team = UserTeam.new + end + + def create + @team = UserTeam.new(params[:user_team]) + @team.owner = current_user unless params[:owner] + @team.path = @team.name.dup.parameterize if @team.name + + if @team.save + redirect_to team_path(@team) + else + render action: :new + end + end + + # Get authored or assigned open merge requests + def merge_requests + projects + @merge_requests = MergeRequest.of_user_team(user_team) + @merge_requests = FilterContext.new(@merge_requests, params).execute + @merge_requests = @merge_requests.recent.page(params[:page]).per(20) + end + + # Get only assigned issues + def issues + projects + @issues = Issue.of_user_team(user_team) + @issues = FilterContext.new(@issues, params).execute + @issues = @issues.recent.page(params[:page]).per(20) + @issues = @issues.includes(:author, :project) + end + + protected + + def projects + @projects ||= user_team.projects.sorted_by_activity + end + + def user_team + @team ||= current_user.authorized_teams.find_by_path(params[:id]) + end +end diff --git a/app/decorators/user_decorator.rb b/app/decorators/user_decorator.rb index af9c6a63..b781f237 100644 --- a/app/decorators/user_decorator.rb +++ b/app/decorators/user_decorator.rb @@ -8,4 +8,8 @@ class UserDecorator < ApplicationDecorator def tm_of(project) project.team_member_by_id(self.id) end + + def name_with_email + "#{name} (#{email})" + end end diff --git a/app/helpers/admin/teams/members_helper.rb b/app/helpers/admin/teams/members_helper.rb new file mode 100644 index 00000000..58b9f189 --- /dev/null +++ b/app/helpers/admin/teams/members_helper.rb @@ -0,0 +1,5 @@ +module Admin::Teams::MembersHelper + def member_since(team, member) + team.user_team_user_relationships.find_by_user_id(member).created_at + end +end diff --git a/app/helpers/admin/teams/projects_helper.rb b/app/helpers/admin/teams/projects_helper.rb new file mode 100644 index 00000000..b97cc403 --- /dev/null +++ b/app/helpers/admin/teams/projects_helper.rb @@ -0,0 +1,5 @@ +module Admin::Teams::ProjectsHelper + def assigned_since(team, project) + team.user_team_project_relationships.find_by_project_id(project).created_at + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6478982c..196105f0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,8 +72,9 @@ module ApplicationHelper end def search_autocomplete_source - projects = current_user.authorized_projects.map { |p| { label: p.name_with_namespace, url: project_path(p) } } - groups = current_user.authorized_groups.map { |group| { label: " #{group.name}", url: group_path(group) } } + projects = current_user.authorized_projects.map { |p| { label: "project: #{p.name_with_namespace}", url: project_path(p) } } + groups = current_user.authorized_groups.map { |group| { label: "group: #{group.name}", url: group_path(group) } } + teams = current_user.authorized_teams.map { |team| { label: "team: #{team.name}", url: team_path(team) } } default_nav = [ { label: "My Profile", url: profile_path }, @@ -83,29 +84,29 @@ module ApplicationHelper ] help_nav = [ - { label: "API Help", url: help_api_path }, - { label: "Markdown Help", url: help_markdown_path }, - { label: "Permissions Help", url: help_permissions_path }, - { label: "Public Access Help", url: help_public_access_path }, - { label: "Rake Tasks Help", url: help_raketasks_path }, - { label: "SSH Keys Help", url: help_ssh_path }, - { label: "System Hooks Help", url: help_system_hooks_path }, - { label: "Web Hooks Help", url: help_web_hooks_path }, - { label: "Workflow Help", url: help_workflow_path }, + { label: "help: API Help", url: help_api_path }, + { label: "help: Markdown Help", url: help_markdown_path }, + { label: "help: Permissions Help", url: help_permissions_path }, + { label: "help: Public Access Help", url: help_public_access_path }, + { label: "help: Rake Tasks Help", url: help_raketasks_path }, + { label: "help: SSH Keys Help", url: help_ssh_path }, + { label: "help: System Hooks Help", url: help_system_hooks_path }, + { label: "help: Web Hooks Help", url: help_web_hooks_path }, + { label: "help: Workflow Help", url: help_workflow_path }, ] project_nav = [] if @project && @project.repository && @project.repository.root_ref project_nav = [ - { label: "#{@project.name} Issues", url: project_issues_path(@project) }, - { label: "#{@project.name} Commits", url: project_commits_path(@project, @ref || @project.repository.root_ref) }, - { label: "#{@project.name} Merge Requests", url: project_merge_requests_path(@project) }, - { label: "#{@project.name} Milestones", url: project_milestones_path(@project) }, - { label: "#{@project.name} Snippets", url: project_snippets_path(@project) }, - { label: "#{@project.name} Team", url: project_team_index_path(@project) }, - { label: "#{@project.name} Tree", url: project_tree_path(@project, @ref || @project.repository.root_ref) }, - { label: "#{@project.name} Wall", url: wall_project_path(@project) }, - { label: "#{@project.name} Wiki", url: project_wikis_path(@project) }, + { label: "#{@project.name_with_namespace} - Issues", url: project_issues_path(@project) }, + { label: "#{@project.name_with_namespace} - Commits", url: project_commits_path(@project, @ref || @project.repository.root_ref) }, + { label: "#{@project.name_with_namespace} - Merge Requests", url: project_merge_requests_path(@project) }, + { label: "#{@project.name_with_namespace} - Milestones", url: project_milestones_path(@project) }, + { label: "#{@project.name_with_namespace} - Snippets", url: project_snippets_path(@project) }, + { label: "#{@project.name_with_namespace} - Team", url: project_team_index_path(@project) }, + { label: "#{@project.name_with_namespace} - Tree", url: project_tree_path(@project, @ref || @project.repository.root_ref) }, + { label: "#{@project.name_with_namespace} - Wall", url: wall_project_path(@project) }, + { label: "#{@project.name_with_namespace} - Wiki", url: project_wikis_path(@project) }, ] end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 4b5d8bd9..6d2ce2fe 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -59,9 +59,9 @@ module CommitsHelper def image_diff_class(diff) if diff.deleted_file - "diff_removed" + "deleted" elsif diff.new_file - "diff_added" + "added" else nil end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 0baa5b41..c759dffa 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -9,9 +9,9 @@ module DashboardHelper case entity when 'issue' then - dashboard_issues_path(options) + issues_dashboard_path(options) when 'merge_request' - dashboard_merge_requests_path(options) + merge_requests_dashboard_path(options) end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 158925ba..4f0a8071 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -3,8 +3,12 @@ module ProjectsHelper @project.users_projects.sort_by(&:project_access).reverse.group_by(&:project_access) end - def remove_from_team_message(project, member) - "You are going to remove #{member.user_name} from #{project.name}. Are you sure?" + def grouper_project_teams(project) + @project.user_team_project_relationships.sort_by(&:greatest_access).reverse.group_by(&:greatest_access) + end + + def remove_from_project_team_message(project, user) + "You are going to remove #{user.name} from #{project.name} project team. Are you sure?" end def link_to_project project @@ -51,7 +55,9 @@ module ProjectsHelper def project_title project if project.group - project.name_with_namespace + content_tag :span do + link_to(project.group.name, group_path(project.group)) + " / " + project.name + end else project.name end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index d52d8af6..5bd6de89 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -39,7 +39,12 @@ module TabHelper # Returns a list item element String def nav_link(options = {}, &block) if path = options.delete(:path) - c, a, _ = path.split('#') + if path.respond_to?(:each) + c = path.map { |p| p.split('#').first } + a = path.map { |p| p.split('#').last } + else + c, a, _ = path.split('#') + end else c = options.delete(:controller) a = options.delete(:action) diff --git a/app/helpers/user_teams_helper.rb b/app/helpers/user_teams_helper.rb new file mode 100644 index 00000000..2055bb3c --- /dev/null +++ b/app/helpers/user_teams_helper.rb @@ -0,0 +1,26 @@ +module UserTeamsHelper + def team_filter_path(entity, options={}) + exist_opts = { + status: params[:status], + project_id: params[:project_id], + } + + options = exist_opts.merge(options) + + case entity + when 'issue' then + issues_team_path(@team, options) + when 'merge_request' + merge_requests_team_path(@team, options) + end + end + + def grouped_user_team_members(team) + team.user_team_user_relationships.sort_by(&:permission).reverse.group_by(&:permission) + end + + def remove_from_user_team_message(team, member) + "You are going to remove #{member.name} from #{team.name}. Are you sure?" + end + +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 9d33501f..6d087a95 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,15 +1,25 @@ class Ability class << self - def allowed(object, subject) + def allowed(user, subject) + return [] unless user.kind_of?(User) + case subject.class.name - when "Project" then project_abilities(object, subject) - when "Issue" then issue_abilities(object, subject) - when "Note" then note_abilities(object, subject) - when "Snippet" then snippet_abilities(object, subject) - when "MergeRequest" then merge_request_abilities(object, subject) - when "Group", "Namespace" then group_abilities(object, subject) + when "Project" then project_abilities(user, subject) + when "Issue" then issue_abilities(user, subject) + when "Note" then note_abilities(user, subject) + when "Snippet" then snippet_abilities(user, subject) + when "MergeRequest" then merge_request_abilities(user, subject) + when "Group", "Namespace" then group_abilities(user, subject) + when "UserTeam" then user_team_abilities(user, subject) else [] - end + end.concat(global_abilities(user)) + end + + def global_abilities(user) + rules = [] + rules << :create_group if user.can_create_group + rules << :create_team if user.can_create_team + rules end def project_abilities(user, project) @@ -110,6 +120,22 @@ class Ability rules.flatten end + def user_team_abilities user, team + rules = [] + + # Only group owner and administrators can manage group + if team.owner == user || team.admin?(user) || user.admin? + rules << [ :manage_user_team ] + end + + if team.owner == user || user.admin? + rules << [ :admin_user_team ] + end + + rules.flatten + end + + [:issue, :note, :snippet, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| if subject.author == user diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index d1717d3b..8872cf59 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -22,6 +22,7 @@ module Issuable scope :opened, where(closed: false) scope :closed, where(closed: true) scope :of_group, ->(group) { where(project_id: group.project_ids) } + scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) } scope :assigned, ->(u) { where(assignee_id: u.id)} scope :recent, order("created_at DESC") diff --git a/app/models/project.rb b/app/models/project.rb index fa38093b..cb6986ce 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -33,28 +33,31 @@ class Project < ActiveRecord::Base attr_accessor :error_code # Relations - belongs_to :group, foreign_key: "namespace_id", conditions: "type = 'Group'" + belongs_to :creator, foreign_key: "creator_id", class_name: "User" + belongs_to :group, foreign_key: "namespace_id", conditions: "type = 'Group'" belongs_to :namespace - belongs_to :creator, - class_name: "User", - foreign_key: "creator_id" - - has_many :users, through: :users_projects - has_many :events, dependent: :destroy - has_many :merge_requests, dependent: :destroy - has_many :issues, dependent: :destroy, order: "closed, created_at DESC" - has_many :milestones, dependent: :destroy - has_many :users_projects, dependent: :destroy - has_many :notes, dependent: :destroy - has_many :snippets, dependent: :destroy - has_many :deploy_keys, dependent: :destroy, foreign_key: "project_id", class_name: "Key" - has_many :hooks, dependent: :destroy, class_name: "ProjectHook" - has_many :wikis, dependent: :destroy - has_many :protected_branches, dependent: :destroy has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id' has_one :gitlab_ci_service, dependent: :destroy + has_many :events, dependent: :destroy + has_many :merge_requests, dependent: :destroy + has_many :issues, dependent: :destroy, order: "closed, created_at DESC" + has_many :milestones, dependent: :destroy + has_many :users_projects, dependent: :destroy + has_many :notes, dependent: :destroy + has_many :snippets, dependent: :destroy + has_many :deploy_keys, dependent: :destroy, class_name: "Key", foreign_key: "project_id" + has_many :hooks, dependent: :destroy, class_name: "ProjectHook" + has_many :wikis, dependent: :destroy + has_many :protected_branches, dependent: :destroy + has_many :user_team_project_relationships, dependent: :destroy + + has_many :users, through: :users_projects + has_many :user_teams, through: :user_team_project_relationships + has_many :user_team_user_relationships, through: :user_teams + has_many :user_teams_members, through: :user_team_user_relationships + delegate :name, to: :owner, allow_nil: true, prefix: true # Validations @@ -77,6 +80,8 @@ class Project < ActiveRecord::Base # Scopes scope :without_user, ->(user) { where("id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } scope :not_in_group, ->(group) { where("id NOT IN (:ids)", ids: group.project_ids ) } + scope :without_team, ->(team) { team.projects.present? ? where("id NOT IN (:ids)", ids: team.projects.map(&:id)) : scoped } + scope :in_team, ->(team) { where("id IN (:ids)", ids: team.projects.map(&:id)) } scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } scope :sorted_by_activity, ->() { order("(SELECT max(events.created_at) FROM events WHERE events.project_id = projects.id) DESC") } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } @@ -122,7 +127,7 @@ class Project < ActiveRecord::Base end def team - @team ||= Team.new(self) + @team ||= ProjectTeam.new(self) end def repository @@ -335,7 +340,7 @@ class Project < ActiveRecord::Base end def execute_hooks(data) - hooks.each { |hook| hook.execute(data) } + hooks.each { |hook| hook.async_execute(data) } end def execute_services(data) @@ -489,6 +494,11 @@ class Project < ActiveRecord::Base http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') end + def project_access_human(member) + project_user_relation = self.users_projects.find_by_user_id(member.id) + self.class.access_options.key(project_user_relation.project_access) + end + # Check if current branch name is marked as protected in the system def protected_branch? branch_name protected_branches.map(&:name).include?(branch_name) diff --git a/app/models/team.rb b/app/models/project_team.rb similarity index 99% rename from app/models/team.rb rename to app/models/project_team.rb index 51f4ff68..2cc76974 100644 --- a/app/models/team.rb +++ b/app/models/project_team.rb @@ -1,4 +1,4 @@ -class Team +class ProjectTeam attr_accessor :project def initialize(project) diff --git a/app/models/user.rb b/app/models/user.rb index 35a693fd..5a95deec 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,23 +40,32 @@ class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation, :remember_me, :bio, :name, :username, :skype, :linkedin, :twitter, :dark_scheme, :theme_id, :force_random_password, :extern_uid, :provider, as: [:default, :admin] - attr_accessible :projects_limit, as: :admin + attr_accessible :projects_limit, :can_create_team, :can_create_group, as: :admin attr_accessor :force_random_password # Namespace for personal projects - has_one :namespace, class_name: "Namespace", foreign_key: :owner_id, conditions: 'type IS NULL', dependent: :destroy - has_many :groups, class_name: "Group", foreign_key: :owner_id + has_one :namespace, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace", conditions: 'type IS NULL' - has_many :keys, dependent: :destroy - has_many :users_projects, dependent: :destroy - has_many :issues, foreign_key: :author_id, dependent: :destroy - has_many :notes, foreign_key: :author_id, dependent: :destroy - has_many :merge_requests, foreign_key: :author_id, dependent: :destroy - has_many :events, class_name: "Event", foreign_key: :author_id, dependent: :destroy - has_many :recent_events, class_name: "Event", foreign_key: :author_id, order: "id DESC" - has_many :assigned_issues, class_name: "Issue", foreign_key: :assignee_id, dependent: :destroy - has_many :assigned_merge_requests, class_name: "MergeRequest", foreign_key: :assignee_id, dependent: :destroy + has_many :keys, dependent: :destroy + has_many :users_projects, dependent: :destroy + has_many :issues, dependent: :destroy, foreign_key: :author_id + has_many :notes, dependent: :destroy, foreign_key: :author_id + has_many :merge_requests, dependent: :destroy, foreign_key: :author_id + has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event" + has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" + has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" + + has_many :groups, class_name: "Group", foreign_key: :owner_id + has_many :recent_events, class_name: "Event", foreign_key: :author_id, order: "id DESC" + + has_many :projects, through: :users_projects + + has_many :user_team_user_relationships, dependent: :destroy + + has_many :user_teams, through: :user_team_user_relationships + has_many :user_team_project_relationships, through: :user_teams + has_many :team_projects, through: :user_team_project_relationships validates :name, presence: true validates :bio, length: { within: 0..255 } @@ -80,6 +89,9 @@ class User < ActiveRecord::Base scope :blocked, where(blocked: true) scope :active, where(blocked: false) scope :alphabetically, order('name ASC') + scope :in_team, ->(team){ where(id: team.member_ids) } + scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) } + scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active } # # Class methods @@ -131,6 +143,11 @@ class User < ActiveRecord::Base # # Instance methods # + + def to_param + username + end + def generate_password if self.force_random_password self.password = self.password_confirmation = Devise.friendly_token.first(8) @@ -220,7 +237,7 @@ class User < ActiveRecord::Base end def can_create_group? - is_admin? + can?(:create_group, nil) end def abilities @@ -283,4 +300,15 @@ class User < ActiveRecord::Base def namespace_id namespace.try :id end + + def authorized_teams + @authorized_teams ||= begin + ids = [] + ids << UserTeam.with_member(self).pluck('user_teams.id') + ids << UserTeam.created_by(self).pluck('user_teams.id') + ids.flatten + + UserTeam.where(id: ids) + end + end end diff --git a/app/models/user_team.rb b/app/models/user_team.rb new file mode 100644 index 00000000..b28a6a04 --- /dev/null +++ b/app/models/user_team.rb @@ -0,0 +1,97 @@ +class UserTeam < ActiveRecord::Base + attr_accessible :name, :owner_id, :path + + belongs_to :owner, class_name: User + + has_many :user_team_project_relationships, dependent: :destroy + has_many :user_team_user_relationships, dependent: :destroy + + has_many :projects, through: :user_team_project_relationships + has_many :members, through: :user_team_user_relationships, source: :user + + validates :name, presence: true, uniqueness: true + validates :owner, presence: true + validates :path, uniqueness: true, presence: true, length: { within: 1..255 }, + format: { with: Gitlab::Regex.path_regex, + message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" } + + scope :with_member, ->(user){ joins(:user_team_user_relationships).where(user_team_user_relationships: {user_id: user.id}) } + scope :with_project, ->(project){ joins(:user_team_project_relationships).where(user_team_project_relationships: {project_id: project})} + scope :without_project, ->(project){ where("user_teams.id NOT IN (:ids)", ids: (a = with_project(project); a.blank? ? 0 : a))} + scope :created_by, ->(user){ where(owner_id: user) } + + class << self + def search query + where("name LIKE :query OR path LIKE :query", query: "%#{query}%") + end + + def global_id + 'GLN' + end + + def access_roles + UsersProject.access_roles + end + end + + def to_param + path + end + + def assign_to_projects(projects, access) + projects.each do |project| + assign_to_project(project, access) + end + end + + def assign_to_project(project, access) + Gitlab::UserTeamManager.assign(self, project, access) + end + + def resign_from_project(project) + Gitlab::UserTeamManager.resign(self, project) + end + + def add_members(users, access, group_admin) + users.each do |user| + add_member(user, access, group_admin) + end + end + + def add_member(user, access, group_admin) + Gitlab::UserTeamManager.add_member_into_team(self, user, access, group_admin) + end + + def remove_member(user) + Gitlab::UserTeamManager.remove_member_from_team(self, user) + end + + def update_membership(user, options) + Gitlab::UserTeamManager.update_team_user_membership(self, user, options) + end + + def update_project_access(project, permission) + Gitlab::UserTeamManager.update_project_greates_access(self, project, permission) + end + + def max_project_access(project) + user_team_project_relationships.find_by_project_id(project).greatest_access + end + + def human_max_project_access(project) + self.class.access_roles.invert[max_project_access(project)] + end + + def default_projects_access(member) + user_team_user_relationships.find_by_user_id(member).permission + end + + def human_default_projects_access(member) + self.class.access_roles.invert[default_projects_access(member)] + end + + def admin?(member) + user_team_user_relationships.with_user(member).first.group_admin? + end + +end diff --git a/app/models/user_team_project_relationship.rb b/app/models/user_team_project_relationship.rb new file mode 100644 index 00000000..1b0368c7 --- /dev/null +++ b/app/models/user_team_project_relationship.rb @@ -0,0 +1,28 @@ +class UserTeamProjectRelationship < ActiveRecord::Base + attr_accessible :greatest_access, :project_id, :user_team_id + + belongs_to :user_team + belongs_to :project + + validates :project, presence: true + validates :user_team, presence: true + validate :check_greatest_access + + scope :with_project, ->(project){ where(project_id: project.id) } + + def team_name + user_team.name + end + + private + + def check_greatest_access + errors.add(:base, :incorrect_access_code) unless correct_access? + end + + def correct_access? + return false if greatest_access.blank? + return true if UsersProject.access_roles.has_value?(greatest_access) + false + end +end diff --git a/app/models/user_team_user_relationship.rb b/app/models/user_team_user_relationship.rb new file mode 100644 index 00000000..63bdc49e --- /dev/null +++ b/app/models/user_team_user_relationship.rb @@ -0,0 +1,19 @@ +class UserTeamUserRelationship < ActiveRecord::Base + attr_accessible :group_admin, :permission, :user_id, :user_team_id + + belongs_to :user_team + belongs_to :user + + validates :user_team, presence: true + validates :user, presence: true + + scope :with_user, ->(user) { where(user_id: user.id) } + + def user_name + user.name + end + + def access_human + UsersProject.access_roles.invert[permission] + end +end diff --git a/app/models/users_project.rb b/app/models/users_project.rb index 79146289..ca5048ca 100644 --- a/app/models/users_project.rb +++ b/app/models/users_project.rb @@ -39,7 +39,10 @@ class UsersProject < ActiveRecord::Base scope :reporters, where(project_access: REPORTER) scope :developers, where(project_access: DEVELOPER) scope :masters, where(project_access: MASTER) + scope :in_project, ->(project) { where(project_id: project.id) } + scope :in_projects, ->(projects) { where(project_id: project_ids) } + scope :with_user, ->(user) { where(user_id: user.id) } class << self diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index df58fa93..efa27f31 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -34,4 +34,8 @@ class WebHook < ActiveRecord::Base basic_auth: {username: parsed_url.user, password: parsed_url.password}) end end + + def async_execute(data) + Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data) + end end diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 0a25b125..e347f916 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -72,16 +72,17 @@ %th Users %th Project Access: - - @group.users.each do |u| - %tr{class: "user_#{u.id}"} - %td.name= link_to u.name, admin_user_path(u) + - @group.users.each do |user| + - next unless user + %tr{class: "user_#{user.id}"} + %td.name= link_to user.name, admin_user_path(user) %td.projects_access - - u.authorized_projects.in_namespace(@group).each do |project| - - u_p = u.users_projects.in_project(project).first + - user.authorized_projects.in_namespace(@group).each do |project| + - u_p = user.users_projects.in_project(project).first - next unless u_p %span - = project.name - = link_to "(#{ u_p.project_access_human })", edit_admin_team_member_path(u_p) + = project.name_with_namespace + = link_to "(#{ u_p.project_access_human })", edit_admin_project_member_path(project, user) %tr %td.input= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' %td= select_tag :project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3"} diff --git a/app/views/admin/projects/members/_form.html.haml b/app/views/admin/projects/members/_form.html.haml new file mode 100644 index 00000000..f1bb6cfa --- /dev/null +++ b/app/views/admin/projects/members/_form.html.haml @@ -0,0 +1,16 @@ += form_for @team_member_relation, as: :team_member, url: admin_project_member_path(@project, @member) do |f| + -if @team_member_relation.errors.any? + .alert-message.block-message.error + %ul + - @team_member_relation.errors.full_messages.each do |msg| + %li= msg + + .clearfix + %label Project Access: + .input + = f.select :project_access, options_for_select(Project.access_options, @team_member_relation.project_access), {}, class: "project-access-select chosen span3" + + %br + .actions + = f.submit 'Save', class: "btn primary" + = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/admin/projects/members/edit.html.haml b/app/views/admin/projects/members/edit.html.haml new file mode 100644 index 00000000..2d76deb2 --- /dev/null +++ b/app/views/admin/projects/members/edit.html.haml @@ -0,0 +1,8 @@ +%p.slead + Edit access for + = link_to @member.name, admin_user_path(@member) + in + = link_to @project.name_with_namespace, admin_project_path(@project) + +%hr += render 'form' diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 8e0d8232..a213c09d 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -114,9 +114,9 @@ %h5 Team %small - (#{@project.users_projects.count}) + (#{@project.users.count}) %br -%table.zebra-striped +%table.zebra-striped.team_members %thead %tr %th Name @@ -124,13 +124,13 @@ %th Repository Access %th - - @project.users_projects.each do |tm| + - @project.users.each do |tm| %tr %td - = link_to tm.user_name, admin_user_path(tm.user) - %td= tm.project_access_human - %td= link_to 'Edit Access', edit_admin_team_member_path(tm), class: "btn small" - %td= link_to 'Remove from team', admin_team_member_path(tm), confirm: 'Are you sure?', method: :delete, class: "btn danger small" + = link_to tm.name, admin_user_path(tm) + %td= @project.project_access_human(tm) + %td= link_to 'Edit Access', edit_admin_project_member_path(@project, tm), class: "btn small" + %td= link_to 'Remove from team', admin_project_member_path(@project, tm), confirm: 'Are you sure?', method: :delete, class: "btn danger small" %br %h5 Add new team member diff --git a/app/views/admin/team_members/_form.html.haml b/app/views/admin/team_members/_form.html.haml deleted file mode 100644 index 9cd94fdd..00000000 --- a/app/views/admin/team_members/_form.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -= form_for @admin_team_member, as: :team_member, url: admin_team_member_path(@admin_team_member) do |f| - -if @admin_team_member.errors.any? - .alert-message.block-message.error - %ul - - @admin_team_member.errors.full_messages.each do |msg| - %li= msg - - .clearfix - %label Project Access: - .input - = f.select :project_access, options_for_select(Project.access_options, @admin_team_member.project_access), {}, class: "project-access-select chosen span3" - - %br - .actions - = f.submit 'Save', class: "btn primary" - = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/admin/team_members/edit.html.haml b/app/views/admin/team_members/edit.html.haml deleted file mode 100644 index aea9bd70..00000000 --- a/app/views/admin/team_members/edit.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%p.slead - Edit access for - = link_to @admin_team_member.user_name, admin_user_path(@admin_team_member) - in - = link_to @admin_team_member.project.name_with_namespace, admin_project_path(@admin_team_member) - -%hr -= render 'form' diff --git a/app/views/admin/teams/edit.html.haml b/app/views/admin/teams/edit.html.haml new file mode 100644 index 00000000..b2499ef6 --- /dev/null +++ b/app/views/admin/teams/edit.html.haml @@ -0,0 +1,23 @@ +%h3.page_title Rename Team +%hr += form_for @team, url: admin_team_path(@team), method: :put do |f| + - if @team.errors.any? + .alert-message.block-message.error + %span= @team.errors.full_messages.first + .clearfix.team_name_holder + = f.label :name do + Team name is + .input + = f.text_field :name, placeholder: "Example Team", class: "xxlarge" + + .clearfix.team_name_holder + = f.label :path do + %span.cred Team path is + .input + = f.text_field :path, placeholder: "example-team", class: "xxlarge danger" + %ul.cred + %li It will change web url for access team and team projects. + + .form-actions + = f.submit 'Rename team', class: "btn danger" + = link_to 'Cancel', admin_teams_path, class: "btn cancel-btn" diff --git a/app/views/admin/teams/index.html.haml b/app/views/admin/teams/index.html.haml new file mode 100644 index 00000000..3ab57448 --- /dev/null +++ b/app/views/admin/teams/index.html.haml @@ -0,0 +1,38 @@ +%h3.page_title + Teams + %small + simple Teams description + + = link_to 'New Team', new_admin_team_path, class: "btn small right" + %br + += form_tag admin_teams_path, method: :get, class: 'form-inline' do + = text_field_tag :name, params[:name], class: "xlarge" + = submit_tag "Search", class: "btn submit primary" + +%table + %thead + %tr + %th + Name + %i.icon-sort-down + %th Path + %th Projects + %th Members + %th Owner + %th.cred Danger Zone! + + - @teams.each do |team| + %tr + %td + %strong= link_to team.name, admin_team_path(team) + %td= team.path + %td= team.projects.count + %td= team.members.count + %td + = link_to team.owner.name, admin_user_path(team.owner_id) + %td.bgred + = link_to 'Rename', edit_admin_team_path(team), id: "edit_#{dom_id(team)}", class: "btn small" + = link_to 'Destroy', admin_team_path(team), confirm: "REMOVE #{team.name}? Are you sure?", method: :delete, class: "btn small danger" + += paginate @teams, theme: "admin" diff --git a/app/views/admin/teams/members/_form.html.haml b/app/views/admin/teams/members/_form.html.haml new file mode 100644 index 00000000..b75d788a --- /dev/null +++ b/app/views/admin/teams/members/_form.html.haml @@ -0,0 +1,20 @@ += form_tag admin_team_member_path(@team, @member), method: :put do + -if @member.errors.any? + .alert-message.block-message.error + %ul + - @member.errors.full_messages.each do |msg| + %li= msg + + .clearfix + %label Default access for Team projects: + .input + = select_tag :default_project_access, options_for_select(UserTeam.access_roles, @team.default_projects_access(@member)), class: "project-access-select chosen span3" + .clearfix + %label Team admin? + .input + = check_box_tag :group_admin, true, @team.admin?(@member) + + %br + .actions + = submit_tag 'Save', class: "btn primary" + = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/admin/teams/members/edit.html.haml b/app/views/admin/teams/members/edit.html.haml new file mode 100644 index 00000000..a82847ee --- /dev/null +++ b/app/views/admin/teams/members/edit.html.haml @@ -0,0 +1,16 @@ +%h3 + Edit access #{@member.name} in #{@team.name} team + +%hr +%table.zebra-striped + %tr + %td User: + %td= @member.name + %tr + %td Team: + %td= @team.name + %tr + %td Since: + %td= member_since(@team, @member).stamp("Nov 11, 2010") + += render 'form' diff --git a/app/views/admin/teams/members/new.html.haml b/app/views/admin/teams/members/new.html.haml new file mode 100644 index 00000000..066ab19f --- /dev/null +++ b/app/views/admin/teams/members/new.html.haml @@ -0,0 +1,29 @@ +%h3.page_title + Team: #{@team.name} + +%fieldset + %legend Members (#{@team.members.count}) + = form_tag admin_team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do + %table#members_list + %thead + %tr + %th User name + %th Default project access + %th Team access + %th + - @team.members.each do |member| + %tr.member + %td + = link_to [:admin, member] do + = member.name + %small= "(#{member.email})" + %td= @team.human_default_projects_access(member) + %td= @team.admin?(member) ? "Admin" : "Member" + %td + %tr + %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_email), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' + %td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } + %td + %span= check_box_tag :group_admin + %span Admin? + %td= submit_tag 'Add', class: "btn primary", id: :add_members_to_team diff --git a/app/views/admin/teams/new.html.haml b/app/views/admin/teams/new.html.haml new file mode 100644 index 00000000..a40a2c4e --- /dev/null +++ b/app/views/admin/teams/new.html.haml @@ -0,0 +1,19 @@ +%h3.page_title New Team +%hr += form_for @team, url: admin_teams_path do |f| + - if @team.errors.any? + .alert-message.block-message.error + %span= @team.errors.full_messages.first + .clearfix + = f.label :name do + Team name is + .input + = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" +   + = f.submit 'Create team', class: "btn primary" + %hr + .padded + %ul + %li All created teams are public (users can view who enter into team and which project are assigned for this team) + %li People within a team see only projects they have access to + %li You will be able to assign existing projects for team diff --git a/app/views/admin/teams/projects/_form.html.haml b/app/views/admin/teams/projects/_form.html.haml new file mode 100644 index 00000000..db4fe85b --- /dev/null +++ b/app/views/admin/teams/projects/_form.html.haml @@ -0,0 +1,16 @@ += form_tag admin_team_project_path(@team, @project), method: :put do + -if @project.errors.any? + .alert-message.block-message.error + %ul + - @project.errors.full_messages.each do |msg| + %li= msg + + .clearfix + %label Max access for Team members: + .input + = select_tag :greatest_project_access, options_for_select(UserTeam.access_roles, @team.max_project_access(@project)), class: "project-access-select chosen span3" + + %br + .actions + = submit_tag 'Save', class: "btn primary" + = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/admin/teams/projects/edit.html.haml b/app/views/admin/teams/projects/edit.html.haml new file mode 100644 index 00000000..b91a4982 --- /dev/null +++ b/app/views/admin/teams/projects/edit.html.haml @@ -0,0 +1,16 @@ +%h3 + Edit max access in #{@project.name} for #{@team.name} team + +%hr +%table.zebra-striped + %tr + %td Project: + %td= @project.name + %tr + %td Team: + %td= @team.name + %tr + %td Since: + %td= assigned_since(@team, @project).stamp("Nov 11, 2010") + += render 'form' diff --git a/app/views/admin/teams/projects/new.html.haml b/app/views/admin/teams/projects/new.html.haml new file mode 100644 index 00000000..8a0a18a4 --- /dev/null +++ b/app/views/admin/teams/projects/new.html.haml @@ -0,0 +1,23 @@ +%h3.page_title + Team: #{@team.name} + +%fieldset + %legend Projects (#{@team.projects.count}) + = form_tag admin_team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do + %table#projects_list + %thead + %tr + %th Project name + %th Max access + %th + - @team.projects.each do |project| + %tr.project + %td + = link_to project.name_with_namespace, [:admin, project] + %td + %span= @team.human_max_project_access(project) + %td + %tr + %td= select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5' + %td= select_tag :greatest_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } + %td= submit_tag 'Add', class: "btn primary", id: :assign_projects_to_team diff --git a/app/views/admin/teams/show.html.haml b/app/views/admin/teams/show.html.haml new file mode 100644 index 00000000..6a1deaff --- /dev/null +++ b/app/views/admin/teams/show.html.haml @@ -0,0 +1,101 @@ +%h3.page_title + Team: #{@team.name} + +%br +%table.zebra-striped + %thead + %tr + %th Team + %th + %tr + %td + %b + Name: + %td + = @team.name +   + = link_to edit_admin_team_path(@team), class: "btn btn-small right" do + %i.icon-edit + Rename + %tr + %td + %b + Owner: + %td + = @team.owner.name + .right + = link_to "#", class: "btn btn-small change-owner-link" do + %i.icon-edit + Change owner + + %tr.change-owner-holder.hide + %td.bgred + %b.cred + New Owner: + %td.bgred + = form_for @team, url: admin_team_path(@team) do |f| + = f.select :owner_id, User.all.map { |user| [user.name, user.id] }, {}, {class: 'chosen'} + %div + = f.submit 'Change Owner', class: "btn danger" + = link_to "Cancel", "#", class: "btn change-owner-cancel-link" + +%fieldset + %legend + Members (#{@team.members.count}) + %span= link_to 'Add members', new_admin_team_member_path(@team), class: "btn success small right", id: :add_members_to_team + - if @team.members.any? + %table#members_list + %thead + %tr + %th User name + %th Default project access + %th Team access + %th.cred.span3 Danger Zone! + - @team.members.each do |member| + %tr.member{ class: "user_#{member.id}"} + %td + = link_to [:admin, member] do + = member.name + %small= "(#{member.email})" + %td= @team.human_default_projects_access(member) + %td= @team.admin?(member) ? "Admin" : "Member" + %td.bgred + = link_to 'Edit', edit_admin_team_member_path(@team, member), class: "btn small" +   + = 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}" + +%fieldset + %legend + Projects (#{@team.projects.count}) + %span= link_to 'Add projects', new_admin_team_project_path(@team), class: "btn success small right", id: :assign_projects_to_team + - if @team.projects.any? + %table#projects_list + %thead + %tr + %th Project name + %th Max access + %th.cred.span3 Danger Zone! + - @team.projects.each do |project| + %tr.project + %td + = link_to project.name_with_namespace, [:admin, project] + %td + %span= @team.human_max_project_access(project) + %td.bgred + = link_to 'Edit', edit_admin_team_project_path(@team, project), class: "btn small" +   + = 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}" + +:javascript + $(function(){ + var modal = $('.change-owner-holder'); + $('.change-owner-link').bind("click", function(){ + $(this).hide(); + modal.show(); + }); + $('.change-owner-cancel-link').bind("click", function(){ + modal.hide(); + $('.change-owner-link').show(); + }) + }) + diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 45195152..465568ad 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -46,6 +46,14 @@ = f.label :projects_limit .input= f.number_field :projects_limit + .clearfix + = f.label :can_create_group + .input= f.check_box :can_create_group + + .clearfix + = f.label :can_create_team + .input= f.check_box :can_create_team + .clearfix = f.label :admin do %strong.cred Administrator diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index a3be6614..d9d720da 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -123,5 +123,5 @@ %tr %td= link_to project.name_with_namespace, admin_project_path(project) %td= tm.project_access_human - %td= link_to 'Edit Access', edit_admin_team_member_path(tm), class: "btn small" - %td= link_to 'Remove from team', admin_team_member_path(tm), confirm: 'Are you sure?', method: :delete, class: "btn small danger" + %td= link_to 'Edit Access', edit_admin_project_member_path(project, tm.user), class: "btn small" + %td= link_to 'Remove from team', admin_project_member_path(project, tm.user), confirm: 'Are you sure?', method: :delete, class: "btn small danger" diff --git a/app/views/commit/show.html.haml b/app/views/commit/show.html.haml index f920534e..6bee6493 100644 --- a/app/views/commit/show.html.haml +++ b/app/views/commit/show.html.haml @@ -11,19 +11,7 @@ :javascript $(function(){ - var w, h; - $('.diff_file').each(function(){ - $('.image.diff_removed img', this).on('load', $.proxy(function(event){ - var w = event.currentTarget.naturalWidth - , h = event.currentTarget.naturalHeight; - $('.image.diff_removed .image-info', this).append(' | W: ' + w + 'px | H: ' + h + 'px'); - }, this)); - $('.image.diff_added img', this).on('load', $.proxy(function(event){ - var w = event.currentTarget.naturalWidth - , h = event.currentTarget.naturalHeight; - $('.image.diff_added .image-info', this).append(' | W: ' + w + 'px | H: ' + h + 'px'); - }, this)); - + $('.files .file').each(function(){ + new CommitFile(this); }); - }); diff --git a/app/views/commits/_commits.html.haml b/app/views/commits/_commits.html.haml index 0dc6664c..19132093 100644 --- a/app/views/commits/_commits.html.haml +++ b/app/views/commits/_commits.html.haml @@ -2,5 +2,5 @@ %div.ui-box %h5.title %i.icon-calendar - = day.stamp("28 Aug, 2010") + %span= day.stamp("28 Aug, 2010") %ul.well-list= render commits diff --git a/app/views/commits/_diffs.html.haml b/app/views/commits/_diffs.html.haml index 7fe45aa2..9a9aed39 100644 --- a/app/views/commits/_diffs.html.haml +++ b/app/views/commits/_diffs.html.haml @@ -12,50 +12,38 @@ .file-stats = render "commits/diff_head", diffs: diffs -- unless @suppress_diff - - diffs.each_with_index do |diff, i| - - next if diff.diff.empty? - - file = (@commit.tree / diff.new_path) - - file = (@commit.prev_commit.tree / diff.old_path) unless file - - next unless file - .diff_file{id: "diff-#{i}"} - .diff_file_header - - if diff.deleted_file - %span= diff.old_path +.files + - unless @suppress_diff + - diffs.each_with_index do |diff, i| + - next if diff.diff.empty? + - file = (@commit.tree / diff.new_path) + - file = (@commit.prev_commit.tree / diff.old_path) unless file + - next unless file + .file{id: "diff-#{i}"} + .header + - if diff.deleted_file + %span= diff.old_path - - if @commit.prev_commit - = link_to project_tree_path(@project, tree_join(@commit.prev_commit_id, diff.new_path)), {:class => 'btn right view-commit'} do + - if @commit.prev_commit + = link_to project_tree_path(@project, tree_join(@commit.prev_commit_id, diff.new_path)), {:class => 'btn right view-file'} do + View file @ + %span.commit-short-id= @commit.short_id(6) + - else + %span= diff.new_path + - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode + %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" + + = link_to project_tree_path(@project, tree_join(@commit.id, diff.new_path)), {:class => 'btn very_small right view-file'} do View file @ %span.commit-short-id= @commit.short_id(6) - - else - %span= diff.new_path - - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode - %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" - = link_to project_tree_path(@project, tree_join(@commit.id, diff.new_path)), {:class => 'btn very_small right view-commit'} do - View file @ - %span.commit-short-id= @commit.short_id(6) - - %br/ - .diff_file_content - -# Skip all non-supported blobs - - next unless file.respond_to?('text?') - - if file.text? - = render "commits/text_diff", diff: diff, index: i - - elsif file.image? - - old_file = (@commit.prev_commit.tree / diff.old_path) if !@commit.prev_commit.nil? - - if diff.renamed_file || diff.new_file || diff.deleted_file - .diff_file_content_image - .image{class: image_diff_class(diff)} - %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} - %div.image-info= "#{number_to_human_size file.size}" + .content + -# Skipp all non non-supported blobs + - next unless file.respond_to?('text?') + - if file.text? + = render "commits/text_file", diff: diff, index: i + - elsif file.image? + - old_file = (@commit.prev_commit.tree / diff.old_path) if !@commit.prev_commit.nil? + = render "commits/image", diff: diff, old_file: old_file, file: file, index: i - else - .diff_file_content_image.img_compared - .image.diff_removed - %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(old_file.data)}"} - %div.image-info= "#{number_to_human_size file.size}" - .image.diff_added - %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} - %div.image-info= "#{number_to_human_size file.size}" - - else - %p.nothing_here_message No preview for this file type + %p.nothing_here_message No preview for this file type diff --git a/app/views/commits/_image.html.haml b/app/views/commits/_image.html.haml new file mode 100644 index 00000000..db02fa33 --- /dev/null +++ b/app/views/commits/_image.html.haml @@ -0,0 +1,63 @@ +- if diff.renamed_file || diff.new_file || diff.deleted_file + .image + %span.wrap + .frame{class: image_diff_class(diff)} + %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + %p.image-info= "#{number_to_human_size file.size}" +- else + .image + %div.two-up.view + %span.wrap + .frame.deleted + %a{href: project_tree_path(@project, tree_join(@commit.id, diff.old_path))} + %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} + %p.image-info.hide + %span.meta-filesize= "#{number_to_human_size old_file.size}" + | + %b W: + %span.meta-width + | + %b H: + %span.meta-height + %span.wrap + .frame.added + %a{href: project_tree_path(@project, tree_join(@commit.id, diff.new_path))} + %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + %p.image-info.hide + %span.meta-filesize= "#{number_to_human_size file.size}" + | + %b W: + %span.meta-width + | + %b H: + %span.meta-height + + %div.swipe.view.hide + .swipe-frame + .frame.deleted + %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} + .swipe-wrap + .frame.added + %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + %span.swipe-bar + %span.top-handle + %span.bottom-handle + + %div.onion-skin.view.hide + .onion-skin-frame + .frame.deleted + %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} + .frame.added + %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} + .controls + .transparent + .drag-track + .dragger{:style => "left: 0px;"} + .opaque + + + .view-modes.hide + %ul.view-modes-menu + %li.two-up{data: {mode: 'two-up'}} 2-up + %li.swipe{data: {mode: 'swipe'}} Swipe + %li.onion-skin{data: {mode: 'onion-skin'}} Onion skin \ No newline at end of file diff --git a/app/views/commits/_text_diff.html.haml b/app/views/commits/_text_file.html.haml similarity index 95% rename from app/views/commits/_text_diff.html.haml rename to app/views/commits/_text_file.html.haml index 8afad96b..760fd07e 100644 --- a/app/views/commits/_text_diff.html.haml +++ b/app/views/commits/_text_file.html.haml @@ -2,7 +2,7 @@ - if too_big %a.supp_diff_link Diff suppressed. Click to show -%table{class: "#{'hide' if too_big}"} +%table.text-file{class: "#{'hide' if too_big}"} - each_diff_line(diff, index) do |line, type, line_code, line_new, line_old| %tr.line_holder{ id: line_code } - if type == "match" diff --git a/app/views/commits/show.html.haml b/app/views/commits/show.html.haml index 9451a038..d180b8ec 100644 --- a/app/views/commits/show.html.haml +++ b/app/views/commits/show.html.haml @@ -5,7 +5,7 @@ = breadcrumbs %div{id: dom_id(@project)} - #commits_list= render "commits" + #commits-list= render "commits" .clear .loading{ style: "display:none;"} diff --git a/app/views/dashboard/_groups.html.haml b/app/views/dashboard/_groups.html.haml index 7f544406..f9774669 100644 --- a/app/views/dashboard/_groups.html.haml +++ b/app/views/dashboard/_groups.html.haml @@ -1,11 +1,11 @@ -.groups_box +.ui-box %h5.title Groups %small (#{groups.count}) - if current_user.can_create_group? %span.right - = link_to new_admin_group_path, class: "btn very_small info" do + = link_to new_group_path, class: "btn very_small info" do %i.icon-plus New Group %ul.well-list @@ -13,8 +13,6 @@ %li = link_to group_path(id: group.path), class: dom_class(group) do %strong.well-title= truncate(group.name, length: 35) - %span.arrow - → - %span.last_activity - %strong Projects: - %span= current_user.authorized_projects.where(namespace_id: group.id).count + %span.right.light + - if group.owner == current_user + %i.icon-wrench diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 6c1304ee..f2acd2b0 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -2,19 +2,12 @@ %h5.title Projects %small - (#{projects.total_count}) + (#{@projects_count}) - if current_user.can_create_project? %span.right = link_to new_project_path, class: "btn very_small info" do %i.icon-plus New Project - %ul.nav.nav-projects-tabs - = nav_tab :scope, nil do - = link_to "All", dashboard_path - = nav_tab :scope, 'personal' do - = link_to "Personal", dashboard_path(scope: 'personal') - = nav_tab :scope, 'joined' do - = link_to "Joined", dashboard_path(scope: 'joined') %ul.well-list - projects.each do |project| @@ -33,4 +26,6 @@ - if projects.blank? %li %h3.nothing_here_message There are no projects here. - .bottom= paginate projects, theme: "gitlab" + - if @projects_count > 20 + %li.bottom + %strong= link_to "show all projects", projects_dashboard_path diff --git a/app/views/dashboard/_sidebar.html.haml b/app/views/dashboard/_sidebar.html.haml index 9830cdf4..7c6daf6e 100644 --- a/app/views/dashboard/_sidebar.html.haml +++ b/app/views/dashboard/_sidebar.html.haml @@ -1,3 +1,5 @@ +- if @teams.present? + = render "teams", teams: @teams - if @groups.present? = render "groups", groups: @groups = render "projects", projects: @projects diff --git a/app/views/dashboard/_teams.html.haml b/app/views/dashboard/_teams.html.haml new file mode 100644 index 00000000..5b2ea7a2 --- /dev/null +++ b/app/views/dashboard/_teams.html.haml @@ -0,0 +1,20 @@ +.ui-box.teams-box + %h5.title + Teams + %small + (#{@teams.count}) + %span.right + = link_to new_team_path, class: "btn very_small info" do + %i.icon-plus + New Team + %ul.well-list + - @teams.each do |team| + %li + = link_to team_path(id: team.path), class: dom_class(team) do + %strong.well-title= truncate(team.name, length: 35) + %span.right.light + - if team.owner == current_user + %i.icon-wrench + - tm = current_user.user_team_user_relationships.find_by_user_team_id(team.id) + - if tm + = tm.access_human diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 28bdc5ed..0f0f3466 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{current_user.name} issues" - xml.link :href => dashboard_issues_url(:atom, :private_token => current_user.private_token), :rel => "self", :type => "application/atom+xml" - xml.link :href => dashboard_issues_url(:private_token => current_user.private_token), :rel => "alternate", :type => "text/html" - xml.id dashboard_issues_url(:private_token => current_user.private_token) + xml.link :href => issues_dashboard_url(:atom, :private_token => current_user.private_token), :rel => "self", :type => "application/atom+xml" + xml.link :href => issues_dashboard_url(:private_token => current_user.private_token), :rel => "alternate", :type => "text/html" + xml.id issues_dashboard_url(:private_token => current_user.private_token) xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml new file mode 100644 index 00000000..e6c710e6 --- /dev/null +++ b/app/views/dashboard/projects.html.haml @@ -0,0 +1,56 @@ +%h3.page_title + Projects + %span + (#{@projects.total_count}) + - if current_user.can_create_project? + %span.right + = link_to new_project_path, class: "btn very_small info" do + %i.icon-plus + New Project + + +%hr +.row + .span3 + %ul.nav.nav-pills.nav-stacked + = nav_tab :scope, nil do + = link_to "All", projects_dashboard_path + = nav_tab :scope, 'personal' do + = link_to "Personal", projects_dashboard_path(scope: 'personal') + = nav_tab :scope, 'joined' do + = link_to "Joined", projects_dashboard_path(scope: 'joined') + + .span9 + = form_tag projects_dashboard_path, method: 'get' do + %fieldset.dashboard-search-filter + = hidden_field_tag "scope", params[:scope] + = search_field_tag "search", params[:search], { placeholder: 'Search', class: 'left input-xxlarge' } + = button_tag type: 'submit', class: 'btn' do + %i.icon-search + + %ul.well-list + - @projects.each do |project| + %li.clearfix + .left + = link_to project_path(project), class: dom_class(project) do + - if project.namespace + = project.namespace.human_name + \/ + %strong.well-title + = truncate(project.name, length: 25) + %br + %small.light + %strong Last activity: + %span= project_last_activity(project) + .right.light + - if project.owner == current_user + %i.icon-wrench + - tm = project.team.get_tm(current_user.id) + - if tm + = tm.project_access_human + + - if @projects.blank? + %li + %h3.nothing_here_message There are no projects here. + .bottom= paginate @projects, theme: "gitlab" + diff --git a/app/views/dashboard/index.atom.builder b/app/views/dashboard/show.atom.builder similarity index 100% rename from app/views/dashboard/index.atom.builder rename to app/views/dashboard/show.atom.builder diff --git a/app/views/dashboard/index.html.haml b/app/views/dashboard/show.html.haml similarity index 100% rename from app/views/dashboard/index.html.haml rename to app/views/dashboard/show.html.haml diff --git a/app/views/dashboard/index.js.haml b/app/views/dashboard/show.js.haml similarity index 100% rename from app/views/dashboard/index.js.haml rename to app/views/dashboard/show.js.haml diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 5bd07bcd..701747bd 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@user.name} issues" - xml.link :href => dashboard_issues_url(:atom, :private_token => @user.private_token), :rel => "self", :type => "application/atom+xml" - xml.link :href => dashboard_issues_url(:private_token => @user.private_token), :rel => "alternate", :type => "text/html" - xml.id dashboard_issues_url(:private_token => @user.private_token) + xml.link :href => issues_dashboard_url(:atom, :private_token => @user.private_token), :rel => "self", :type => "application/atom+xml" + xml.link :href => issues_dashboard_url(:private_token => @user.private_token), :rel => "alternate", :type => "text/html" + xml.id issues_dashboard_url(:private_token => @user.private_token) xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml new file mode 100644 index 00000000..b6d5f465 --- /dev/null +++ b/app/views/groups/new.html.haml @@ -0,0 +1,21 @@ +%h3.page_title New Group +%hr += form_for @group do |f| + - if @group.errors.any? + .alert-message.block-message.error + %span= @group.errors.full_messages.first + .clearfix + = f.label :name do + Group name is + .input + = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" +   + = f.submit 'Create group', class: "btn primary" + %hr + .padded + %ul + %li Group is kind of directory for several projects + %li All created groups are private + %li People within a group see only projects they have access to + %li All projects of group will be stored in group directory + %li You will be able to move existing projects into group diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index f4b2228a..8f4f3d78 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -17,7 +17,7 @@ = link_to new_project_path, title: "Create New Project", class: 'has_bottom_tooltip', 'data-original-title' => 'New project' do %i.icon-plus %li - = link_to profile_path, title: "Your Profile", class: 'has_bottom_tooltip', 'data-original-title' => 'Your profile' do + = link_to profile_path, title: "My Profile", class: 'has_bottom_tooltip', 'data-original-title' => 'Your profile' do %i.icon-user %li.separator %li diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 7ea90798..c484af04 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,6 +1,8 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| = text_field_tag "search", nil, placeholder: "Search", class: "search-input" + = hidden_field_tag :group_id, @group.try(:id) + = hidden_field_tag :project_id, @project.try(:id) :javascript $(function(){ diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index a60e7feb..28626b9c 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -10,6 +10,8 @@ = link_to "Stats", admin_root_path = nav_link(controller: :projects) do = link_to "Projects", admin_projects_path + = nav_link(controller: :teams) do + = link_to "Teams", admin_teams_path = nav_link(controller: :groups) do = link_to "Groups", admin_groups_path = nav_link(controller: :users) do diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 88da5c98..261a8608 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -6,14 +6,17 @@ = render "layouts/head_panel", title: "Dashboard" .container %ul.main_menu - = nav_link(path: 'dashboard#index', html_options: {class: 'home'}) do + = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do = link_to "Home", root_path, title: "Home" + = nav_link(path: 'dashboard#projects') do + = link_to projects_dashboard_path do + Projects = nav_link(path: 'dashboard#issues') do - = link_to dashboard_issues_path do + = link_to issues_dashboard_path do Issues %span.count= current_user.assigned_issues.opened.count = nav_link(path: 'dashboard#merge_requests') do - = link_to dashboard_merge_requests_path do + = link_to merge_requests_dashboard_path do Merge Requests %span.count= current_user.cared_merge_requests.opened.count = nav_link(path: 'search#show') do diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index f47e8b3e..46bc9ef1 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -3,7 +3,7 @@ = render "layouts/head", title: "#{@group.name}" %body{class: "#{app_theme} application"} = render "layouts/flash" - = render "layouts/head_panel", title: "#{@group.name}" + = render "layouts/head_panel", title: "group: #{@group.name}" .container %ul.main_menu = nav_link(path: 'groups#show', html_options: {class: 'home'}) do diff --git a/app/views/layouts/user_team.html.haml b/app/views/layouts/user_team.html.haml new file mode 100644 index 00000000..2d397e80 --- /dev/null +++ b/app/views/layouts/user_team.html.haml @@ -0,0 +1,38 @@ +!!! 5 +%html{ lang: "en"} + = render "layouts/head", title: "#{@team.name}" + %body{class: "#{app_theme} application"} + = render "layouts/flash" + = render "layouts/head_panel", title: "team: #{@team.name}" + .container + %ul.main_menu + = nav_link(path: 'teams#show', html_options: {class: 'home'}) do + = link_to "Home", team_path(@team), title: "Home" + + = nav_link(path: 'teams#issues') do + = link_to issues_team_path(@team) do + Issues + %span.count= Issue.opened.of_user_team(@team).count + + = nav_link(path: 'teams#merge_requests') do + = link_to merge_requests_team_path(@team) do + Merge Requests + %span.count= MergeRequest.opened.of_user_team(@team).count + + = nav_link(controller: [:members]) do + = link_to team_members_path(@team), class: "team-tab tab" do + Members + %span.count= @team.members.count + + - if can? current_user, :admin_user_team, @team + = nav_link(controller: [:projects]) do + = link_to team_projects_path(@team), class: "team-tab tab" do + Projects + %span.count= @team.projects.count + + = nav_link(path: 'teams#edit') do + = link_to edit_team_path(@team), class: "stat-tab tab " do + %i.icon-edit + Edit Team + + .content= yield diff --git a/app/views/notes/_discussion.html.haml b/app/views/notes/_discussion.html.haml index a9a11fc2..24cb4228 100644 --- a/app/views/notes/_discussion.html.haml +++ b/app/views/notes/_discussion.html.haml @@ -38,7 +38,7 @@ - if note.for_diff_line? - if note.diff .content - .diff_file= render "notes/discussion_diff", discussion_notes: discussion_notes, note: note + .file= render "notes/discussion_diff", discussion_notes: discussion_notes, note: note - else = link_to 'show outdated discussion', '#', class: 'js-show-outdated-discussion' %div.hide.outdated-discussion diff --git a/app/views/notes/_discussion_diff.html.haml b/app/views/notes/_discussion_diff.html.haml index 93ab59c7..790b7733 100644 --- a/app/views/notes/_discussion_diff.html.haml +++ b/app/views/notes/_discussion_diff.html.haml @@ -1,5 +1,5 @@ - diff = note.diff -.diff_file_header +.header - if diff.deleted_file %span= diff.old_path - else @@ -7,7 +7,7 @@ - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" %br/ -.diff_file_content +.content %table - each_diff_line(diff, note.diff_file_index) do |line, type, line_code, line_new, line_old| %tr.line_holder{ id: line_code } diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 687463b6..7a3177f0 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -31,6 +31,20 @@ .controls = f.text_field :email, class: "input-xlarge", required: true %span.help-block We also use email for avatar detection. + .control-group + = f.label :skype, class: "control-label" + .controls= f.text_field :skype, class: "input-xlarge" + .control-group + = f.label :linkedin, class: "control-label" + .controls= f.text_field :linkedin, class: "input-xlarge" + .control-group + = f.label :twitter, class: "control-label" + .controls= f.text_field :twitter, class: "input-xlarge" + .control-group + = f.label :bio, class: "control-label" + .controls + = f.text_area :bio, rows: 6, class: "input-xlarge", maxlength: 250 + %span.help-block Tell us about yourself in fewer than 250 characters. .span5.right %fieldset.tips @@ -47,24 +61,18 @@ %p You can login through #{@user.provider.titleize}! = link_to "click here to change", account_profile_path - - .row - .span7 - .control-group - = f.label :skype, class: "control-label" - .controls= f.text_field :skype, class: "input-xlarge" - .control-group - = f.label :linkedin, class: "control-label" - .controls= f.text_field :linkedin, class: "input-xlarge" - .control-group - = f.label :twitter, class: "control-label" - .controls= f.text_field :twitter, class: "input-xlarge" - .control-group - = f.label :bio, class: "control-label" - .controls - = f.text_area :bio, rows: 6, class: "input-xlarge", maxlength: 250 - %span.help-block Tell us about yourself in fewer than 250 characters. - .span5.right + - if current_user.can_create_group? + %li + %p + Need a group for several dependent projects? + = link_to new_group_path, class: "btn very_small" do + Create a group + - if current_user.can_create_team? + %li + %p + Want to share a team between projects? + = link_to new_team_path, class: "btn very_small" do + Create a team %fieldset %legend Personal projects: @@ -79,7 +87,8 @@ %fieldset %legend SSH public keys: - %strong.right= link_to current_user.keys.count, keys_path + %span.right + = link_to pluralize(current_user.keys.count, 'key'), keys_path .padded = link_to "Add Public Key", new_key_path, class: "btn small" diff --git a/app/views/projects/_new_form.html.haml b/app/views/projects/_new_form.html.haml index 2d5f3923..5f7348d4 100644 --- a/app/views/projects/_new_form.html.haml +++ b/app/views/projects/_new_form.html.haml @@ -15,6 +15,20 @@ %span Namespace .input = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'chosen'} - %hr + %p.padded New projects are private by default. You choose who can see the project and commit to repository. + %hr + + - if current_user.can_create_group? + .clearfix + .input.light + Need a group for several dependent projects? + = link_to new_group_path, class: "btn very_small" do + Create a group + - if current_user.can_create_team? + .clearfix + .input.light + Want to share a project between team? + = link_to new_team_path, class: "btn very_small" do + Create a team diff --git a/app/views/projects/_project_head.html.haml b/app/views/projects/_project_head.html.haml index 94052650..cc215502 100644 --- a/app/views/projects/_project_head.html.haml +++ b/app/views/projects/_project_head.html.haml @@ -3,7 +3,7 @@ = link_to project_path(@project), class: "activities-tab tab" do %i.icon-home Show - = nav_link(controller: :team_members) do + = nav_link(controller: [:team_members, :teams]) do = link_to project_team_index_path(@project), class: "team-tab tab" do %i.icon-user Team diff --git a/app/views/projects/graph.html.haml b/app/views/projects/graph.html.haml index 4e0b0e36..72d9cb5e 100644 --- a/app/views/projects/graph.html.haml +++ b/app/views/projects/graph.html.haml @@ -1,5 +1,6 @@ %h3.page_title Project Network Graph %br + .graph_holder %h4 %small You can move around the graph by using the arrow keys. @@ -11,6 +12,6 @@ $(function(){ branch_graph = new BranchGraph($("#holder"), { url: '#{url_for controller: 'projects', action: 'graph', format: :json}', - commit_url: '#{url_for controller: 'projects', action: 'show'}/commits/%s' + commit_url: '#{project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")}' }); }); diff --git a/app/views/projects/teams/available.html.haml b/app/views/projects/teams/available.html.haml new file mode 100644 index 00000000..814e216d --- /dev/null +++ b/app/views/projects/teams/available.html.haml @@ -0,0 +1,22 @@ += render "projects/project_head" + +%h3.page_title + = "Assign project to team of users" +%hr +%p.slead + Read more about assign to team of users #{link_to "here", '#', class: 'vlink'}. += form_tag assign_project_teams_path(@project), method: 'post' do + %p.slead Choose Team of users you want to assign: + .padded + = label_tag :team_id, "Team" + .input= select_tag(:team_id, options_from_collection_for_select(@teams, :id, :name), prompt: "Select team", class: "chosen xxlarge", required: true) + %p.slead Choose greatest user acces in team you want to assign: + .padded + = label_tag :team_ids, "Permission" + .input= select_tag :greatest_project_access, options_for_select(UserTeam.access_roles), {class: "project-access-select chosen span3" } + + + .actions + = submit_tag 'Assign', class: "btn save-btn" + = link_to "Cancel", project_team_index_path(@project), class: "btn cancel-btn" + diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml new file mode 100644 index 00000000..3fe17dce --- /dev/null +++ b/app/views/search/_filter.html.haml @@ -0,0 +1,24 @@ +%fieldset + %legend Groups: + %ul.nav.nav-pills.nav-stacked + %li{class: ("active" if params[:group_id].blank?)} + = link_to search_path(group_id: nil, search: params[:search]) do + Any + - current_user.authorized_groups.each do |group| + %li{class: ("active" if params[:group_id] == group.id.to_s)} + = link_to search_path(group_id: group.id, search: params[:search]) do + = group.name + +%fieldset + %legend Projects: + %ul.nav.nav-pills.nav-stacked + %li{class: ("active" if params[:project_id].blank?)} + = link_to search_path(project_id: nil, search: params[:search]) do + Any + - current_user.authorized_projects.each do |project| + %li{class: ("active" if params[:project_id] == project.id.to_s)} + = link_to search_path(project_id: project.id, search: params[:search]) do + = project.name_with_namespace + += hidden_field_tag :group_id, params[:group_id] += hidden_field_tag :project_id, params[:project_id] diff --git a/app/views/search/_result.html.haml b/app/views/search/_result.html.haml index 79bed4f7..bfa46075 100644 --- a/app/views/search/_result.html.haml +++ b/app/views/search/_result.html.haml @@ -1,83 +1,38 @@ -%br -%h3.page_title - Search results - %span.cgray (#{@projects.count + @merge_requests.count + @issues.count + @wiki_pages.count}) -%hr +%fieldset + %legend + Search results + %span.cgray (#{@projects.count + @merge_requests.count + @issues.count + @wiki_pages.count}) .search_results - .row - .span6 - %table - %thead - %tr - %th Projects - %tbody - - @projects.each do |project| - %tr - %td - = link_to project do - %strong.term= project.name_with_namespace - %small.cgray - last activity at - = project.last_activity_date.stamp("Aug 25, 2011") - - if @projects.blank? - %tr - %td - %h4.nothing_here_message No Projects - %br - %table - %thead - %tr - %th Merge Requests - %tbody - - @merge_requests.each do |merge_request| - %tr - %td - = link_to [merge_request.project, merge_request] do - %span.badge.badge-info ##{merge_request.id} - – - %strong.term= truncate merge_request.title, length: 50 - %strong.right - %span.label= merge_request.project.name - - if @merge_requests.blank? - %tr - %td - %h4.nothing_here_message No Merge Requests - .span6 - %table - %thead - %tr - %th Issues - %tbody - - @issues.each do |issue| - %tr - %td - = link_to [issue.project, issue] do - %span.badge.badge-info ##{issue.id} - – - %strong.term= truncate issue.title, length: 40 - %strong.right - %span.label= issue.project.name - - if @issues.blank? - %tr - %td - %h4.nothing_here_message No Issues - .span6 - %table - %thead - %tr - %th Wiki - %tbody - - @wiki_pages.each do |wiki_page| - %tr - %td - = link_to project_wiki_path(wiki_page.project, wiki_page) do - %strong.term= truncate wiki_page.title, length: 40 - %strong.right - %span.label= wiki_page.project.name - - if @wiki_pages.blank? - %tr - %td - %h4.nothing_here_message No wiki pages + %ul.well-list + - @projects.each do |project| + %li + project: + = link_to project do + %strong.term= project.name_with_namespace + - @merge_requests.each do |merge_request| + %li + merge request: + = link_to [merge_request.project, merge_request] do + %span ##{merge_request.id} + %strong.term + = truncate merge_request.title, length: 50 + %span.light (#{merge_request.project.name_with_namespace}) + - @issues.each do |issue| + %li + issue: + = link_to [issue.project, issue] do + %span ##{issue.id} + %strong.term + = truncate issue.title, length: 50 + %span.light (#{issue.project.name_with_namespace}) + - @wiki_pages.each do |wiki_page| + %li + wiki: + = link_to project_wiki_path(wiki_page.project, wiki_page) do + %strong.term + = truncate wiki_page.title, length: 50 + %span.light (#{wiki_page.project.name_with_namespace}) + :javascript $(function() { $(".search_results .term").highlight("#{escape_javascript(params[:search])}"); diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index aa0d6d70..22e1ae50 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,9 +1,15 @@ = form_tag search_path, method: :get, class: 'form-inline' do |f| - .padded + .search-holder = label_tag :search do - %strong Looking for + %span Looking for .input = search_field_tag :search, params[:search], placeholder: "issue 143", class: "input-xxlarge search-text-input", id: "dashboard_search" = submit_tag 'Search', class: "btn primary wide" -- if params[:search].present? - = render 'search/result' + .clearfix + .row + .span3 + = render 'filter', f: f + .span9 + .results + - if params[:search].present? + = render 'search/result' diff --git a/app/views/team_members/_form.html.haml b/app/views/team_members/_form.html.haml index a963e462..f9ee49db 100644 --- a/app/views/team_members/_form.html.haml +++ b/app/views/team_members/_form.html.haml @@ -1,11 +1,11 @@ %h3.page_title = "New Team member(s)" %hr -= form_for @team_member, as: :team_member, url: project_team_members_path(@project, @team_member) do |f| - -if @team_member.errors.any? += form_for @user_project_relation, as: :team_member, url: project_team_members_path(@project) do |f| + -if @user_project_relation.errors.any? .alert-message.block-message.error %ul - - @team_member.errors.full_messages.each do |msg| + - @user_project_relation.errors.full_messages.each do |msg| %li= msg %h6 1. Choose people you want in the team @@ -16,7 +16,7 @@ %h6 2. Set access level for them .clearfix = f.label :project_access, "Project Access" - .input= select_tag :project_access, options_for_select(Project.access_options, @team_member.project_access), class: "project-access-select chosen" + .input= select_tag :project_access, options_for_select(Project.access_options, @user_project_relation.project_access), class: "project-access-select chosen" .actions = f.submit 'Save', class: "btn save-btn" diff --git a/app/views/team_members/_show.html.haml b/app/views/team_members/_show.html.haml index 8082f47f..52992033 100644 --- a/app/views/team_members/_show.html.haml +++ b/app/views/team_members/_show.html.haml @@ -1,11 +1,11 @@ - user = member.user - allow_admin = can? current_user, :admin_project, @project -%li{id: dom_id(member), class: "team_member_row user_#{user.id}"} +%li{id: dom_id(user), class: "team_member_row user_#{user.id}"} .row .span6 - = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do + = link_to project_team_member_path(@project, user), title: user.name, class: "dark" do = image_tag gravatar_icon(user.email, 40), class: "avatar s32" - = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do + = link_to project_team_member_path(@project, user), title: user.name, class: "dark" do %strong= truncate(user.name, lenght: 40) %br %small.cgray= user.email @@ -13,7 +13,7 @@ .span5.right - if allow_admin .left - = form_for(member, as: :team_member, url: project_team_member_path(@project, member)) do |f| + = form_for(member, as: :team_member, url: project_team_member_path(@project, member.user)) do |f| = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2" .right - if current_user == user @@ -23,6 +23,6 @@ - elsif user.blocked %span.btn.disabled.blocked Blocked - elsif allow_admin - = 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 + = link_to project_team_member_path(@project, user), confirm: remove_from_project_team_message(@project, user), method: :delete, class: "very_small btn danger" do %i.icon-minus.icon-white diff --git a/app/views/team_members/_show_team.html.haml b/app/views/team_members/_show_team.html.haml new file mode 100644 index 00000000..da0262ef --- /dev/null +++ b/app/views/team_members/_show_team.html.haml @@ -0,0 +1,15 @@ +- team = team_rel.user_team +- allow_admin = can? current_user, :admin_team_member, @project +%li{id: dom_id(team), class: "user_team_row team_#{team.id}"} + .row + .span6 + %strong= link_to team.name, team_path(team), title: team.name, class: "dark" + %br + %small.cgray Members: #{team.members.count} + + .span5.right + .right + - if allow_admin + .left + = link_to resign_project_team_path(@project, team), method: :delete, confirm: "Are you shure?", class: "btn danger small" do + %i.icon-minus.icon-white diff --git a/app/views/team_members/_teams.html.haml b/app/views/team_members/_teams.html.haml new file mode 100644 index 00000000..156fdd1b --- /dev/null +++ b/app/views/team_members/_teams.html.haml @@ -0,0 +1,16 @@ +- grouper_project_teams(@project).each do |access, teams| + .ui-box + %h5.title + = UserTeam.access_roles.key(access).pluralize + %small= teams.size + %ul.well-list + - teams.sort_by(&:team_name).each do |tofr| + = render(partial: 'team_members/show_team', locals: {team_rel: tofr}) + + +:javascript + $(function(){ + $('.repo-access-select, .project-access-select').live("change", function() { + $(this.form).submit(); + }); + }) diff --git a/app/views/team_members/create.js.haml b/app/views/team_members/create.js.haml index d5ae5d0c..b7dff35a 100644 --- a/app/views/team_members/create.js.haml +++ b/app/views/team_members/create.js.haml @@ -1,4 +1,4 @@ -- if @team_member.valid? +- if @user_project_relation.valid? :plain $("#new_team_member").hide("slide", { direction: "right" }, 150, function(){ $("#team-table").show("slide", { direction: "left" }, 150, function() { diff --git a/app/views/team_members/import.html.haml b/app/views/team_members/import.html.haml index de82f416..135db946 100644 --- a/app/views/team_members/import.html.haml +++ b/app/views/team_members/import.html.haml @@ -4,7 +4,7 @@ = "Import team from another project" %hr %p.slead - Read more about team import #{link_to "here", '#', class: 'vlink'}. + Read more about project team import #{link_to "here", '#', class: 'vlink'}. = form_tag apply_import_project_team_members_path(@project), method: 'post' do %p.slead Choose project you want to use as team source: .padded diff --git a/app/views/team_members/index.html.haml b/app/views/team_members/index.html.haml index e413c81b..6425302b 100644 --- a/app/views/team_members/index.html.haml +++ b/app/views/team_members/index.html.haml @@ -1,7 +1,7 @@ = render "projects/project_head" %h3.page_title Team Members - (#{@project.users_projects.count}) + (#{@project.users.count}) %small Read more about project permissions %strong= link_to "here", help_permissions_path, class: "vlink" @@ -10,11 +10,24 @@ %span.right = link_to import_project_team_members_path(@project), class: "btn small grouped", title: "Import team from another project" do Import team from another project + = link_to available_project_teams_path(@project), class: "btn small grouped", title: "Assign project to team of users" do + Assign project to Team of users = link_to new_project_team_member_path(@project), class: "btn success small grouped", title: "New Team Member" do New Team Member -%hr +%hr .clearfix %div.team-table = render partial: "team_members/team", locals: {project: @project} + + +%h3.page_title + Assigned teams + (#{@project.user_teams.count}) + +%hr + +.clearfix +%div.team-table + = render partial: "team_members/teams", locals: {project: @project} diff --git a/app/views/team_members/show.html.haml b/app/views/team_members/show.html.haml index 4008e8bd..a6a7152e 100644 --- a/app/views/team_members/show.html.haml +++ b/app/views/team_members/show.html.haml @@ -1,14 +1,13 @@ - allow_admin = can? current_user, :admin_project, @project -- user = @team_member.user .team_member_show - if can? current_user, :admin_project, @project - = 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" + = link_to 'Remove from team', project_team_member_path(@project, @member), confirm: 'Are you sure?', method: :delete, class: "right btn danger" .profile_avatar_holder - = image_tag gravatar_icon(user.email, 60), class: "borders" + = image_tag gravatar_icon(@member.email, 60), class: "borders" %h3.page_title - = user.name - %small (@#{user.username}) + = @member.name + %small (@#{@member.username}) %hr .back_link @@ -21,34 +20,34 @@ %table.lite %tr %td Email - %td= mail_to user.email + %td= mail_to @member.email %tr %td Skype - %td= user.skype - - unless user.linkedin.blank? + %td= @member.skype + - unless @member.linkedin.blank? %tr %td LinkedIn - %td= user.linkedin - - unless user.twitter.blank? + %td= @member.linkedin + - unless @member.twitter.blank? %tr %td Twitter - %td= user.twitter - - unless user.bio.blank? + %td= @member.twitter + - unless @member.bio.blank? %tr %td Bio - %td= user.bio + %td= @member.bio .span6 %table.lite %tr %td Member since - %td= @team_member.created_at.stamp("Aug 21, 2011") + %td= @user_project_relation.created_at.stamp("Aug 21, 2011") %tr %td Project Access: %small (#{link_to "read more", help_permissions_path, class: "vlink"}) %td - = form_for(@team_member, as: :team_member, url: project_team_member_path(@project, @team_member)) do |f| - = f.select :project_access, options_for_select(Project.access_options, @team_member.project_access), {}, class: "project-access-select", disabled: !allow_admin + = form_for(@user_project_relation, as: :team_member, url: project_team_member_path(@project, @member)) do |f| + = f.select :project_access, options_for_select(Project.access_options, @user_project_relation.project_access), {}, class: "project-access-select", disabled: !allow_admin %hr = render @events :javascript diff --git a/app/views/team_members/update.js.haml b/app/views/team_members/update.js.haml index 6d7f8816..c68fe957 100644 --- a/app/views/team_members/update.js.haml +++ b/app/views/team_members/update.js.haml @@ -1,6 +1,6 @@ -- if @team_member.valid? +- if @user_project_relation.valid? :plain - $("##{dom_id(@team_member)}").effect("highlight", {color: "#529214"}, 1000);; + $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#529214"}, 1000);; - else :plain - $("##{dom_id(@team_member)}").effect("highlight", {color: "#D12F19"}, 1000);; + $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#D12F19"}, 1000);; diff --git a/app/views/teams/_filter.html.haml b/app/views/teams/_filter.html.haml new file mode 100644 index 00000000..8e358319 --- /dev/null +++ b/app/views/teams/_filter.html.haml @@ -0,0 +1,33 @@ += form_tag team_filter_path(entity), method: 'get' do + %fieldset.dashboard-search-filter + = search_field_tag "search", params[:search], { placeholder: 'Search', class: 'search-text-input' } + = button_tag type: 'submit', class: 'btn' do + %i.icon-search + + %fieldset + %legend Status: + %ul.nav.nav-pills.nav-stacked + %li{class: ("active" if !params[:status])} + = link_to team_filter_path(entity, status: nil) do + Open + %li{class: ("active" if params[:status] == 'closed')} + = link_to team_filter_path(entity, status: 'closed') do + Closed + %li{class: ("active" if params[:status] == 'all')} + = link_to team_filter_path(entity, status: 'all') do + All + + %fieldset + %legend Projects: + %ul.nav.nav-pills.nav-stacked + - @projects.each do |project| + - unless entities_per_project(project, entity).zero? + %li{class: ("active" if params[:project_id] == project.id.to_s)} + = link_to team_filter_path(entity, project_id: project.id) do + = project.name_with_namespace + %small.right= entities_per_project(project, entity) + + %fieldset + %hr + = link_to "Reset", team_filter_path(entity), class: 'btn right' + diff --git a/app/views/teams/_projects.html.haml b/app/views/teams/_projects.html.haml new file mode 100644 index 00000000..4d99d5c2 --- /dev/null +++ b/app/views/teams/_projects.html.haml @@ -0,0 +1,22 @@ +.projects_box + %h5.title + Projects + %small + (#{projects.count}) + - if can? current_user, :manage_user_team, @team + %span.right + = link_to new_team_project_path(@team), class: "btn very_small info" do + %i.icon-plus + Assign Project + %ul.well-list + - if projects.blank? + %p.nothing_here_message This team has no projects yet + - projects.each do |project| + %li + = link_to project_path(project), class: dom_class(project) do + %strong.well-title= truncate(project.name, length: 25) + %span.arrow + → + %span.last_activity + %strong Last activity: + %span= project_last_activity(project) diff --git a/app/views/teams/edit.html.haml b/app/views/teams/edit.html.haml new file mode 100644 index 00000000..60535330 --- /dev/null +++ b/app/views/teams/edit.html.haml @@ -0,0 +1,22 @@ +%h3.page_title= "Edit Team #{@team.name}" +%hr += form_for @team, url: teams_path do |f| + - if @team.errors.any? + .alert-message.block-message.error + %span= @team.errors.full_messages.first + .clearfix + = f.label :name do + Team name is + .input + = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left" + + .clearfix + = f.label :path do + Team path is + .input + = f.text_field :path, placeholder: "opensource", class: "xxlarge left" + .clearfix + .input.span3.center + = f.submit 'Save team changes', class: "btn primary" + .input.span3.center + = link_to 'Delete team', team_path(@team), method: :delete, confirm: "You are shure?", class: "btn danger" diff --git a/app/views/teams/issues.html.haml b/app/views/teams/issues.html.haml new file mode 100644 index 00000000..4481e2ea --- /dev/null +++ b/app/views/teams/issues.html.haml @@ -0,0 +1,23 @@ +%h3.page_title + Issues + %small (in Team projects assigned to Team members) + %small.right #{@issues.total_count} issues + +%hr +.row + .span3 + = render 'filter', entity: 'issue' + .span9 + - if @issues.any? + - @issues.group_by(&:project).each do |group| + %div.ui-box + - @project = group[0] + %h5.title + = link_to_project @project + %ul.well-list.issues_table + - group[1].each do |issue| + = render(partial: 'issues/show', locals: {issue: issue}) + %hr + = paginate @issues, theme: "gitlab" + - else + %p.nothing_here_message Nothing to show here diff --git a/app/views/teams/members/_form.html.haml b/app/views/teams/members/_form.html.haml new file mode 100644 index 00000000..b75d788a --- /dev/null +++ b/app/views/teams/members/_form.html.haml @@ -0,0 +1,20 @@ += form_tag admin_team_member_path(@team, @member), method: :put do + -if @member.errors.any? + .alert-message.block-message.error + %ul + - @member.errors.full_messages.each do |msg| + %li= msg + + .clearfix + %label Default access for Team projects: + .input + = select_tag :default_project_access, options_for_select(UserTeam.access_roles, @team.default_projects_access(@member)), class: "project-access-select chosen span3" + .clearfix + %label Team admin? + .input + = check_box_tag :group_admin, true, @team.admin?(@member) + + %br + .actions + = submit_tag 'Save', class: "btn primary" + = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/teams/members/_show.html.haml b/app/views/teams/members/_show.html.haml new file mode 100644 index 00000000..740d5a49 --- /dev/null +++ b/app/views/teams/members/_show.html.haml @@ -0,0 +1,31 @@ +- user = member.user +- allow_admin = can? current_user, :manage_user_team, @team +%li{id: dom_id(member), class: "team_member_row user_#{user.id}"} + .row + .span5 + = link_to user_path(user.username), title: user.name, class: "dark" do + = image_tag gravatar_icon(user.email, 40), class: "avatar s32" + = link_to user_path(user.username), title: user.name, class: "dark" do + %strong= truncate(user.name, lenght: 40) + %br + %small.cgray= user.email + + .span6.right + - if allow_admin + .left.span2 + = form_for(member, as: :team_member, url: team_member_path(@team, user)) do |f| + = f.select :permission, options_for_select(UsersProject.access_roles, @team.default_projects_access(user)), {}, class: "medium project-access-select span2" + .left.span2 + %span + Admin access + = check_box_tag :group_admin, true, @team.admin?(user) + .right + - if current_user == user + %span.btn.disabled This is you! + - if @team.owner == user + %span.btn.disabled.success Owner + - elsif user.blocked + %span.btn.disabled.blocked Blocked + - elsif allow_admin + = link_to team_member_path(@team, user), confirm: remove_from_user_team_message(@team, user), method: :delete, class: "very_small btn danger" do + %i.icon-minus.icon-white diff --git a/app/views/teams/members/_team.html.haml b/app/views/teams/members/_team.html.haml new file mode 100644 index 00000000..d8afc1fa --- /dev/null +++ b/app/views/teams/members/_team.html.haml @@ -0,0 +1,16 @@ +- grouped_user_team_members(@team).each do |access, members| + .ui-box + %h5.title + = Project.access_options.key(access).pluralize + %small= members.size + %ul.well-list + - members.sort_by(&:user_name).each do |up| + = render(partial: 'teams/members/show', locals: {member: up}) + + +:javascript + $(function(){ + $('.repo-access-select, .project-access-select').live("change", function() { + $(this.form).submit(); + }); + }) diff --git a/app/views/teams/members/edit.html.haml b/app/views/teams/members/edit.html.haml new file mode 100644 index 00000000..37588049 --- /dev/null +++ b/app/views/teams/members/edit.html.haml @@ -0,0 +1,16 @@ +%h3.page_title + Edit access #{@member.name} in #{@team.name} team + +%hr +%table.zebra-striped + %tr + %td User: + %td= @member.name + %tr + %td Team: + %td= @team.name + %tr + %td Since: + %td= member_since(@team, @member).stamp("Nov 11, 2010") + += render 'form' diff --git a/app/views/teams/members/index.html.haml b/app/views/teams/members/index.html.haml new file mode 100644 index 00000000..90fa0aef --- /dev/null +++ b/app/views/teams/members/index.html.haml @@ -0,0 +1,17 @@ +%h3.page_title + Team Members + (#{@members.count}) + %small + Read more about project permissions + %strong= link_to "here", help_permissions_path, class: "vlink" + + - if can? current_user, :manage_user_team, @team + %span.right + = link_to new_team_member_path(@team), class: "btn success small grouped", title: "New Team Member" do + New Team Member +%hr + + +.clearfix +%div.team-table + = render partial: "teams/members/team", locals: {project: @team} diff --git a/app/views/teams/members/new.html.haml b/app/views/teams/members/new.html.haml new file mode 100644 index 00000000..274cdbad --- /dev/null +++ b/app/views/teams/members/new.html.haml @@ -0,0 +1,28 @@ +%h3.page_title + Team: #{@team.name} + +%fieldset + %legend Members (#{@team.members.count}) + = form_tag team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do + %table#members_list + %thead + %tr + %th User name + %th Default project access + %th Team access + %th + - @team.members.each do |member| + %tr.member + %td + = member.name + %small= "(#{member.email})" + %td= @team.human_default_projects_access(member) + %td= @team.admin?(member) ? "Admin" : "Member" + %td + %tr + %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_email), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5' + %td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" } + %td + %span= check_box_tag :group_admin + %span Admin? + %td= submit_tag 'Add', class: "btn primary", id: :add_members_to_team diff --git a/app/views/teams/members/show.html.haml b/app/views/teams/members/show.html.haml new file mode 100644 index 00000000..4008e8bd --- /dev/null +++ b/app/views/teams/members/show.html.haml @@ -0,0 +1,60 @@ +- allow_admin = can? current_user, :admin_project, @project +- user = @team_member.user + +.team_member_show + - if can? current_user, :admin_project, @project + = 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" + .profile_avatar_holder + = image_tag gravatar_icon(user.email, 60), class: "borders" + %h3.page_title + = user.name + %small (@#{user.username}) + + %hr + .back_link + %br + = link_to project_team_index_path(@project), class: "" do + ← To team list + %br + .row + .span6 + %table.lite + %tr + %td Email + %td= mail_to user.email + %tr + %td Skype + %td= user.skype + - unless user.linkedin.blank? + %tr + %td LinkedIn + %td= user.linkedin + - unless user.twitter.blank? + %tr + %td Twitter + %td= user.twitter + - unless user.bio.blank? + %tr + %td Bio + %td= user.bio + .span6 + %table.lite + %tr + %td Member since + %td= @team_member.created_at.stamp("Aug 21, 2011") + %tr + %td + Project Access: + %small (#{link_to "read more", help_permissions_path, class: "vlink"}) + %td + = form_for(@team_member, as: :team_member, url: project_team_member_path(@project, @team_member)) do |f| + = f.select :project_access, options_for_select(Project.access_options, @team_member.project_access), {}, class: "project-access-select", disabled: !allow_admin + %hr + = render @events +:javascript + $(function(){ + $('.repo-access-select, .project-access-select').live("change", function() { + $(this.form).submit(); + }); + }) + diff --git a/app/views/teams/merge_requests.html.haml b/app/views/teams/merge_requests.html.haml new file mode 100644 index 00000000..c9af529e --- /dev/null +++ b/app/views/teams/merge_requests.html.haml @@ -0,0 +1,24 @@ +%h3.page_title + Merge Requests + %small (authored by or assigned to Team members) + %small.right #{@merge_requests.total_count} merge requests + +%hr +.row + .span3 + = render 'filter', entity: 'merge_request' + .span9 + - if @merge_requests.any? + - @merge_requests.group_by(&:project).each do |group| + .ui-box + - @project = group[0] + %h5.title + = link_to_project @project + %ul.well-list + - group[1].each do |merge_request| + = render(partial: 'merge_requests/merge_request', locals: {merge_request: merge_request}) + %hr + = paginate @merge_requests, theme: "gitlab" + + - else + %h3.nothing_here_message Nothing to show here diff --git a/app/views/teams/new.html.haml b/app/views/teams/new.html.haml new file mode 100644 index 00000000..12695f2b --- /dev/null +++ b/app/views/teams/new.html.haml @@ -0,0 +1,19 @@ +%h3.page_title New Team +%hr += form_for @team, url: teams_path do |f| + - if @team.errors.any? + .alert-message.block-message.error + %span= @team.errors.full_messages.first + .clearfix + = f.label :name do + Team name is + .input + = f.text_field :name, placeholder: "Ex. Ruby Developers", class: "xxlarge left" +   + = f.submit 'Create team', class: "btn primary" + %hr + .padded + %ul + %li All created teams are public (users can view who enter into team and which project are assigned for this team) + %li People within a team see only projects they have access to + %li You will be able to assign existing projects for team diff --git a/app/views/teams/projects/_form.html.haml b/app/views/teams/projects/_form.html.haml new file mode 100644 index 00000000..3749dbc4 --- /dev/null +++ b/app/views/teams/projects/_form.html.haml @@ -0,0 +1,16 @@ += form_tag team_project_path(@team, @project), method: :put do + -if @project.errors.any? + .alert-message.block-message.error + %ul + - @project.errors.full_messages.each do |msg| + %li= msg + + .clearfix + %label Max access for Team members: + .input + = select_tag :greatest_project_access, options_for_select(UserTeam.access_roles, @team.max_project_access(@project)), class: "project-access-select chosen span3" + + %br + .actions + = submit_tag 'Save', class: "btn primary" + = link_to 'Cancel', :back, class: "btn" diff --git a/app/views/teams/projects/edit.html.haml b/app/views/teams/projects/edit.html.haml new file mode 100644 index 00000000..b91a4982 --- /dev/null +++ b/app/views/teams/projects/edit.html.haml @@ -0,0 +1,16 @@ +%h3 + Edit max access in #{@project.name} for #{@team.name} team + +%hr +%table.zebra-striped + %tr + %td Project: + %td= @project.name + %tr + %td Team: + %td= @team.name + %tr + %td Since: + %td= assigned_since(@team, @project).stamp("Nov 11, 2010") + += render 'form' diff --git a/app/views/teams/projects/index.html.haml b/app/views/teams/projects/index.html.haml new file mode 100644 index 00000000..493fc2c5 --- /dev/null +++ b/app/views/teams/projects/index.html.haml @@ -0,0 +1,36 @@ +%h3.page_title + Assigned projects (#{@team.projects.count}) + %small + Read more about project permissions + %strong= link_to "here", help_permissions_path, class: "vlink" + + - if current_user.can?(:manage_user_team, @team) && @avaliable_projects.any? + %span.right + = link_to new_team_project_path(@team), class: "btn success small grouped", title: "New Team Member" do + Assign project to Team + +%hr + +- if @team.projects.present? + %table.projects-table + %thead + %tr + %th Project name + %th Max access + - if current_user.can?(:admin_user_team, @team) + %th.span3 + + - @team.projects.each do |project| + %tr.project + %td + = link_to project.name_with_namespace, project_path(project) + %td + %span= @team.human_max_project_access(project) + + - if current_user.can?(:admin_user_team, @team) + %td.bgred + = link_to 'Edit max access', edit_team_project_path(@team, project), class: "btn small" + = 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" + +- else + %p.nothing_here_message This team has no projects yet diff --git a/app/views/teams/projects/new.html.haml b/app/views/teams/projects/new.html.haml new file mode 100644 index 00000000..d57f56b2 --- /dev/null +++ b/app/views/teams/projects/new.html.haml @@ -0,0 +1,23 @@ +%h3.page_title + Team: #{@team.name} + +%fieldset + %legend Projects (#{@team.projects.count}) + = form_tag team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do + %table#projects_list + %thead + %tr + %th Project name + %th Max access + %th + - @team.projects.each do |project| + %tr.project + %td + = link_to project.name_with_namespace, team_project_path(@team, project) + %td + %span= @team.human_max_project_access(project) + %td + %tr + %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' + %td= select_tag :greatest_project_access, options_for_select(UserTeam.access_roles), {class: "project-access-select chosen span3" } + %td= submit_tag 'Add', class: "btn primary", id: :assign_projects_to_team diff --git a/app/views/teams/show.html.haml b/app/views/teams/show.html.haml new file mode 100644 index 00000000..d9257ab0 --- /dev/null +++ b/app/views/teams/show.html.haml @@ -0,0 +1,28 @@ +.projects + .activities.span8 + = link_to dashboard_path, class: 'btn very_small' do + ← To dashboard +   + %span.cgray Events and projects are filtered in scope of team + %hr + - if @events.any? + .content_list + - else + %p.nothing_here_message Projects activity will be displayed here + .loading.hide + .side.span4 + = render "projects", projects: @projects + %div + %span.rss-icon + = link_to dashboard_path(:atom, { private_token: current_user.private_token }) do + = image_tag "rss_ui.png", title: "feed" + %strong News Feed + + %hr + .gitlab-promo + = link_to "Homepage", "http://gitlabhq.com" + = link_to "Blog", "http://blog.gitlabhq.com" + = link_to "@gitlabhq", "https://twitter.com/gitlabhq" + +:javascript + $(function(){ Pager.init(20, true); }); diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml new file mode 100644 index 00000000..ab6538f0 --- /dev/null +++ b/app/views/users/_profile.html.haml @@ -0,0 +1,23 @@ +.ui-box + %h5.title + Profile + %ul.well-list + %li + %strong Email + %span.right= mail_to @user.email + - unless @user.skype.blank? + %li + %strong Skype + %span.right= @user.skype + - unless @user.linkedin.blank? + %li + %strong LinkedIn + %span.right= @user.linkedin + - unless @user.twitter.blank? + %li + %strong Twitter + %span.right= @user.twitter + - unless @user.bio.blank? + %li + %strong Bio + %span.right= @user.bio diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 2a77c6bf..64482628 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -3,6 +3,11 @@ %h3.page_title = image_tag gravatar_icon(@user.email, 90), class: "avatar s90" = @user.name + - if @user == current_user + .right + = link_to profile_path, class: 'btn small' do + %i.icon-edit + Edit Profile %br %small @#{@user.username} %br @@ -12,26 +17,5 @@ %h5 Recent events = render @events .span4 - .ui-box - %h5.title Profile - %ul.well-list - %li - %strong Email - %span.right= mail_to @user.email - - unless @user.skype.blank? - %li - %strong Skype - %span.right= @user.skype - - unless @user.linkedin.blank? - %li - %strong LinkedIn - %span.right= @user.linkedin - - unless @user.twitter.blank? - %li - %strong Twitter - %span.right= @user.twitter - - unless @user.bio.blank? - %li - %strong Bio - %span.right= @user.bio + = render 'profile' = render 'projects' diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb new file mode 100644 index 00000000..9f9b9b1d --- /dev/null +++ b/app/workers/project_web_hook_worker.rb @@ -0,0 +1,9 @@ +class ProjectWebHookWorker + include Sidekiq::Worker + + sidekiq_options queue: :project_web_hook + + def perform(hook_id, data) + WebHook.find(hook_id).execute data + end +end diff --git a/config/routes.rb b/config/routes.rb index c7e81b6f..7ffa081a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,7 +21,7 @@ Gitlab::Application.routes.draw do project_root: Gitlab.config.gitolite.repos_path, upload_pack: Gitlab.config.gitolite.upload_pack, receive_pack: Gitlab.config.gitolite.receive_pack - }), at: '/', constraints: lambda { |request| /[-\/\w\.-]+\.git\//.match(request.path_info) } + }), at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) } # # Help @@ -49,13 +49,14 @@ Gitlab::Application.routes.draw do # Admin Area # namespace :admin do - resources :users do + resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do member do put :team_update put :block put :unblock end end + resources :groups, constraints: { id: /[^\/]+/ } do member do put :project_update @@ -63,18 +64,31 @@ Gitlab::Application.routes.draw do delete :remove_project end end + + resources :teams, constraints: { id: /[^\/]+/ } do + scope module: :teams do + resources :members, only: [:edit, :update, :destroy, :new, :create] + resources :projects, only: [:edit, :update, :destroy, :new, :create], constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } + end + end + + resources :hooks, only: [:index, :create, :destroy] do + get :test + end + + resource :logs, only: [:show] + resource :resque, controller: 'resque', only: [:show] + resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, except: [:new, :create] do member do get :team put :team_update end + scope module: :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do + resources :members, only: [:edit, :update, :destroy] + end end - resources :team_members, only: [:edit, :update, :destroy] - resources :hooks, only: [:index, :create, :destroy] do - get :test - end - resource :logs, only: [:show] - resource :resque, controller: 'resque', only: [:show] + root to: "dashboard#index" end @@ -104,15 +118,18 @@ Gitlab::Application.routes.draw do # # Dashboard Area # - get "dashboard" => "dashboard#index" - get "dashboard/issues" => "dashboard#issues" - get "dashboard/merge_requests" => "dashboard#merge_requests" - + resource :dashboard, controller: "dashboard" do + member do + get :projects + get :issues + get :merge_requests + end + end # # Groups Area # - resources :groups, constraints: { id: /[^\/]+/ }, only: [:show] do + resources :groups, constraints: { id: /[^\/]+/ }, only: [:show, :new, :create] do member do get :issues get :merge_requests @@ -122,6 +139,20 @@ Gitlab::Application.routes.draw do end end + # + # Teams Area + # + resources :teams, constraints: { id: /[^\/]+/ } do + member do + get :issues + get :merge_requests + end + scope module: :teams do + resources :members, only: [:index, :new, :create, :edit, :update, :destroy] + resources :projects, only: [:index, :new, :create, :edit, :update, :destroy], constraints: { id: /[a-zA-Z.0-9_\-\/]+/ } + end + end + resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create] devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations } @@ -238,6 +269,18 @@ Gitlab::Application.routes.draw do end end + scope module: :projects do + resources :teams, only: [] do + collection do + get :available + post :assign + end + member do + delete :resign + end + end + end + resources :notes, only: [:index, :create, :destroy] do collection do post :preview @@ -245,5 +288,5 @@ Gitlab::Application.routes.draw do end end - root to: "dashboard#index" + root to: "dashboard#show" end diff --git a/db/migrate/20121219183753_create_user_teams.rb b/db/migrate/20121219183753_create_user_teams.rb new file mode 100644 index 00000000..65c4d053 --- /dev/null +++ b/db/migrate/20121219183753_create_user_teams.rb @@ -0,0 +1,11 @@ +class CreateUserTeams < ActiveRecord::Migration + def change + create_table :user_teams do |t| + t.string :name + t.string :path + t.integer :owner_id + + t.timestamps + end + end +end diff --git a/db/migrate/20121220064104_create_user_team_project_relationships.rb b/db/migrate/20121220064104_create_user_team_project_relationships.rb new file mode 100644 index 00000000..8eb654c8 --- /dev/null +++ b/db/migrate/20121220064104_create_user_team_project_relationships.rb @@ -0,0 +1,11 @@ +class CreateUserTeamProjectRelationships < ActiveRecord::Migration + def change + create_table :user_team_project_relationships do |t| + t.integer :project_id + t.integer :user_team_id + t.integer :greatest_access + + t.timestamps + end + end +end diff --git a/db/migrate/20121220064453_create_user_team_user_relationships.rb b/db/migrate/20121220064453_create_user_team_user_relationships.rb new file mode 100644 index 00000000..7783b0ae --- /dev/null +++ b/db/migrate/20121220064453_create_user_team_user_relationships.rb @@ -0,0 +1,12 @@ +class CreateUserTeamUserRelationships < ActiveRecord::Migration + def change + create_table :user_team_user_relationships do |t| + t.integer :user_id + t.integer :user_team_id + t.boolean :group_admin + t.integer :permission + + t.timestamps + end + end +end diff --git a/db/migrate/20130125090214_add_user_permissions.rb b/db/migrate/20130125090214_add_user_permissions.rb new file mode 100644 index 00000000..38b5f439 --- /dev/null +++ b/db/migrate/20130125090214_add_user_permissions.rb @@ -0,0 +1,11 @@ +class AddUserPermissions < ActiveRecord::Migration + def up + add_column :users, :can_create_group, :boolean, default: true, null: false + add_column :users, :can_create_team, :boolean, default: true, null: false + end + + def down + remove_column :users, :can_create_group + remove_column :users, :can_create_team + end +end diff --git a/db/schema.rb b/db/schema.rb index 4b3a2243..144f4a57 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20130110172407) do +ActiveRecord::Schema.define(:version => 20130125090214) do create_table "events", :force => true do |t| t.string "target_type" @@ -213,6 +213,31 @@ ActiveRecord::Schema.define(:version => 20130110172407) do t.string "name" end + create_table "user_team_project_relationships", :force => true do |t| + t.integer "project_id" + t.integer "user_team_id" + t.integer "greatest_access" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "user_team_user_relationships", :force => true do |t| + t.integer "user_id" + t.integer "user_team_id" + t.boolean "group_admin" + t.integer "permission" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "user_teams", :force => true do |t| + t.string "name" + t.string "path" + t.integer "owner_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "users", :force => true do |t| t.string "email", :default => "", :null => false t.string "encrypted_password", :default => "", :null => false @@ -242,6 +267,8 @@ ActiveRecord::Schema.define(:version => 20130110172407) do t.string "extern_uid" t.string "provider" t.string "username" + t.boolean "can_create_group", :default => true, :null => false + t.boolean "can_create_team", :default => true, :null => false end add_index "users", ["admin"], :name => "index_users_on_admin" diff --git a/features/admin/teams.feature b/features/admin/teams.feature new file mode 100644 index 00000000..6a15fddc --- /dev/null +++ b/features/admin/teams.feature @@ -0,0 +1,70 @@ +Feature: Admin Teams + Background: + Given I sign in as an admin + And Create gitlab user "John" + + Scenario: Create a team + When I visit admin teams page + And I click new team link + And submit form with new team info + Then I should be redirected to team page + And I should see newly created team + + Scenario: Add user to team + When I visit admin teams page + When I have clean "HardCoders" team + And I visit "HardCoders" team page + When I click to "Add members" link + When I select user "John" from user list as "Developer" + And submit form with new team member info + Then I should see "John" in teams members list as "Developer" + + Scenario: Assign team to existing project + When I visit admin teams page + When I have "HardCoders" team with "John" member with "Developer" role + When I have "Shop" project + And I visit "HardCoders" team page + Then I should see empty projects table + When I click to "Add projects" link + When I select project "Shop" with max access "Reporter" + And submit form with new team project info + Then I should see "Shop" project in projects list + When I visit "Shop" project admin page + Then I should see "John" user with role "Reporter" in team table + + Scenario: Add user to team with ptojects + When I visit admin teams page + When I have "HardCoders" team with "John" member with "Developer" role + And "HardCoders" team assigned to "Shop" project with "Developer" max role access + When I have gitlab user "Jimm" + And I visit "HardCoders" team page + Then I should see members table without "Jimm" member + When I click to "Add members" link + When I select user "Jimm" ub team members list as "Master" + And submit form with new team member info + Then I should see "Jimm" in teams members list as "Master" + + Scenario: Remove member from team + Given I have users team "HardCoders" + And gitlab user "John" is a member "HardCoders" team + And gitlab user "Jimm" is a member "HardCoders" team + And "HardCoders" team is assigned to "Shop" project + When I visit admin teams page + When I visit "HardCoders" team admin page + Then I shoould see "John" in members list + And I should see "Jimm" in members list + And I should see "Shop" in projects list + When I click on remove "Jimm" user link + Then I should be redirected to "HardCoders" team admin page + And I should not to see "Jimm" user in members list + + Scenario: Remove project from team + Given I have users team "HardCoders" + And gitlab user "John" is a member "HardCoders" team + And gitlab user "Jimm" is a member "HardCoders" team + And "HardCoders" team is assigned to "Shop" project + When I visit admin teams page + When I visit "HardCoders" team admin page + Then I should see "Shop" project in projects list + When I click on "Relegate" link on "Shop" project + Then I should see projects liston team page without "Shop" project diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 75984369..695148b5 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -16,12 +16,6 @@ Feature: Dashboard And I visit dashboard page Then I should see groups list - Scenario: I should see correct projects count - Given I have group with projects - And group has a projects that does not belongs to me - When I visit dashboard page - Then I should see 1 project at group list - Scenario: I should see last push widget Then I should see last push widget And I click "Create Merge Request" link diff --git a/features/dashboard/projects.feature b/features/dashboard/projects.feature new file mode 100644 index 00000000..17022dab --- /dev/null +++ b/features/dashboard/projects.feature @@ -0,0 +1,8 @@ +Feature: Dashboard + Background: + Given I sign in as a user + And I own project "Shop" + And I visit dashboard projects page + + Scenario: I should see issues list + Then I should see projects list diff --git a/features/group/create_group.feature b/features/group/create_group.feature new file mode 100644 index 00000000..b77f3599 --- /dev/null +++ b/features/group/create_group.feature @@ -0,0 +1,11 @@ +Feature: Groups + Background: + Given I sign in as a user + + Scenario: Create a group from dasboard + Given I have group with projects + And I visit dashboard page + When I click new group link + And submit form with new group info + Then I should be redirected to group page + And I should see newly created group diff --git a/features/steps/admin/admin_teams.rb b/features/steps/admin/admin_teams.rb new file mode 100644 index 00000000..5c66b24b --- /dev/null +++ b/features/steps/admin/admin_teams.rb @@ -0,0 +1,234 @@ +class AdminTeams < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedActiveTab + include SharedAdmin + + And 'I have own project' do + create :project + end + + And 'Create gitlab user "John"' do + @user = create(:user, :name => "John") + end + + And 'I click new team link' do + click_link "New Team" + end + + And 'submit form with new team info' do + fill_in 'user_team_name', with: 'gitlab' + click_button 'Create team' + end + + Then 'I should be redirected to team page' do + current_path.should == admin_team_path(UserTeam.last) + end + + And 'I should see newly created team' do + page.should have_content "Team: gitlab" + end + + When 'I visit admin teams page' do + visit admin_teams_path + end + + When 'I have clean "HardCoders" team' do + @team = create :user_team, name: "HardCoders", owner: current_user + end + + And 'I visit "HardCoders" team page' do + visit admin_team_path(UserTeam.find_by_name("HardCoders")) + end + + Then 'I should see only me in members table' do + members_list = find("#members_list .member") + members_list.should have_content(current_user.name) + members_list.should have_content(current_user.email) + end + + When 'I select user "John" from user list as "Developer"' do + @user ||= User.find_by_name("John") + within "#team_members" do + select @user.name, :from => "user_ids" + select "Developer", :from => "default_project_access" + end + end + + And 'submit form with new team member info' do + click_button 'add_members_to_team' + end + + Then 'I should see "John" in teams members list as "Developer"' do + @user ||= User.find_by_name("John") + find_in_list("#members_list .member", @user).must_equal true + end + + When 'I visit "John" user admin page' do + pending 'step not implemented' + end + + Then 'I should see "HardCoders" team in teams table' do + pending 'step not implemented' + end + + When 'I have "HardCoders" team with "John" member with "Developer" role' do + @team = create :user_team, name: "HardCoders", owner: current_user + @user ||= User.find_by_name("John") + @team.add_member(@user, UserTeam.access_roles["Developer"], group_admin: false) + end + + When 'I have "Shop" project' do + @project = create :project, name: "Shop" + end + + Then 'I should see empty projects table' do + page.has_no_css?("#projects_list").must_equal true + end + + When 'I select project "Shop" with max access "Reporter"' do + @project ||= Project.find_by_name("Shop") + within "#assign_projects" do + select @project.name, :from => "project_ids" + select "Reporter", :from => "greatest_project_access" + end + + end + + And 'submit form with new team project info' do + click_button 'assign_projects_to_team' + end + + Then 'I should see "Shop" project in projects list' do + project = Project.find_by_name("Shop") + find_in_list("#projects_list .project", project).must_equal true + end + + When 'I visit "Shop" project admin page' do + project = Project.find_by_name("Shop") + visit admin_project_path(project) + end + + And '"HardCoders" team assigned to "Shop" project with "Developer" max role access' do + @team = UserTeam.find_by_name("HardCoders") + @project = create :project, name: "Shop" + @team.assign_to_project(@project, UserTeam.access_roles["Developer"]) + end + + When 'I have gitlab user "Jimm"' do + create :user, name: "Jimm" + end + + Then 'I should see members table without "Jimm" member' do + user = User.find_by_name("Jimm") + find_in_list("#members_list .member", user).must_equal false + end + + When 'I select user "Jimm" ub team members list as "Master"' do + user = User.find_by_name("Jimm") + within "#team_members" do + select user.name, :from => "user_ids" + select "Developer", :from => "default_project_access" + end + end + + Then 'I should see "Jimm" in teams members list as "Master"' do + user = User.find_by_name("Jimm") + find_in_list("#members_list .member", user).must_equal true + end + + Given 'I have users team "HardCoders"' do + @team = create :user_team, name: "HardCoders" + end + + And 'gitlab user "John" is a member "HardCoders" team' do + @team = UserTeam.find_by_name("HardCoders") + @user = User.find_by_name("John") + @user = create :user, name: "John" unless @user + @team.add_member(@user, UserTeam.access_roles["Master"], group_admin: false) + end + + And 'gitlab user "Jimm" is a member "HardCoders" team' do + @team = UserTeam.find_by_name("HardCoders") + @user = User.find_by_name("Jimm") + @user = create :user, name: "Jimm" unless @user + @team.add_member(@user, UserTeam.access_roles["Master"], group_admin: false) + end + + And '"HardCoders" team is assigned to "Shop" project' do + @team = UserTeam.find_by_name("HardCoders") + @project = create :project, name: "Shop" + @team.assign_to_project(@project, UserTeam.access_roles["Developer"]) + end + + When 'I visit "HardCoders" team admin page' do + visit admin_team_path(UserTeam.find_by_name("HardCoders")) + end + + Then 'I shoould see "John" in members list' do + user = User.find_by_name("John") + find_in_list("#members_list .member", user).must_equal true + end + + And 'I should see "Jimm" in members list' do + user = User.find_by_name("Jimm") + find_in_list("#members_list .member", user).must_equal true + end + + And 'I should see "Shop" in projects list' do + project = Project.find_by_name("Shop") + find_in_list("#projects_list .project", project).must_equal true + end + + When 'I click on remove "Jimm" user link' do + user = User.find_by_name("Jimm") + click_link "remove_member_#{user.id}" + end + + Then 'I should be redirected to "HardCoders" team admin page' do + current_path.should == admin_team_path(UserTeam.find_by_name("HardCoders")) + end + + And 'I should not to see "Jimm" user in members list' do + user = User.find_by_name("Jimm") + find_in_list("#members_list .member", user).must_equal false + end + + When 'I click on "Relegate" link on "Shop" project' do + project = Project.find_by_name("Shop") + click_link "relegate_project_#{project.id}" + end + + Then 'I should see projects liston team page without "Shop" project' do + project = Project.find_by_name("Shop") + find_in_list("#projects_list .project", project).must_equal false + end + + Then 'I should see "John" user with role "Reporter" in team table' do + user = User.find_by_name("John") + find_in_list(".team_members", user).must_equal true + end + + When 'I click to "Add members" link' do + click_link "Add members" + end + + When 'I click to "Add projects" link' do + click_link "Add projects" + end + + protected + + def current_team + @team ||= Team.first + end + + def find_in_list(selector, item) + members_list = all(selector) + entered = false + members_list.each do |member_item| + entered = true if member_item.has_content?(item.name) + end + entered + end +end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 4bcefba7..8c13ad0e 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -63,6 +63,12 @@ class Dashboard < Spinach::FeatureSteps @project.team << [current_user, :master] end + Then 'I should see projects list' do + @user.authorized_projects.all.each do |project| + page.should have_link project.name_with_namespace + end + end + Then 'I should see groups list' do Group.all.each do |group| page.should have_link group.name diff --git a/features/steps/group/group.rb b/features/steps/group/group.rb index 04d8c874..c6c6b4b5 100644 --- a/features/steps/group/group.rb +++ b/features/steps/group/group.rb @@ -64,6 +64,24 @@ class Groups < Spinach::FeatureSteps author: current_user end + When 'I click new group link' do + click_link "New Group" + end + + And 'submit form with new group info' do + fill_in 'group_name', :with => 'Samurai' + click_button "Create group" + end + + Then 'I should see newly created group' do + page.should have_content "Samurai" + page.should have_content "You will only see events from projects in this group" + end + + Then 'I should be redirected to group page' do + current_path.should == group_path(Group.last) + end + protected def current_group diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 2ae0f124..04862338 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -2,27 +2,27 @@ module SharedDiffNote include Spinach::DSL Given 'I cancel the diff comment' do - within(".diff_file") do + within(".file") do find(".js-close-discussion-note-form").trigger("click") end end Given 'I delete a diff comment' do sleep 1 - within(".diff_file") do + within(".file") do first(".js-note-delete").trigger("click") end end Given 'I haven\'t written any diff comment text' do - within(".diff_file") do + within(".file") do fill_in "note[note]", with: "" end end Given 'I leave a diff comment like "Typo, please fix"' do find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click") - within(".diff_file") do + within(".file") do fill_in "note[note]", with: "Typo, please fix" #click_button("Add Comment") find(".js-comment-button").trigger("click") @@ -32,7 +32,7 @@ module SharedDiffNote Given 'I preview a diff comment text like "Should fix it :smile:"' do find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click") - within(".diff_file") do + within(".file") do fill_in "note[note]", with: "Should fix it :smile:" find(".js-note-preview-button").trigger("click") end @@ -40,7 +40,7 @@ module SharedDiffNote Given 'I preview another diff comment text like "DRY this up"' do find("#586fb7c4e1add2d4d24e27566ed7064680098646_57_41.line_holder .js-add-diff-note-button").trigger("click") - within(".diff_file") do + within(".file") do fill_in "note[note]", with: "DRY this up" find(".js-note-preview-button").trigger("click") end @@ -55,13 +55,13 @@ module SharedDiffNote end Given 'I write a diff comment like ":-1: I don\'t like this"' do - within(".diff_file") do + within(".file") do fill_in "note[note]", with: ":-1: I don\'t like this" end end Given 'I submit the diff comment' do - within(".diff_file") do + within(".file") do click_button("Add Comment") end end @@ -69,49 +69,49 @@ module SharedDiffNote Then 'I should not see the diff comment form' do - within(".diff_file") do + within(".file") do page.should_not have_css("form.new_note") end end Then 'I should not see the diff comment preview button' do - within(".diff_file") do + within(".file") do page.should have_css(".js-note-preview-button", visible: false) end end Then 'I should not see the diff comment text field' do - within(".diff_file") do + within(".file") do page.should have_css(".js-note-text", visible: false) end end Then 'I should only see one diff form' do - within(".diff_file") do + within(".file") do page.should have_css("form.new_note", count: 1) end end Then 'I should see a diff comment form with ":-1: I don\'t like this"' do - within(".diff_file") do + within(".file") do page.should have_field("note[note]", with: ":-1: I don\'t like this") end end Then 'I should see a diff comment saying "Typo, please fix"' do - within(".diff_file .note") do + within(".file .note") do page.should have_content("Typo, please fix") end end Then 'I should see a discussion reply button' do - within(".diff_file") do + within(".file") do page.should have_link("Reply") end end Then 'I should see a temporary diff comment form' do - within(".diff_file") do + within(".file") do page.should have_css(".js-temp-notes-holder form.new_note") end end @@ -121,37 +121,37 @@ module SharedDiffNote end Then 'I should see an empty diff comment form' do - within(".diff_file") do + within(".file") do page.should have_field("note[note]", with: "") end end Then 'I should see the cancel comment button' do - within(".diff_file form") do + within(".file form") do page.should have_css(".js-close-discussion-note-form", text: "Cancel") end end Then 'I should see the diff comment preview' do - within(".diff_file form") do + within(".file form") do page.should have_css(".js-note-preview", visible: false) end end Then 'I should see the diff comment edit button' do - within(".diff_file") do + within(".file") do page.should have_css(".js-note-edit-button", visible: true) end end Then 'I should see the diff comment preview button' do - within(".diff_file") do + within(".file") do page.should have_css(".js-note-preview-button", visible: true) end end Then 'I should see two separate previews' do - within(".diff_file") do + within(".file") do page.should have_css(".js-note-preview", visible: true, count: 2) page.should have_content("Should fix it") page.should have_content("DRY this up") diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index c046c4e6..42ef40d6 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -33,12 +33,16 @@ module SharedPaths visit dashboard_path end + Given 'I visit dashboard projects page' do + visit projects_dashboard_path + end + Given 'I visit dashboard issues page' do - visit dashboard_issues_path + visit issues_dashboard_path end Given 'I visit dashboard merge requests page' do - visit dashboard_merge_requests_path + visit merge_requests_dashboard_path end Given 'I visit dashboard search page' do @@ -105,6 +109,10 @@ module SharedPaths visit admin_groups_path end + When 'I visit admin teams page' do + visit admin_teams_path + end + # ---------------------------------------- # Generic Project # ---------------------------------------- diff --git a/features/steps/userteams/userteams.rb b/features/steps/userteams/userteams.rb new file mode 100644 index 00000000..be83b4ba --- /dev/null +++ b/features/steps/userteams/userteams.rb @@ -0,0 +1,254 @@ +class Userteams < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + When 'I do not have teams with me' do + UserTeam.with_member(current_user).destroy_all + end + + Then 'I should see dashboard page without teams info block' do + page.has_no_css?(".teams-box").must_equal true + end + + When 'I have teams with my membership' do + team = create :user_team, owner: current_user + team.add_member(current_user, UserTeam.access_roles["Master"], true) + end + + Then 'I should see dashboard page with teams information block' do + page.should have_css(".teams-box") + end + + When 'exist user teams' do + team = create :user_team + team.add_member(current_user, UserTeam.access_roles["Master"], true) + end + + And 'I click on "All teams" link' do + click_link("All Teams") + end + + Then 'I should see "All teams" page' do + current_path.should == teams_path + end + + And 'I should see exist teams in teams list' do + team = UserTeam.last + find_in_list(".teams_list tr", team).must_equal true + end + + When 'I click to "New team" link' do + click_link("New Team") + end + + And 'I submit form with new team info' do + fill_in 'name', with: 'gitlab' + click_button 'Create team' + end + + Then 'I should be redirected to new team page' do + team = UserTeam.last + current_path.should == team_path(team) + end + + When 'I have teams with projects and members' do + team = create :user_team, owner: current_user + @project = create :project + team.add_member(current_user, UserTeam.access_roles["Master"], true) + team.assign_to_project(@project, UserTeam.access_roles["Master"]) + @event = create(:closed_issue_event, project: @project) + end + + When 'I visit team page' do + visit team_path(UserTeam.last) + end + + Then 'I should see projects list' do + page.should have_css(".projects_box") + projects_box = find(".projects_box") + projects_box.should have_content(@project.name) + end + + And 'project from team has issues assigned to me' do + team = UserTeam.last + team.projects.each do |project| + project.issues << create(:issue, assignee: current_user) + end + end + + When 'I visit team issues page' do + team = UserTeam.last + visit issues_team_path(team) + end + + Then 'I should see issues from this team assigned to me' do + team = UserTeam.last + team.projects.each do |project| + project.issues.assigned(current_user).each do |issue| + page.should have_content issue.title + end + end + end + + Given 'I have team with projects and members' do + team = create :user_team, owner: current_user + project = create :project + user = create :user + team.add_member(current_user, UserTeam.access_roles["Master"], true) + team.add_member(user, UserTeam.access_roles["Developer"], false) + team.assign_to_project(project, UserTeam.access_roles["Master"]) + end + + Given 'project from team has issues assigned to teams members' do + team = UserTeam.last + team.projects.each do |project| + team.members.each do |member| + project.issues << create(:issue, assignee: member) + end + end + end + + Then 'I should see issues from this team assigned to teams members' do + team = UserTeam.last + team.projects.each do |project| + team.members.each do |member| + project.issues.assigned(member).each do |issue| + page.should have_content issue.title + end + end + end + end + + Given 'project from team has merge requests assigned to me' do + team = UserTeam.last + team.projects.each do |project| + team.members.each do |member| + 3.times { project.merge_requests << create(:merge_request, assignee: member) } + end + end + end + + When 'I visit team merge requests page' do + team = UserTeam.last + visit merge_requests_team_path(team) + end + + Then 'I should see merge requests from this team assigned to me' do + team = UserTeam.last + team.projects.each do |project| + team.members.each do |member| + project.issues.assigned(member).each do |merge_request| + page.should have_content merge_request.title + end + end + end + end + + Given 'project from team has merge requests assigned to team members' do + team = UserTeam.last + team.projects.each do |project| + team.members.each do |member| + 3.times { project.merge_requests << create(:merge_request, assignee: member) } + end + end + end + + Then 'I should see merge requests from this team assigned to me' do + team = UserTeam.last + team.projects.each do |project| + team.members.each do |member| + project.issues.assigned(member).each do |merge_request| + page.should have_content merge_request.title + end + end + end + end + + Given 'I have new user "John"' do + create :user, name: "John" + end + + When 'I visit team people page' do + team = UserTeam.last + visit team_members_path(team) + end + + And 'I select user "John" from list with role "Reporter"' do + user = User.find_by_name("John") + within "#team_members" do + select user.name, :from => "user_ids" + select "Reporter", :from => "default_project_access" + end + click_button "Add" + end + + Then 'I should see user "John" in team list' do + user = User.find_by_name("John") + team_members_list = find(".team-table") + team_members_list.should have_content user.name + end + + And 'I have my own project without teams' do + @project = create :project, namespace: current_user.namespace + end + + And 'I visit my team page' do + team = UserTeam.where(owner_id: current_user.id).last + visit team_path(team) + end + + When 'I click on link "Projects"' do + click_link "Projects" + end + + And 'I click link "Assign project to Team"' do + click_link "Assign project to Team" + end + + Then 'I should see form with my own project in avaliable projects list' do + projects_select = find("#project_ids") + projects_select.should have_content(@project.name) + end + + When 'I submit form with selected project and max access' do + within "#assign_projects" do + select @project.name_with_namespace, :from => "project_ids" + select "Reporter", :from => "greatest_project_access" + end + click_button "Add" + end + + Then 'I should see my own project in team projects list' do + projects = find(".projects-table") + projects.should have_content(@project.name) + end + + When 'I click link "New Team Member"' do + click_link "New Team Member" + end + + protected + + def current_team + @user_team ||= UserTeam.first + end + + def project + current_team.projects.first + end + + def assigned_to_user key, user + project.send(key).where(assignee_id: user) + end + + def find_in_list(selector, item) + members_list = all(selector) + entered = false + members_list.each do |member_item| + entered = true if member_item.has_content?(item.name) + end + entered + end + +end diff --git a/features/support/env.rb b/features/support/env.rb index be10ad1b..a08aa0de 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -21,7 +21,6 @@ Dir["#{Rails.root}/features/steps/shared/*.rb"].each {|file| require file} include GitoliteStub WebMock.allow_net_connect! - # # JS driver # diff --git a/features/teams/team.feature b/features/teams/team.feature new file mode 100644 index 00000000..9255e0da --- /dev/null +++ b/features/teams/team.feature @@ -0,0 +1,69 @@ +Feature: UserTeams + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" has push event + + Scenario: No teams, no dashboard info block + When I do not have teams with me + And I visit dashboard page + Then I should see dashboard page without teams info block + + Scenario: I should see teams info block + When I have teams with my membership + And I visit dashboard page + Then I should see dashboard page with teams information block + + Scenario: I should can create new team + When I have teams with my membership + And I visit dashboard page + When I click to "New team" link + And I submit form with new team info + Then I should be redirected to new team page + + Scenario: I should see team dashboard list + When I have teams with projects and members + When I visit team page + Then I should see projects list + + Scenario: I should see team issues list + Given I have team with projects and members + And project from team has issues assigned to me + When I visit team issues page + Then I should see issues from this team assigned to me + + Scenario: I should see teams members issues list + Given I have team with projects and members + Given project from team has issues assigned to teams members + When I visit team issues page + Then I should see issues from this team assigned to teams members + + Scenario: I should see team merge requests list + Given I have team with projects and members + Given project from team has merge requests assigned to me + When I visit team merge requests page + Then I should see merge requests from this team assigned to me + + Scenario: I should see teams members merge requests list + Given I have team with projects and members + Given project from team has merge requests assigned to team members + When I visit team merge requests page + Then I should see merge requests from this team assigned to me + + Scenario: I should add user to projects in Team + Given I have team with projects and members + Given I have new user "John" + When I visit team people page + When I click link "New Team Member" + And I select user "John" from list with role "Reporter" + Then I should see user "John" in team list + + Scenario: I should assign my team to my own project + Given I have team with projects and members + And I have my own project without teams + And I visit my team page + When I click on link "Projects" + And I click link "Assign project to Team" + Then I should see form with my own project in avaliable projects list + When I submit form with selected project and max access + Then I should see my own project in team projects list diff --git a/lib/gitlab/backend/gitolite_config.rb b/lib/gitlab/backend/gitolite_config.rb index f12c10ce..e4ebd595 100644 --- a/lib/gitlab/backend/gitolite_config.rb +++ b/lib/gitlab/backend/gitolite_config.rb @@ -88,7 +88,10 @@ module Gitlab end def destroy_project(project) - FileUtils.rm_rf(project.repository.path_to_repo) + # do rm-rf only if repository exists + if project.repository + FileUtils.rm_rf(project.repository.path_to_repo) + end conf.rm_repo(project.path_with_namespace) end diff --git a/lib/gitlab/user_team_manager.rb b/lib/gitlab/user_team_manager.rb new file mode 100644 index 00000000..a8ff4a3d --- /dev/null +++ b/lib/gitlab/user_team_manager.rb @@ -0,0 +1,135 @@ +# UserTeamManager class +# +# Used for manage User teams with project repositories +module Gitlab + class UserTeamManager + class << self + def assign(team, project, access) + project = Project.find(project) unless project.is_a? Project + searched_project = team.user_team_project_relationships.find_by_project_id(project.id) + + unless searched_project.present? + team.user_team_project_relationships.create(project_id: project.id, greatest_access: access) + update_team_users_access_in_project(team, project) + end + end + + def resign(team, project) + project = Project.find(project) unless project.is_a? Project + + team.user_team_project_relationships.with_project(project).destroy_all + + update_team_users_access_in_project(team, project) + end + + def update_team_user_membership(team, member, options) + updates = {} + + if options[:default_projects_access] && options[:default_projects_access] != team.default_projects_access(member) + updates[:permission] = options[:default_projects_access] + end + + if options[:group_admin].to_s != team.admin?(member).to_s + updates[:group_admin] = options[:group_admin].present? + end + + unless updates.blank? + user_team_relationship = team.user_team_user_relationships.find_by_user_id(member) + if user_team_relationship.update_attributes(updates) + if updates[:permission] + rebuild_project_permissions_to_member(team, member) + end + true + else + false + end + else + true + end + end + + def update_project_greates_access(team, project, permission) + project_relation = team.user_team_project_relationships.find_by_project_id(project) + if permission != team.max_project_access(project) + if project_relation.update_attributes(greatest_access: permission) + update_team_users_access_in_project(team, project) + true + else + false + end + else + true + end + end + + def rebuild_project_permissions_to_member(team, member) + team.projects.each do |project| + update_team_user_access_in_project(team, member, project) + end + end + + def update_team_users_access_in_project(team, project) + members = team.members + members.each do |member| + update_team_user_access_in_project(team, member, project) + end + end + + def update_team_user_access_in_project(team, user, project) + granted_access = max_teams_member_permission_in_project(user, project) + + project_team_user = UsersProject.find_by_user_id_and_project_id(user.id, project.id) + project_team_user.destroy if project_team_user.present? + + # project_team_user.project_access != granted_access + project.team << [user, granted_access] if granted_access > 0 + end + + def max_teams_member_permission_in_project(user, project, teams = nil) + result_access = 0 + + user_teams = project.user_teams.with_member(user) + + teams ||= user_teams + + if teams.any? + teams.each do |team| + granted_access = max_team_member_permission_in_project(team, user, project) + result_access = [granted_access, result_access].max + end + end + result_access + end + + def max_team_member_permission_in_project(team, user, project) + member_access = team.default_projects_access(user) + team_access = team.user_team_project_relationships.find_by_project_id(project.id).greatest_access + + [team_access, member_access].min + end + + def add_member_into_team(team, user, access, admin) + user = User.find(user) unless user.is_a? User + + team.user_team_user_relationships.create(user_id: user.id, permission: access, group_admin: admin) + team.projects.each do |project| + update_team_user_access_in_project(team, user, project) + end + end + + def remove_member_from_team(team, user) + user = User.find(user) unless user.is_a? User + + team.user_team_user_relationships.with_user(user).destroy_all + other_teams = [] + team.projects.each do |project| + other_teams << project.user_teams.with_member(user) + end + other_teams.uniq + unless other_teams.any? + UsersProject.in_projects(team.projects).with_user(user).destroy_all + end + end + end + end +end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 826b78ec..1ca723f2 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -169,7 +169,7 @@ namespace :gitlab do else puts "no".red try_fixing_it( - sudo_gitlab("bundle exec rake db:migrate") + sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production") ) fix_and_rerun end @@ -194,7 +194,7 @@ namespace :gitlab do else puts "no".red try_fixing_it( - sudo_gitlab("bundle exec rake gitlab:satellites:create"), + sudo_gitlab("bundle exec rake gitlab:satellites:create RAILS_ENV=production"), "If necessary, remove the tmp/repo_satellites directory ...", "... and rerun the above command" ) @@ -789,7 +789,7 @@ namespace :gitlab do else puts "wrong or missing".red try_fixing_it( - sudo_gitlab("bundle exec rake gitlab:gitolite:update_repos") + sudo_gitlab("bundle exec rake gitlab:gitolite:update_repos RAILS_ENV=production") ) for_more_information( "doc/raketasks/maintenance.md" @@ -895,7 +895,7 @@ namespace :gitlab do else puts "no".red try_fixing_it( - sudo_gitlab("bundle exec rake sidekiq:start") + sudo_gitlab("bundle exec rake sidekiq:start RAILS_ENV=production") ) for_more_information( see_installation_guide_section("Install Init Script"), diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake index 01da919d..0d2ec6f3 100644 --- a/lib/tasks/sidekiq.rake +++ b/lib/tasks/sidekiq.rake @@ -6,7 +6,7 @@ namespace :sidekiq do desc "GITLAB | Start sidekiq" task :start do - run "nohup bundle exec sidekiq -q post_receive,mailer,system_hook,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1 &" + run "nohup bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1 &" end def pidfile diff --git a/public/deploy.html b/public/deploy.html new file mode 100644 index 00000000..d8c28780 --- /dev/null +++ b/public/deploy.html @@ -0,0 +1,11 @@ + + + + Deploy in progress. Please try again in few minutes + + + +

Deploy in progress

+

Please try again in few minutes or contact your administrator.

+ + diff --git a/spec/factories/user_team_project_relationships.rb b/spec/factories/user_team_project_relationships.rb new file mode 100644 index 00000000..93c7b57d --- /dev/null +++ b/spec/factories/user_team_project_relationships.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :user_team_project_relationship do + project + user_team + greatest_access { UsersProject::MASTER } + end +end diff --git a/spec/factories/user_team_user_relationships.rb b/spec/factories/user_team_user_relationships.rb new file mode 100644 index 00000000..55179f9a --- /dev/null +++ b/spec/factories/user_team_user_relationships.rb @@ -0,0 +1,10 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :user_team_user_relationship do + user + user_team + group_admin false + permission { UsersProject::MASTER } + end +end diff --git a/spec/factories/user_teams.rb b/spec/factories/user_teams.rb new file mode 100644 index 00000000..f4fe45cb --- /dev/null +++ b/spec/factories/user_teams.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :user_team do + sequence(:name) { |n| "team#{n}" } + path { name.downcase.gsub(/\s/, '_') } + owner + end +end diff --git a/spec/models/project_hooks_spec.rb b/spec/models/project_hooks_spec.rb index 60457e20..3559c72f 100644 --- a/spec/models/project_hooks_spec.rb +++ b/spec/models/project_hooks_spec.rb @@ -38,11 +38,14 @@ describe Project, "Hooks" do @project_hook = create(:project_hook) @project_hook_2 = create(:project_hook) project.hooks << [@project_hook, @project_hook_2] + + stub_request(:post, @project_hook.url) + stub_request(:post, @project_hook_2.url) end it "executes multiple web hook" do - @project_hook.should_receive(:execute).once - @project_hook_2.should_receive(:execute).once + @project_hook.should_receive(:async_execute).once + @project_hook_2.should_receive(:async_execute).once project.trigger_post_receive('oldrev', 'newrev', 'refs/heads/master', @user) end diff --git a/spec/models/team_spec.rb b/spec/models/project_team_spec.rb similarity index 94% rename from spec/models/team_spec.rb rename to spec/models/project_team_spec.rb index 65ffe13b..7803811f 100644 --- a/spec/models/team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe Team do +describe ProjectTeam do let(:team) { create(:project).team } describe "Respond to" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f19c40b5..2ca82edf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -189,7 +189,7 @@ describe User do it { user.is_admin?.should be_false } it { user.require_ssh_key?.should be_true } - it { user.can_create_group?.should be_false } + it { user.can_create_group?.should be_true } it { user.can_create_project?.should be_true } it { user.first_name.should == 'John' } end diff --git a/spec/models/user_team_project_relationship_spec.rb b/spec/models/user_team_project_relationship_spec.rb new file mode 100644 index 00000000..81051d59 --- /dev/null +++ b/spec/models/user_team_project_relationship_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe UserTeamProjectRelationship do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_team_spec.rb b/spec/models/user_team_spec.rb new file mode 100644 index 00000000..2d1b99db --- /dev/null +++ b/spec/models/user_team_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe UserTeam do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_team_user_relationship_spec.rb b/spec/models/user_team_user_relationship_spec.rb new file mode 100644 index 00000000..309f1975 --- /dev/null +++ b/spec/models/user_team_user_relationship_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe UserTeamUserRelationship do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/atom/dashboard_issues_spec.rb b/spec/requests/atom/dashboard_issues_spec.rb index 8ce64cd4..6f5d51d1 100644 --- a/spec/requests/atom/dashboard_issues_spec.rb +++ b/spec/requests/atom/dashboard_issues_spec.rb @@ -10,7 +10,7 @@ describe "Dashboard Issues Feed" do describe "atom feed" do it "should render atom feed via private token" do - visit dashboard_issues_path(:atom, private_token: user.private_token) + visit issues_dashboard_path(:atom, private_token: user.private_token) page.response_headers['Content-Type'].should have_content("application/atom+xml") page.body.should have_selector("title", text: "#{user.name} issues") diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index fb26bf98..3e0e4bb3 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -95,20 +95,20 @@ describe Admin::ProjectsController, "routing" do end end -# edit_admin_team_member GET /admin/team_members/:id/edit(.:format) admin/team_members#edit -# admin_team_member PUT /admin/team_members/:id(.:format) admin/team_members#update -# DELETE /admin/team_members/:id(.:format) admin/team_members#destroy -describe Admin::TeamMembersController, "routing" do +# edit_admin_project_member GET /admin/projects/:project_id/members/:id/edit(.:format) admin/projects/members#edit {:id=>/[^\/]+/, :project_id=>/[^\/]+/} +# admin_project_member PUT /admin/projects/:project_id/members/:id(.:format) admin/projects/members#update {:id=>/[^\/]+/, :project_id=>/[^\/]+/} +# DELETE /admin/projects/:project_id/members/:id(.:format) admin/projects/members#destroy {:id=>/[^\/]+/, :project_id=>/[^\/]+/} +describe Admin::Projects::MembersController, "routing" do it "to #edit" do - get("/admin/team_members/1/edit").should route_to('admin/team_members#edit', id: '1') + get("/admin/projects/test/members/1/edit").should route_to('admin/projects/members#edit', project_id: 'test', id: '1') end it "to #update" do - put("/admin/team_members/1").should route_to('admin/team_members#update', id: '1') + put("/admin/projects/test/members/1").should route_to('admin/projects/members#update', project_id: 'test', id: '1') end it "to #destroy" do - delete("/admin/team_members/1").should route_to('admin/team_members#destroy', id: '1') + delete("/admin/projects/test/members/1").should route_to('admin/projects/members#destroy', project_id: 'test', id: '1') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 57fd70e7..5ad8165e 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -146,14 +146,14 @@ describe KeysController, "routing" do end end -# dashboard GET /dashboard(.:format) dashboard#index +# dashboard GET /dashboard(.:format) dashboard#show # dashboard_issues GET /dashboard/issues(.:format) dashboard#issues # dashboard_merge_requests GET /dashboard/merge_requests(.:format) dashboard#merge_requests -# root / dashboard#index +# root / dashboard#show describe DashboardController, "routing" do it "to #index" do - get("/dashboard").should route_to('dashboard#index') - get("/").should route_to('dashboard#index') + get("/dashboard").should route_to('dashboard#show') + get("/").should route_to('dashboard#show') end it "to #issues" do diff --git a/vendor/assets/javascripts/branch-graph.js b/vendor/assets/javascripts/branch-graph.js index af3b572a..93849c79 100644 --- a/vendor/assets/javascripts/branch-graph.js +++ b/vendor/assets/javascripts/branch-graph.js @@ -65,15 +65,15 @@ BranchGraph.prototype.buildGraph = function(){ var graphWidth = $(this.element).width() - , ch = this.mspace * 20 + 20 - , cw = Math.max(graphWidth, this.mtime * 20 + 20) + , ch = this.mspace * 20 + 100 + , cw = Math.max(graphWidth, this.mtime * 20 + 260) , r = Raphael(this.element.get(0), cw, ch) , top = r.set() , cuday = 0 , cumonth = "" , offsetX = 20 , offsetY = 60 - , barWidth = Math.max(graphWidth, this.dayCount * 20 + 80); + , barWidth = Math.max(graphWidth, this.dayCount * 20 + 320); this.raphael = r;