Merge branch 'master' into 369-bootstrap
Conflicts: Gemfile.lock app/models/concerns/feed/git.rb app/models/issue.rb
This commit is contained in:
commit
90f6bca72d
2
Gemfile
2
Gemfile
|
@ -1,6 +1,6 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'rails', '4.0.9'
|
||||
gem 'rails', '4.0.11'
|
||||
|
||||
gem 'activeadmin', github: 'gregbell/active_admin'
|
||||
gem 'pg', '~> 0.17.1'
|
||||
|
|
51
Gemfile.lock
51
Gemfile.lock
|
@ -52,25 +52,25 @@ GEM
|
|||
remote: https://rubygems.org/
|
||||
specs:
|
||||
RedCloth (4.2.9)
|
||||
actionmailer (4.0.9)
|
||||
actionpack (= 4.0.9)
|
||||
mail (~> 2.5.4)
|
||||
actionpack (4.0.9)
|
||||
activesupport (= 4.0.9)
|
||||
actionmailer (4.0.11)
|
||||
actionpack (= 4.0.11)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
actionpack (4.0.11)
|
||||
activesupport (= 4.0.11)
|
||||
builder (~> 3.1.0)
|
||||
erubis (~> 2.7.0)
|
||||
rack (~> 1.5.2)
|
||||
rack-test (~> 0.6.2)
|
||||
activemodel (4.0.9)
|
||||
activesupport (= 4.0.9)
|
||||
activemodel (4.0.11)
|
||||
activesupport (= 4.0.11)
|
||||
builder (~> 3.1.0)
|
||||
activerecord (4.0.9)
|
||||
activemodel (= 4.0.9)
|
||||
activerecord (4.0.11)
|
||||
activemodel (= 4.0.11)
|
||||
activerecord-deprecated_finders (~> 1.0.2)
|
||||
activesupport (= 4.0.9)
|
||||
activesupport (= 4.0.11)
|
||||
arel (~> 4.0.0)
|
||||
activerecord-deprecated_finders (1.0.3)
|
||||
activesupport (4.0.9)
|
||||
activesupport (4.0.11)
|
||||
i18n (~> 0.6, >= 0.6.9)
|
||||
minitest (~> 4.2)
|
||||
multi_json (~> 1.3)
|
||||
|
@ -238,9 +238,8 @@ GEM
|
|||
rack
|
||||
rest-client
|
||||
ya2yaml
|
||||
mail (2.5.4)
|
||||
mime-types (~> 1.16)
|
||||
treetop (~> 1.4.8)
|
||||
mail (2.6.1)
|
||||
mime-types (>= 1.16, < 3)
|
||||
mailcatcher (0.2.4)
|
||||
eventmachine
|
||||
haml
|
||||
|
@ -320,7 +319,6 @@ GEM
|
|||
pg (0.17.1)
|
||||
polyamorous (1.1.0)
|
||||
activerecord (>= 3.0)
|
||||
polyglot (0.3.5)
|
||||
posix-spawn (0.3.9)
|
||||
protected_attributes (1.0.8)
|
||||
activemodel (>= 4.0.1, < 5.0)
|
||||
|
@ -338,21 +336,21 @@ GEM
|
|||
rack (>= 1.0)
|
||||
rack-throttle (0.3.0)
|
||||
rack (>= 1.0.0)
|
||||
rails (4.0.9)
|
||||
actionmailer (= 4.0.9)
|
||||
actionpack (= 4.0.9)
|
||||
activerecord (= 4.0.9)
|
||||
activesupport (= 4.0.9)
|
||||
rails (4.0.11)
|
||||
actionmailer (= 4.0.11)
|
||||
actionpack (= 4.0.11)
|
||||
activerecord (= 4.0.11)
|
||||
activesupport (= 4.0.11)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 4.0.9)
|
||||
railties (= 4.0.11)
|
||||
sprockets-rails (~> 2.0)
|
||||
rails3-generators (1.0.0)
|
||||
railties (>= 3.0.0)
|
||||
rails3-jquery-autocomplete (1.0.14)
|
||||
rails (>= 3.0)
|
||||
railties (4.0.9)
|
||||
actionpack (= 4.0.9)
|
||||
activesupport (= 4.0.9)
|
||||
railties (4.0.11)
|
||||
actionpack (= 4.0.11)
|
||||
activesupport (= 4.0.11)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rake (10.3.2)
|
||||
|
@ -500,9 +498,6 @@ GEM
|
|||
i18n
|
||||
timecop (0.7.1)
|
||||
tmp_cache (0.1.1)
|
||||
treetop (1.4.15)
|
||||
polyglot
|
||||
polyglot (>= 0.3.1)
|
||||
tzinfo (0.3.42)
|
||||
uglifier (2.5.3)
|
||||
execjs (>= 0.3.0)
|
||||
|
@ -593,7 +588,7 @@ DEPENDENCIES
|
|||
protected_attributes
|
||||
puma
|
||||
rack-throttle (~> 0.3.0)
|
||||
rails (= 4.0.9)
|
||||
rails (= 4.0.11)
|
||||
rails3-generators
|
||||
rails3-jquery-autocomplete
|
||||
rake
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
RosaABF.controller 'StatisticsController', ['$scope', '$http', '$timeout', ($scope, $http, $timeout) ->
|
||||
|
||||
$scope.users_or_groups = null
|
||||
$scope.range = 'last_30_days'
|
||||
$scope.range_start = $('#range_start').attr('value')
|
||||
$scope.range_end = $('#range_end').attr('value')
|
||||
$scope.loading = false
|
||||
$scope.statistics = {}
|
||||
$scope.statistics_path = '/statistics'
|
||||
|
||||
$scope.colors = [
|
||||
'56, 132, 158',
|
||||
'77, 169, 68',
|
||||
'241, 128, 73',
|
||||
'174, 199, 232',
|
||||
# '255, 187, 120',
|
||||
# '152, 223, 138',
|
||||
# '214, 39, 40',
|
||||
# '31, 119, 180'
|
||||
]
|
||||
$scope.charts = {}
|
||||
|
||||
$scope.dateOptions =
|
||||
formatYear: 'yy'
|
||||
startingDay: 1
|
||||
|
||||
$('#users_or_groups').on 'autocompleteselect', (e) ->
|
||||
$timeout($scope.update, 100)
|
||||
|
||||
$scope.init = ->
|
||||
$('#statistics-form .date_picker').datepicker
|
||||
'dateFormat': 'yy-mm-dd'
|
||||
maxDate: 0
|
||||
minDate: -366
|
||||
showButtonPanel: true
|
||||
|
||||
$scope.update()
|
||||
true
|
||||
|
||||
$scope.openRangeStart = ($event) ->
|
||||
return if $scope.loading
|
||||
$event.preventDefault()
|
||||
$event.stopPropagation()
|
||||
$scope.range_start_opened = true
|
||||
|
||||
$scope.openRangeEnd = ($event) ->
|
||||
return if $scope.loading
|
||||
$event.preventDefault()
|
||||
$event.stopPropagation()
|
||||
$scope.range_end_opened = true
|
||||
|
||||
$scope.prepareRange = ->
|
||||
range_start = new Date($scope.range_start)
|
||||
range_end = new Date($scope.range_end)
|
||||
|
||||
if range_start > range_end
|
||||
tmp = $scope.range_start
|
||||
$scope.range_start = $scope.range_end
|
||||
$scope.range_end = tmp
|
||||
|
||||
$scope.prepareUsersOrGroups = ->
|
||||
if $scope.users_or_groups
|
||||
items = _.uniq $('#users_or_groups').val().replace(/\s/g, '').split(/,/)
|
||||
items = _.reject items, (i) ->
|
||||
_.isEmpty(i)
|
||||
$scope.users_or_groups = _.first(items, 3).join(', ') + ', '
|
||||
|
||||
$scope.update = ->
|
||||
return if $scope.loading
|
||||
$scope.loading = true
|
||||
$scope.statistics = {}
|
||||
|
||||
$scope.prepareRange()
|
||||
$scope.prepareUsersOrGroups()
|
||||
$('.doughnut-legend').remove()
|
||||
|
||||
params =
|
||||
range: $scope.range
|
||||
range_start: $scope.range_start
|
||||
range_end: $scope.range_end
|
||||
users_or_groups: $scope.users_or_groups
|
||||
format: 'json'
|
||||
|
||||
$http.get($scope.statistics_path, params: params).success (results) ->
|
||||
$scope.statistics = results
|
||||
|
||||
$scope.loading = false
|
||||
|
||||
# BuildLists
|
||||
if $scope.statistics.build_lists
|
||||
$scope.initBuildListsChart()
|
||||
|
||||
# PullRequests
|
||||
if $scope.statistics.pull_requests
|
||||
$scope.initPullRequestsChart()
|
||||
|
||||
# Issues
|
||||
if $scope.statistics.issues
|
||||
$scope.initIssuesChart()
|
||||
|
||||
# Commits
|
||||
if $scope.statistics.commits
|
||||
$scope.initCommitsChart()
|
||||
|
||||
.error (data, status, headers, config) ->
|
||||
console.log 'error:'
|
||||
$scope.loading = false
|
||||
|
||||
$scope.dateChart = (id, collections) ->
|
||||
new_collections = $.grep collections, ( c ) ->
|
||||
return c
|
||||
|
||||
if collections.length == new_collections.length
|
||||
$scope.charts[id].destroy() if $scope.charts[id]
|
||||
|
||||
points = collections[0]
|
||||
factor = points.length // 45 + 1
|
||||
|
||||
tooltipTitles = []
|
||||
labels = _.map points, (d, index) ->
|
||||
x = d.x
|
||||
tooltipTitles.push x
|
||||
if index %% factor == 0
|
||||
x
|
||||
else
|
||||
''
|
||||
|
||||
datasets = _.map collections, (collection, index) ->
|
||||
data = _.map collection, (d) ->
|
||||
d.y
|
||||
|
||||
dataset =
|
||||
fillColor: "rgba(#{ $scope.colors[index] }, 0.1)"
|
||||
strokeColor: "rgba(#{ $scope.colors[index] }, 1)"
|
||||
pointColor: "rgba(#{ $scope.colors[index] }, 1)"
|
||||
pointStrokeColor: "#fff"
|
||||
data: data
|
||||
|
||||
data =
|
||||
datasets: datasets
|
||||
# We display only limited count of labels on X axis, but tooltips should have titles
|
||||
# See: Chart.js "Added by avokhmin"
|
||||
labels: labels
|
||||
tooltipTitles: tooltipTitles
|
||||
|
||||
options =
|
||||
responsive: true
|
||||
|
||||
context = $(id)[0].getContext('2d')
|
||||
$scope.charts[id] = new Chart(context).Line(data, options)
|
||||
|
||||
$scope.initBuildListsChart = ->
|
||||
$scope.dateChart '#build_lists_chart', [
|
||||
$scope.statistics.build_lists.build_started,
|
||||
$scope.statistics.build_lists.success,
|
||||
$scope.statistics.build_lists.build_error,
|
||||
$scope.statistics.build_lists.build_published
|
||||
]
|
||||
|
||||
$scope.initCommitsChart = ->
|
||||
$scope.dateChart '#commits_chart', [
|
||||
$scope.statistics.commits.chart
|
||||
]
|
||||
|
||||
$scope.initPullRequestsChart = ->
|
||||
$scope.dateChart '#pull_requests_chart', [
|
||||
$scope.statistics.pull_requests.open,
|
||||
$scope.statistics.pull_requests.merged
|
||||
$scope.statistics.pull_requests.closed,
|
||||
]
|
||||
|
||||
$scope.initIssuesChart = ->
|
||||
$scope.dateChart '#issues_chart', [
|
||||
$scope.statistics.issues.open,
|
||||
$scope.statistics.issues.reopen,
|
||||
$scope.statistics.issues.closed
|
||||
]
|
||||
|
||||
]
|
|
@ -1,9 +1,10 @@
|
|||
RosaABF.controller 'StatisticsController', ['$scope', '$http', ($scope, $http) ->
|
||||
RosaABF.controller 'StatisticsController', ['$scope', '$http', '$timeout', ($scope, $http, $timeout) ->
|
||||
|
||||
$scope.users_or_groups = null
|
||||
$scope.range = 'last_30_days'
|
||||
$scope.range_start = $('#range_start').attr('value')
|
||||
$scope.range_end = $('#range_end').attr('value')
|
||||
$scope.loading = true
|
||||
$scope.loading = false
|
||||
$scope.statistics = {}
|
||||
$scope.statistics_path = '/statistics'
|
||||
|
||||
|
@ -12,22 +13,24 @@ RosaABF.controller 'StatisticsController', ['$scope', '$http', ($scope, $http) -
|
|||
'77, 169, 68',
|
||||
'241, 128, 73',
|
||||
'174, 199, 232',
|
||||
'255, 187, 120',
|
||||
'152, 223, 138',
|
||||
'214, 39, 40',
|
||||
'31, 119, 180'
|
||||
# '255, 187, 120',
|
||||
# '152, 223, 138',
|
||||
# '214, 39, 40',
|
||||
# '31, 119, 180'
|
||||
]
|
||||
$scope.charts = {}
|
||||
|
||||
$('#users_or_groups').on 'autocompleteselect', (e) ->
|
||||
$timeout($scope.update, 100)
|
||||
|
||||
$scope.init = ->
|
||||
$('#range-form .date_picker').datepicker
|
||||
$('#statistics-form .date_picker').datepicker
|
||||
'dateFormat': 'yy-mm-dd'
|
||||
maxDate: 0
|
||||
minDate: -366
|
||||
showButtonPanel: true
|
||||
|
||||
$scope.rangeChange()
|
||||
$scope.update()
|
||||
true
|
||||
|
||||
$scope.prepareRange = ->
|
||||
|
@ -39,18 +42,27 @@ RosaABF.controller 'StatisticsController', ['$scope', '$http', ($scope, $http) -
|
|||
$scope.range_start = $scope.range_end
|
||||
$scope.range_end = tmp
|
||||
|
||||
$scope.prepareUsersOrGroups = ->
|
||||
if $scope.users_or_groups
|
||||
items = _.uniq $('#users_or_groups').val().replace(/\s/g, '').split(/,/)
|
||||
items = _.reject items, (i) ->
|
||||
_.isEmpty(i)
|
||||
$scope.users_or_groups = _.first(items, 3).join(', ') + ', '
|
||||
|
||||
$scope.rangeChange = ->
|
||||
$scope.update = ->
|
||||
return if $scope.loading
|
||||
$scope.loading = true
|
||||
$scope.statistics = {}
|
||||
|
||||
$scope.prepareRange()
|
||||
$scope.prepareUsersOrGroups()
|
||||
$('.doughnut-legend').remove()
|
||||
|
||||
params =
|
||||
range: $scope.range
|
||||
range_start: $scope.range_start
|
||||
range_end: $scope.range_end
|
||||
users_or_groups: $scope.users_or_groups
|
||||
format: 'json'
|
||||
|
||||
$http.get($scope.statistics_path, params: params).success (results) ->
|
||||
|
@ -70,6 +82,10 @@ RosaABF.controller 'StatisticsController', ['$scope', '$http', ($scope, $http) -
|
|||
if $scope.statistics.issues
|
||||
$scope.initIssuesChart()
|
||||
|
||||
# Commits
|
||||
if $scope.statistics.commits
|
||||
$scope.initCommitsChart()
|
||||
|
||||
.error (data, status, headers, config) ->
|
||||
console.log 'error:'
|
||||
$scope.loading = false
|
||||
|
@ -98,7 +114,7 @@ RosaABF.controller 'StatisticsController', ['$scope', '$http', ($scope, $http) -
|
|||
d.y
|
||||
|
||||
dataset =
|
||||
fillColor: "rgba(#{ $scope.colors[index] }, 0.5)"
|
||||
fillColor: "rgba(#{ $scope.colors[index] }, 0.1)"
|
||||
strokeColor: "rgba(#{ $scope.colors[index] }, 1)"
|
||||
pointColor: "rgba(#{ $scope.colors[index] }, 1)"
|
||||
pointStrokeColor: "#fff"
|
||||
|
@ -125,18 +141,23 @@ RosaABF.controller 'StatisticsController', ['$scope', '$http', ($scope, $http) -
|
|||
$scope.statistics.build_lists.build_published
|
||||
]
|
||||
|
||||
$scope.initCommitsChart = ->
|
||||
$scope.dateChart '#commits_chart', [
|
||||
$scope.statistics.commits.chart
|
||||
]
|
||||
|
||||
$scope.initPullRequestsChart = ->
|
||||
$scope.dateChart '#pull_requests_chart', [
|
||||
$scope.statistics.pull_requests.open,
|
||||
$scope.statistics.pull_requests.merged
|
||||
$scope.statistics.pull_requests.closed,
|
||||
$scope.statistics.pull_requests.approved
|
||||
]
|
||||
|
||||
$scope.initIssuesChart = ->
|
||||
$scope.dateChart '#issues_chart', [
|
||||
$scope.statistics.issues.open,
|
||||
$scope.statistics.issues.closed,
|
||||
$scope.statistics.issues.approved
|
||||
$scope.statistics.issues.reopen,
|
||||
$scope.statistics.issues.closed
|
||||
]
|
||||
|
||||
]
|
|
@ -35,6 +35,8 @@
|
|||
|
||||
//= require zeroclipboard
|
||||
|
||||
//= require lib/Chart
|
||||
|
||||
//= require_self
|
||||
|
||||
function setCookie (name, value, expires, path, domain, secure) {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
#manage-statistics
|
||||
.graph-key-color1
|
||||
background-color: #38849e
|
||||
.graph-key-color2
|
||||
background-color: #4da944
|
||||
.graph-key-color3
|
||||
background-color: #f18049
|
||||
.graph-key-color4
|
||||
background-color: #aec7e8
|
||||
.graph-key-color5
|
||||
background-color: #ffbb78
|
||||
|
||||
.graph-wrapper span
|
||||
display: inline-block
|
||||
width: 10px
|
||||
height: 10px
|
||||
margin-left: 15px
|
|
@ -53,6 +53,7 @@ class Api::V1::PullRequestsController < Api::V1::BaseController
|
|||
@pull.to_ref, @pull.from_ref = pull_params[:to_ref], pull_params[:from_ref]
|
||||
@pull.issue.assignee_id = pull_params[:assignee_id] if can?(:write, @project)
|
||||
@pull.issue.user, @pull.issue.project = current_user, @project
|
||||
@pull.issue.new_pull_request = true
|
||||
render_validation_error(@pull, "#{@pull.class.name} has not been created") && return unless @pull.valid?
|
||||
|
||||
@pull.save # set pull id
|
||||
|
|
|
@ -4,6 +4,13 @@ class AutocompletesController < ApplicationController
|
|||
autocomplete :group, :uname
|
||||
autocomplete :user, :uname
|
||||
|
||||
def autocomplete_user_or_group
|
||||
results = []
|
||||
results << User.opened.search(params[:term]).search_order.limit(5).pluck(:uname)
|
||||
results << Group.search(params[:term]).search_order.limit(5).pluck(:uname)
|
||||
render json: results.flatten.sort.map{ |r| { label: r } }
|
||||
end
|
||||
|
||||
def autocomplete_extra_build_list
|
||||
bl = BuildList.for_extra_build_lists(params[:term], current_ability, save_to_platform).first
|
||||
results << { :id => bl.id,
|
||||
|
|
|
@ -41,6 +41,7 @@ class Projects::PullRequestsController < Projects::BaseController
|
|||
@pull.issue.user, @pull.issue.project, @pull.from_project = current_user, to_project, @project
|
||||
@pull.from_project_owner_uname = @pull.from_project.owner.uname
|
||||
@pull.from_project_name = @pull.from_project.name
|
||||
@pull.issue.new_pull_request = true
|
||||
|
||||
if @pull.valid? # FIXME more clean/clever logics
|
||||
@pull.save # set pull id
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class StatisticsController < ApplicationController
|
||||
layout 'bootstrap'
|
||||
|
||||
RANGES = [
|
||||
RANGE_TWENTY_FOUR_HOURS = 'twenty_four_hours',
|
||||
|
@ -16,7 +17,12 @@ class StatisticsController < ApplicationController
|
|||
format.html
|
||||
format.json do
|
||||
init_variables
|
||||
render json: StatisticPresenter.new(range_start: @range_start, range_end: @range_end, unit: @unit)
|
||||
render json: StatisticPresenter.new(
|
||||
range_start: @range_start,
|
||||
range_end: @range_end,
|
||||
unit: @unit,
|
||||
users_or_groups: params[:users_or_groups]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -749,7 +749,8 @@ class BuildList < ActiveRecord::Base
|
|||
if extra_params.present?
|
||||
params = extra_params.slice(*BuildList::EXTRA_PARAMS)
|
||||
params.update(params) do |k,v|
|
||||
v.strip.gsub(I18n.t("activerecord.attributes.build_list.extra_params.#{k}"), '').gsub(/[^\w\s-]/, '')
|
||||
v.strip.gsub(I18n.t("activerecord.attributes.build_list.extra_params.#{k}"), '').
|
||||
gsub(/[^\w\s\-["]]/, '')
|
||||
end
|
||||
self.extra_params = params.select{ |k,v| v.present? }
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ module BuildListObserver
|
|||
def update_statistic
|
||||
Statistic.statsd_increment(
|
||||
activity_at: Time.now,
|
||||
key: "build_list.#{status}",
|
||||
key: "#{Statistic::KEY_BUILD_LIST}.#{status}",
|
||||
project_id: project_id,
|
||||
user_id: user_id,
|
||||
) if status_changed?
|
||||
|
|
|
@ -32,7 +32,17 @@ module Feed::Git
|
|||
commits = commits[0...-3]
|
||||
options.merge!({other_commits_count: commits.count, other_commits: "#{commits[0].sha[0..9]}...#{commits[-1].sha[0..9]}"})
|
||||
end
|
||||
Comment.create_link_on_issues_from_item(record, all_commits) if all_commits.count > 0
|
||||
|
||||
if all_commits.count > 0
|
||||
Statistic.statsd_increment(
|
||||
activity_at: Time.now,
|
||||
key: Statistic::KEY_COMMIT,
|
||||
project_id: record.project.id,
|
||||
user_id: record.user.id,
|
||||
counter: all_commits.count
|
||||
)
|
||||
Comment.create_link_on_issues_from_item(record, all_commits)
|
||||
end
|
||||
end
|
||||
options.merge!({user_id: record.user.id, user_name: record.user.name, user_email: record.user.email}) if record.user
|
||||
|
||||
|
|
|
@ -1,17 +1,39 @@
|
|||
class Issue < ActiveRecord::Base
|
||||
include Feed::Issue
|
||||
STATUSES = ['open', 'closed']
|
||||
|
||||
STATUSES = [
|
||||
STATUS_OPEN = 'open',
|
||||
STATUS_REOPEN = 'reopen',
|
||||
STATUS_CLOSED = 'closed'
|
||||
]
|
||||
HASH_TAG_REGEXP = /([a-zA-Z0-9\-_]*\/)?([a-zA-Z0-9\-_]*)?#([0-9]+)/
|
||||
self.per_page = 20
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :user
|
||||
belongs_to :assignee, class_name: 'User', foreign_key: 'assignee_id'
|
||||
belongs_to :closer, class_name: 'User', foreign_key: 'closed_by'
|
||||
belongs_to :assignee,
|
||||
class_name: 'User',
|
||||
foreign_key: 'assignee_id'
|
||||
|
||||
belongs_to :closer,
|
||||
class_name: 'User',
|
||||
foreign_key: 'closed_by'
|
||||
|
||||
has_many :comments,
|
||||
as: :commentable,
|
||||
dependent: :destroy
|
||||
|
||||
has_many :subscribes,
|
||||
as: :subscribeable,
|
||||
dependent: :destroy
|
||||
|
||||
has_many :labelings,
|
||||
dependent: :destroy
|
||||
|
||||
has_many :labels,
|
||||
-> { uniq },
|
||||
through: :labelings
|
||||
|
||||
has_many :comments, as: :commentable, dependent: :destroy
|
||||
has_many :subscribes, as: :subscribeable, dependent: :destroy
|
||||
has_many :labelings, dependent: :destroy
|
||||
has_many :labels, -> { uniq }, through: :labelings
|
||||
has_one :pull_request#, dependent: :destroy
|
||||
|
||||
validates :title, :body, :project_id, presence: true
|
||||
|
@ -20,22 +42,29 @@ class Issue < ActiveRecord::Base
|
|||
after_create :subscribe_users
|
||||
after_update :subscribe_issue_assigned_user
|
||||
|
||||
before_create :update_statistic
|
||||
before_update :update_statistic
|
||||
|
||||
attr_accessible :labelings_attributes, :title, :body, :assignee_id
|
||||
accepts_nested_attributes_for :labelings, allow_destroy: true
|
||||
|
||||
scope :opened, -> { where(status: 'open') }
|
||||
scope :closed, -> { where(status: 'closed') }
|
||||
scope :opened, -> { where(status: [STATUS_OPEN, STATUS_REOPEN]) }
|
||||
scope :closed, -> { where(status: STATUS_CLOSED) }
|
||||
|
||||
scope :needed_checking, -> { where(issues: {status: ['open', 'blocked', 'ready', 'already']}) }
|
||||
scope :needed_checking, -> { where(issues: { status: %w(open reopen blocked ready already) }) }
|
||||
scope :not_closed_or_merged, -> { needed_checking }
|
||||
scope :closed_or_merged, -> { where(issues: {status: ['closed', 'merged']}) }
|
||||
scope :closed_or_merged, -> { where(issues: { status: %w(closed merged) }) }
|
||||
# Using mb_chars for correct transform to lowercase ('Русский Текст'.downcase => "Русский Текст")
|
||||
scope :search, ->(q) { where("#{table_name}.title ILIKE ?", "%#{q.mb_chars.downcase}%") if q.present? }
|
||||
scope :search, ->(q) {
|
||||
where("#{table_name}.title ILIKE ?", "%#{q.mb_chars.downcase}%") if q.present?
|
||||
}
|
||||
scope :without_pull_requests, -> {
|
||||
where('NOT EXISTS (select null from pull_requests as pr where pr.issue_id = issues.id)').
|
||||
references(:pull_requests)
|
||||
}
|
||||
|
||||
attr_accessor :new_pull_request
|
||||
|
||||
def assign_uname
|
||||
assignee.uname if assignee
|
||||
end
|
||||
|
@ -45,24 +74,24 @@ class Issue < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def subscribe_creator(creator_id)
|
||||
if !self.subscribes.exists?(user_id: creator_id)
|
||||
unless self.subscribes.exists?(user_id: creator_id)
|
||||
self.subscribes.create(user_id: creator_id)
|
||||
end
|
||||
end
|
||||
|
||||
def closed?
|
||||
closed_by && closed_at && status == 'closed'
|
||||
closed_by && closed_at && status == STATUS_CLOSED
|
||||
end
|
||||
|
||||
def set_close(closed_by)
|
||||
self.closed_at = Time.now.utc
|
||||
self.closer = closed_by
|
||||
self.status = 'closed'
|
||||
self.status = STATUS_CLOSED
|
||||
end
|
||||
|
||||
def set_open
|
||||
self.closed_at = self.closed_by = nil
|
||||
self.status = 'open'
|
||||
self.status = STATUS_REOPEN
|
||||
end
|
||||
|
||||
def collect_recipients
|
||||
|
@ -71,8 +100,8 @@ class Issue < ActiveRecord::Base
|
|||
recipients
|
||||
end
|
||||
|
||||
def self.find_by_hash_tag hash_tag, current_ability, project
|
||||
hash_tag =~ /([a-zA-Z0-9\-_]*\/)?([a-zA-Z0-9\-_]*)?#([0-9]+)/
|
||||
def self.find_by_hash_tag(hash_tag, current_ability, project)
|
||||
hash_tag =~ HASH_TAG_REGEXP
|
||||
owner_uname = Regexp.last_match[1].presence || Regexp.last_match[2].presence || project.owner.uname
|
||||
project_name = Regexp.last_match[1] ? Regexp.last_match[2] : project.name
|
||||
serial_id = Regexp.last_match[3]
|
||||
|
@ -84,6 +113,16 @@ class Issue < ActiveRecord::Base
|
|||
|
||||
protected
|
||||
|
||||
def update_statistic
|
||||
key = (pull_request || new_pull_request) ? Statistic::KEY_PULL_REQUEST : Statistic::KEY_ISSUE
|
||||
Statistic.statsd_increment(
|
||||
activity_at: Time.now,
|
||||
key: "#{key}.#{status}",
|
||||
project_id: project_id,
|
||||
user_id: closed_by || user_id,
|
||||
) if new_record? || status_changed?
|
||||
end
|
||||
|
||||
def set_serial_id
|
||||
self.serial_id = self.project.issues.count
|
||||
self.save!
|
||||
|
|
|
@ -1,10 +1,40 @@
|
|||
class PullRequest < ActiveRecord::Base
|
||||
STATUSES = %w(ready already blocked merged closed)
|
||||
belongs_to :issue, autosave: true, dependent: :destroy, touch: true, validate: true
|
||||
belongs_to :to_project, class_name: 'Project', foreign_key: 'to_project_id'
|
||||
belongs_to :from_project, class_name: 'Project', foreign_key: 'from_project_id'
|
||||
delegate :user, :user_id, :title, :body, :serial_id, :assignee, :status, :to_param,
|
||||
:created_at, :updated_at, :comments, :status=, to: :issue, allow_nil: true
|
||||
STATUSES = [
|
||||
STATUS_OPEN = 'open',
|
||||
STATUS_READY = 'ready',
|
||||
STATUS_ALREADY = 'already',
|
||||
STATUS_BLOCKED = 'blocked',
|
||||
STATUS_MERGED = 'merged',
|
||||
STATUS_CLOSED = 'closed'
|
||||
]
|
||||
|
||||
belongs_to :issue,
|
||||
autosave: true,
|
||||
dependent: :destroy,
|
||||
touch: true,
|
||||
validate: true
|
||||
|
||||
belongs_to :to_project,
|
||||
class_name: 'Project',
|
||||
foreign_key: 'to_project_id'
|
||||
|
||||
belongs_to :from_project,
|
||||
class_name: 'Project',
|
||||
foreign_key: 'from_project_id'
|
||||
|
||||
delegate :user,
|
||||
:user_id,
|
||||
:title,
|
||||
:body,
|
||||
:serial_id,
|
||||
:assignee,
|
||||
:status,
|
||||
:to_param,
|
||||
:created_at,
|
||||
:updated_at,
|
||||
:comments,
|
||||
:status=,
|
||||
to: :issue, allow_nil: true
|
||||
|
||||
validates :from_project, :to_project, presence: true
|
||||
validate :uniq_merge, if: ->(pull) { pull.to_project.present? }
|
||||
|
@ -19,9 +49,9 @@ class PullRequest < ActiveRecord::Base
|
|||
accepts_nested_attributes_for :issue
|
||||
attr_accessible :issue_attributes, :to_ref, :from_ref
|
||||
|
||||
scope :needed_checking, -> { includes(:issue).where(issues: {status: ['open', 'blocked', 'ready']}) }
|
||||
scope :needed_checking, -> { includes(:issue).where(issues: { status: [STATUS_OPEN, STATUS_BLOCKED, STATUS_READY] }) }
|
||||
scope :not_closed_or_merged, -> { needed_checking }
|
||||
scope :closed_or_merged, -> { where(issues: {status: ['closed', 'merged']}) }
|
||||
scope :closed_or_merged, -> { where(issues: { status: [STATUS_CLOSED, STATUS_MERGED] }) }
|
||||
|
||||
state_machine :status, initial: :open do
|
||||
event :ready do
|
||||
|
@ -87,7 +117,7 @@ class PullRequest < ActiveRecord::Base
|
|||
new_status == 'already' ? (ready; merging) : send(new_status)
|
||||
self.update_inline_comments
|
||||
else
|
||||
self.status = new_status == 'block' ? 'blocked' : new_status
|
||||
self.status = new_status == 'block' ? STATUS_BLOCKED : new_status
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -163,8 +193,7 @@ class PullRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def repo
|
||||
return @repo if @repo.present? #&& !id_changed?
|
||||
@repo = Grit::Repo.new path
|
||||
@repo ||= Grit::Repo.new(path)
|
||||
end
|
||||
|
||||
def from_commit
|
||||
|
|
|
@ -1,4 +1,21 @@
|
|||
class Statistic < ActiveRecord::Base
|
||||
KEYS = [
|
||||
KEY_COMMIT = 'commit',
|
||||
KEY_BUILD_LIST = 'build_list',
|
||||
KEY_BUILD_LIST_BUILD_STARTED = "#{KEY_BUILD_LIST}.#{BuildList::BUILD_STARTED}",
|
||||
KEY_BUILD_LIST_SUCCESS = "#{KEY_BUILD_LIST}.#{BuildList::SUCCESS}",
|
||||
KEY_BUILD_LIST_BUILD_ERROR = "#{KEY_BUILD_LIST}.#{BuildList::BUILD_ERROR}",
|
||||
KEY_BUILD_LIST_BUILD_PUBLISHED = "#{KEY_BUILD_LIST}.#{BuildList::BUILD_PUBLISHED}",
|
||||
KEY_ISSUE = 'issue',
|
||||
KEY_ISSUES_OPEN = "#{KEY_ISSUE}.#{Issue::STATUS_OPEN}",
|
||||
KEY_ISSUES_REOPEN = "#{KEY_ISSUE}.#{Issue::STATUS_REOPEN}",
|
||||
KEY_ISSUES_CLOSED = "#{KEY_ISSUE}.#{Issue::STATUS_CLOSED}",
|
||||
KEY_PULL_REQUEST = 'pull_request',
|
||||
KEY_PULL_REQUESTS_OPEN = "#{KEY_PULL_REQUEST}.#{PullRequest::STATUS_OPEN}",
|
||||
KEY_PULL_REQUESTS_MERGED = "#{KEY_PULL_REQUEST}.#{PullRequest::STATUS_MERGED}",
|
||||
KEY_PULL_REQUESTS_CLOSED = "#{KEY_PULL_REQUEST}.#{PullRequest::STATUS_CLOSED}",
|
||||
]
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :project
|
||||
|
||||
|
@ -32,16 +49,41 @@ class Statistic < ActiveRecord::Base
|
|||
:counter,
|
||||
:activity_at
|
||||
|
||||
scope :for_period, -> (start_date, end_date) { where(activity_at: (start_date..end_date)) }
|
||||
scope :for_period, -> (start_date, end_date) {
|
||||
where(activity_at: (start_date..end_date))
|
||||
}
|
||||
scope :for_users, -> (user_ids) {
|
||||
where(user_id: user_ids) if user_ids.present?
|
||||
}
|
||||
scope :for_groups, -> (group_ids) {
|
||||
where(["project_id = ANY (
|
||||
ARRAY (
|
||||
SELECT target_id
|
||||
FROM relations
|
||||
INNER JOIN projects ON projects.id = relations.target_id
|
||||
WHERE relations.target_type = 'Project' AND
|
||||
projects.owner_type = 'Group' AND
|
||||
relations.actor_type = 'Group' AND
|
||||
relations.actor_id IN (:groups)
|
||||
)
|
||||
)", { groups: group_ids }
|
||||
]) if group_ids.present?
|
||||
}
|
||||
|
||||
scope :build_lists_started, -> { where(key: "build_list.#{BuildList::BUILD_STARTED}") }
|
||||
scope :build_lists_success, -> { where(key: "build_list.#{BuildList::SUCCESS}") }
|
||||
scope :build_lists_error, -> { where(key: "build_list.#{BuildList::BUILD_ERROR}") }
|
||||
scope :build_lists_published, -> { where(key: "build_list.#{BuildList::BUILD_PUBLISHED}") }
|
||||
scope :build_lists_started, -> { where(key: KEY_BUILD_LIST_BUILD_STARTED) }
|
||||
scope :build_lists_success, -> { where(key: KEY_BUILD_LIST_SUCCESS) }
|
||||
scope :build_lists_error, -> { where(key: KEY_BUILD_LIST_BUILD_ERROR) }
|
||||
scope :build_lists_published, -> { where(key: KEY_BUILD_LIST_BUILD_PUBLISHED) }
|
||||
scope :commits, -> { where(key: KEY_COMMIT) }
|
||||
scope :issues_open, -> { where(key: KEY_ISSUES_OPEN) }
|
||||
scope :issues_reopen, -> { where(key: KEY_ISSUES_REOPEN) }
|
||||
scope :issues_closed, -> { where(key: KEY_ISSUES_CLOSED) }
|
||||
scope :pull_requests_open, -> { where(key: KEY_PULL_REQUESTS_OPEN) }
|
||||
scope :pull_requests_merged, -> { where(key: KEY_PULL_REQUESTS_MERGED) }
|
||||
scope :pull_requests_closed, -> { where(key: KEY_PULL_REQUESTS_CLOSED) }
|
||||
|
||||
|
||||
|
||||
def self.now_statsd_increment(activity_at: nil, user_id: nil, project_id: nil, key: nil)
|
||||
def self.now_statsd_increment(activity_at: nil, user_id: nil, project_id: nil, key: nil, counter: 1)
|
||||
# Truncates a DateTime to the minute
|
||||
activity_at = activity_at.utc.change(min: 0)
|
||||
user = User.find user_id
|
||||
|
@ -62,25 +104,7 @@ class Statistic < ActiveRecord::Base
|
|||
project_id: project_id,
|
||||
key: key,
|
||||
activity_at: activity_at
|
||||
).update_all('counter = counter + 1')
|
||||
end
|
||||
|
||||
# TODO: remove later
|
||||
def self.fill_in_build_lists
|
||||
BuildList.find_each do |bl|
|
||||
Statistic.now_statsd_increment({
|
||||
activity_at: bl.created_at,
|
||||
key: "build_list.#{BuildList::BUILD_STARTED}",
|
||||
project_id: bl.project_id,
|
||||
user_id: bl.user_id,
|
||||
})
|
||||
Statistic.now_statsd_increment({
|
||||
activity_at: bl.updated_at,
|
||||
key: "build_list.#{bl.status}",
|
||||
project_id: bl.project_id,
|
||||
user_id: bl.user_id,
|
||||
})
|
||||
end
|
||||
).update_all(['counter = counter + ?', counter]) if user_id.present? && project_id.present?
|
||||
end
|
||||
|
||||
def self.statsd_increment(options = {})
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
class StatisticPresenter < ApplicationPresenter
|
||||
|
||||
attr_accessor :range_start, :range_end, :unit
|
||||
attr_accessor :range_start, :range_end, :unit, :users_or_groups
|
||||
|
||||
def initialize(range_start: nil, range_end: nil, unit: nil)
|
||||
def initialize(range_start: nil, range_end: nil, unit: nil, users_or_groups: nil)
|
||||
@range_start = range_start
|
||||
@range_end = range_end
|
||||
@unit = unit
|
||||
@users_or_groups = users_or_groups.to_s.split(/,/).map(&:strip).select(&:present?).first(3)
|
||||
end
|
||||
|
||||
def as_json(options = nil)
|
||||
|
@ -20,36 +21,91 @@ class StatisticPresenter < ApplicationPresenter
|
|||
success_count: build_lists_success.sum(&:count),
|
||||
build_error_count: build_lists_error.sum(&:count),
|
||||
build_published_count: build_lists_published.sum(&:count),
|
||||
},
|
||||
commits: {
|
||||
chart: prepare_collection(commits_chart),
|
||||
commits_count: commits_chart.sum(&:count)
|
||||
},
|
||||
issues: {
|
||||
open: prepare_collection(issues_open),
|
||||
reopen: prepare_collection(issues_reopen),
|
||||
closed: prepare_collection(issues_closed),
|
||||
|
||||
open_count: issues_open.sum(&:count),
|
||||
reopen_count: issues_reopen.sum(&:count),
|
||||
closed_count: issues_closed.sum(&:count)
|
||||
},
|
||||
pull_requests: {
|
||||
open: prepare_collection(pull_requests_open),
|
||||
merged: prepare_collection(pull_requests_merged),
|
||||
closed: prepare_collection(pull_requests_closed),
|
||||
|
||||
open_count: pull_requests_open.sum(&:count),
|
||||
merged_count: pull_requests_merged.sum(&:count),
|
||||
closed_count: pull_requests_closed.sum(&:count)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope
|
||||
@scope ||= Statistic.for_period(range_start, range_end)
|
||||
def user_ids
|
||||
@user_ids ||= User.where(uname: users_or_groups).pluck(:id)
|
||||
end
|
||||
|
||||
def build_lists
|
||||
@build_lists ||= scope.
|
||||
def group_ids
|
||||
@group_ids ||= Group.where(uname: users_or_groups).pluck(:id)
|
||||
end
|
||||
|
||||
def scope
|
||||
@scope ||= Statistic.for_period(range_start, range_end).
|
||||
for_users(user_ids).for_groups(group_ids).
|
||||
select("SUM(counter) as count, date_trunc('#{ unit }', activity_at) as activity_at").
|
||||
group("date_trunc('#{ unit }', activity_at)").order('activity_at')
|
||||
end
|
||||
|
||||
def issues_open
|
||||
@issues_open ||= scope.issues_open.to_a
|
||||
end
|
||||
|
||||
def issues_reopen
|
||||
@issues_reopen ||= scope.issues_reopen.to_a
|
||||
end
|
||||
|
||||
def issues_closed
|
||||
@issues_closed ||= scope.issues_closed.to_a
|
||||
end
|
||||
|
||||
def pull_requests_open
|
||||
@pull_requests_open ||= scope.pull_requests_open.to_a
|
||||
end
|
||||
|
||||
def pull_requests_merged
|
||||
@pull_requests_merged ||= scope.pull_requests_merged.to_a
|
||||
end
|
||||
|
||||
def pull_requests_closed
|
||||
@pull_requests_closed ||= scope.pull_requests_closed.to_a
|
||||
end
|
||||
|
||||
def commits_chart
|
||||
@commits_chart ||= scope.commits.to_a
|
||||
end
|
||||
|
||||
def build_lists_started
|
||||
@build_lists_started ||= build_lists.build_lists_started.to_a
|
||||
@build_lists_started ||= scope.build_lists_started.to_a
|
||||
end
|
||||
|
||||
def build_lists_success
|
||||
@build_lists_success ||= build_lists.build_lists_success.to_a
|
||||
@build_lists_success ||= scope.build_lists_success.to_a
|
||||
end
|
||||
|
||||
def build_lists_error
|
||||
@build_lists_error ||= build_lists.build_lists_error.to_a
|
||||
@build_lists_error ||= scope.build_lists_error.to_a
|
||||
end
|
||||
|
||||
def build_lists_published
|
||||
@build_lists_published ||= build_lists.build_lists_published.to_a
|
||||
@build_lists_published ||= scope.build_lists_published.to_a
|
||||
end
|
||||
|
||||
def prepare_collection(items)
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
.row
|
||||
.span8
|
||||
.graph-wrapper
|
||||
%h3
|
||||
%span.graph-key-color1
|
||||
= t('.build_started_title')
|
||||
%span.graph-key-color2
|
||||
= t('.success_title')
|
||||
%span.graph-key-color3
|
||||
= t('.build_error_title')
|
||||
%span.graph-key-color4
|
||||
= t('.build_published_title')
|
||||
.centered.graph-loading{ ng_show: 'loading' }
|
||||
= image_tag 'loading-large.gif'
|
||||
.no-data{ ng_hide: 'loading || statistics.build_lists' }
|
||||
= t('.no_data')
|
||||
%canvas#build_lists_chart{ ng_show: 'statistics.build_lists' }
|
||||
|
||||
.span3
|
||||
.panel-wrapper
|
||||
%h3
|
||||
= t('.total_build_started')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data{ ng_hide: 'loading || statistics.build_lists.build_started_count >= 0' }
|
||||
= t('.no_data')
|
||||
{{ statistics.build_lists.build_started_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
%h3
|
||||
= t('.total_success')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data{ ng_hide: 'loading || statistics.build_lists.success_count >= 0' }
|
||||
= t('.no_data')
|
||||
{{ statistics.build_lists.success_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
%h3
|
||||
= t('.total_build_error')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data{ ng_hide: 'loading || statistics.build_lists.build_error_count >= 0' }
|
||||
= t('.no_data')
|
||||
{{ statistics.build_lists.build_error_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
%h3
|
||||
= t('.total_build_published')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data{ ng_hide: 'loading || statistics.build_lists.build_published_count >= 0' }
|
||||
= t('.no_data')
|
||||
{{ statistics.build_lists.build_published_count | number }}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
.row
|
||||
.col-md-12
|
||||
h3.text-info
|
||||
= t('.header')
|
||||
|
||||
.row
|
||||
.col-md-8
|
||||
.graph-wrapper
|
||||
h4
|
||||
span.graph-key-color1>
|
||||
= t('.build_started_title')
|
||||
span.graph-key-color2>
|
||||
= t('.success_title')
|
||||
span.graph-key-color3>
|
||||
= t('.build_error_title')
|
||||
span.graph-key-color4>
|
||||
= t('.build_published_title')
|
||||
.text-center.graph-loading ng-show='loading'
|
||||
= image_tag 'loading-large.gif'
|
||||
.text-center.no-data ng-hide='loading || statistics.build_lists'
|
||||
= t('.no_data')
|
||||
canvas#build_lists_chart ng-show='statistics.build_lists'
|
||||
|
||||
.col-md-3
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_build_started')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.build_lists.build_started_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.build_lists.build_started_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_success')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.build_lists.success_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.build_lists.success_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_build_error')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.build_lists.build_error_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.build_lists.build_error_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_build_published')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.build_lists.build_published_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.build_lists.build_published_count | number }}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
.row
|
||||
.col-md-12
|
||||
h3.text-info
|
||||
= t('.header')
|
||||
|
||||
.row
|
||||
.col-md-8
|
||||
.graph-wrapper
|
||||
h4
|
||||
span.graph-key-color1>
|
||||
= t('.commits_title')
|
||||
.text-center.graph-loading ng-show='loading'
|
||||
= image_tag 'loading-large.gif'
|
||||
.text-center.no-data ng-hide='loading || statistics.commits'
|
||||
= t('.no_data')
|
||||
canvas#commits_chart ng-show='statistics.commits'
|
||||
|
||||
.col-md-3
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_commits')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.commits.commits_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.commits.commits_count | number }}
|
|
@ -1,14 +0,0 @@
|
|||
.row
|
||||
.span11.flash_notify
|
||||
|
||||
%h3.text-info
|
||||
= t('.header')
|
||||
|
||||
= form_tag '#', class: 'form-inline alert alert-info centered', id: 'range-form' do
|
||||
%label.control-label
|
||||
= t('.range_label')
|
||||
= select_tag 'range', statistics_range_options, id: 'range_select', class: 'select input-medium', ng_model: 'range', ng_change: 'rangeChange()'
|
||||
%span{ ng_show: "range == 'custom'" }
|
||||
= text_field_tag :range_start, Date.today - 1.month, class: 'date_picker input-medium', placeholder: t('.range_start_placeholder'), ng_model: 'range_start', ng_change: 'rangeChange()', readonly: true
|
||||
= t('.range_separator')
|
||||
= text_field_tag :range_end, Date.today, class: 'date_picker input-medium', placeholder: t('.range_end_placeholder'), ng_model: 'range_end', ng_change: 'rangeChange()', readonly: true
|
|
@ -0,0 +1,65 @@
|
|||
.row
|
||||
.col-md-12
|
||||
|
||||
h3.text-info
|
||||
= t('.header')
|
||||
|
||||
form#statistics-form.form-inline.alert.alert-info.centered
|
||||
.form-group>
|
||||
label.control-label>
|
||||
= t('.range_label')
|
||||
= select_tag 'range', statistics_range_options, id: 'range_select', class: 'form-control input-medium', ng_model: 'range', ng_change: 'update()', ng_disabled: 'loading'
|
||||
.form-group ng-show="range == 'custom'"
|
||||
|
||||
.input-group
|
||||
= text_field_tag :range_start, Date.today - 1.month,
|
||||
class: 'form-control',
|
||||
ng_model: 'range_start',
|
||||
ng_change: 'update()',
|
||||
size: 7,
|
||||
readonly: true,
|
||||
ng_disabled: 'loading',
|
||||
datepicker_popup: 'yyyy-MM-dd',
|
||||
datepicker_options: 'dateOptions',
|
||||
is_open: 'range_start_opened',
|
||||
show_button_bar: false
|
||||
|
||||
span.input-group-btn
|
||||
button.btn.btn-default[
|
||||
type = 'button'
|
||||
ng_disabled = 'loading'
|
||||
ng-click = 'openRangeStart($event)' ]
|
||||
|
||||
i.glyphicon.glyphicon-calendar
|
||||
|
||||
= t('.range_separator')
|
||||
|
||||
.input-group
|
||||
= text_field_tag :range_end, Date.today,
|
||||
class: 'form-control',
|
||||
ng_model: 'range_end',
|
||||
ng_change: 'update()',
|
||||
size: 7,
|
||||
ng_disabled: 'loading',
|
||||
readonly: true,
|
||||
datepicker_popup: 'yyyy-MM-dd',
|
||||
datepicker_options: 'dateOptions',
|
||||
is_open: 'range_end_opened',
|
||||
show_button_bar: false
|
||||
|
||||
span.input-group-btn
|
||||
button.btn.btn-default[
|
||||
type = 'button'
|
||||
ng_disabled = 'loading'
|
||||
ng-click = 'openRangeEnd($event)' ]
|
||||
i.glyphicon.glyphicon-calendar
|
||||
|
||||
|
|
||||
.form-group>
|
||||
label.control-label>
|
||||
= t('.users_or_groups_label')
|
||||
= autocomplete_field_tag 'users_or_groups', nil,autocomplete_user_or_group_autocompletes_path, multiple: true, 'data-delimiter' => ', ', ng_model: 'users_or_groups', placeholder: t('.users_or_groups_placeholder'), ng_disabled: 'loading', class: 'form-control'
|
||||
|
||||
a href='#' ng-click='update()'
|
||||
b
|
||||
= t('.refresh')
|
|
@ -0,0 +1,48 @@
|
|||
.row
|
||||
.col-md-12
|
||||
h3.text-info
|
||||
= t('.header')
|
||||
|
||||
.row
|
||||
.col-md-8
|
||||
.graph-wrapper
|
||||
h4
|
||||
span.graph-key-color1>
|
||||
= t('.open_title')
|
||||
span.graph-key-color2>
|
||||
= t('.reopen_title')
|
||||
span.graph-key-color3>
|
||||
= t('.closed_title')
|
||||
.text-center.graph-loading ng-show='loading'
|
||||
= image_tag 'loading-large.gif'
|
||||
.text-center.no-data ng-hide='loading || statistics.issues'
|
||||
= t('.no_data')
|
||||
canvas#issues_chart ng-show='statistics.issues'
|
||||
|
||||
.col-md-3
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_open')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.issues.open_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.issues.open_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_reopen')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.issues.reopen_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.issues.reopen_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_closed')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.issues.closed_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.issues.closed_count | number }}
|
|
@ -0,0 +1,48 @@
|
|||
.row
|
||||
.col-md-12
|
||||
h3.text-info
|
||||
= t('.header')
|
||||
|
||||
.row
|
||||
.col-md-8
|
||||
.graph-wrapper
|
||||
h4
|
||||
span.graph-key-color1>
|
||||
= t('.open_title')
|
||||
span.graph-key-color2>
|
||||
= t('.merged_title')
|
||||
span.graph-key-color3>
|
||||
= t('.closed_title')
|
||||
.text-center.graph-loading ng-show='loading'
|
||||
= image_tag 'loading-large.gif'
|
||||
.text-center.no-data ng-hide='loading || statistics.pull_requests'
|
||||
= t('.no_data')
|
||||
canvas#pull_requests_chart ng-show='statistics.pull_requests'
|
||||
|
||||
.col-md-3
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_open')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.pull_requests.open_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.pull_requests.open_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_merged')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.pull_requests.merged_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.pull_requests.merged_count | number }}
|
||||
|
||||
.panel-wrapper
|
||||
h4
|
||||
= t('.total_closed')
|
||||
.panel-data
|
||||
= image_tag 'loading-small.gif', ng_show: 'loading'
|
||||
.no-data ng-hide='loading || statistics.pull_requests.closed_count >= 0'
|
||||
= t('.no_data')
|
||||
| {{ statistics.pull_requests.closed_count | number }}
|
|
@ -1,7 +0,0 @@
|
|||
- set_meta_tags title: t('.header')
|
||||
|
||||
#manage-statistics{ ng_controller: 'StatisticsController', ng_init: 'init()' }
|
||||
|
||||
= render 'filter'
|
||||
= render 'build_lists'
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
- set_meta_tags title: t('.header')
|
||||
|
||||
.container#manage-statistics ng-controller='StatisticsController' ng-init='init()'
|
||||
|
||||
= render 'filter'
|
||||
= render 'build_lists'
|
||||
= render 'commits'
|
||||
= render 'issues'
|
||||
= render 'pull_requests'
|
||||
|
|
@ -9,9 +9,13 @@ en:
|
|||
range_start_placeholder: "Select a start date"
|
||||
range_separator: " and "
|
||||
range_end_placeholder: "Select an end date"
|
||||
no_data: "No data available"
|
||||
|
||||
users_or_groups_label: "for users or groups"
|
||||
refresh: "refresh"
|
||||
users_or_groups_placeholder: "Enter a nickname here."
|
||||
|
||||
build_lists:
|
||||
header: "Build lists"
|
||||
build_started_title: "Build started"
|
||||
success_title: "Build complete"
|
||||
build_error_title: "Build error"
|
||||
|
@ -21,6 +25,35 @@ en:
|
|||
total_success: "Total build complete"
|
||||
total_build_error: "Total build error"
|
||||
total_build_published: "Total build has been published"
|
||||
no_data: "No data available"
|
||||
|
||||
commits:
|
||||
header: "Commits"
|
||||
commits_title: "Commits"
|
||||
total_commits: "Total commits"
|
||||
no_data: "No data available"
|
||||
|
||||
issues:
|
||||
header: "Issues"
|
||||
open_title: "Open"
|
||||
reopen_title: "Reopen"
|
||||
closed_title: "Closed"
|
||||
|
||||
total_open: "Total opened"
|
||||
total_reopen: "Total reopened"
|
||||
total_closed: "Total closed"
|
||||
no_data: "No data available"
|
||||
|
||||
pull_requests:
|
||||
header: "Pull Requests"
|
||||
open_title: "Open"
|
||||
merged_title: "Merged"
|
||||
closed_title: "Closed"
|
||||
|
||||
total_open: "Total open"
|
||||
total_merged: "Total merged"
|
||||
total_closed: "Total closed"
|
||||
no_data: "No data available"
|
||||
|
||||
helper:
|
||||
period:
|
||||
|
|
|
@ -9,18 +9,51 @@ ru:
|
|||
range_start_placeholder: "Выберите начальную дату"
|
||||
range_separator: " и "
|
||||
range_end_placeholder: "Выберите конечную дату"
|
||||
no_data: "Нет данных"
|
||||
|
||||
users_or_groups_label: "для пользователей или групп"
|
||||
refresh: "обновить"
|
||||
users_or_groups_placeholder: "Введите никнейм здесь."
|
||||
|
||||
build_lists:
|
||||
header: "Сборочные листы"
|
||||
build_started_title: "Cобирается"
|
||||
success_title: "Cобрано"
|
||||
build_error_title: "Ошибка сборки"
|
||||
build_published_title: "Опубликовано"
|
||||
|
||||
total_build_started: "Всего собирается"
|
||||
total_build_started: "Всего запущено"
|
||||
total_success: "Всего собрано"
|
||||
total_build_error: "Всего ошибок сборки"
|
||||
total_build_published: "Всего опубликовано"
|
||||
no_data: "Нет данных"
|
||||
|
||||
commits:
|
||||
header: "Коммиты"
|
||||
commits_title: "Коммиты"
|
||||
total_commits: "Всего коммитов"
|
||||
no_data: "Нет данных"
|
||||
|
||||
issues:
|
||||
header: "Задачи"
|
||||
open_title: "Открыто"
|
||||
reopen_title: "Переоткрыто"
|
||||
closed_title: "Закрыто"
|
||||
|
||||
total_open: "Всего открыто"
|
||||
total_reopen: "Всего переоткрыто"
|
||||
total_closed: "Всего закрыто"
|
||||
no_data: "Нет данных"
|
||||
|
||||
pull_requests:
|
||||
header: "Пулл реквесты"
|
||||
open_title: "Открыто"
|
||||
merged_title: "Принято"
|
||||
closed_title: "Закрыто"
|
||||
|
||||
total_open: "Всего открыто"
|
||||
total_merged: "Всего принято"
|
||||
total_closed: "Всего закрыто"
|
||||
no_data: "Нет данных"
|
||||
|
||||
helper:
|
||||
period:
|
||||
|
|
|
@ -233,6 +233,7 @@ Rosa::Application.routes.draw do
|
|||
get :autocomplete_extra_build_list
|
||||
get :autocomplete_extra_mass_build
|
||||
get :autocomplete_extra_repositories
|
||||
get :autocomplete_user_or_group
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
FactoryGirl.define do
|
||||
factory :pull_request do
|
||||
title { FactoryGirl.generate(:string) }
|
||||
body { FactoryGirl.generate(:string) }
|
||||
association :project, factory: :project
|
||||
association :user, factory: :user
|
||||
association :assignee, factory: :user
|
||||
association :issue, factory: :issue
|
||||
association :from_project, factory: :project
|
||||
association :to_project, factory: :project
|
||||
from_ref { FactoryGirl.generate(:string) }
|
||||
to_ref { FactoryGirl.generate(:string) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,4 +12,10 @@ describe BuildListObserver do
|
|||
expect(build_list.started_at).to_not be_nil
|
||||
end
|
||||
|
||||
it 'updates styatistics' do
|
||||
expect do
|
||||
build_list
|
||||
end.to change(Statistic, :count).by(1)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -291,7 +291,7 @@ describe BuildList do
|
|||
|
||||
end
|
||||
|
||||
describe '#can_publish?' do
|
||||
context '#can_publish?' do
|
||||
let(:build_list) { FactoryGirl.create(:build_list) }
|
||||
|
||||
before do
|
||||
|
@ -319,7 +319,7 @@ describe BuildList do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#can_publish_into_testing?' do
|
||||
context '#can_publish_into_testing?' do
|
||||
let(:build_list) { FactoryGirl.create(:build_list) }
|
||||
|
||||
before do
|
||||
|
@ -337,4 +337,14 @@ describe BuildList do
|
|||
end
|
||||
end
|
||||
|
||||
context '#prepare_extra_params' do
|
||||
let(:build_list) { FactoryGirl.build(:build_list) }
|
||||
|
||||
it 'removes unsafe symbols' do
|
||||
build_list.extra_params = { 'build_rpm' => '--test \'001\' --define "cross armv7hl"{(@' }
|
||||
build_list.send :prepare_extra_params
|
||||
expect(build_list.extra_params['build_rpm']).to eq '--test 001 --define "cross armv7hl"'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -16,6 +16,16 @@ describe Issue do
|
|||
stub_symlink_methods
|
||||
any_instance_of(Project, versions: ['v1.0', 'v2.0'])
|
||||
end
|
||||
|
||||
|
||||
context '#update_statistic' do
|
||||
it 'updates styatistics' do
|
||||
expect do
|
||||
FactoryGirl.create(:issue)
|
||||
end.to change(Statistic, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for project admin user' do
|
||||
before(:each) do
|
||||
set_data
|
||||
|
|
|
@ -14,9 +14,10 @@ def set_data_for_pull
|
|||
end
|
||||
|
||||
describe PullRequest do
|
||||
before { stub_symlink_methods }
|
||||
|
||||
context 'for owner user' do
|
||||
before do
|
||||
stub_symlink_methods
|
||||
@user = FactoryGirl.create(:user)
|
||||
set_data_for_pull
|
||||
@pull = @project.pull_requests.new(issue_attributes: {title: 'test', body: 'testing'})
|
||||
|
@ -127,6 +128,20 @@ describe PullRequest do
|
|||
it { should belong_to(:to_project) }
|
||||
it { should belong_to(:from_project) }
|
||||
|
||||
context '#update_statistic' do
|
||||
let(:issue) { FactoryGirl.build(:issue) }
|
||||
let(:pull_request) { FactoryGirl.build(:pull_request, issue: issue) }
|
||||
|
||||
it 'updates styatistics' do
|
||||
allow(PullRequest).to receive(:check_ref).and_return(true)
|
||||
issue.new_pull_request = true
|
||||
expect do
|
||||
pull_request.save
|
||||
end.to change(Statistic, :count).by(1)
|
||||
expect(Statistic.last.key).to eq "#{Statistic::KEY_PULL_REQUEST}.#{Issue::STATUS_OPEN}"
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(APP_CONFIG['root_path'])
|
||||
FileUtils.rm_rf File.join(Rails.root, "tmp", Rails.env, "pull_requests")
|
||||
|
|
|
@ -42,4 +42,19 @@ describe Statistic do
|
|||
end
|
||||
end
|
||||
|
||||
context '#for_groups' do
|
||||
it 'returns projects by group ids' do
|
||||
group1 = FactoryGirl.create(:group)
|
||||
group2 = FactoryGirl.create(:group)
|
||||
project1 = FactoryGirl.create(:project, owner: group1)
|
||||
project2 = FactoryGirl.create(:project, owner: group2)
|
||||
|
||||
FactoryGirl.create(:statistic, project: project1)
|
||||
FactoryGirl.create(:statistic, project: project2)
|
||||
|
||||
expect(Statistic.for_groups([group1.id])).to have(1).item
|
||||
expect(Statistic.for_groups([group1.id, group2])).to have(2).items
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue