diff --git a/Gemfile b/Gemfile index 5ce860275..59c8f6051 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index 38cd1b653..39a225c18 100644 --- a/Gemfile.lock +++ b/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 diff --git a/app/assets/javascripts/angular-new/controllers/statistics_controller.js.coffee b/app/assets/javascripts/angular-new/controllers/statistics_controller.js.coffee new file mode 100644 index 000000000..7aff56bd4 --- /dev/null +++ b/app/assets/javascripts/angular-new/controllers/statistics_controller.js.coffee @@ -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 + ] + +] \ No newline at end of file diff --git a/app/assets/javascripts/angularjs/controllers/statistics_controller.js.coffee b/app/assets/javascripts/angularjs/controllers/statistics_controller.js.coffee index ac0837712..b9345e143 100644 --- a/app/assets/javascripts/angularjs/controllers/statistics_controller.js.coffee +++ b/app/assets/javascripts/angularjs/controllers/statistics_controller.js.coffee @@ -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 + 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 ] ] \ No newline at end of file diff --git a/app/assets/javascripts/new_application.js b/app/assets/javascripts/new_application.js index 607e2b09a..28c4872de 100644 --- a/app/assets/javascripts/new_application.js +++ b/app/assets/javascripts/new_application.js @@ -35,6 +35,8 @@ //= require zeroclipboard +//= require lib/Chart + //= require_self function setCookie (name, value, expires, path, domain, secure) { diff --git a/app/assets/stylesheets/views/statistics.css.sass b/app/assets/stylesheets/views/statistics.css.sass new file mode 100644 index 000000000..2810cb0c5 --- /dev/null +++ b/app/assets/stylesheets/views/statistics.css.sass @@ -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 diff --git a/app/controllers/api/v1/pull_requests_controller.rb b/app/controllers/api/v1/pull_requests_controller.rb index 9c3054f35..7dc35feca 100644 --- a/app/controllers/api/v1/pull_requests_controller.rb +++ b/app/controllers/api/v1/pull_requests_controller.rb @@ -49,10 +49,11 @@ class Api::V1::PullRequestsController < Api::V1::BaseController @pull = @project.pull_requests.new @pull.build_issue title: pull_params[:title], body: pull_params[:body] - @pull.from_project = @project - @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.from_project = @project + @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 diff --git a/app/controllers/autocompletes_controller.rb b/app/controllers/autocompletes_controller.rb index 89715ee36..9a1741c4a 100644 --- a/app/controllers/autocompletes_controller.rb +++ b/app/controllers/autocompletes_controller.rb @@ -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, diff --git a/app/controllers/projects/pull_requests_controller.rb b/app/controllers/projects/pull_requests_controller.rb index c916f4e48..666f2c0aa 100644 --- a/app/controllers/projects/pull_requests_controller.rb +++ b/app/controllers/projects/pull_requests_controller.rb @@ -39,8 +39,9 @@ class Projects::PullRequestsController < Projects::BaseController @pull = to_project.pull_requests.new pull_params @pull.issue.assignee_id = (params[:issue] || {})[:assignee_id] if can?(:write, to_project) @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.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 diff --git a/app/controllers/statistics_controller.rb b/app/controllers/statistics_controller.rb index fed34f05c..e3e324789 100644 --- a/app/controllers/statistics_controller.rb +++ b/app/controllers/statistics_controller.rb @@ -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 diff --git a/app/models/build_list.rb b/app/models/build_list.rb index 214ea3acf..f2282d2a0 100644 --- a/app/models/build_list.rb +++ b/app/models/build_list.rb @@ -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 diff --git a/app/models/concerns/build_list_observer.rb b/app/models/concerns/build_list_observer.rb index 2ff664d5b..3cd62edcb 100644 --- a/app/models/concerns/build_list_observer.rb +++ b/app/models/concerns/build_list_observer.rb @@ -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? diff --git a/app/models/concerns/feed/git.rb b/app/models/concerns/feed/git.rb index 2f2a4a771..b79c3618f 100644 --- a/app/models/concerns/feed/git.rb +++ b/app/models/concerns/feed/git.rb @@ -20,9 +20,9 @@ module Feed::Git last_commits, commits = [[record.newrev, record.message.truncate(70, omission: '…')]], [] all_commits = last_commits else - commits = record.project.repo.commits_between(record.oldrev, record.newrev) - all_commits = commits.collect { |commit| [commit.sha, commit.message.truncate(70, omission: '…')] } - last_commits = all_commits.last(3).reverse + commits = record.project.repo.commits_between(record.oldrev, record.newrev) + all_commits = commits.collect { |commit| [commit.sha, commit.message.truncate(70, omission: '…')] } + last_commits = all_commits.last(3).reverse end kind = 'git_new_push_notification' @@ -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 diff --git a/app/models/issue.rb b/app/models/issue.rb index 6ae953746..3b19de862 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1,17 +1,39 @@ class Issue < ActiveRecord::Base include Feed::Issue - STATUSES = ['open', 'closed'] - self.per_page = 20 + + 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 :not_closed_or_merged, -> { needed_checking } - scope :closed_or_merged, -> { where(issues: {status: ['closed', 'merged']}) } + 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: %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.closed_at = Time.now.utc + self.closer = closed_by + self.status = STATUS_CLOSED end def set_open - self.closed_at = self.closed_by = nil - self.status = 'open' + self.closed_at = self.closed_by = nil + self.status = STATUS_REOPEN end def collect_recipients @@ -71,12 +100,12 @@ 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]+)/ - 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] - project = Project.find_by_owner_and_name(owner_uname.chomp('/'), project_name) + 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] + project = Project.find_by_owner_and_name(owner_uname.chomp('/'), project_name) return nil unless project return nil unless current_ability.can? :show, project project.issues.where(serial_id: serial_id).first @@ -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! diff --git a/app/models/pull_request.rb b/app/models/pull_request.rb index 843012cd5..2d485dfce 100644 --- a/app/models/pull_request.rb +++ b/app/models/pull_request.rb @@ -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 :not_closed_or_merged, -> { needed_checking } - scope :closed_or_merged, -> { where(issues: {status: ['closed', 'merged']}) } + 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: [STATUS_CLOSED, STATUS_MERGED] }) } state_machine :status, initial: :open do event :ready do @@ -53,8 +83,8 @@ class PullRequest < ActiveRecord::Base FileUtils.mv path(old_from_project_name), path, force: true if old_from_project_name return unless Dir.exists?(path) Dir.chdir(path) do - system 'git', 'remote', 'set-url', 'origin', to_project.path - system 'git', 'remote', 'set-url', 'head', from_project.path if cross_pull? + system 'git', 'remote', 'set-url', 'origin', to_project.path + system 'git', 'remote', 'set-url', 'head', from_project.path if cross_pull? end end later :update_relations, queue: :middle @@ -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 @@ -140,7 +170,7 @@ class PullRequest < ActiveRecord::Base def set_user_and_time user issue.closed_at = Time.now.utc - issue.closer = user + issue.closer = user end def self.check_ref(record, attr, value) @@ -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 @@ -243,6 +272,6 @@ class PullRequest < ActiveRecord::Base def set_add_data self.from_project_owner_uname = from_project.owner.uname - self.from_project_name = from_project.name + self.from_project_name = from_project.name end end diff --git a/app/models/statistic.rb b/app/models/statistic.rb index 2a132b4ba..c4357b0e0 100644 --- a/app/models/statistic.rb +++ b/app/models/statistic.rb @@ -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 = {}) diff --git a/app/presenters/statistic_presenter.rb b/app/presenters/statistic_presenter.rb index dafe97563..580a19d5c 100644 --- a/app/presenters/statistic_presenter.rb +++ b/app/presenters/statistic_presenter.rb @@ -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) - @range_start = range_start - @range_end = range_end - @unit = unit + 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) diff --git a/app/views/statistics/_build_lists.html.haml b/app/views/statistics/_build_lists.html.haml deleted file mode 100644 index 789086f17..000000000 --- a/app/views/statistics/_build_lists.html.haml +++ /dev/null @@ -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 }} - diff --git a/app/views/statistics/_build_lists.html.slim b/app/views/statistics/_build_lists.html.slim new file mode 100644 index 000000000..2f8598704 --- /dev/null +++ b/app/views/statistics/_build_lists.html.slim @@ -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 }} + diff --git a/app/views/statistics/_commits.html.slim b/app/views/statistics/_commits.html.slim new file mode 100644 index 000000000..34f989fcb --- /dev/null +++ b/app/views/statistics/_commits.html.slim @@ -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 }} diff --git a/app/views/statistics/_filter.html.haml b/app/views/statistics/_filter.html.haml deleted file mode 100644 index 943e7eece..000000000 --- a/app/views/statistics/_filter.html.haml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/views/statistics/_filter.html.slim b/app/views/statistics/_filter.html.slim new file mode 100644 index 000000000..0b6b16a5a --- /dev/null +++ b/app/views/statistics/_filter.html.slim @@ -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') diff --git a/app/views/statistics/_issues.html.slim b/app/views/statistics/_issues.html.slim new file mode 100644 index 000000000..c96042904 --- /dev/null +++ b/app/views/statistics/_issues.html.slim @@ -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 }} diff --git a/app/views/statistics/_pull_requests.html.slim b/app/views/statistics/_pull_requests.html.slim new file mode 100644 index 000000000..5ed5fce6e --- /dev/null +++ b/app/views/statistics/_pull_requests.html.slim @@ -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 }} diff --git a/app/views/statistics/index.html.haml b/app/views/statistics/index.html.haml deleted file mode 100644 index e3d5a6d20..000000000 --- a/app/views/statistics/index.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- set_meta_tags title: t('.header') - -#manage-statistics{ ng_controller: 'StatisticsController', ng_init: 'init()' } - - = render 'filter' - = render 'build_lists' - diff --git a/app/views/statistics/index.html.slim b/app/views/statistics/index.html.slim new file mode 100644 index 000000000..279b43d52 --- /dev/null +++ b/app/views/statistics/index.html.slim @@ -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' + diff --git a/config/locales/models/statistics.en.yml b/config/locales/models/statistics.en.yml index 0246ffe98..5f38fcb49 100644 --- a/config/locales/models/statistics.en.yml +++ b/config/locales/models/statistics.en.yml @@ -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: diff --git a/config/locales/models/statistics.ru.yml b/config/locales/models/statistics.ru.yml index 53233f3c7..dae355c64 100644 --- a/config/locales/models/statistics.ru.yml +++ b/config/locales/models/statistics.ru.yml @@ -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: diff --git a/config/routes.rb b/config/routes.rb index a1dc1e8b7..39b5f20d8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/spec/factories/pull_request.rb b/spec/factories/pull_request.rb index 60e1e8c44..91a13c5f0 100644 --- a/spec/factories/pull_request.rb +++ b/spec/factories/pull_request.rb @@ -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 diff --git a/spec/models/build_list_observer_spec.rb b/spec/models/build_list_observer_spec.rb index dbedfe1b2..76b9c2641 100644 --- a/spec/models/build_list_observer_spec.rb +++ b/spec/models/build_list_observer_spec.rb @@ -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 diff --git a/spec/models/build_list_spec.rb b/spec/models/build_list_spec.rb index 36929f6f3..48a156bda 100644 --- a/spec/models/build_list_spec.rb +++ b/spec/models/build_list_spec.rb @@ -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 diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index c9c1e4a93..5cd1667c6 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -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 diff --git a/spec/models/pull_request_spec.rb b/spec/models/pull_request_spec.rb index b68ac98ab..0d0e01275 100644 --- a/spec/models/pull_request_spec.rb +++ b/spec/models/pull_request_spec.rb @@ -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") diff --git a/spec/models/statistic_spec.rb b/spec/models/statistic_spec.rb index 180cac481..e17ea638e 100644 --- a/spec/models/statistic_spec.rb +++ b/spec/models/statistic_spec.rb @@ -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