diff --git a/app/assets/javascripts/angularjs/controllers/statistics_controller.js.coffee b/app/assets/javascripts/angularjs/controllers/statistics_controller.js.coffee index 6ec964d1b..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) -> @@ -102,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" @@ -137,15 +149,15 @@ RosaABF.controller 'StatisticsController', ['$scope', '$http', ($scope, $http) - $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/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 aec8143bc..562a9bb53 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..e715713c8 100644 --- a/app/controllers/statistics_controller.rb +++ b/app/controllers/statistics_controller.rb @@ -16,7 +16,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/issue.rb b/app/models/issue.rb index 7c0fd8c64..364d6a5fd 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1,16 +1,38 @@ 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]+)/ 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 @@ -19,21 +41,28 @@ 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)') } + attr_accessor :new_pull_request + def assign_uname assignee.uname if assignee end @@ -43,24 +72,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 @@ -69,12 +98,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 @@ -82,6 +111,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 8d5d02dc3..c4357b0e0 100644 --- a/app/models/statistic.rb +++ b/app/models/statistic.rb @@ -5,7 +5,15 @@ class Statistic < ActiveRecord::Base 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_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 @@ -41,14 +49,38 @@ 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: 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, counter: 1) diff --git a/app/presenters/statistic_presenter.rb b/app/presenters/statistic_presenter.rb index 1c371bc26..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) @@ -24,18 +25,69 @@ class StatisticPresenter < ApplicationPresenter 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 user_ids + @user_ids ||= User.where(uname: users_or_groups).pluck(:id) + end + + 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 diff --git a/app/views/statistics/_filter.html.haml b/app/views/statistics/_filter.html.haml index 943e7eece..336057665 100644 --- a/app/views/statistics/_filter.html.haml +++ b/app/views/statistics/_filter.html.haml @@ -4,11 +4,19 @@ %h3.text-info = t('.header') - = form_tag '#', class: 'form-inline alert alert-info centered', id: 'range-form' do + %form#statistics-form.form-inline.alert.alert-info.centered %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()' + = select_tag 'range', statistics_range_options, id: 'range_select', class: 'select input-medium', ng_model: 'range', ng_change: 'update()', ng_disabled: 'loading' %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 + = 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: 'update()', readonly: true, size: 10, ng_disabled: 'loading' = 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 + = text_field_tag :range_end, Date.today, class: 'date_picker input-medium', placeholder: t('.range_end_placeholder'), ng_model: 'range_end', ng_change: 'update()', readonly: true, size: 10, ng_disabled: 'loading' + + %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' + + %a{ href: '#', ng_click: 'update()' } + %b + = t('.refresh') diff --git a/app/views/statistics/_issues.html.haml b/app/views/statistics/_issues.html.haml new file mode 100644 index 000000000..cafd8892a --- /dev/null +++ b/app/views/statistics/_issues.html.haml @@ -0,0 +1,48 @@ +.row + .span12 + %h3.text-info + = t('.header') + +.row + .span8 + .graph-wrapper + %h3 + %span.graph-key-color1 + = t('.open_title') + %span.graph-key-color2 + = t('.reopen_title') + %span.graph-key-color3 + = t('.closed_title') + .centered.graph-loading{ ng_show: 'loading' } + = image_tag 'loading-large.gif' + .no-data{ ng_hide: 'loading || statistics.issues' } + = t('.no_data') + %canvas#issues_chart{ ng_show: 'statistics.issues' } + + .span3 + .panel-wrapper + %h3 + = 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 + %h3 + = 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 + %h3 + = 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.haml b/app/views/statistics/_pull_requests.html.haml new file mode 100644 index 000000000..efc8f4254 --- /dev/null +++ b/app/views/statistics/_pull_requests.html.haml @@ -0,0 +1,48 @@ +.row + .span12 + %h3.text-info + = t('.header') + +.row + .span8 + .graph-wrapper + %h3 + %span.graph-key-color1 + = t('.open_title') + %span.graph-key-color2 + = t('.merged_title') + %span.graph-key-color3 + = t('.closed_title') + .centered.graph-loading{ ng_show: 'loading' } + = image_tag 'loading-large.gif' + .no-data{ ng_hide: 'loading || statistics.pull_requests' } + = t('.no_data') + %canvas#pull_requests_chart{ ng_show: 'statistics.pull_requests' } + + .span3 + .panel-wrapper + %h3 + = 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 + %h3 + = 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 + %h3 + = 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 index 4e4883cb9..a0208abef 100644 --- a/app/views/statistics/index.html.haml +++ b/app/views/statistics/index.html.haml @@ -5,4 +5,6 @@ = 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 debcb6200..5f38fcb49 100644 --- a/config/locales/models/statistics.en.yml +++ b/config/locales/models/statistics.en.yml @@ -10,6 +10,10 @@ en: range_separator: " and " range_end_placeholder: "Select an end date" + 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" @@ -29,6 +33,28 @@ en: 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: twenty_four_hours: "the last 24 hours" diff --git a/config/locales/models/statistics.ru.yml b/config/locales/models/statistics.ru.yml index 2277ebae2..2f3d5eb13 100644 --- a/config/locales/models/statistics.ru.yml +++ b/config/locales/models/statistics.ru.yml @@ -10,6 +10,10 @@ ru: range_separator: " и " range_end_placeholder: "Выберите конечную дату" + users_or_groups_label: "для пользователей или групп" + refresh: "обновить" + users_or_groups_placeholder: "Введите никнейм здесь." + build_lists: header: "Сборочные листы" build_started_title: "Cобирается" @@ -29,6 +33,28 @@ ru: 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: twenty_four_hours: "последние 24 часа" diff --git a/config/routes.rb b/config/routes.rb index 9c7b3a5fb..a8137f8ba 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/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