diff --git a/.gitignore b/.gitignore index d0186c4a3..6af1a6cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Ignore lock config file *.log +.rubocop.yml .env # mac *.DS_Store diff --git a/Gemfile b/Gemfile index 4e7d9d77b..442da5fab 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem 'roo-xls' gem 'simple_xlsx_reader', '~>1.0.4' gem 'rubyzip' - +gem 'sonarqube', :git => 'https://gitlink.org.cn/KingChan/sonarqube.git' gem 'spreadsheet' gem 'ruby-ole' # 导出为xlsx @@ -70,6 +70,7 @@ group :development do gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' gem 'spring' + gem 'pry-rails' gem 'spring-watcher-listen', '~> 2.0.0' gem "annotate", "~> 2.6.0" end @@ -114,7 +115,6 @@ gem 'aasm' gem 'enumerize' gem 'diffy' - gem 'deep_cloneable', '~> 3.0.0' # oauth2 @@ -142,3 +142,5 @@ gem 'doorkeeper' gem 'doorkeeper-jwt' gem 'gitea-client', '~> 1.5.8' + +gem 'loofah', '~> 2.20.0' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index b7f060b2a..4dff7c2ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: https://gitlink.org.cn/KingChan/sonarqube.git + revision: 80f07d427322ef02c0714c77a382e87aed0bef81 + specs: + sonarqube (1.3.0) + httparty (~> 0.14, >= 0.14.0) + terminal-table (~> 1.5, >= 1.5.1) + GEM remote: https://mirrors.cloud.tencent.com/rubygems/ specs: @@ -135,7 +143,7 @@ GEM fugit (1.4.1) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.4) - gitea-client (1.4.2) + gitea-client (1.4.6) rest-client (~> 2.1.0) globalid (0.4.2) activesupport (>= 4.2.0) @@ -150,6 +158,9 @@ GEM http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) + httparty (0.21.0) + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) i18n (1.8.2) concurrent-ruby (~> 1.0) io-like (0.3.1) @@ -187,9 +198,9 @@ GEM mimemagic (~> 0.3.2) maruku (0.7.3) method_source (0.9.2) - mime-types (3.4.1) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2023.0218.1) + mime-types-data (3.2024.0507) mimemagic (0.3.10) nokogiri (~> 1) rake @@ -245,13 +256,12 @@ GEM powerpack (0.1.2) prettier (0.18.2) public_suffix (4.0.3) - puma (3.12.2) + puma (5.6.8) + nio4r (~> 2.0) raabro (1.4.0) rack (2.0.9) rack-cors (1.1.1) rack (>= 2.0.0) - rack-mini-profiler (2.0.1) - rack (>= 1.2.0) rack-protection (2.0.8.1) rack rack-test (1.1.0) @@ -438,6 +448,8 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) thor (1.0.1) thread_safe (0.3.6) tilt (2.0.10) @@ -450,7 +462,7 @@ GEM execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext - unf_ext (0.0.8.2) + unf_ext (0.0.9.1) unicode-display_width (1.6.1) web-console (3.7.0) actionview (>= 5.0) @@ -492,7 +504,7 @@ DEPENDENCIES enumerize faraday (~> 0.15.4) font-awesome-sass (= 4.7.0) - gitea-client (~> 1.4.2) + gitea-client (~> 1.4.3) grape-entity (~> 0.7.1) groupdate (~> 4.1.0) harmonious_dictionary (~> 0.0.1) @@ -514,9 +526,8 @@ DEPENDENCIES parallel (~> 1.19, >= 1.19.1) pdfkit prettier - puma (~> 3.11) + puma (~> 5.6.5) rack-cors - rack-mini-profiler rails (~> 5.2.0) rails-i18n (~> 5.1) ransack @@ -538,9 +549,10 @@ DEPENDENCIES sidekiq-cron (= 1.2.0) sidekiq-failures simple_form - simple_xlsx_reader + simple_xlsx_reader (~> 1.0.4) sinatra solargraph (~> 0.38.0) + sonarqube! spreadsheet spring spring-watcher-listen (~> 2.0.0) diff --git a/app/assets/javascripts/api/pm/issue_links.js b/app/assets/javascripts/api/pm/issue_links.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/api/pm/issue_links.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/api/pm/projects.js b/app/assets/javascripts/api/pm/projects.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/api/pm/projects.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/api/v1/pm_issues.js b/app/assets/javascripts/api/v1/pm_issues.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/api/v1/pm_issues.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/api/v1/sonarqube/issues.js b/app/assets/javascripts/api/v1/sonarqube/issues.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/api/v1/sonarqube/issues.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/api/v1/sonarqubes.js b/app/assets/javascripts/api/v1/sonarqubes.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/api/v1/sonarqubes.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/javascripts/pm/journals.js b/app/assets/javascripts/pm/journals.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/pm/journals.js @@ -0,0 +1,2 @@ +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/app/assets/stylesheets/api/pm/issue_links.scss b/app/assets/stylesheets/api/pm/issue_links.scss new file mode 100644 index 000000000..730f1f3e1 --- /dev/null +++ b/app/assets/stylesheets/api/pm/issue_links.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/pm/issue_links controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/pm/projects.scss b/app/assets/stylesheets/api/pm/projects.scss new file mode 100644 index 000000000..7053c94f2 --- /dev/null +++ b/app/assets/stylesheets/api/pm/projects.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/pm/projects controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/pm_issues.scss b/app/assets/stylesheets/api/v1/pm_issues.scss new file mode 100644 index 000000000..92defb491 --- /dev/null +++ b/app/assets/stylesheets/api/v1/pm_issues.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/pm_issues controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/sonarqube/issues.scss b/app/assets/stylesheets/api/v1/sonarqube/issues.scss new file mode 100644 index 000000000..a74cba738 --- /dev/null +++ b/app/assets/stylesheets/api/v1/sonarqube/issues.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/sonarqube/issues controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/sonarqubes.scss b/app/assets/stylesheets/api/v1/sonarqubes.scss new file mode 100644 index 000000000..8b651fe3a --- /dev/null +++ b/app/assets/stylesheets/api/v1/sonarqubes.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/sonarqubes controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/pm/journals.scss b/app/assets/stylesheets/pm/journals.scss new file mode 100644 index 000000000..45dbf18b4 --- /dev/null +++ b/app/assets/stylesheets/pm/journals.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the pm/journals controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/action/nodes_controller.rb b/app/controllers/action/nodes_controller.rb index e1e7799f4..d85a6094a 100644 --- a/app/controllers/action/nodes_controller.rb +++ b/app/controllers/action/nodes_controller.rb @@ -61,9 +61,9 @@ class Action::NodesController < ApplicationController def node_params if params.require(:action_node) - params.require(:action_node).permit(:name, :full_name, :description, :icon, :action_node_types_id, :is_local, :local_url, :yaml, :sort_no) + params.require(:action_node).permit(:name, :label, :full_name, :description, :icon, :action_node_types_id, :is_local, :local_url, :yaml, :sort_no) else - params.permit(:name, :full_name, :description, :icon, :action_node_types_id, :is_local, :local_url, :yaml, :sort_no) + params.permit(:name, :label, :full_name, :description, :icon, :action_node_types_id, :is_local, :local_url, :yaml, :sort_no) end end end diff --git a/app/controllers/admins/dashboards_controller.rb b/app/controllers/admins/dashboards_controller.rb index 6940ed1cf..37b69dc0d 100644 --- a/app/controllers/admins/dashboards_controller.rb +++ b/app/controllers/admins/dashboards_controller.rb @@ -69,26 +69,26 @@ class Admins::DashboardsController < Admins::BaseController @subject_icon = ["fa-user","fa-git", "fa-sitemap", "fa-warning", "fa-comments", "fa-share-alt", "fa-upload"] @subject_data = [@user_count, @project_count, @organization_count, @issue_count, @comment_count, @pr_count, @commit_count] - - tongji_service = Baidu::TongjiService.new - @access_token = tongji_service.access_token - Rails.logger.info "baidu_tongji_auth access_token ===== #{@access_token}" - # @overview_data = tongji_service.api_overview - last_date = DailyPlatformStatistic.order(:date).last - start_date = last_date.date - end_date = Time.now - if @access_token.present? - @overview_data = Rails.cache.fetch("dashboardscontroller:baidu_tongji:overview_data", expires_in: 10.minutes) do - tongji_service.source_from_batch_add(start_date, end_date) - @overview_data = tongji_service.overview_batch_add(start_date, end_date) - @overview_data + if EduSetting.get("open_baidu_tongji").to_s == "true" + tongji_service = Baidu::TongjiService.new + @access_token = tongji_service.access_token + Rails.logger.info "baidu_tongji_auth access_token ===== #{@access_token}" + # @overview_data = tongji_service.api_overview + last_date = DailyPlatformStatistic.order(:date).last || Time.now + start_date = last_date.date + end_date = Time.now + if @access_token.present? + @overview_data = Rails.cache.fetch("dashboardscontroller:baidu_tongji:overview_data", expires_in: 10.minutes) do + tongji_service.source_from_batch_add(start_date, end_date) + @overview_data = tongji_service.overview_batch_add(start_date, end_date) + @overview_data + end end + + @current_week_statistic = DailyPlatformStatistic.where(date: current_week) + @pre_week_statistic = DailyPlatformStatistic.where(date: pre_week) end - @current_week_statistic = DailyPlatformStatistic.where(date: current_week) - @pre_week_statistic = DailyPlatformStatistic.where(date: pre_week) - - end diff --git a/app/controllers/api/pm/base_controller.rb b/app/controllers/api/pm/base_controller.rb new file mode 100644 index 000000000..f9754f33d --- /dev/null +++ b/app/controllers/api/pm/base_controller.rb @@ -0,0 +1,60 @@ +class Api::Pm::BaseController < ApplicationController + + include Api::ProjectHelper + include Api::UserHelper + include Api::PullHelper + + # before_action :doorkeeper_authorize! + # skip_before_action :user_setup + + protected + + def kaminary_select_paginate(relation) + limit = params[:limit] || params[:per_page] + limit = (limit.to_i.zero? || limit.to_i > 200) ? 200 : limit.to_i + page = params[:page].to_i.zero? ? 1 : params[:page].to_i + + relation.page(page).per(limit) + end + + def limit + params.fetch(:limit, 15) + end + + def page + params.fetch(:page, 1) + end + + def load_project + @project = Project.find_by_id(params[:project_id]) || Project.new(id: 0, user_id: 0, name: 'pm_mm', identifier: 'pm_mm', is_public:true) + end + + def load_issue + return render_parameter_missing if params[:pm_project_id].blank? + @issue = Issue.issue_issue.where(pm_project_id: params[:pm_project_id]).find_by_id(params[:issue_id]) + render_not_found('疑修不存在!') if @issue.blank? + end + # 具有对仓库的管理权限 + def require_manager_above + @project = load_project + return render_forbidden if !current_user.admin? && !@project.manager?(current_user) + end + + # 具有对仓库的操作权限 + def require_operate_above + @project = load_project + return render_forbidden if !current_user.admin? && !@project.operator?(current_user) + end + + # 具有仓库的操作权限或者fork仓库的操作权限 + def require_operate_above_or_fork_project + @project = load_project + return render_forbidden if !current_user.admin? && !@project.operator?(current_user) && !(@project.fork_project.present? && @project.fork_project.operator?(current_user)) + end + + # 具有对仓库的访问权限 + def require_public_and_member_above + @project = load_project + return render_forbidden if !@project.is_public && !current_user.admin? && !@project.member?(current_user) + end +end \ No newline at end of file diff --git a/app/controllers/api/pm/issue_links_controller.rb b/app/controllers/api/pm/issue_links_controller.rb new file mode 100644 index 000000000..867dcaf41 --- /dev/null +++ b/app/controllers/api/pm/issue_links_controller.rb @@ -0,0 +1,46 @@ +class Api::Pm::IssueLinksController < Api::Pm::BaseController + before_action :load_project + before_action :load_issue + def index + @links = PmLink.where(be_linkable_id: @issue.id,be_linkable_type: 'Issue').or(PmLink.where(linkable_id: @issue.id,linkable_type: 'Issue')) + end + + def create + begin + ActiveRecord::Base.transaction do + params[:link_ids].each do |e| + @issue.pm_links.find_or_create_by!(be_linkable_type: 'Issue', be_linkable_id: e) + tag_issue = Issue.find_by_id(e) + next unless tag_issue.present? + journal = tag_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "tag_link_issue", prop_key: "1", value: @issue.id.to_s}) + end + journal = @issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "tag_link_issue", prop_key: "#{params[:link_ids].size}", value: params[:link_ids].join(",")}) + end + render_ok + rescue + render_error('创建失败!') + end + end + + def destroy + begin + ActiveRecord::Base.transaction do + @links = PmLink.where(be_linkable_id: @issue.id, be_linkable_type: 'Issue', linkable_id: params[:id], linkable_type: 'Issue').or(PmLink.where(linkable_id: @issue.id, linkable_type: 'Issue', be_linkable_id: params[:id], be_linkable_type: 'Issue')) + journal = @issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "tag_link_issue", prop_key: "1", old_value: params[:id].to_s}) + another_issue = Issue.find_by_id(params[:id]) + if another_issue.present? + journal = another_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "tag_link_issue", prop_key: "1", old_value: @issue.id.to_s}) + end + @link = @links.last + @link.destroy! + end + render_ok + rescue + render_error('删除失败!') + end + end +end diff --git a/app/controllers/api/pm/issue_tags_controller.rb b/app/controllers/api/pm/issue_tags_controller.rb new file mode 100644 index 000000000..58feb6e8d --- /dev/null +++ b/app/controllers/api/pm/issue_tags_controller.rb @@ -0,0 +1,71 @@ +class Api::Pm::IssueTagsController < Api::Pm::BaseController + + def index + @issue_tags = IssueTag.pm_able + if params[:organization_id].present? + IssueTag.pm_org_init_data(params[:organization_id]) unless $redis_cache.hget("pm_org_init_issue_tags", params[:organization_id]) + @issue_tags = @issue_tags.where(organization_id: params[:organization_id]) + end + @issue_tags = @issue_tags.where(pm_project_id: params[:pm_project_id]) if params[:pm_project_id].present? + @issue_tags = @issue_tags.ransack(name_cont: params[:keyword]).result if params[:keyword].present? + @issue_tags = @issue_tags.reorder("#{tag_sort_by} #{tag_sort_direction}") + @issue_tags = kaminari_paginate(@issue_tags) + render "api/v1/issues/issue_tags/index" + end + + def create + return render_error("请输入正确的OrganizationID") unless Organization.exists?(id: issue_tag_create_params[:organization_id]) + return render_error("项目标记名称不能为空!") unless issue_tag_create_params[:name].present? + @issue_tag = IssueTag.new(issue_tag_create_params.merge!(project_id: 0)) + if @issue_tag.save! + render_ok + else + render_error("创建标记失败!") + end + end + + before_action :load_issue_tag, only: [:update, :destroy] + + def update + @issue_tag.attributes = issue_tag_update_params + if @issue_tag.save! + render_ok + else + render_error("更新标记失败!") + end + end + + def destroy + if @issue_tag.destroy! + render_ok + else + render_error("删除标记失败!") + end + end + + + private + def tag_sort_by + sort_by = params.fetch(:sort_by, "created_at") + sort_by = IssueTag.column_names.include?(sort_by) ? sort_by : "created_at" + sort_by + end + + def tag_sort_direction + sort_direction = params.fetch(:sort_direction, "desc")&.downcase + sort_direction = %w(desc asc).include?(sort_direction) ? sort_direction : "desc" + sort_direction + end + + def issue_tag_create_params + params.permit(:name, :description, :color, :pm_project_id, :organization_id) + end + + def issue_tag_update_params + params.permit(:name, :description, :color) + end + + def load_issue_tag + @issue_tag = IssueTag.pm_able.find_by_id(params[:id]) + end +end \ No newline at end of file diff --git a/app/controllers/api/pm/issues_controller.rb b/app/controllers/api/pm/issues_controller.rb new file mode 100644 index 000000000..f6dfd39b3 --- /dev/null +++ b/app/controllers/api/pm/issues_controller.rb @@ -0,0 +1,328 @@ +class Api::Pm::IssuesController < Api::Pm::BaseController + before_action :require_login, except: [:index] + before_action :load_project + before_action :load_issue, only: %i[show update destroy link_index link_issues parent_issues] + before_action :load_issues, only: %i[batch_update batch_destroy] + before_action :check_issue_operate_permission, only: %i[update destroy] + + def index + @object_result = Api::V1::Issues::ListService.call(@project, query_params, current_user) + @total_issues_count = @object_result[:total_issues_count] + @opened_issues_count = @object_result[:opened_issues_count] + @closed_issues_count = @object_result[:closed_issues_count] + @complete_issues_count = @object_result[:complete_issues_count] + if params[:only_name].present? + @issues = kaminary_select_paginate( + @object_result[:data].select(:id, :subject, :project_issues_index, :updated_on, :created_on)) + else + @issues = kaminari_paginate(@object_result[:data]) + end + render 'api/v1/issues/index' + end + + def link_index + pm_issue_type = params[:pm_issue_type] || [1, 2, 3] + not_join_id = case params[:issue_filter_type] + when 'leaf_issue' + Issue.where(root_id: @issue.id).pluck(:id) + when 'link_issue' + @issue.pm_links.pluck(:be_linkable_id) + end + + not_join_id << @issue.id + object_issues = Issue.where( + pm_project_id: params[:pm_project_id], + pm_issue_type: pm_issue_type + ).where.not(id: not_join_id).order(updated_on: :desc) + + object_issues = object_issues.where(root_id: nil, child_count: 0) if params[:issue_filter_type] == 'leaf_issue' + @issues = kaminari_paginate(object_issues) + render 'api/v1/issues/index' + end + + def parent_issues + @issues = Issue.where(pm_project_id: params[:pm_project_id]) + .where.not(id: @issue.id) + .where.not(id: Issue.full_children_issues(@issue).map{|i|i.id}) + @issues = @issues.where(pm_issue_type: params[:pm_issue_type]) if params[:pm_issue_type].present? + @issues = @issues.ransack(id_or_project_issues_index_eq: params[:keyword]).result.or(@issues.ransack(subject_or_description_cont: params[:keyword]).result) if params[:keyword].present? + @issues = @issues.reorder("#{issue_sort_by} #{issue_sort_direction}") + if params[:only_name].present? + @issues = kaminary_select_paginate( + @issues.select(:id, :subject, :project_issues_index, :updated_on, :created_on)) + else + @issues = @issues.includes(:priority, :issue_status, :user, :show_assigners, :show_issue_tags, :version, :comment_journals) + @issues = kaminari_paginate(@issues) + end + end + + def show + @issue.associate_attachment_container + render 'api/v1/issues/show' + end + + def create + @object_result = Api::Pm::Issues::CreateService.call(@project, issue_params, current_user) + render 'api/v1/issues/create' + end + + def update + @object_result = Api::Pm::Issues::UpdateService.call(@project, @issue, issue_params, current_user) + render 'api/v1/issues/update' + end + + def batch_update + @object_result = Api::Pm::Issues::BatchUpdateService.call(@project, @issues, batch_issue_params, current_user) + if @object_result + render_ok + else + render_error('批量更新疑修失败!') + end + end + + def batch_destroy + return render_ok if params[:ids].is_a?(Array) && params[:ids].blank? + @object_result = Api::Pm::Issues::BatchDeleteService.call(@project, @issues, current_user) + if @object_result + render_ok + else + render_error('批量删除疑修失败!') + end + end + + def priorities + @priorities = IssuePriority.order(position: :asc) + @priorities = @priorities.ransack(name_cont: params[:keyword]).result if params[:keyword] + @priorities = kaminary_select_paginate(@priorities) + render "api/v1/issues/issue_priorities/index" + end + + def tags + # IssueTag.pm_init_data(params[:pm_project_id]) unless $redis_cache.hget("pm_project_init_issue_tags", params[:pm_project_id]) + @issue_tags = IssueTag.where(pm_project_id: params[:pm_project_id]).reorder("#{tag_sort_by} #{tag_sort_direction}") + @issue_tags = @issue_tags.ransack(name_cont: params[:keyword]).result if params[:keyword].present? + params[:only_name] = true #强制渲染 不走project + @issue_tags = kaminary_select_paginate(@issue_tags.select(:id, :name, :color)) + render "api/v1/issues/issue_tags/index" + end + + def statues + @statues = IssueStatus.order("position asc") + @statues = @statues.ransack(name_cont: params[:keyword]).result if params[:keyword].present? + @statues = kaminary_select_paginate(@statues) + render "api/v1/issues/statues/index" + end + + + + def destroy + @object_result = Api::Pm::Issues::DeleteService.call(@project, @issue, current_user) + if @object_result + render_ok + else + render_error('删除疑修失败!') + end + end + + def export + return render_error('请输入正确的项目ID.') if params[:pm_project_id].blank? + Axlsx::Package.new do |p| + [['requirement', 1], ['task', 2], ['bug', 3]].each do |type| + p.workbook.add_worksheet(:name => type[0]) do |sheet| + @issues = Issue.where(pm_project_id: params[:pm_project_id], pm_issue_type: type[1]) + sheet.add_row ["ID", "标题*", "正文", "创建者*", "创建时间", "修改者", "更新时间", "状态", "负责人", "优先级", "标记", "开始时间","结束时间", "预估工时"] + @issues.each do |issue| + sheet.add_row [issue.id, issue.subject, issue.description, issue.user.try(:login), issue.created_on.strftime("%Y-%m-%d %H:%M:%S"), issue.changer.try(:login), issue.updated_on.strftime("%Y-%m-%d %H:%M:%S"), issue.status_id, issue.assigners.pluck(:login).join(","), issue.priority_id, issue.issue_tags.pluck(:name, :color).join(","), issue.start_date.present? ? issue.start_date.strftime("%Y-%m-%d") : "", issue.due_date.present? ? issue.due_date.strftime("%Y-%m-%d") : "", issue.time_scale] + end + end + end + p.workbook.add_worksheet(:name => 'leaf_relations') do |sheet| + leaf_issues = Issue.where(pm_project_id: params[:pm_project_id]).where.not(root_id: nil) + sheet.add_row ["ID", "父工作项ID"] + leaf_issues.each do |issue| + sheet.add_row [issue.id, issue.root_id] + end + end + p.workbook.add_worksheet(:name => 'link_relations') do |sheet| + # links = PmLink.joins(:linkable_issue).where(issues: {pm_project_id: params[:pm_project_id]}) + links = PmLink.find_by_sql("SELECT `pm_links`.* FROM `pm_links` INNER JOIN `issues` ON `issues`.`id` = `pm_links`.`linkable_id` AND `pm_links`.`linkable_type` = 'Issue' WHERE `issues`.`pm_project_id` = #{params[pm_project_id]}") + sheet.add_row ["ID", "被关联工作项ID"] + links.each do |link| + sheet.add_row [link.linkable_id, link.be_linkable_id] + end + end + p.serialize('public/导出工作项.xlsx') + end + + send_file('public/导出工作项.xlsx', :type => 'application/octet-stream;charset=utf-8') + end + + def import + begin + return render_error('请上传正确的文件') if params[:file].blank? || !params[:file].is_a?(ActionDispatch::Http::UploadedFile) + return render_error('请输入正确的项目ID.') if params[:pm_project_id].blank? + return render_error('请输入正确的组织ID.') if params[:organization_id].blank? + ActiveRecord::Base.transaction do + types = {requirement: 1, task: 2, bug: 3} + doc = SimpleXlsxReader.open(params[:file].tempfile) + doc.sheets.each do |sheet| + case sheet.name + when 'requirement', 'task', 'bug' + + type = types["#{sheet.name}".to_sym] + + sheet.rows.each.with_index do |row, index| + next if index == 0 + issue = Issue.new(issue_classify: "issue", project_id: 0, pm_project_id: params[:pm_project_id], pm_issue_type: type, tracker_id: Tracker.first.id) + issue.fake_id = row[0] + issue.subject = row[1] + issue.description = row[2] + author = User.find_by(login: row[3]) + issue.user = author + issue.created_on = row[4] + changer = User.find_by(login: row[5]) + issue.changer = changer + issue.updated_on = row[6] + issue.status_id = row[7].to_i + if row[8].present? + row[8].split(',').each do |a| + u = User.find_by(login: a) + next unless u.present? + issue.assigners << u + end + end + issue.priority_id = row[9] + if row[10].present? + row[10].split(',').each_slice(2).to_a.each do |t| + tag = IssueTag.find_by(project_id: 0, organization_id: params[:organization_id], name: t[0]) + if tag.present? + issue.issue_tags << tag + else + tag = IssueTag.create(project_id: 0,organization_id: params[:organization_id], name: t[0], color: t[1]) + issue.issue_tags << tag + end + end + end + issue.start_date = row[11] + issue.due_date = row[12] + issue.time_scale = row[13] + issue.save! + end + when 'leaf_relations' + sheet.rows.each.with_index do |row, index| + next if index == 0 + children_issue = Issue.where(fake_id: row[0]).last + parent_issue = Issue.where(fake_id: row[1]).last + next if children_issue.blank? || parent_issue.blank? + children_issue.root_id = parent_issue.id + children_issue.save(touch: false) + end + when 'link_relations' + sheet.rows.each.with_index do |row, index| + next if index == 0 + link_issue = Issue.where(fake_id: row[0]).last + be_link_issue = Issue.where(fake_id: row[1]).last + next if link_issue.blank? || be_link_issue.blank? + PmLink.create!(linkable_type: 'Issue', linkable_id: link_issue.id, be_linkable_type: 'Issue', be_linkable_id: be_link_issue.id) + end + else + return render_error('导入失败,请上传正确格式的excel文件') + end + + end + end + render_ok + rescue + return render_error('导入失败,请上传正确格式的excel文件') + end + end + + private + def check_issue_operate_permission + return if params[:project_id].to_i.zero? + render_forbidden('您没有操作权限!') unless @project.member?(current_user) || current_user.admin? || @issue.user == current_user + end + + def load_issue + return render_parameter_missing if params[:pm_project_id].blank? + @issue = Issue.issue_issue.where(pm_project_id: params[:pm_project_id]).find_by_id(params[:id]) + render_not_found('疑修不存在!') if @issue.blank? + end + + def load_issues + return render_error('请输入正确的ID数组!') unless params[:ids].is_a?(Array) + params[:ids].each do |id| + @issue = Issue.find_by(id: id, pm_project_id: params[:pm_project_id]) + return render_not_found("ID为#{id}的疑修不存在!") if @issue.blank? + end + if params[:ids].blank? + @issues = Issue.where(pm_project_id: params[:pm_project_id]) + else + @issues = Issue.where(id: params[:ids], pm_project_id: params[:pm_project_id]) + end + end + + + def query_params + params.permit( + :only_name, + :category, + :participant_category, + :keyword, :author_id, + :milestone_id, :assigner_id, + :status_id, :priority_id, + :begin_date, :end_date, + :update_begin_date, :update_end_date, + :sort_by, :sort_direction, :root_id, + :issue_tag_ids, :pm_project_id, :pm_sprint_id, :pm_issue_type, :pm_project_ids, + :status_ids, :ids, :exclude_ids, :pm_issue_types, :participator_id + ) + end + + + + def issue_params + params.permit( + :status_id, :priority_id, :milestone_id, + :branch_name, :start_date, :due_date, :time_scale, + :subject, :description, :blockchain_token_num, :root_subject, + :pm_project_id, :pm_sprint_id, :pm_issue_type, :root_id, :link_able_id, :project_id, + issue_tag_ids: [], + assigner_ids: [], + attachment_ids: [], + receivers_login: [] + ) + end + + def batch_issue_params + params.permit( + :status_id, :priority_id, :milestone_id, :pm_sprint_id, :due_date, :pm_issue_type, :root_id, :target_pm_project_id, :project_id, + :issue_tag_ids => [], + :assigner_ids => [] ) + end + + def issue_sort_by + sort_by = params.fetch(:sort_by, "updated_on") + sort_by = Issue.column_names.include?(sort_by) ? sort_by : "updated_on" + sort_by + end + + def issue_sort_direction + sort_direction = params.fetch(:sort_direction, "desc").downcase + sort_direction = %w(desc asc).include?(sort_direction) ? sort_direction : "desc" + sort_direction + end + + def tag_sort_by + sort_by = params.fetch(:sort_by, "created_at") + sort_by = IssueTag.column_names.include?(sort_by) ? sort_by : "created_at" + sort_by + end + + def tag_sort_direction + sort_direction = params.fetch(:sort_direction, "desc").downcase + sort_direction = %w(desc asc).include?(sort_direction) ? sort_direction : "desc" + sort_direction + end + +end diff --git a/app/controllers/api/pm/journals_controller.rb b/app/controllers/api/pm/journals_controller.rb new file mode 100644 index 000000000..b10cb4829 --- /dev/null +++ b/app/controllers/api/pm/journals_controller.rb @@ -0,0 +1,61 @@ +class Api::Pm::JournalsController < Api::Pm::BaseController + before_action :require_login, except: [:index, :children_journals] + before_action :load_project + before_action :load_issue + before_action :load_journal, only: [:children_journals, :update, :destroy] + + def index + @object_result = Api::V1::Issues::Journals::ListService.call(@issue, query_params, current_user) + @total_journals_count = @object_result[:total_journals_count] + @total_operate_journals_count = @object_result[:total_operate_journals_count] + @total_comment_journals_count = @object_result[:total_comment_journals_count] + @journals = kaminary_select_paginate(@object_result[:data]) + render 'api/pm/issues/journals/index' + end + + def create + @object_result = Api::V1::Issues::Journals::CreateService.call(@issue, journal_params, current_user) + render 'api/v1/issues/journals/create' + end + + def children_journals + @object_results = Api::V1::Issues::Journals::ChildrenListService.call(@issue, @journal, query_params, current_user) + @journals = kaminari_paginate(@object_results) + render 'api/v1/issues/journals/children_journals' + end + + def update + @object_result = Api::V1::Issues::Journals::UpdateService.call(@issue, @journal, journal_params, current_user) + render 'api/v1/issues/journals/update' + end + + def destroy + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueComment', @issue&.id, current_user.id, @journal.id, 'deleted', JSON.parse(@journal.to_builder.target!)) + if @journal.destroy! + render_ok + else + render_error('删除评论失败!') + end + end + + private + + def query_params + params.permit(:category, :keyword, :sort_by, :sort_direction) + end + + def journal_params + params.permit(:notes, :parent_id, :reply_id, :attachment_ids => [], :receivers_login => []) + end + + def load_issue + @issue = Issue.issue_issue.where(pm_project_id: params[:pm_project_id]).find_by_id(params[:issue_id]) + render_not_found('疑修不存在!') if @issue.blank? + end + + def load_journal + @journal = Journal.find_by_id(params[:id]) + render_not_found('评论不存在!') unless @journal.present? + end + +end \ No newline at end of file diff --git a/app/controllers/api/pm/projects_controller.rb b/app/controllers/api/pm/projects_controller.rb new file mode 100644 index 000000000..8d808bf99 --- /dev/null +++ b/app/controllers/api/pm/projects_controller.rb @@ -0,0 +1,146 @@ +class Api::Pm::ProjectsController < Api::Pm::BaseController + before_action :require_login, except: [:convert] + before_action :load_project, only: [:convert] + def convert + data = { + owner: @project.owner.try(:login), + identifier: @project.identifier, + name: @project.name + } + render_ok(data: data) + end + + def issues_count + return tip_exception '参数错误' unless params[:pm_project_id].present? + @issues = Issue.where(pm_project_id: params[:pm_project_id]) + case params[:category].to_s + when 'closed' + @issues = @issues.closed + when 'opened' + @issues = @issues.opened + end + @participant_category_count = {} + if params[:participant_category].to_s == "authoredme" or params[:participant_category].to_s == "assignedme" + issues_category = @issues.joins(:issue_participants).where(pm_issue_type: [1, 2, 3]).where(issue_participants: {participant_type: %w[authored assigned atme], participant_id: current_user&.id}) + @participant_category_count = issues_category.group(:pm_project_id, "issue_participants.participant_type").count + end + case params[:participant_category].to_s + when 'aboutme' # 关于我的 + @issues = @issues.joins(:issue_participants).where(issue_participants: {participant_type: %w[authored assigned atme], participant_id: current_user&.id}) + when 'authoredme' # 我创建的 + @issues = @issues.joins(:issue_participants).where(issue_participants: {participant_type: 'authored', participant_id: current_user&.id}) + when 'assignedme' # 我负责的 + @issues = @issues.joins(:issue_participants).where(issue_participants: {participant_type: 'assigned', participant_id: current_user&.id}) + when 'atme' # @我的 + @issues = @issues.joins(:issue_participants).where(issue_participants: {participant_type: 'atme', participant_id: current_user&.id}) + end + data = {} + @issues_count = @issues.group(:pm_project_id).count + # requirement 1 task 2 bug 3 + @issues_type_count = @issues.group(:pm_project_id, :pm_issue_type).count + params[:pm_project_id].map(&:to_i).map do |project_id| + data[project_id] = { + total: @issues_count[project_id] || 0, + requirement: @issues_type_count[[project_id, 1]] || 0, + task: @issues_type_count[[project_id, 2]] || 0, + bug: @issues_type_count[[project_id, 3]] || 0, + authoredme: @participant_category_count[[project_id, 0]] || 0, + assignedme: @participant_category_count[[project_id, 1]] || 0, + atme: @participant_category_count[[project_id, 4]] || 0, + } + end + render_ok(data: data) + end + + + def statistics + return tip_exception '参数错误' if params[:pm_project_id].blank? + @issues = Issue.where(pm_project_id: params[:pm_project_id], pm_issue_type:[1, 2, 3]) + type_count_data = @issues.group(:pm_issue_type).count + type_status = @issues.group(:pm_issue_type,:status_id).count + type_status_data = {} + IssueStatus.all.map do |e| + # next if e.id == 5 + [1,2,3].map{ |type| + type_status_data[type] = {} if type_status_data[type].nil? + if type_status[[type,e.id]].nil? + type_status_data[type][e.id] = 0 + else + type_status_data[type][e.id] = type_status[[type,e.id]] + end + } + end + open_data = { + "1": type_status_data[1][1].to_i + type_status_data[1][2].to_i, + "2": type_status_data[2][1].to_i + type_status_data[2][2].to_i, + "3": type_status_data[3][1].to_i + type_status_data[3][2].to_i, + } + if type_count_data.keys.size < 3 + nedd_add = [1,2,3] - type_count_data.keys + nedd_add.map{ |e| + type_count_data[e] = 0 + } + end + data = { + pie_chart: type_count_data, + bar_chart: type_status_data, + open_data: open_data + } + render_ok(data: data) + end + + def polyline + return tip_exception '参数错误' if params[:pm_project_id].blank? + time_line = (Time.current.beginning_of_day - 6.day) .. Time.current + @create_issues = Issue.where(pm_project_id: params[:pm_project_id],created_on: time_line) + @due_issues = Issue.where(pm_project_id: params[:pm_project_id],status_id:[3,5],due_date: time_line) + @create_issues_count = @create_issues.group(:pm_issue_type,"DATE(created_on)").count + @due_issues_count = @due_issues.group(:pm_issue_type,"DATE(due_date)").count + data = { + create_issues: {}, + due_issues: {} + } + 7.times do |time| + current_time = Date.current - time.day + if @create_issues_count.present? + data[:create_issues][current_time] = { + "1": @create_issues_count[[1,current_time]] || 0, + "2": @create_issues_count[[2,current_time]] || 0, + "3": @create_issues_count[[3,current_time]] || 0 + } + else + data[:create_issues][current_time] = { + "1": 0, + "2": 0, + "3": 0 + } + end + if @due_issues_count.present? + data[:due_issues][current_time] = { + "1": @due_issues_count[[1,current_time]] || 0, + "2": @due_issues_count[[2,current_time]] || 0, + "3": @due_issues_count[[3,current_time]] || 0 + } + else + data[:due_issues][current_time] = { + "1": 0, + "2": 0, + "3": 0 + } + end + end + render_ok(data: data) + end + + def bind_project + return render_forbidden('您没有操作权限!') unless @project.member?(current_user) || current_user.admin? + Issue.where(pm_project_id: params[:pm_project_id], user_id: current_user).update_all(project_id: params[:project_id]) + end + + private + def load_project + @project = Project.joins(:owner).find params[:project_id] + end + + +end diff --git a/app/controllers/api/pm/sprint_issues_controller.rb b/app/controllers/api/pm/sprint_issues_controller.rb new file mode 100644 index 000000000..b004d93d2 --- /dev/null +++ b/app/controllers/api/pm/sprint_issues_controller.rb @@ -0,0 +1,108 @@ +class Api::Pm::SprintIssuesController < Api::Pm::BaseController + + before_action :require_login, except: [:index] + + def index + @issues = Api::Pm::SprintIssues::ListService.call(query_params, current_user) + @issues = kaminari_paginate(@issues) + render 'api/v1/issues/index' + end + + def burndown_charts + return tip_exception '参数错误' if params[:pm_sprint_id].blank? || params[:start_time].blank? || params[:end_time].blank? + @issues = Issue.where(pm_sprint_id: params[:pm_sprint_id]) + start_time = Date.parse params[:start_time] + end_time = Date.parse params[:end_time] + time_count = (end_time - start_time).to_i + 1 # 计算间隔时间 加上最后一天 + data = [] + curren_issues = @issues.group(:status_id, :due_date).count + total_count = @issues.count + cardinality = BigDecimal.new(total_count) / BigDecimal.new(time_count) + time_count.times do |x| + e_time = start_time + x + completed = curren_issues[[5, e_time]].to_i + curren_issues[[3, e_time]].to_i - @issues.where(pm_issue_type: 3, status_id: 3).size + total_count = total_count - completed + data << { time: e_time, undone: total_count, completed: completed, base_number: (cardinality * (time_count - x - 1)).to_f.round(2) } + end + render_ok(data: data) + end + + def statistics + pm_sprint_ids = params[:pm_sprint_ids].split(",") rescue [] + return tip_exception '参数错误' if pm_sprint_ids.blank? + @issues = Issue.where(pm_sprint_id: pm_sprint_ids) + data = {} + # requirement 1 task 2 bug 3 + @issues_count = @issues.group(:pm_sprint_id).count + @issues_type_count = @issues.group(:pm_sprint_id, :status_id).count + @issues_pm_type_count = @issues.group(:pm_sprint_id, :pm_issue_type).count + @issues_hour_count = @issues.group(:pm_sprint_id).sum(:time_scale) + @issues_hour_type_count = @issues.group(:pm_sprint_id, :status_id).sum(:time_scale) + @issues_hour_pm_type_count = @issues.group(:pm_sprint_id, :pm_issue_type).sum(:time_scale) + @issues_status_pm_type_count = @issues.group(:pm_sprint_id, :pm_issue_type, :status_id).count + pm_sprint_ids.map(&:to_i).map do |sprint_id| + # count_closed 工作项已完成/已关闭数量,需排除已修复的缺陷数量 + count_closed = @issues_type_count[[sprint_id, 5]].to_i + @issues_type_count[[sprint_id, 3]].to_i - @issues.where(pm_sprint_id: sprint_id, pm_issue_type: 3, status_id: 3).size + # hour_closed 已完成/已关闭 预估工时之和,需排除已修复的缺陷预估工时 + hour_closed = @issues_hour_type_count[[sprint_id, 5]].to_f + @issues_hour_type_count[[sprint_id, 3]].to_f - @issues.where(pm_sprint_id: sprint_id, pm_issue_type: 3, status_id: 3).sum(:time_scale).to_f + data[sprint_id] = { + count_total: @issues_count[sprint_id] || 0, + count_closed: count_closed || 0, + hour_total: @issues_hour_count[sprint_id].to_f || 0, + hour_closed: hour_closed || 0, + requirement: @issues_pm_type_count[[sprint_id, 1]] || 0, + task: @issues_pm_type_count[[sprint_id, 2]] || 0, + bug: @issues_pm_type_count[[sprint_id, 3]] || 0, + requirement_hour: @issues_hour_pm_type_count[[sprint_id, 1]].to_i || 0, + task_hour: @issues_hour_pm_type_count[[sprint_id, 2]].to_i || 0, + bug_hour: @issues_hour_pm_type_count[[sprint_id, 3]].to_i || 0, + requirement_open: (@issues_status_pm_type_count[[sprint_id, 1, 1]].to_i + @issues_status_pm_type_count[[sprint_id, 1, 2]].to_i) || 0, + task_open: @issues_status_pm_type_count[[sprint_id, 2, 1]].to_i + @issues_status_pm_type_count[[sprint_id, 2, 2]].to_i || 0, + bug_open: @issues_status_pm_type_count[[sprint_id, 3, 1]].to_i + @issues_status_pm_type_count[[sprint_id, 3, 2]].to_i || 0 + } + end + render_ok(data: data) + end + + before_action :load_uncomplete_issues, only: [:complete] + + def complete + begin + case complete_params[:complete_type].to_i + when 1 + @issues.update_all(status_id: 5) + when 2 + @issues.update_all(pm_sprint_id: 0) + when 3 + @issues.update_all(pm_sprint_id: complete_params[:target_pm_project_sprint_id]) + end + render_ok + rescue => e + render_error(e.message) + end + end + + private + + def load_uncomplete_issues + @issues = Issue.where(pm_sprint_id: complete_params[:pm_project_sprint_id]).where.not(status_id: 5) + end + + def complete_params + params.permit(:pm_project_sprint_id, :complete_type, :target_pm_project_sprint_id) + end + + def query_params + params.permit( + :category, + :pm_project_id, + :pm_issue_type, # 需求1 任务2 缺陷3 + :assigner_id, + :priority_id, + :status_id, + :keyword, :status_ids, :pm_issue_types, + :sort_by, :sort_direction + ) + end + +end \ No newline at end of file diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index ea2266390..d9db4b99c 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -62,7 +62,7 @@ class Api::V1::BaseController < ApplicationController # 具有对仓库的访问权限 def require_public_and_member_above - @project = load_project + @project = load_project return render_forbidden if !@project.is_public && !current_user.admin? && !@project.member?(current_user) end end \ No newline at end of file diff --git a/app/controllers/api/v1/issues/issue_priorities_controller.rb b/app/controllers/api/v1/issues/issue_priorities_controller.rb index 2df1288f7..319994a28 100644 --- a/app/controllers/api/v1/issues/issue_priorities_controller.rb +++ b/app/controllers/api/v1/issues/issue_priorities_controller.rb @@ -7,12 +7,4 @@ class Api::V1::Issues::IssuePrioritiesController < Api::V1::BaseController @priorities = @priorities.ransack(name_cont: params[:keyword]).result if params[:keyword] @priorities = kaminary_select_paginate(@priorities) end - - def pm_index - @priorities = IssuePriority.order(position: :asc) - @priorities = @priorities.ransack(name_cont: params[:keyword]).result if params[:keyword] - @priorities = kaminary_select_paginate(@priorities) - render "index" - end - end \ No newline at end of file diff --git a/app/controllers/api/v1/issues/issue_tags_controller.rb b/app/controllers/api/v1/issues/issue_tags_controller.rb index 39534c313..f712a3ba4 100644 --- a/app/controllers/api/v1/issues/issue_tags_controller.rb +++ b/app/controllers/api/v1/issues/issue_tags_controller.rb @@ -13,12 +13,7 @@ class Api::V1::Issues::IssueTagsController < Api::V1::BaseController end end - def pm_index - @issue_tags = IssueTag.init_mp_issues_tags - render_ok(@issue_tags) - end - - def create + def create @issue_tag = @project.issue_tags.new(issue_tag_params) if @issue_tag.save! render_ok diff --git a/app/controllers/api/v1/issues/statues_controller.rb b/app/controllers/api/v1/issues/statues_controller.rb index c6495ee26..5a7fbc338 100644 --- a/app/controllers/api/v1/issues/statues_controller.rb +++ b/app/controllers/api/v1/issues/statues_controller.rb @@ -8,11 +8,4 @@ class Api::V1::Issues::StatuesController < Api::V1::BaseController @statues = @statues.ransack(name_cont: params[:keyword]).result if params[:keyword].present? @statues = kaminary_select_paginate(@statues) end - - def pm_index - @statues = IssueStatus.order("position asc") - @statues = @statues.ransack(name_cont: params[:keyword]).result if params[:keyword].present? - @statues = kaminary_select_paginate(@statues) - render "index" - end end \ No newline at end of file diff --git a/app/controllers/api/v1/pm_issues_controller.rb b/app/controllers/api/v1/pm_issues_controller.rb new file mode 100644 index 000000000..5c2e0f987 --- /dev/null +++ b/app/controllers/api/v1/pm_issues_controller.rb @@ -0,0 +1,36 @@ +class Api::V1::PmIssuesController < ApplicationController + before_action :require_login, except: [:index, :show] + + def index + project = Project.find_by_id(params[:project_id]) || Project.new( id: 0, user_id: 0, name:"pm_mm", identifier:"pm_mm" ) + object_result = Api::V1::Issues::ListService.call(@project, query_params, current_user) + @total_issues_count = @object_result[:total_issues_count] + @opened_issues_count = @object_result[:opened_issues_count] + @closed_issues_count = @object_result[:closed_issues_count] + if params[:only_name].present? + @issues = kaminary_select_paginate(@object_result[:data].select(:id, :subject, :project_issues_index, :updated_on, :created_on)) + else + @issues = kaminari_paginate(@object_result[:data]) + end + end + + def create + project = Project.find_by_id(params[:project_id]) || Project.new( id: 0, user_id: 0, name:"pm_mm", identifier:"pm_mm" ) + @object_result = Api::V1::Issues::CreateService.call(project, issue_params, current_user) + end + + private + def issue_params + params.permit( + :status_id, :priority_id, :milestone_id, + :branch_name, :start_date, :due_date, + :subject, :description, :blockchain_token_num, + :pm_project_id, :pm_sprint_id, + :issue_tag_ids => [], + :assigner_ids => [], + :attachment_ids => [], + :receivers_login => [] + ) + end + +end diff --git a/app/controllers/api/v1/projects/pipelines_controller.rb b/app/controllers/api/v1/projects/pipelines_controller.rb new file mode 100644 index 000000000..888e68960 --- /dev/null +++ b/app/controllers/api/v1/projects/pipelines_controller.rb @@ -0,0 +1,463 @@ +class Api::V1::Projects::PipelinesController < Api::V1::BaseController + before_action :require_operate_above + + def index + @pipelines = Action::Pipeline.where(project_id: @project.id).order(updated_at: :desc) + @pipelines = paginate @pipelines + end + + def create + size = Action::Pipeline.where(pipeline_name: params[:pipeline_name], project_id: @project.id).size + tip_exception("已经存在#{params[:pipeline_name]}流水线!") if size > 0 + @pipeline = Action::Pipeline.new(pipeline_name: params[:pipeline_name], project_id: @project.id) + @pipeline.file_name = ".gitea/workflows/#{@pipeline.pipeline_name}.yml" + @pipeline.branch = params[:branch] || @project.default_branch + @pipeline.json = params[:pipeline_json].to_json + pipeline_yaml = build_pipeline_yaml(params[:pipeline_name], params[:pipeline_json]) + tip_exception("流水线yaml内空不能为空") if pipeline_yaml.blank? + @pipeline.yaml = pipeline_yaml + @pipeline.save! + sha = get_pipeline_file_sha(@pipeline.file_name, @pipeline.branch) + tip_exception("#{@pipeline.file_name}已存在") if sha + interactor = Gitea::CreateFileInteractor.call(current_user.gitea_token, @owner.login, content_params("create")) + tip_exception(interactor.error) unless interactor.success? + render_ok({ id: @pipeline.id }) + end + + def save_yaml + @pipeline = Action::Pipeline.new(pipeline_name: params[:pipeline_name], project_id: @project.id) + @pipeline.file_name = ".gitea/workflows/#{@pipeline.pipeline_name}.yml" + @pipeline.branch = params[:branch] || @project.default_branch + @pipeline.json = params[:pipeline_json].to_json + pipeline_yaml = build_pipeline_yaml(params[:pipeline_name], params[:pipeline_json]) + tip_exception("流水线yaml内空不能为空") if pipeline_yaml.blank? + @pipeline.yaml = pipeline_yaml + Rails.logger.info "pipeline_yaml base64=========================#{Base64.encode64(@pipeline.yaml).gsub(/\n/, '')}" + sha = get_pipeline_file_sha(@pipeline.file_name, @pipeline.branch) + interactor = sha.present? ? Gitea::UpdateFileInteractor.call(current_user.gitea_token, @owner.login, content_params("update").merge(sha: sha)) : Gitea::CreateFileInteractor.call(current_user.gitea_token, @owner.login, content_params("create")) + tip_exception(interactor.error) unless interactor.success? + file = interactor.result + render_ok({ pipeline_yaml: pipeline_yaml, pipeline_name: params[:pipeline_name], file_name: @pipeline.file_name, sha: sha.present? ? sha : file['content']['sha'] }) + end + + def build_yaml + if params[:pipeline_json].present? + pipeline_yaml = build_pipeline_yaml(params[:pipeline_name], params[:pipeline_json]) + else + pipeline_yaml = build_test_yaml + end + # render plain: pipeline_yaml + render_ok({ pipeline_yaml: pipeline_yaml }) + end + + def update + @pipeline = Action::Pipeline.find(params[:id]) + @pipeline.pipeline_name = params[:pipeline_name] + @pipeline.file_name = ".gitea/workflows/#{@pipeline.pipeline_name}.yml" + @pipeline.branch = params[:branch] || @project.default_branch + @pipeline.json = params[:pipeline_json].to_json + pipeline_yaml = build_pipeline_yaml(params[:pipeline_name], params[:pipeline_json]) + tip_exception("流水线yaml内空不能为空") if pipeline_yaml.blank? + @pipeline.yaml = pipeline_yaml + @pipeline.save + sha = get_pipeline_file_sha(@pipeline.file_name, @pipeline.branch) + interactor = Gitea::UpdateFileInteractor.call(current_user.gitea_token, @owner.login, content_params("create").merge(sha: sha)) + tip_exception(interactor.error) unless interactor.success? + file = interactor.result + render_ok({ pipeline_yaml: pipeline_yaml, pipeline_name: params[:pipeline_name], file_name: @pipeline.file_name, sha: file['content']['sha'] }) + end + + def destroy + @pipeline = Action::Pipeline.find(params[:id]) + if pipeline + interactor = Gitea::DeleteFileInteractor.call(current_user.gitea_token, @owner.login, content_params("update")) + tip_exception(interactor.error) unless interactor.success? + @pipeline.destroy! + end + render_ok + end + + def show + @pipeline = Action::Pipeline.find_by(id: params[:id]) + @pipeline = Action::Pipeline.new(id: 0, pipeline_name: "test-ss", yaml: build_test_yaml) if @pipeline.blank? + end + + def build_pipeline_yaml(pipeline_name, pipeline_json) + if pipeline_json.present? && pipeline_json.present? + @pipeline_name = pipeline_name + params_nodes = pipeline_json["nodes"].select { |node| !["on-push", "on-schedule"].include?(node["name"]) } + on_nodes = pipeline_json["nodes"].select { |node| ["on-push", "on-schedule"].include?(node["name"]) } + @on_nodes = build_nodes(on_nodes) + @steps_nodes = build_nodes(params_nodes) + yaml = ERB.new(File.read(File.join(Rails.root, "app/views/api/v1/projects/pipelines", "build_pipeline.yaml.erb"))).result(binding) + # 删除空行内容 + pipeline_yaml = yaml.gsub(/^\s*\n/, "") + else + pipeline_yaml = params[:pipeline_yaml] + end + Rails.logger.info "pipeline_yaml=========================" + Rails.logger.info pipeline_yaml + pipeline_yaml + end + + def build_test_yaml + @pipeline_name = "I like it" + params_nodes = JSON.parse(demo.to_json)["nodes"].select { |node| !["on-push", "on-schedule"].include?(node["name"]) } + on_nodes = JSON.parse(demo.to_json)["nodes"].select { |node| ["on-push", "on-schedule"].include?(node["name"]) } + @on_nodes = build_nodes(on_nodes) + @steps_nodes = [] + params_nodes.each do |input_node| + # Rails.logger.info "input_node=====0===#{input_node["name"]}======#{input_node["inputs"]}" + node = Action::Node.find_by(name: input_node["name"]) + next if node.blank? + node.label = input_node["label"] if input_node["label"].present? + run_values = {} + input_values = {} + if input_node["inputs"].present? + Rails.logger.info "@inputs=====11===#{input_node["name"]}======#{input_node["inputs"]}" + input_node["inputs"].each do |input| + # Rails.logger.info "@inputs.input_name===#{input[:name]}" + # Rails.logger.info "@inputs.input_value===#{input["value"]}" + if input[:name].to_s.gsub("--", "") == "run" + run_values = run_values.merge({ "#{input[:name].gsub("--", "")}": "#{input["value"]}" }) + else + input_values = input_values.merge({ "#{input[:name].gsub("--", "")}": "#{input["value"]}" }) + end + end + node.run_values = run_values + node.input_values = input_values + # Rails.logger.info "@input_values run_values===#{node.run_values.to_json}" + # Rails.logger.info "@input_values input_values===#{node.input_values.to_json}" + end + @steps_nodes.push(node) + end + Rails.logger.info "@@on_nodes===#{@on_nodes.to_json}" + Rails.logger.info "@steps_nodes===#{@steps_nodes.to_json}" + yaml = ERB.new(File.read(File.join(Rails.root, "app/views/api/v1/projects/pipelines", "build_pipeline.yaml.erb"))).result(binding) + pipeline_yaml = yaml.gsub(/^\s*\n/, "") + Rails.logger.info "=========================" + Rails.logger.info pipeline_yaml + pipeline_yaml + end + + private + + def get_pipeline_file_sha(file_name, branch) + file_path_uri = URI.parse(URI.encode(file_name)) + interactor = Repositories::EntriesInteractor.call(@project.owner, @project.identifier, file_path_uri, ref: branch || @project.default_branch) + if interactor.success? + file = interactor.result + file['sha'] + else + nil + end + end + + def content_params(opt) + { + filepath: ".gitea/workflows/#{@pipeline.pipeline_name}.yml", + branch: @pipeline.branch, + new_branch: @pipeline.branch, + content: opt == "create" ? Base64.encode64(@pipeline.yaml).gsub(/\n/, '') : @pipeline.yaml, + message: "#{opt} pipeline", + committer: { + email: current_user.mail, + name: current_user.login + }, + identifier: @project.identifier + } + end + + def build_nodes(params_nodes) + steps_nodes = [] + params_nodes.each do |input_node| + node = Action::Node.find_by(name: input_node["name"]) + next if node.blank? + node.label = input_node["label"] if input_node["label"].present? + run_values = {} + input_values = {} + if input_node["inputs"].present? + Rails.logger.info "@inputs=====11===#{input_node["name"]}======#{input_node["inputs"]}" + input_node["inputs"].each do |input| + # Rails.logger.info "@inputs.input_name===#{input[:name]}" + # Rails.logger.info "@inputs.input_value===#{input["value"]}" + if input[:name].to_s.gsub("--", "") == "run" + run_values = run_values.merge({ "#{input[:name].gsub("--", "")}": "#{input["value"]}" }) + else + input_values = input_values.merge({ "#{input[:name].gsub("--", "")}": "#{input["value"]}" }) + end + end + node.run_values = run_values + node.input_values = input_values + end + steps_nodes.push(node) + end + steps_nodes + end + + def demo + { + "nodes": [{ + "id": "on-schedule-2fcf505", + "name": "on-schedule", + "full_name": "on-schedule", + "description": " 定时器计划器", + "icon": "https://testforgeplus.trustie.net/api/attachments/0445403c-5d9e-4495-8414-339f87981ca1", + "action_node_types_id": 3, + "yaml": "", + "sort_no": 0, + "use_count": 0, + "inputs": [{ + "id": 8, + "name": "cron", + "input_type": "input", + "description": "示例:\r\n- cron: '20 8 * * *'", + "is_required": true, + "value": "- corn: '0 10 * * *'" + }], + "x": 586, + "y": 165.328125, + "label": "on-schedule", + "img": "https://testforgeplus.trustie.net/api/attachments/0445403c-5d9e-4495-8414-339f87981ca1", + "isCluster": false, + "type": "rect-node", + "size": [110, 36], + "labelCfg": { + "style": { + "fill": "transparent", + "fontSize": 0, + "boxShadow": "0px 0px 12px rgba(75, 84, 137, 0.05)", + "overflow": "hidden", + "x": -20, + "y": 0, + "textAlign": "left", + "textBaseline": "middle" + } + }, + "style": { + "active": { + "fill": "rgb(247, 250, 255)", + "stroke": "rgb(95, 149, 255)", + "lineWidth": 2, + "shadowColor": "rgb(95, 149, 255)", + "shadowBlur": 10 + }, + "selected": { + "fill": "rgb(255, 255, 255)", + "stroke": "rgb(95, 149, 255)", + "lineWidth": 4, + "shadowColor": "rgb(95, 149, 255)", + "shadowBlur": 10, + "text-shape": { + "fontWeight": 500 + } + }, + "highlight": { + "fill": "rgb(223, 234, 255)", + "stroke": "#4572d9", + "lineWidth": 2, + "text-shape": { + "fontWeight": 500 + } + }, + "inactive": { + "fill": "rgb(247, 250, 255)", + "stroke": "rgb(191, 213, 255)", + "lineWidth": 1 + }, + "disable": { + "fill": "rgb(250, 250, 250)", + "stroke": "rgb(224, 224, 224)", + "lineWidth": 1 + }, + "nodeSelected": { + "fill": "red", + "shadowColor": "red", + "stroke": "red", + "text-shape": { + "fill": "red", + "stroke": "red" + } + }, + "fill": "#fff", + "stroke": "transparent", + "cursor": "pointer", + "radius": 10, + "overflow": "hidden", + "lineWidth": 0.5, + "shadowColor": "rgba(75,84,137,0.05)", + "shadowBlur": 12 + }, + "cron": "- corn: '0 10 * * *'", + "depth": 0 + }, { + "id": "actions/setup-node@v3-257f29d", + "name": "node", + "full_name": "actions/setup-node@v3", + "description": "", + "icon": "https://testforgeplus.trustie.net/api/attachments/c4774fc1-ecd9-47fd-9878-1847bdaf98f6", + "action_node_types_id": 1, + "yaml": "", + "sort_no": 0, + "use_count": 0, + "inputs": [{ + "id": 2, + "name": "node-version", + "input_type": "select", + "is_required": false, + "value": 55 + }], + "x": 608, + "y": 357.328125, + "label": "node", + "img": "https://testforgeplus.trustie.net/api/attachments/c4774fc1-ecd9-47fd-9878-1847bdaf98f6", + "isCluster": false, + "type": "rect-node", + "size": [110, 36], + "labelCfg": { + "style": { + "fill": "transparent", + "fontSize": 0, + "boxShadow": "0px 0px 12px rgba(75, 84, 137, 0.05)", + "overflow": "hidden", + "x": -20, + "y": 0, + "textAlign": "left", + "textBaseline": "middle" + } + }, + "style": { + "active": { + "fill": "rgb(247, 250, 255)", + "stroke": "rgb(95, 149, 255)", + "lineWidth": 2, + "shadowColor": "rgb(95, 149, 255)", + "shadowBlur": 10 + }, + "selected": { + "fill": "rgb(255, 255, 255)", + "stroke": "rgb(95, 149, 255)", + "lineWidth": 4, + "shadowColor": "rgb(95, 149, 255)", + "shadowBlur": 10, + "text-shape": { + "fontWeight": 500 + } + }, + "highlight": { + "fill": "rgb(223, 234, 255)", + "stroke": "#4572d9", + "lineWidth": 2, + "text-shape": { + "fontWeight": 500 + } + }, + "inactive": { + "fill": "rgb(247, 250, 255)", + "stroke": "rgb(191, 213, 255)", + "lineWidth": 1 + }, + "disable": { + "fill": "rgb(250, 250, 250)", + "stroke": "rgb(224, 224, 224)", + "lineWidth": 1 + }, + "nodeSelected": { + "fill": "red", + "shadowColor": "red", + "stroke": "red", + "text-shape": { + "fill": "red", + "stroke": "red" + } + }, + "fill": "#fff", + "stroke": "transparent", + "cursor": "pointer", + "radius": 10, + "overflow": "hidden", + "lineWidth": 0.5, + "shadowColor": "rgba(75,84,137,0.05)", + "shadowBlur": 12 + }, + "depth": 0, + "node-version": 55 + }], + "edges": [{ + "source": "on-schedule-2fcf505", + "target": "actions/setup-node@v3-257f29d", + "style": { + "active": { + "stroke": "rgb(95, 149, 255)", + "lineWidth": 1 + }, + "selected": { + "stroke": "rgb(95, 149, 255)", + "lineWidth": 2, + "shadowColor": "rgb(95, 149, 255)", + "shadowBlur": 10, + "text-shape": { + "fontWeight": 500 + } + }, + "highlight": { + "stroke": "rgb(95, 149, 255)", + "lineWidth": 2, + "text-shape": { + "fontWeight": 500 + } + }, + "inactive": { + "stroke": "rgb(234, 234, 234)", + "lineWidth": 1 + }, + "disable": { + "stroke": "rgb(245, 245, 245)", + "lineWidth": 1 + }, + "endArrow": { + "path": "M 6,0 L 9,-1.5 L 9,1.5 Z", + "d": 4.5, + "fill": "#CDD0DC" + }, + "cursor": "pointer", + "lineWidth": 1, + "opacity": 1, + "stroke": "#CDD0DC", + "radius": 1 + }, + "nodeStateStyle": { + "hover": { + "opacity": 1, + "stroke": "#8fe8ff" + } + }, + "labelCfg": { + "autoRotate": true, + "style": { + "fontSize": 10, + "fill": "#FFF" + } + }, + "id": "edge-0.96904321945951241716516719464", + "startPoint": { + "x": 586, + "y": 183.578125, + "anchorIndex": 1 + }, + "endPoint": { + "x": 608, + "y": 339.078125, + "anchorIndex": 0 + }, + "sourceAnchor": 1, + "targetAnchor": 0, + "type": "cubic-vertical", + "curveOffset": [0, 0], + "curvePosition": [0.5, 0.5], + "minCurveOffset": [0, 0] + }], + "combos": [] + } + end +end diff --git a/app/controllers/api/v1/projects_controller.rb b/app/controllers/api/v1/projects_controller.rb index 810c40171..d6a90e14a 100644 --- a/app/controllers/api/v1/projects_controller.rb +++ b/app/controllers/api/v1/projects_controller.rb @@ -1,5 +1,5 @@ class Api::V1::ProjectsController < Api::V1::BaseController - before_action :require_public_and_member_above, only: [:show, :compare, :blame] + before_action :require_public_and_member_above, only: [:show, :compare, :blame, :sonar_search] def index render_ok @@ -9,6 +9,7 @@ class Api::V1::ProjectsController < Api::V1::BaseController @result_object = Api::V1::Projects::GetService.call(@project, current_user.gitea_token) end + def compare @result_object = Api::V1::Projects::CompareService.call(@project, params[:from], params[:to], current_user&.gitea_token) end diff --git a/app/controllers/api/v1/sonarqubes_controller.rb b/app/controllers/api/v1/sonarqubes_controller.rb new file mode 100644 index 000000000..c6f718de0 --- /dev/null +++ b/app/controllers/api/v1/sonarqubes_controller.rb @@ -0,0 +1,171 @@ +class Api::V1::SonarqubesController < Api::V1::BaseController + before_action :load_repository + def sonar_initialize + gitea_params = { has_actions: params[:has_actions] == 'true' ? true :false } + gitea_setting = Gitea::Repository::UpdateService.call(@owner, @project.identifier, gitea_params) + if gitea_setting['has_actions'] == true + Gitea::Repository::ActionSecretsService.new(@owner, @project.identifier, 'SONAR_HOST_URL', Rails.application.config_for(:configuration)['sonarqube']['url'] ).call + Gitea::Repository::ActionSecretsService.new(@owner, @project.identifier, 'SONAR_TOKEN', Rails.application.config_for(:configuration)['sonarqube']['secret'] ).call + else + Gitea::Repository::ActionSecretsService.new(@owner, @project.identifier, 'SONAR_HOST_URL', Rails.application.config_for(:configuration)['sonarqube']['url'] ).destroy + Gitea::Repository::ActionSecretsService.new(@owner, @project.identifier, 'SONAR_TOKEN', Rails.application.config_for(:configuration)['sonarqube']['secret'] ).destroy + end + @project.update(gitea_params) + render_ok + end + + def insert_file + checkout_url = 'https://gitlink.org.cn/KingChan/checkout@v4' + scanner_url = 'https://gitlink.org.cn/KingChan/sonarqube-scan-action@master' + begin + config = Rails.application.config_for(:configuration) + sonarqube_config = config.dig('sonarqube') + + if sonarqube_config.present? && sonarqube_config['checkout'].present? + checkout_url = sonarqube_config['checkout'] + end + if sonarqube_config.present? && sonarqube_config['scanner'].present? + scanner_url = sonarqube_config['scanner'] + end + + raise 'sonar config missing' if sonarqube_config.blank? + rescue => ex + raise ex if Rails.env.production? + + puts %Q{\033[33m [warning] soanrqube config or configuration.yml missing, + please add it or execute 'cp config/configuration.yml.example config/configuration.yml' \033[0m} + end + + sonar_scanner_content = { + filepath: '.gitea/workflows/SonarScanner.yaml', + branch: params[:branch], + new_branch: nil, + content: " + on: + # Trigger analysis when pushing to your main branches, and when creating a pull request. + push: + branches: + - main + - master + - develop + - 'releases/**' + pull_request: + types: [opened, synchronize, reopened] + + name: Main Workflow + jobs: + sonarqube: + runs-on: ubuntu-latest + steps: + - uses: #{checkout_url} + with: + # Disabling shallow clones is recommended for improving the relevancy of reporting + fetch-depth: 0 + - name: SonarQube Scan + uses: #{scanner_url} + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + ", + message: 'Add .gitea/workflows/SonarScanner.yaml', + committer: { + email: @owner.mail, + name: @owner.login + }, + identifier: @project.identifier + } + @path = GiteaService.gitea_config[:domain]+"/#{@project.owner.login}/#{@project.identifier}/raw/branch/#{params[:branch]}/" + sonar_scanner_exit = Repositories::EntriesInteractor.call(@owner, @project.identifier, '.gitea/workflows/SonarScanner.yaml', ref: params[:branch]) + if sonar_scanner_exit.success? + Gitea::UpdateFileInteractor.call(@owner.gitea_token, @owner.login, sonar_scanner_content.merge(sha:sonar_scanner_exit.result['sha'])) + else + sonar_scanner_content[:content] = Base64.strict_encode64(sonar_scanner_content[:content]) + Gitea::CreateFileInteractor.call(@owner.gitea_token, @owner.login, sonar_scanner_content) + end + + sonar_project_content = { + filepath: 'sonar-project.properties', + branch: params[:branch], + new_branch: nil, + "content": "sonar.projectKey=#{params[:owner]}-#{@project.id}\nsonar.sources=.\nsonar.java.binaries=.", + "message": 'Add sonar-project.properties', + committer: { + email: @owner.mail, + name: @owner.login + }, + identifier: @project.identifier + } + sonar_project_exit = Repositories::EntriesInteractor.call(@owner, @project.identifier, 'sonar-project.properties', ref: params[:branch]) + if sonar_project_exit.success? + Gitea::UpdateFileInteractor.call(@owner.gitea_token, @owner.login, sonar_project_content.merge(sha:sonar_project_exit.result['sha'])) + else + sonar_project_content[:content] = Base64.strict_encode64(sonar_project_content[:content]) + Gitea::CreateFileInteractor.call(@owner.gitea_token, @owner.login, sonar_project_content) + end + render_ok + end + + def issues_search + params_data = { + components: "#{params[:owner]}-#{@project.id}", + s: params[:s], + impactSoftwareQualities: params[:impactSoftwareQualities], + issueStatuses: params[:issueStatuses], + ps: params[:ps], + p: params[:p], + facets: params[:facets], + additionalFields: params[:additionalFields], + timeZone: params[:timeZone], + types: params[:types], + impactSeverities: params[:impactSeverities], + tags: params[:tags] + } + data = Sonarqube.client.get('/api/issues/search', query: params_data) + render_ok data + end + + def ce_component + params_data = { + components: "#{params[:owner]}-#{@project.id}", + } + data = Sonarqube.client.get('/api/ce/component', query: params_data) + render_ok data + end + + def sources_issue_snippet + params_data = { + issueKey: params[:issueKey] + } + data = Sonarqube.client.get('/api/sources/issue_snippets', query: params_data) + render_ok data + end + + def rules_show + params_data = { + key: params[:key] + } + data = Sonarqube.client.get('/api/rules/show', query: params_data) + render_ok data + end + + def measures_search_history + params_data = { + from: params[:form], + component: "#{params[:owner]}-#{@project.id}", + metrics: params[:metrics], + ps: params[:ps] + } + data = Sonarqube.client.get('/api/measures/search_history', query: params_data) + render_ok data + end + + def measures_component + params_data = { + component: "#{params[:owner]}-#{@project.id}", + additionalFields: params[:additionalFields], + metricKeys: params[:metricKeys] + } + data = Sonarqube.client.get('/api/measures/component', query: params_data) + render_ok data + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6957922d4..f6ee02bd6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -326,19 +326,19 @@ class ApplicationController < ActionController::Base User.current = find_current_user uid_logger("user_setup: " + (User.current.logged? ? "#{User.current.try(:login)} (id=#{User.current.try(:id)})" : "anonymous")) - # 开放课程通过链接访问的用户 - if !User.current.logged? && !params[:chinaoocTimestamp].blank? && !params[:websiteName].blank? && !params[:chinaoocKey].blank? - content = "#{OPENKEY}#{params[:websiteName]}#{params[:chinaoocTimestamp]}" - - if Digest::MD5.hexdigest(content) == params[:chinaoocKey] - user = open_class_user - if user - start_user_session(user) - set_autologin_cookie(user) - end - User.current = user - end - end + # # 开放课程通过链接访问的用户 + # if !User.current.logged? && !params[:chinaoocTimestamp].blank? && !params[:websiteName].blank? && !params[:chinaoocKey].blank? + # content = "#{OPENKEY}#{params[:websiteName]}#{params[:chinaoocTimestamp]}" + # + # if Digest::MD5.hexdigest(content) == params[:chinaoocKey] + # user = open_class_user + # if user + # start_user_session(user) + # set_autologin_cookie(user) + # end + # User.current = user + # end + # end if !User.current.logged? && Rails.env.development? user = User.find 1 @@ -356,7 +356,7 @@ class ApplicationController < ActionController::Base User.current = User.find 8686 elsif params[:debug] == 'admin' logger.info "@@@@@@@@@@@@@@@@@@@@@@ debug mode....." - user = User.find 36480 + user = User.find 1 User.current = user cookies.signed[:user_id] = user.id end @@ -371,15 +371,14 @@ class ApplicationController < ActionController::Base uid_logger("user setup start: session[:user_id] is #{session[:user_id]}") uid_logger("0000000000000user setup start: default_yun_session is #{default_yun_session}, session[:current_user_id] is #{session[:"#{default_yun_session}"]}") current_domain_session = session[:"#{default_yun_session}"] - if current_domain_session - # existing session - User.current = (User.active.find(current_domain_session) rescue nil) - elsif autologin_user = try_to_autologin - autologin_user - elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth? - # RSS key authentication does not start a session - User.find_by_rss_key(params[:key]) + autologin_user = try_to_autologin + uid_logger("user setup start: autologin_user is #{autologin_user}") + # 多浏览器退出账号时,token不存在处理 + if current_domain_session && autologin_user.nil? + autologin_user = (User.active.find(current_domain_session) rescue nil) + set_autologin_cookie(autologin_user) end + autologin_user end def try_to_autologin diff --git a/app/controllers/organizations/projects_controller.rb b/app/controllers/organizations/projects_controller.rb index ab5c9ef5d..e0525eaff 100644 --- a/app/controllers/organizations/projects_controller.rb +++ b/app/controllers/organizations/projects_controller.rb @@ -10,6 +10,9 @@ class Organizations::ProjectsController < Organizations::BaseController @projects = Project.from("( #{ public_projects_sql} UNION #{ private_projects_sql } ) AS projects") # 表情处理 keywords = params[:search].to_s.each_char.select { |c| c.bytes.first < 240 }.join('') + @projects = @projects.where(id: params[:pm_project_repository_ids].split(',')) if params[:pm_project_repository_ids].present? + @projects = @projects.where.not(id: params[:exclude_ids].to_s.split(",")) if params[:exclude_ids].present? + @projects = @projects.where("gpid is not null") if params[:actived].present? @projects = @projects.ransack(name_or_identifier_cont: keywords).result if params[:search].present? @projects = @projects.includes(:owner).order("projects.#{sort} #{sort_direction}") @projects = paginate(@projects) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index b3a0c3aa5..202bae458 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -250,15 +250,6 @@ class ProjectsController < ApplicationController def show end - def mp_show - @project = Project.joins(:owner).find params[:project_id] - data={ - owner:@project.owner.try(:login), - identifier:@project.identifier - } - render_ok(data:data) - end - def destroy if current_user.admin? || @project.manager?(current_user) ActiveRecord::Base.transaction do diff --git a/app/helpers/api/pm/issue_links_helper.rb b/app/helpers/api/pm/issue_links_helper.rb new file mode 100644 index 000000000..ff7d1ef33 --- /dev/null +++ b/app/helpers/api/pm/issue_links_helper.rb @@ -0,0 +1,2 @@ +module Api::Pm::IssueLinksHelper +end diff --git a/app/helpers/api/pm/projects_helper.rb b/app/helpers/api/pm/projects_helper.rb new file mode 100644 index 000000000..172c270e7 --- /dev/null +++ b/app/helpers/api/pm/projects_helper.rb @@ -0,0 +1,2 @@ +module Api::Pm::ProjectsHelper +end diff --git a/app/helpers/api/v1/pm_issues_helper.rb b/app/helpers/api/v1/pm_issues_helper.rb new file mode 100644 index 000000000..ced4b55c9 --- /dev/null +++ b/app/helpers/api/v1/pm_issues_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::PmIssuesHelper +end diff --git a/app/helpers/api/v1/sonarqube/issues_helper.rb b/app/helpers/api/v1/sonarqube/issues_helper.rb new file mode 100644 index 000000000..7dbee46e4 --- /dev/null +++ b/app/helpers/api/v1/sonarqube/issues_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::Sonarqube::IssuesHelper +end diff --git a/app/helpers/api/v1/sonarqubes_helper.rb b/app/helpers/api/v1/sonarqubes_helper.rb new file mode 100644 index 000000000..94205dc10 --- /dev/null +++ b/app/helpers/api/v1/sonarqubes_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::SonarqubesHelper +end diff --git a/app/helpers/pm/journals_helper.rb b/app/helpers/pm/journals_helper.rb new file mode 100644 index 000000000..e1a99e5ee --- /dev/null +++ b/app/helpers/pm/journals_helper.rb @@ -0,0 +1,2 @@ +module Pm::JournalsHelper +end diff --git a/app/models/action/node.rb b/app/models/action/node.rb index 69e45b3a8..79c54293d 100644 --- a/app/models/action/node.rb +++ b/app/models/action/node.rb @@ -16,11 +16,12 @@ # user_id :integer # created_at :datetime not null # updated_at :datetime not null +# label :string(255) # # Indexes # -# index_action_nodes_on_action_types_id (action_node_types_id) -# index_action_nodes_on_user_id (user_id) +# index_action_nodes_on_action_node_types_id (action_node_types_id) +# index_action_nodes_on_user_id (user_id) # class Action::Node < ApplicationRecord @@ -33,39 +34,36 @@ class Action::Node < ApplicationRecord belongs_to :user, optional: true + attr_accessor :cust_name, :run_values, :input_values - # def content_yaml - # "foo".to_yaml - # <<~YAML - # - name: Set up JDK ${{ matrix.java }} - # uses: actions/setup-java@v3 - # with: - # distribution: 'temurin' - # java-version: ${{ matrix.java }} - # YAML - # end + validates :name, presence: { message: "不能为空" } + validates :full_name, length: { maximum: 200, too_long: "不能超过200个字符" } + validates :label, length: { maximum: 200, too_long: "不能超过200个字符" } + validates :description, length: { maximum: 65535, too_long: "不能超过65535个字符"} - def yaml_hash + + def content_yaml + "foo".to_yaml <<~YAML - name: Check dist - - on: - push: - branches: - - main - paths-ignore: - - '**.md' - pull_request: - paths-ignore: - - '**.md' - workflow_dispatch: - - jobs: - call-check-dist: - name: Check dist/ - uses: actions/reusable-workflows/.github/workflows/check-dist.yml@main - with: - node-version: '20.x' + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} YAML end + + def node + self + end + + def build_yaml + yaml = ERB.new(File.read(File.join(Rails.root, "app/views/api/v1/projects/pipelines", "build_node.yaml.erb"))).result(binding) + # 删除空行内容 + yaml = yaml.gsub(/^\s*\n/, "") + # Rails.logger.info "=========================" + # Rails.logger.info yaml + yaml + end + end diff --git a/app/models/action/node_input.rb b/app/models/action/node_input.rb index 4f3825170..3710e1633 100644 --- a/app/models/action/node_input.rb +++ b/app/models/action/node_input.rb @@ -8,7 +8,7 @@ # input_type :string(255) # description :string(255) # is_required :boolean default("0") -# sort_no :string(255) default("0") +# sort_no :integer default("0") # user_id :integer # created_at :datetime not null # updated_at :datetime not null @@ -24,4 +24,7 @@ class Action::NodeInput < ApplicationRecord default_scope { order(sort_no: :asc) } belongs_to :action_node, :class_name => 'Action::Node', foreign_key: "action_nodes_id" + + validates :name, presence: { message: "不能为空" } + validates :description, length: { maximum: 65535, too_long: "不能超过65535个字符"} end diff --git a/app/models/action/node_select.rb b/app/models/action/node_select.rb index 25be51f99..23cd87bb8 100644 --- a/app/models/action/node_select.rb +++ b/app/models/action/node_select.rb @@ -29,6 +29,9 @@ class Action::NodeSelect < ApplicationRecord belongs_to :action_node, :class_name => 'Action::Node', foreign_key: "action_nodes_id" belongs_to :user, optional: true + validates :name, presence: { message: "不能为空" } + validates :description, length: { maximum: 65535, too_long: "不能超过65535个字符"} + def value if self.val_ext.blank? self.val diff --git a/app/models/action/node_type.rb b/app/models/action/node_type.rb index 7ce78b0fb..64d9eabce 100644 --- a/app/models/action/node_type.rb +++ b/app/models/action/node_type.rb @@ -5,7 +5,7 @@ # id :integer not null, primary key # name :string(255) # description :string(255) -# sort_no :integer +# sort_no :integer default("0") # created_at :datetime not null # updated_at :datetime not null # @@ -15,4 +15,7 @@ class Action::NodeType < ApplicationRecord default_scope { order(sort_no: :asc) } has_many :action_nodes, :class_name => 'Action::Node', foreign_key: "action_node_types_id" + + validates :name, presence: { message: "不能为空" } + validates :description, length: { maximum: 65535, too_long: "不能超过65535个字符"} end diff --git a/app/models/action/pipeline.rb b/app/models/action/pipeline.rb new file mode 100644 index 000000000..227fa4aa8 --- /dev/null +++ b/app/models/action/pipeline.rb @@ -0,0 +1,39 @@ +# == Schema Information +# +# Table name: action_pipelines +# +# id :integer not null, primary key +# project_id :integer +# user_id :integer +# pipeline_name :string(255) +# pipeline_status :string(255) +# description :string(255) +# file_name :string(255) +# is_graphic_design :boolean default("0") +# repo_name :string(255) +# repo_identifier :string(255) +# repo_owner :string(255) +# branch :string(255) +# event :string(255) +# sha :string(255) +# json :text(65535) +# yaml :text(65535) +# disable :boolean default("0") +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_action_pipelines_on_project_id (project_id) +# index_action_pipelines_on_user_id (user_id) +# + +class Action::Pipeline < ApplicationRecord + self.table_name = 'action_pipelines' + belongs_to :user, optional: true + belongs_to :project + + validates :name, presence: { message: "不能为空" } + validates :json, length: { maximum: 65535, too_long: "不能超过65535个字符"} + validates :yaml, length: { maximum: 65535, too_long: "不能超过65535个字符"} +end diff --git a/app/models/action/template.rb b/app/models/action/template.rb index 34b669f66..d256c7f67 100644 --- a/app/models/action/template.rb +++ b/app/models/action/template.rb @@ -6,7 +6,7 @@ # name :string(255) # description :string(255) # img :string(255) -# sort_no :string(255) default("0") +# sort_no :integer default("0") # json :text(65535) # yaml :text(65535) # created_at :datetime not null @@ -17,4 +17,7 @@ class Action::Template < ApplicationRecord self.table_name = 'action_templates' default_scope { order(sort_no: :asc) } + validates :name, presence: { message: "不能为空" } + validates :description, length: { maximum: 65535, too_long: "不能超过65535个字符"} + end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index a7d874805..330979e82 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -26,8 +26,6 @@ # cloud_url :string(255) default("") # course_second_category_id :integer default("0") # delay_publish :boolean default("0") -# memo_image :boolean default("0") -# extra_type :integer default("0") # uuid :string(255) # # Indexes @@ -38,12 +36,14 @@ # index_attachments_on_created_on (created_on) # index_attachments_on_is_public (is_public) # index_attachments_on_quotes (quotes) +# index_attachments_on_uuid (uuid) # + class Attachment < ApplicationRecord include BaseModel include Publicable @@ -76,6 +76,42 @@ class Attachment < ApplicationRecord DCODES = %W(2 3 4 5 6 7 8 9 a b c f e f g h i j k l m n o p q r s t u v w x y z) + def self.build_from_remote_url(user, name, url, container=nil) + ext = name.split('.')[-1] + tmp_path = "#{Rails.root}/#{name}" + uri = URI(url) + size = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| + response = http.get(uri.path) + File.open(tmp_path, 'wb') do |file| + file.write(response.body) + end + end + digest = "#{Digest::MD5.file(tmp_path).hexdigest}_#{(Time.now.to_f * 1000).to_i}.#{ext}" + month_folder = "#{Time.now.year}/#{Time.now.month.to_s.rjust(2, '0')}" + save_path = "#{Rails.root}#{EduSetting.get("attachment_folder")}#{month_folder}" + unless Dir.exists?(save_path) + FileUtils.mkdir_p(save_path) ##不成功这里会抛异常 + end + path = File.join(save_path, digest) + FileUtils.mv(tmp_path, path) + attachment = Attachment.new + attachment.filename = name + attachment.disk_filename = path[save_path.size+1, path.size] + attachment.filesize = size + attachment.content_type = 'application/octet-stream' + attachment.digest = digest.split('.')[0] + attachment.author_id = user.id + attachment.disk_directory = month_folder + attachment.cloud_url = url + attachment.uuid = SecureRandom.uuid + attachment.container = container + attachment.save! + + return attachment + rescue + return nil + end + def diskfile File.join(File.join(Rails.root, "files"), disk_directory.to_s, disk_filename.to_s) end diff --git a/app/models/gitlink_competition_apply.rb b/app/models/gitlink_competition_apply.rb index f3b7d4ce1..cc942bd45 100644 --- a/app/models/gitlink_competition_apply.rb +++ b/app/models/gitlink_competition_apply.rb @@ -1,3 +1,23 @@ +# == Schema Information +# +# Table name: gitlink_competition_applies +# +# id :integer not null, primary key +# competition_id :integer +# competition_identifier :string(255) +# team_id :integer +# team_name :string(255) +# school_name :string(255) +# educoder_login :string(255) +# nickname :string(255) +# phone :string(255) +# email :string(255) +# identity :string(255) +# role :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# + # == Schema Information # # Table name: gitlink_competition_applies diff --git a/app/models/issue.rb b/app/models/issue.rb index 26c170e56..955f903e9 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -63,16 +63,17 @@ class Issue < ApplicationRecord has_many :project_trends, as: :trend, dependent: :destroy has_one :pull_request # belongs_to :issue_tag,optional: true - belongs_to :priority, :class_name => 'IssuePriority', foreign_key: :priority_id,optional: true + belongs_to :priority, class_name: 'IssuePriority', foreign_key: :priority_id,optional: true belongs_to :version, foreign_key: :fixed_version_id,optional: true, counter_cache: true belongs_to :user,optional: true, foreign_key: :author_id belongs_to :issue_status, foreign_key: :status_id,optional: true + belongs_to :parent_issue, class_name: 'Issue', optional: true, foreign_key: :root_id, counter_cache: :child_count has_many :commit_issues has_many :attachments, as: :container, dependent: :destroy # has_many :memos - has_many :journals, :as => :journalized, :dependent => :destroy + has_many :journals, as: :journalized, dependent: :destroy has_many :journal_details, through: :journals - has_many :claims, :dependent => :destroy + has_many :claims, dependent: :destroy has_many :claim_users, through: :claims, source: :user has_many :issue_tags_relates, dependent: :destroy has_many :issue_tags, through: :issue_tags_relates @@ -82,25 +83,67 @@ class Issue < ApplicationRecord has_many :assigners, through: :issue_assigners has_many :issue_participants, dependent: :destroy has_many :participants, through: :issue_participants - has_many :show_participants, -> {joins(:issue_participants).where.not(issue_participants: {participant_type: "atme"}).distinct}, through: :issue_participants, source: :participant + has_many :children_issues, class_name: 'Issue', foreign_key: :root_id, dependent: :destroy + has_many :show_participants, -> {joins(:issue_participants).where.not(issue_participants: {participant_type: 'atme'}).distinct}, through: :issue_participants, source: :participant has_many :show_assigners, -> {joins(:issue_assigners).distinct}, through: :issue_assigners, source: :assigner has_many :show_issue_tags, -> {joins(:issue_tags_relates).distinct}, through: :issue_tags_relates, source: :issue_tag - has_many :comment_journals, -> {where.not(notes: nil)}, class_name: "Journal", :as => :journalized - has_many :operate_journals, -> {where(notes: nil)}, class_name: "Journal", :as => :journalized - has_many :pull_attached_issues, dependent: :destroy + has_many :comment_journals, -> {where.not(notes: nil)}, class_name: 'Journal', as: :journalized + has_many :operate_journals, -> {where(notes: nil)}, class_name: 'Journal', as: :journalized + has_many :pull_attached_issues, dependent: :destroy has_many :attach_pull_requests, through: :pull_attached_issues, source: :pull_request + # PM 关联工作项目 + has_many :pm_links, as: :linkable, dependent: :destroy + + belongs_to :changer, class_name: 'User', foreign_key: :changer_id, optional: true scope :issue_includes, ->{includes(:user)} scope :issue_many_includes, ->{includes(journals: :user)} - scope :issue_issue, ->{where(issue_classify: [nil,"issue"])} - scope :issue_pull_request, ->{where(issue_classify: "pull_request")} + scope :issue_issue, ->{where(issue_classify: [nil, 'issue'])} + scope :issue_pull_request, ->{where(issue_classify: 'pull_request')} scope :issue_index_includes, ->{includes(:tracker, :priority, :version, :issue_status, :journals,:issue_tags,user: :user_extension)} scope :closed, ->{where(status_id: 5)} scope :opened, ->{where.not(status_id: 5)} after_create :incre_project_common, :incre_user_statistic, :incre_platform_statistic - after_save :incre_or_decre_closed_issues_count, :change_versions_count, :send_update_message_to_notice_system, :associate_attachment_container - after_destroy :update_closed_issues_count_in_project!, :decre_project_common, :decre_user_statistic, :decre_platform_statistic + before_save :check_pm_and_update_due_date + after_save :incre_or_decre_closed_issues_count, :change_versions_count, :send_update_message_to_notice_system, :associate_attachment_container, :generate_uuid + after_destroy :update_closed_issues_count_in_project!, :decre_project_common, :decre_user_statistic, :decre_platform_statistic, :destroy_be_pm_links + + + def pm_issue_type_string + case pm_issue_type + when 1 + "requirement" + when 2 + "task" + when 3 + "bug" + else + "issue" + end + end + + def destroy_be_pm_links + PmLink.where(be_linkable_type:"Issue",be_linkable_id:self.id).map(&:destroy) + end + + def check_pm_and_update_due_date + if pm_project_id.present? && pm_issue_type.present? && status_id_changed? + status_ids = case pm_issue_type + when 1 + [3,5] + when 2 + [3,5] + when 3 + [5] + else + [] + end + if status_ids.include? self.status_id + self.due_date = self.due_date || Time.current + end + end + end def is_issuely_issue? self.issue_classify.nil? || self.issue_classify == 'issue' @@ -118,27 +161,34 @@ class Issue < ApplicationRecord end def incre_project_common - CacheAsyncSetJob.perform_later("project_common_service", {issues: 1}, self.project_id) if is_issuely_issue? + CacheAsyncSetJob.perform_later('project_common_service', {issues: 1}, self.project_id) if is_issuely_issue? end def decre_project_common - CacheAsyncSetJob.perform_later("project_common_service", {issues: -1}, self.project_id) if is_issuely_issue? + CacheAsyncSetJob.perform_later('project_common_service', {issues: -1}, self.project_id) if is_issuely_issue? end def incre_user_statistic - CacheAsyncSetJob.perform_later("user_statistic_service", {issue_count: 1}, self.author_id) if is_issuely_issue? + CacheAsyncSetJob.perform_later('user_statistic_service', {issue_count: 1}, self.author_id) if is_issuely_issue? end def decre_user_statistic - CacheAsyncSetJob.perform_later("user_statistic_service", {issue_count: -1}, self.author_id) if is_issuely_issue? + CacheAsyncSetJob.perform_later('user_statistic_service', {issue_count: -1}, self.author_id) if is_issuely_issue? + end + + def refresh_root_issue_count + return if root_id.nil? || root_id.zero? + root_issue = Issue.find_by(id: root_id) + root_count = Issue.where(root_id: root_id).count + root_issue.update(child_count: root_count) end def incre_platform_statistic - CacheAsyncSetJob.perform_later("platform_statistic_service", {issue_count: 1}) if is_issuely_issue? + CacheAsyncSetJob.perform_later('platform_statistic_service', {issue_count: 1}) if is_issuely_issue? end def decre_platform_statistic - CacheAsyncSetJob.perform_later("platform_statistic_service", {issue_count: -1}) if is_issuely_issue? + CacheAsyncSetJob.perform_later('platform_statistic_service', {issue_count: -1}) if is_issuely_issue? end def get_assign_user @@ -147,20 +197,20 @@ class Issue < ApplicationRecord def create_journal_detail(change_files, issue_files, issue_file_ids, user_id) journal_params = { - journalized_id: self.id, journalized_type: "Issue", user_id: user_id + journalized_id: self.id, journalized_type: 'Issue', user_id: user_id } journal = Journal.new journal_params if journal.save if change_files - old_attachment_names = self.attachments.select(:filename,:id).where(id: issue_file_ids).pluck(:filename).join(",") - new_attachment_name = self.attachments.select(:filename,:id).where(id: issue_files).pluck(:filename).join(",") - journal.journal_details.create(property: "attachment", prop_key: "#{issue_files.size}", old_value: old_attachment_names, value: new_attachment_name) + old_attachment_names = self.attachments.select(:filename,:id).where(id: issue_file_ids).pluck(:filename).join(',') + new_attachment_name = self.attachments.select(:filename,:id).where(id: issue_files).pluck(:filename).join(',') + journal.journal_details.create(property: 'attachment', prop_key: "#{issue_files.size}", old_value: old_attachment_names, value: new_attachment_name) end change_values = %w(subject description is_private assigned_to_id tracker_id status_id priority_id fixed_version_id start_date due_date estimated_hours done_ratio issue_tags_value issue_type token branch_name) change_values.each do |at| if self.send("saved_change_to_#{at}?") - journal.journal_details.create(property: "attr", prop_key: "#{at}", old_value: self.send("#{at}_before_last_save"), value: self.send(at)) + journal.journal_details.create(property: 'attr', prop_key: "#{at}", old_value: self.send("#{at}_before_last_save"), value: self.send(at)) end end end @@ -168,11 +218,11 @@ class Issue < ApplicationRecord def custom_journal_detail(prop_key, old_value, value, user_id) journal_params = { - journalized_id: self.id, journalized_type: "Issue", user_id: user_id + journalized_id: self.id, journalized_type: 'Issue', user_id: user_id } journal = Journal.new journal_params if journal.save - journal.journal_details.create(property: "attr", prop_key: prop_key, old_value: old_value, value: value) + journal.journal_details.create(property: 'attr', prop_key: prop_key, old_value: old_value, value: value) end end @@ -192,20 +242,29 @@ class Issue < ApplicationRecord end end + def generate_uuid + # return if pm_project_id.nil? + # attachments.map(&:generate_uuid) + end + def is_collaborators? - self.assigned_to_id.present? ? self.project.member?(self.assigned_to_id) : false + if self.assigned_to_id.present? && self.project.present? + self.project.member?(self.assigned_to_id) + else + false + end end def get_issue_tags_name if issue_tags.present? - issue_tags.select(:name).uniq.pluck(:name).join(",") + issue_tags.select(:name).uniq.pluck(:name).join(',') else nil end end def only_reply_journals - journals.where.not(notes: [nil, ""]).journal_includes.limit(2) + journals.where.not(notes: [nil, '']).journal_includes.limit(2) end def change_versions_count @@ -235,7 +294,7 @@ class Issue < ApplicationRecord end def update_closed_issues_count_in_project! - self.project.decrement!(:closed_issues_count) if self.status_id == 5 + self.project.decrement!(:closed_issues_count) if self.status_id == 5 && self.project.present? end def send_update_message_to_notice_system @@ -267,8 +326,8 @@ class Issue < ApplicationRecord def to_builder Jbuilder.new do |issue| issue.(self, :id, :project_issues_index, :subject, :description, :branch_name, :start_date, :due_date) - issue.created_at self.created_on.strftime("%Y-%m-%d %H:%M") - issue.updated_at self.updated_on.strftime("%Y-%m-%d %H:%M") + issue.created_at self.created_on.strftime('%Y-%m-%d %H:%M') + issue.updated_at self.updated_on.strftime('%Y-%m-%d %H:%M') issue.tags self.show_issue_tags.map{|t| JSON.parse(t.to_builder.target!)} issue.status self.issue_status.to_builder if self.priority.present? @@ -290,4 +349,12 @@ class Issue < ApplicationRecord end end + def self.full_children_issues(issue, issues = []) + issue.children_issues.each do |i| + issues << i + full_children_issues(i, issues) + end + issues + end + end diff --git a/app/models/issue_priority.rb b/app/models/issue_priority.rb index 5bf70da05..5904af649 100644 --- a/app/models/issue_priority.rb +++ b/app/models/issue_priority.rb @@ -38,4 +38,21 @@ class IssuePriority < ApplicationRecord priority.(self, :id, :name) end end + + def pm_color + case name + when '低' + '#13b33e' + when '正常' + '#0d5ef8' + when '高' + '#ff6f00' + when '紧急' + '#d20f0f' + # when '立刻' + # '#f5222d' + else + '#13b33e' + end + end end diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb index fde871182..63c15bd89 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -45,9 +45,28 @@ class IssueStatus < ApplicationRecord end end - def to_builder + def to_builder Jbuilder.new do |status| status.(self, :id, :name) end end + + def pm_color + case name + when '新增' + '#ff6f00' + when '正在解决' + '#0d5ef8' + when '已解决' + '#13b33e' + when '关闭' + '#b1aaa5' + # when '反馈' + # '#13c2c2' + when '拒绝' + '#ff0000' + else + '#ff6f00' + end + end end diff --git a/app/models/issue_tag.rb b/app/models/issue_tag.rb index f5f0abbd8..496cc907e 100644 --- a/app/models/issue_tag.rb +++ b/app/models/issue_tag.rb @@ -15,9 +15,11 @@ # gitea_url :string(255) # pull_requests_count :integer default("0") # pm_project_id :integer +# organization_id :integer # # Indexes # +# index_issue_tags_on_organization_id (organization_id) # index_issue_tags_on_user_id_and_name_and_project_id (user_id,name,project_id) # @@ -29,8 +31,15 @@ class IssueTag < ApplicationRecord has_many :pull_request_issues, -> {where(issue_classify: "pull_request")}, source: :issue, through: :issue_tags_relates belongs_to :project, optional: true, counter_cache: true belongs_to :user, optional: true + belongs_to :organization, optional: true - validates :name, uniqueness: {scope: :project_id, message: "已存在" } + scope :pm_able, -> {where(project_id: 0)} + + validates :name, uniqueness: {scope: :project_id, message: "已存在" }, if: :pm_project? + + def pm_project? + !project_id.zero? + end def self.init_data(project_id) data = init_issue_tag_data @@ -41,6 +50,24 @@ class IssueTag < ApplicationRecord $redis_cache.hset("project_init_issue_tags", project_id, 1) end + def self.pm_init_data(pm_project_id) + data = init_issue_tag_data + data.each do |item| + next if IssueTag.exists?(pm_project_id: pm_project_id, project_id: 0, name: item[0]) + IssueTag.create!(pm_project_id: pm_project_id, project_id: 0, name: item[0], description: item[1], color: item[2]) + end + $redis_cache.hset("pm_project_init_issue_tags", pm_project_id, 1) + end + + def self.pm_org_init_data(organization_id) + data = init_issue_tag_data + data.each do |item| + next if IssueTag.exists?(organization_id: organization_id, project_id: 0, name: item[0]) + IssueTag.create!(organization_id: organization_id, project_id: 0, name: item[0], description: item[1], color: item[2]) + end + $redis_cache.hset("pm_org_init_issue_tags", organization_id, 1) + end + def reset_counter_field self.update_column(:issues_count, issue_issues.size) self.update_column(:pull_requests_count, pull_request_issues.size) diff --git a/app/models/journal.rb b/app/models/journal.rb index 0fdca0f9d..377ca1331 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -82,6 +82,423 @@ class Journal < ApplicationRecord end end + def pm_operate_category + detail = self.journal_details.take + if %w(requirement task bug).include?(detail.property) && detail.prop_key.to_s == "1" + return "issue" + else + return %w(requirement task bug attr).include?(detail.property) ? detail.prop_key : detail.property + end + end + + def pm_operate_content + content = "" + detail = self.journal_details.take + case detail.property + when 'requirement' + case detail.prop_key + when 'status_id' + old_value = IssueStatus.find_by_id(detail.old_value)&.name + new_value = IssueStatus.find_by_id(detail.value)&.name + content += "将状态" + if detail.old_value.nil? || detail.old_value.blank? + content += "设置为#{new_value}" + else + new_value = "未设置" if detail.value.blank? + content += "由#{old_value}更改为#{new_value}" + end + content.gsub!('新增', '待评审') + content.gsub!('正在解决', '进行中') + content.gsub!('已解决', '已完成') + content.gsub!('关闭', '已关闭') + content.gsub!('拒绝', '已拒绝') + return content + when 'root_id' + old_value = "<#{Issue.find_by_id(detail.old_value)&.subject}>" + new_value = "<#{Issue.find_by_id(detail.value)&.subject}>" + if detail.old_value.nil? || detail.old_value.blank? + content += "关联了父需求#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "取消了关联的父需求#{old_value}" + else + content += "将关联的父需求由#{old_value}更改为#{new_value}" + end + end + return content + when 'leaf_issue' + old_value = Issue.where(id: detail.old_value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + new_value = Issue.where(id: detail.value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "新建了子需求#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "删除了关联的子需求#{old_value}" + else + content += "新建了子需求#{new_value}" + end + end + return content + when 'tag_leaf_issue' + old_value = Issue.where(id: detail.old_value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + new_value = Issue.where(id: detail.value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "关联了子需求#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "取消了关联的子需求#{old_value}" + else + content += "关联了子需求#{new_value}" + end + end + return content + else + return "创建了需求" + end + when 'task' + case detail.prop_key + when 'status_id' + old_value = IssueStatus.find_by_id(detail.old_value)&.name + new_value = IssueStatus.find_by_id(detail.value)&.name + content += "将状态" + if detail.old_value.nil? || detail.old_value.blank? + content += "设置为#{new_value}" + else + new_value = "未设置" if detail.value.blank? + content += "由#{old_value}更改为#{new_value}" + end + content.gsub!('新增', '待处理') + content.gsub!('正在解决', '进行中') + content.gsub!('已解决', '已完成') + content.gsub!('关闭', '已关闭') + content.gsub!('拒绝', '已拒绝') + return content + when 'root_id' + old_value = "<#{Issue.find_by_id(detail.old_value)&.subject}>" + new_value = "<#{Issue.find_by_id(detail.value)&.subject}>" + if detail.old_value.nil? || detail.old_value.blank? + content += "关联了父任务#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "取消了关联的父任务#{old_value}" + else + content += "将关联的父任务由#{old_value}>更改为#{new_value}" + end + end + return content + when 'leaf_issue' + old_value = Issue.where(id: detail.old_value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + new_value = Issue.where(id: detail.value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "新建了子任务#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "删除了关联的子任务#{old_value}" + else + content += "新建了子任务#{new_value}" + end + end + return content + when 'tag_leaf_issue' + old_value = Issue.where(id: detail.old_value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + new_value = Issue.where(id: detail.value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "关联了子任务#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "取消了关联的子任务#{old_value}" + else + content += "关联了子任务#{new_value}" + end + end + return content + else + return "创建了任务" + end + when 'bug' + case detail.prop_key + when 'status_id' + old_value = IssueStatus.find_by_id(detail.old_value)&.name + new_value = IssueStatus.find_by_id(detail.value)&.name + content += "将状态" + if detail.old_value.nil? || detail.old_value.blank? + content += "设置为#{new_value}" + else + new_value = "未设置" if detail.value.blank? + content += "由#{old_value}更改为#{new_value}" + end + content.gsub!('新增', '待修复') + content.gsub!('正在解决', '修复中') + content.gsub!('已解决', '已修复') + content.gsub!('关闭', '已关闭') + content.gsub!('拒绝', '已拒绝') + return content + when 'root_id' + old_value = "<#{Issue.find_by_id(detail.old_value)&.subject}>" + new_value = "<#{Issue.find_by_id(detail.value)&.subject}>" + if detail.old_value.nil? || detail.old_value.blank? + content += "关联了父缺陷#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "取消了关联的父缺陷#{old_value}" + else + content += "将关联的父缺陷由#{old_value}更改为#{new_value}" + end + end + return content + when 'leaf_issue' + old_value = Issue.where(id: detail.old_value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + new_value = Issue.where(id: detail.value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "新建了子缺陷#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "删除了关联的子缺陷#{old_value}" + else + content += "新建了子缺陷#{new_value}" + end + end + return content + when 'tag_leaf_issue' + old_value = Issue.where(id: detail.old_value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + new_value = Issue.where(id: detail.value.to_s.split(",")).map{|i| "<#{i.subject}>"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "关联了子缺陷#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "取消了关联的子缺陷#{old_value}" + else + content += "关联了子缺陷#{new_value}" + end + end + return content + else + return "创建了缺陷" + end + when 'attr' + case detail.prop_key + when 'subject' + return "修改了标题" + when 'description' + return "修改了正文" + when 'priority_id' + old_value = IssuePriority.find_by_id(detail.old_value)&.name + new_value = IssuePriority.find_by_id(detail.value)&.name + if detail.old_value.nil? || detail.old_value.blank? + content += "将优先级设置为#{new_value}" + else + new_value = "未设置" if detail.value.blank? + content += "将优先级由#{old_value}更改为#{new_value}" + end + return content + when 'status_id' + old_value = IssueStatus.find_by_id(detail.old_value)&.name + new_value = IssueStatus.find_by_id(detail.value)&.name + if detail.old_value.nil? || detail.old_value.blank? + content += "将状态设置为#{new_value}" + else + new_value = "未设置" if detail.value.blank? + content += "将状态由#{old_value}更改为#{new_value}" + end + case self.issue.pm_issue_type.to_i + when 1 + content.gsub!('新增', '待评审') + content.gsub!('正在解决', '进行中') + content.gsub!('已解决', '已完成') + content.gsub!('关闭', '已关闭') + content.gsub!('拒绝', '已拒绝') + when 2 + content.gsub!('新增', '待处理') + content.gsub!('正在解决', '进行中') + content.gsub!('已解决', '已完成') + content.gsub!('关闭', '已关闭') + content.gsub!('拒绝', '已拒绝') + when 3 + content.gsub!('新增', '待修复') + content.gsub!('正在解决', '修复中') + content.gsub!('已解决', '已修复') + content.gsub!('关闭', '已关闭') + content.gsub!('拒绝', '已拒绝') + end + return content + when 'pm_issue_type' + old_value = detail.old_value + new_value = detail.value + if detail.old_value.nil? || detail.old_value.blank? + content += "将工作项类型设置为#{new_value}" + else + new_value = "未设置" if detail.value.blank? + content += "将工作项类型由#{old_value}更改为#{new_value}" + end + content.gsub!('1', '需求') + content.gsub!('2', '任务') + content.gsub!('3', '缺陷') + return content + when 'pm_sprint_id' + old_value = detail.old_value + new_value = detail.value + if detail.old_value.nil? || detail.old_value.blank? + content += "添加了关联迭代" + else + if detail.value.nil? || detail.value.blank? + content += "将关联迭代更改为未设置" + else + content += "变更了关联迭代" + end + end + return content + when 'project_id' + old_value = Project.find_by_id(detail.old_value)&.name + new_value = Project.find_by_id(detail.value)&.name + if detail.old_value.nil? || detail.old_value.blank? + content += "添加关联代码库#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "将关联代码库更改为未设置" + else + content += "将关联代码库由#{old_value}改为#{new_value}" + end + end + return content + when 'branch_name' + old_value = detail.old_value + new_value = detail.value + if detail.old_value.nil? || detail.old_value.blank? + content += "添加关联分支#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "将关联分支更改为未设置" + else + content += "将关联分支由#{old_value}改为#{new_value}" + end + end + return content + + when 'start_date' + old_value = detail.old_value + new_value = detail.value + if detail.old_value.nil? || detail.old_value.blank? + content += "添加开始时间#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "将开始时间更改为未设置" + else + content += "将开始时间由#{old_value}改为#{new_value}" + end + end + return content + + when 'due_date' + old_value = detail.old_value + new_value = detail.value + if detail.old_value.nil? || detail.old_value.blank? + content += "添加结束时间#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "将结束时间更改为未设置" + else + content += "将结束时间由#{old_value}改为#{new_value}" + end + end + return content + when 'time_scale' + old_value = detail.old_value + new_value = detail.value + if detail.old_value.nil? || detail.old_value.blank? + content += "添加预估工时#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "将预估工时更改为未设置" + else + content += "将预估工时由#{old_value}改为#{new_value}" + end + end + return content + end + when 'attachment' + old_value = detail.old_value.to_s + new_value = detail.value.to_s + if detail.old_value.nil? || detail.old_value.blank? + content += "上传了附件" + else + if detail.value.nil? || detail.value.blank? + content += "删除了附件" + else + content += "上传了附件" + end + end + return content + when 'assigner' + old_value = User.where(id: detail.old_value.split(",")).map{|u| "#{u.real_name}"}.join("、") + new_value = User.where(id: detail.value.split(",")).map{|u| "#{u.real_name}"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "添加负责人#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "将负责人更改为未设置" + else + content += "将负责人由#{old_value}更改为#{new_value}" + end + end + return content + when 'issue_tag' + old_value = IssueTag.where(id: detail.old_value.split(",")).map{|t| "#{t.name}"}.join("、") + new_value = IssueTag.where(id: detail.value.split(",")).map{|t| "#{t.name}"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "添加标记#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "将标记更改为未设置" + else + content += "将标记由#{old_value}更改为#{new_value}" + end + end + return content + when 'link_issue' + old_value = Issue.where(id: detail.old_value.to_s.split(",")).map{|i| "[#{i.pm_issue_type}]-<#{i.subject}>"}.join("、") + new_value = Issue.where(id: detail.value.to_s.split(",")).map{|i| "[#{i.pm_issue_type}]-<#{i.subject}>"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "新建了关联的工作项#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "删除了关联的工作项#{old_value}" + else + content += "新建了关联的工作项#{new_value}" + end + end + content.gsub!('1', "需求") + content.gsub!('2', "任务") + content.gsub!('3', "缺陷") + return content + when 'tag_link_issue' + old_value = Issue.where(id: detail.old_value.to_s.split(",")).map{|i| "[#{i.pm_issue_type}]-<#{i.subject}>"}.join("、") + new_value = Issue.where(id: detail.value.to_s.split(",")).map{|i| "[#{i.pm_issue_type}]-<#{i.subject}>"}.join("、") + if detail.old_value.nil? || detail.old_value.blank? + content += "关联了工作项#{new_value}" + else + if detail.value.nil? || detail.value.blank? + content += "取消了关联的工作项#{old_value}" + else + content += "关联了工作项#{new_value}" + end + end + content.gsub!('1', "需求") + content.gsub!('2', "任务") + content.gsub!('3', "缺陷") + return content + when 'issue' + issue = self.issue + case issue.pm_issue_type + when 1 + return "创建了需求" + when 2 + return "创建了任务" + when 3 + return "创建了缺陷" + end + end + end + def operate_content content = "" detail = self.journal_details.take @@ -89,8 +506,8 @@ class Journal < ApplicationRecord when 'issue' return "创建了疑修" when 'attachment' - old_value = Attachment.where("id in (?) or uuid in (?)", detail.old_value.to_s.split(","), detail.old_value.to_s.split(",")).pluck(:filename).join("、") - new_value = Attachment.where("id in (?) or uuid in (?)", detail.value.to_s.split(","), detail.value.to_s.split(",")).pluck(:filename).join("、") + old_value = Attachment.where("BINARY id in (?) or uuid in (?)", detail.old_value.to_s.split(","), detail.old_value.to_s.split(",")).pluck(:filename).join("、") + new_value = Attachment.where("BINARY id in (?) or uuid in (?)", detail.value.to_s.split(","), detail.value.to_s.split(",")).pluck(:filename).join("、") if old_value.nil? || old_value.blank? content += "添加了#{new_value}附件" else diff --git a/app/models/member.rb b/app/models/member.rb index aaaf34efc..521f939c5 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -11,7 +11,6 @@ # course_group_id :integer default("0") # is_collect :integer default("1") # graduation_group_id :integer default("0") -# is_apply_signature :boolean default("0") # team_user_id :integer # # Indexes diff --git a/app/models/organization_user.rb b/app/models/organization_user.rb index 900710a9a..4ff6946b7 100644 --- a/app/models/organization_user.rb +++ b/app/models/organization_user.rb @@ -5,7 +5,6 @@ # id :integer not null, primary key # user_id :integer # organization_id :integer -# is_creator :boolean default("0") # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/pm_link.rb b/app/models/pm_link.rb new file mode 100644 index 000000000..d9d9a7893 --- /dev/null +++ b/app/models/pm_link.rb @@ -0,0 +1,28 @@ +# == Schema Information +# +# Table name: pm_links +# +# id :integer not null, primary key +# be_linkable_type :string(255) not null +# be_linkable_id :integer not null +# linkable_type :string(255) not null +# linkable_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_pm_links_on_linkable_id (linkable_id) +# index_pm_links_on_linkable_type (linkable_type) +# + +class PmLink < ApplicationRecord + belongs_to :linkable, polymorphic: true + belongs_to :be_linkable, polymorphic: true + # belongs_to :linkable_issue, -> {where(pm_links: {linkable_type: 'Issue'})}, foreign_key: 'linkable_id', class_name: 'Issue' + # belongs_to :be_linkable_issue, -> {where(pm_links: {be_linkable_type: 'Issue'})}, foreign_key: 'be_linkable_id', class_name: 'Issue' + + def be_linkable + be_linkable_type.constantize.find be_linkable_id + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 99f87fe30..093fb8b1a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -59,6 +59,7 @@ # is_pinned :boolean default("0") # recommend_index :integer default("0") # pr_view_admin :boolean default("0") +# has_actions :boolean default("0") # # Indexes # diff --git a/app/models/project_category.rb b/app/models/project_category.rb index bc6f8427d..97a304259 100644 --- a/app/models/project_category.rb +++ b/app/models/project_category.rb @@ -15,7 +15,6 @@ # Indexes # # index_project_categories_on_ancestry (ancestry) -# index_project_categories_on_id (id) # class ProjectCategory < ApplicationRecord diff --git a/app/models/project_language.rb b/app/models/project_language.rb index 22a4a81ff..0770a1efa 100644 --- a/app/models/project_language.rb +++ b/app/models/project_language.rb @@ -9,10 +9,6 @@ # created_at :datetime not null # updated_at :datetime not null # -# Indexes -# -# index_project_languages_on_id (id) -# class ProjectLanguage < ApplicationRecord include Projectable diff --git a/app/models/repository.rb b/app/models/repository.rb index 7d3f207ea..f2815dde7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -27,7 +27,6 @@ # # Indexes # -# index_name (project_id) # index_repositories_on_identifier (identifier) # index_repositories_on_project_id (project_id) # index_repositories_on_user_id (user_id) diff --git a/app/models/sync_repositories/gitee.rb b/app/models/sync_repositories/gitee.rb index 0a51b21c8..29309c99b 100644 --- a/app/models/sync_repositories/gitee.rb +++ b/app/models/sync_repositories/gitee.rb @@ -11,6 +11,8 @@ # sync_direction :integer # created_at :datetime not null # updated_at :datetime not null +# external_token :string(255) +# webhook_gid :integer # # Indexes # diff --git a/app/models/sync_repositories/github.rb b/app/models/sync_repositories/github.rb index 1ef413a54..10845dea7 100644 --- a/app/models/sync_repositories/github.rb +++ b/app/models/sync_repositories/github.rb @@ -11,6 +11,8 @@ # sync_direction :integer # created_at :datetime not null # updated_at :datetime not null +# external_token :string(255) +# webhook_gid :integer # # Indexes # diff --git a/app/models/token.rb b/app/models/token.rb index fac516eb8..9b9ea5895 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -15,6 +15,7 @@ # + # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -44,7 +45,7 @@ class Token < ActiveRecord::Base def self.get_or_create_permanent_login_token(user, type) token = Token.get_token_from_user(user, type) - Rails.logger.info "###### Token.get_token_from_user result: #{token&.value}" + Rails.logger.info "###### Token.get_token_from_user time:#{Time.new.to_i}, result: #{token&.value}" unless token token = Token.create(:user => user, :action => type) Rails.logger.info "###### Token.get_token_from_user is nul and agine create token: #{token&.value}" @@ -117,8 +118,8 @@ class Token < ActiveRecord::Base # Removes obsolete tokens (same user and action) def delete_previous_tokens - if user - Token.where(['user_id = ? AND action = ?', user.id, action]).delete_all - end + # if user + # Token.where(['user_id = ? AND action = ?', user.id, action]).delete_all + # end end end diff --git a/app/models/user_extension.rb b/app/models/user_extension.rb index aeb9a9d83..ef4af5fd3 100644 --- a/app/models/user_extension.rb +++ b/app/models/user_extension.rb @@ -22,9 +22,9 @@ # school_id :integer # description :string(255) # department_id :integer -# province :text(65535) -# custom_department :string(255) +# province :string(255) # city :string(255) +# custom_department :string(255) # show_email :boolean default("0") # show_location :boolean default("0") # show_department :boolean default("0") diff --git a/app/services/api/pm/issues/batch_delete_service.rb b/app/services/api/pm/issues/batch_delete_service.rb new file mode 100644 index 000000000..9afdfdb81 --- /dev/null +++ b/app/services/api/pm/issues/batch_delete_service.rb @@ -0,0 +1,38 @@ +class Api::Pm::Issues::BatchDeleteService < ApplicationService + include ActiveModel::Model + + attr_reader :project, :issues, :current_user + + validates :project, :issues, :current_user, presence: true + + def initialize(project, issues, current_user = nil) + @project = project + @issues = issues.includes(:assigners) + @current_user = current_user + end + + def call + raise Error, errors.full_messages.join(", ") unless valid? + try_lock("Api::V1::Issues::DeleteService:#{project.id}") # 开始写数据,加锁 + + delete_issues + + project.incre_project_issue_cache_delete_count(@issues.size) + + if Site.has_notice_menu? && !project.id.zero? + @issues.each do |issue| + SendTemplateMessageJob.perform_later('IssueDeleted', current_user.id, @issue&.subject, @issue.assigners.pluck(:id), @issue.author_id) + end + end + + unlock("Api::V1::Issues::DeleteService:#{project.id}") + + return true + end + + private + + def delete_issues + raise Error, "批量删除疑修失败!" unless @issues.destroy_all + end +end \ No newline at end of file diff --git a/app/services/api/pm/issues/batch_update_service.rb b/app/services/api/pm/issues/batch_update_service.rb new file mode 100644 index 000000000..b7eabf2cb --- /dev/null +++ b/app/services/api/pm/issues/batch_update_service.rb @@ -0,0 +1,33 @@ +class Api::Pm::Issues::BatchUpdateService < ApplicationService + include ActiveModel::Model + include Api::V1::Issues::Concerns::Checkable + include Api::V1::Issues::Concerns::Loadable + + attr_reader :project, :issues, :params, :current_user + attr_reader :status_id, :priority_id, :milestone_id, :project_id + attr_reader :issue_tag_ids, :assigner_ids + + validates :project, :issues, :current_user, presence: true + + def initialize(project, issues, params, current_user = nil) + @project = project + @issues = issues + @params = params + @current_user = current_user + end + + def call + raise Error, errors.full_messages.join(", ") unless valid? + ActiveRecord::Base.transaction do + @issues.each do |issue| + if issue.issue_classify == "issue" + Api::Pm::Issues::UpdateService.call(project, issue, params, current_user) + end + end + + return true + end + end + + +end \ No newline at end of file diff --git a/app/services/api/pm/issues/create_service.rb b/app/services/api/pm/issues/create_service.rb new file mode 100644 index 000000000..5412fe17d --- /dev/null +++ b/app/services/api/pm/issues/create_service.rb @@ -0,0 +1,181 @@ +class Api::Pm::Issues::CreateService < ApplicationService + include ActiveModel::Model + include Api::V1::Issues::Concerns::Checkable + include Api::V1::Issues::Concerns::Loadable + + attr_reader :project, :current_user + attr_reader :status_id, :priority_id, :milestone_id, :branch_name, :start_date, :due_date, :subject, :description, :blockchain_token_num, :root_subject + attr_reader :issue_tag_ids, :assigner_ids, :attachment_ids, :receivers_login + attr_accessor :created_issue + + validates :subject, presence: true + validates :status_id, :priority_id, presence: true + validates :project, :current_user, presence: true + validates :blockchain_token_num, numericality: {greater_than: 0}, allow_blank: true + + def initialize(project, params, current_user = nil) + @project = project + @current_user = current_user + @status_id = params[:status_id] + @priority_id = params[:priority_id] + @milestone_id = params[:milestone_id] + @branch_name = params[:branch_name] + @start_date = params[:start_date] + @due_date = params[:due_date] + @subject = params[:subject] + @description = params[:description] + @blockchain_token_num = params[:blockchain_token_num] + @issue_tag_ids = params[:issue_tag_ids] + @assigner_ids = params[:assigner_ids] + @attachment_ids = params[:attachment_ids] + @receivers_login = params[:receivers_login] + @pm_project_id = params[:pm_project_id] + @pm_sprint_id = params[:pm_sprint_id] + @pm_issue_type = params[:pm_issue_type] + @root_id = params[:root_id] + @time_scale = params[:time_scale] + @linkable_id = params[:link_able_id] + @root_subject = params[:root_subject] + end + + def call + raise Error, errors.full_messages.join(', ') unless valid? + ActiveRecord::Base.transaction do + check_issue_status(status_id) + check_issue_priority(priority_id) + check_milestone(milestone_id) if milestone_id.present? + check_issue_tags(issue_tag_ids) unless issue_tag_ids.blank? + check_assigners(assigner_ids) unless assigner_ids.blank? + check_attachments(attachment_ids) unless attachment_ids.blank? + check_atme_receivers(receivers_login) unless receivers_login.blank? + check_blockchain_token_num(current_user.id, project.id, blockchain_token_num) if blockchain_token_num.present? + load_assigners(assigner_ids) unless assigner_ids.blank? + load_attachments(attachment_ids) unless attachment_ids.blank? + load_issue_tags(issue_tag_ids) unless issue_tag_ids.blank? + load_atme_receivers(receivers_login) unless receivers_login.blank? + try_lock("Api::Pm::Issues::CreateService:#{project.id}") # 开始写数据,加锁 + @created_issue = Issue.new(issue_attributes) + @created_issue.pm_issue_type = @pm_issue_type + if @root_subject.present? && @pm_issue_type.to_i == 4 + @root_issue = Issue.find_by(subject: @root_subject, pm_issue_type: 4, pm_project_id: @pm_project_id) + unless @root_issue.present? + @root_issue = Issue.create(subject: @root_subject, pm_issue_type: 4, pm_project_id: @pm_project_id, status_id: 1, priority_id: 1, tracker_id: Tracker.first.id, project_id: @project.id, author_id: current_user.id) + end + @created_issue.root_id = @root_issue.id + else + @created_issue.root_id = @root_id + end + build_author_participants + build_assigner_participants unless assigner_ids.blank? + build_atme_participants if @atme_receivers.present? + build_issue_journal_details + build_issue_project_trends + @created_issue.assigners = @assigners unless assigner_ids.blank? + @created_issue.attachments = @attachments unless attachment_ids.blank? + @created_issue.issue_tags = @issue_tags unless issue_tag_ids.blank? + @created_issue.pm_project_id = @pm_project_id + @created_issue.pm_sprint_id = @pm_sprint_id + @created_issue.time_scale = @time_scale + @created_issue.issue_tags_value = @issue_tags.order('id asc').pluck(:id).join(',') unless issue_tag_ids.blank? + @created_issue.changer_id = @current_user.id + @created_issue.save! + if @created_issue.parent_issue.present? + parent_issue = @created_issue.parent_issue + if @created_issue.root_id.present? && parent_issue.present? + journal = parent_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: @created_issue.pm_issue_type_string, prop_key: 'leaf_issue', value: @created_issue.id.to_s}) + end + end + if @linkable_id.present? + PmLink.create(be_linkable_type: 'Issue', be_linkable_id: @created_issue.id, linkable_type: 'Issue', linkable_id: @linkable_id) + another_issue = Issue.find_by_id(@linkable_id) + if another_issue.present? + journal = another_issue.journals.create!({user_id: @current_user.id}) + journal.journal_details.create!({property: 'link_issue', prop_key: "1", value: @created_issue.id.to_s}) + end + end + if Site.has_blockchain? && @project.use_blockchain + if @created_issue.blockchain_token_num.present? && @created_issue.blockchain_token_num > 0 + Blockchain::CreateIssue.call({user_id: current_user.id, project_id: @created_issue.project_id, token_num: @created_issue.blockchain_token_num}) + end + + push_activity_2_blockchain('issue_create', @created_issue) + end + + project.del_project_issue_cache_delete_count # 把缓存里存储项目删除issue的个数清除掉 + unless @project.id.zero? + # 新增时向grimoirelab推送事件 + IssueWebhookJob.set(wait: 5.seconds).perform_later(@created_issue.id) + + # @信息发送 + AtmeService.call(current_user, @atme_receivers, @created_issue) unless receivers_login.blank? + + # 发消息 + if Site.has_notice_menu? + SendTemplateMessageJob.perform_later('IssueAssigned', current_user.id, @created_issue&.id, assigner_ids) unless assigner_ids.blank? + SendTemplateMessageJob.perform_later('ProjectIssue', current_user.id, @created_issue&.id) + end + + # 触发webhook + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueCreate', @created_issue&.id, current_user.id) + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueLabel', @created_issue&.id, current_user.id, {issue_tag_ids: [[], issue_tag_ids]}) unless issue_tag_ids.blank? + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueAssign', @created_issue&.id, current_user.id, {assigner_ids: [[], assigner_ids]}) unless assigner_ids.blank? + end + unlock("Api::Pm::Issues::CreateService:#{project.id}") # 结束写数据,解锁 + end + + return @created_issue + end + + private + + def issue_attributes + issue_attributes = { + subject: subject, + project_id: project.id, + author_id: current_user.id, + tracker_id: Tracker.first.id, + status_id: status_id, + priority_id: priority_id, + project_issues_index: (project.get_last_project_issues_index + 1), + issue_type: '1', + issue_classify: 'issue' + } + + issue_attributes.merge!({description: description}) if description.present? + issue_attributes.merge!({fixed_version_id: milestone_id}) if milestone_id.present? + issue_attributes.merge!({start_date: start_date}) if start_date.present? + issue_attributes.merge!({due_date: due_date}) if due_date.present? + issue_attributes.merge!({branch_name: branch_name}) if branch_name.present? + issue_attributes.merge!({blockchain_token_num: blockchain_token_num}) if blockchain_token_num.present? + + issue_attributes + end + + def build_author_participants + @created_issue.issue_participants.new({participant_type: 'authored', participant_id: current_user.id}) + end + + def build_assigner_participants + assigner_ids.each do |aid| + @created_issue.issue_participants.new({participant_type: 'assigned', participant_id: aid}) + end + end + + def build_atme_participants + @atme_receivers.each do |receiver| + @created_issue.issue_participants.new({participant_type: 'atme', participant_id: receiver.id}) + end + end + + def build_issue_project_trends + return if @project.id == 0 + @created_issue.project_trends.new({user_id: current_user.id, project_id: @project.id, action_type: 'create'}) + @created_issue.project_trends.new({user_id: current_user.id, project_id: @project.id, action_type: ProjectTrend::CLOSE}) if status_id.to_i == 5 + end + + def build_issue_journal_details + journal = @created_issue.journals.new({user_id: current_user.id}) + journal.journal_details.new({property: @created_issue.pm_issue_type_string, prop_key: 1, old_value: '', value: ''}) + end +end \ No newline at end of file diff --git a/app/services/api/pm/issues/delete_service.rb b/app/services/api/pm/issues/delete_service.rb new file mode 100644 index 000000000..0a04094c3 --- /dev/null +++ b/app/services/api/pm/issues/delete_service.rb @@ -0,0 +1,48 @@ +class Api::Pm::Issues::DeleteService < ApplicationService + include ActiveModel::Model + + attr_reader :project, :issue, :current_user + + validates :project, :issue, :current_user, presence: true + + def initialize(project, issue, current_user = nil) + @project = project + @issue = issue + @current_user = current_user + end + + def call + raise Error, errors.full_messages.join(", ") unless valid? + try_lock("Api::V1::Issues::DeleteService:#{project.id}") # 开始写数据,加锁 + ActiveRecord::Base.transaction do + parent_issue = @issue.parent_issue + if @issue.root_id.present? && parent_issue.present? + journal = parent_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: @issue.pm_issue_type_string, prop_key: 'leaf_issue', old_value: @issue.id.to_s}) + end + + delete_issue + #删除双向关联 + PmLink.where(be_linkable_id: @issue.id, be_linkable_type: 'Issue').or(PmLink.where(linkable_id: @issue.id, linkable_type: 'Issue')).map(&:destroy) + + project.incre_project_issue_cache_delete_count + + if Site.has_blockchain? && @project.use_blockchain && !project.id.zero? + unlock_balance_on_blockchain(@issue.author_id.to_s, @project.id.to_s, @issue.blockchain_token_num.to_i) if @issue.blockchain_token_num.present? + end + + if Site.has_notice_menu? && !project.id.zero? + SendTemplateMessageJob.perform_later('IssueDeleted', current_user.id, @issue&.subject, @issue.assigners.pluck(:id), @issue.author_id) + end + end + unlock("Api::V1::Issues::DeleteService:#{project.id}") + + return true + end + + private + + def delete_issue + raise Error, "删除疑修失败!" unless issue.destroy! + end +end \ No newline at end of file diff --git a/app/services/api/pm/issues/update_service.rb b/app/services/api/pm/issues/update_service.rb new file mode 100644 index 000000000..78d62fb2b --- /dev/null +++ b/app/services/api/pm/issues/update_service.rb @@ -0,0 +1,326 @@ +class Api::Pm::Issues::UpdateService < ApplicationService + include ActiveModel::Model + include Api::V1::Issues::Concerns::Checkable + include Api::V1::Issues::Concerns::Loadable + + attr_reader :project, :issue, :current_user + attr_reader :status_id, :priority_id, :milestone_id, :branch_name, :start_date, :due_date, :subject, :description, :blockchain_token_num + attr_reader :target_pm_project_id, :pm_sprint_id, :pm_issue_type, :root_id, :time_scale + attr_reader :issue_tag_ids, :assigner_ids, :attachment_ids, :receivers_login, :before_issue_tag_ids, :before_assigner_ids, :project_id + attr_accessor :add_assigner_ids, :previous_issue_changes, :updated_issue, :atme_receivers + + validates :project, :issue, :current_user, presence: true + validates :blockchain_token_num, numericality: {greater_than: 0}, allow_blank: true + + def initialize(project, issue, params, current_user = nil) + @project = project + @issue = issue + @current_user = current_user + @status_id = params[:status_id] + @priority_id = params[:priority_id] + @milestone_id = params[:milestone_id] + @branch_name = params[:branch_name] + @start_date = params[:start_date] + @due_date = params[:due_date] + @subject = params[:subject] + @description = params[:description] + @blockchain_token_num = params[:blockchain_token_num] + @issue_tag_ids = params[:issue_tag_ids] + @assigner_ids = params[:assigner_ids] + @before_issue_tag_ids = issue.issue_tags.pluck(:id) + @before_assigner_ids = issue.assigners.pluck(:id) + @attachment_ids = params[:attachment_ids] + @receivers_login = params[:receivers_login] + @target_pm_project_id = params[:target_pm_project_id] + @pm_sprint_id = params[:pm_sprint_id] + @pm_issue_type = params[:pm_issue_type] + @root_id = params[:root_id] + @time_scale = params[:time_scale] + @project_id = params[:project_id] + @add_assigner_ids = [] + @previous_issue_changes = {} + end + + def call + raise Error, errors.full_messages.join(", ") unless valid? + ActiveRecord::Base.transaction do + check_issue_status(status_id) if status_id.present? + check_issue_priority(priority_id) if priority_id.present? + check_milestone(milestone_id) if milestone_id.present? + check_root_issue(issue, root_id) if root_id.present? + check_issue_tags(issue_tag_ids) unless issue_tag_ids.nil? + check_assigners(assigner_ids) unless assigner_ids.nil? + check_attachments(attachment_ids) unless attachment_ids.nil? + check_atme_receivers(receivers_login) unless receivers_login.nil? + check_blockchain_token_num(issue.author_id, project.id, blockchain_token_num, (@issue.blockchain_token_num || 0)) if blockchain_token_num.present? && current_user.id == @issue.author_id && !PullAttachedIssue.exists?(issue_id: @issue, fixed: true) + load_assigners(assigner_ids) + load_attachments(attachment_ids) + load_issue_tags(issue_tag_ids) + load_atme_receivers(receivers_login) unless receivers_login.nil? + + try_lock("Api::Pm::Issues::UpdateService:#{project.id}:#{issue.id}") + @updated_issue = @issue + issue_load_attributes + build_assigner_issue_journal_details unless assigner_ids.nil?# 操作记录 + build_attachment_issue_journal_details unless attachment_ids.nil? + build_issue_tag_issue_journal_details unless issue_tag_ids.nil? + build_issue_project_trends if status_id.present? # 开关时间记录 + build_assigner_participants unless assigner_ids.nil? # 负责人 + build_edit_participants + build_atme_participants if @atme_receivers.present? + unless assigner_ids.nil? + @previous_issue_changes.merge!(assigned_to_id: [@updated_issue.assigners.pluck(:id), @assigners.pluck(:id)]) + @updated_issue.assigners = @assigners || User.none + end + @updated_issue.attachments = @attachments || Attachment.none unless attachment_ids.nil? + @updated_issue.issue_tags_relates.destroy_all & @updated_issue.issue_tags = @issue_tags || IssueTag.none unless issue_tag_ids.nil? + @updated_issue.issue_tags_value = @issue_tags.order("id asc").pluck(:id).join(",") unless issue_tag_ids.nil? + + #Pm相关 + @updated_issue.pm_project_id = @target_pm_project_id unless @target_pm_project_id.nil? + @updated_issue.pm_sprint_id = @pm_sprint_id unless @pm_sprint_id.nil? + if @updated_issue.children_issues.count == 0 && @updated_issue.parent_id.nil? + @updated_issue.pm_issue_type = @pm_issue_type unless @pm_issue_type.nil? + end + @updated_issue.root_id = @root_id unless @root_id.nil? #不为 nil的时候更新 + @updated_issue.root_id = nil if @root_id.try(:zero?) #为 0 的时候设置为 nil + @updated_issue.time_scale = @time_scale unless @time_scale.nil? + @updated_issue.project_id = @project_id unless @project_id.nil? + @updated_issue.updated_on = Time.now + @updated_issue.changer_id = @current_user.id + @updated_issue.save! + + build_after_issue_journal_details if @updated_issue.previous_changes.present? # 操作记录 + build_previous_issue_changes + build_cirle_blockchain_token if blockchain_token_num.present? + unless @project.id.zero? + # @信息发送 + AtmeService.call(current_user, @atme_receivers, @issue) unless receivers_login.blank? + # 消息发送 + if Site.has_notice_menu? + SendTemplateMessageJob.perform_later('IssueChanged', current_user.id, @issue&.id, previous_issue_changes) unless previous_issue_changes.blank? + SendTemplateMessageJob.perform_later('IssueAssigned', current_user.id, @issue&.id, add_assigner_ids) unless add_assigner_ids.blank? + end + # 触发webhook + Rails.logger.info "################### 触发webhook" + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueUpdate', @updated_issue&.id, current_user.id, previous_issue_changes.except(:issue_tags_value, :assigned_to_id)) + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueLabel', @issue&.id, current_user.id, {issue_tag_ids: [before_issue_tag_ids, issue_tag_ids]}) unless issue_tag_ids.nil? + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueAssign', @issue&.id, current_user.id, {assigner_ids: [before_assigner_ids, assigner_ids]}) unless assigner_ids.nil? + end + + unlock("Api::Pm::Issues::UpdateService:#{project.id}:#{issue.id}") + return @updated_issue + end + end + + private + + def issue_load_attributes + if current_user.id == @updated_issue.author_id && !PullAttachedIssue.exists?(issue_id: @updated_issue, fixed: true) + @updated_issue.blockchain_token_num = blockchain_token_num unless blockchain_token_num.nil? + end + @updated_issue.status_id = status_id if status_id.present? + @updated_issue.priority_id = priority_id if priority_id.present? + @updated_issue.fixed_version_id = milestone_id unless milestone_id.nil? + @updated_issue.branch_name = branch_name unless branch_name.nil? + @updated_issue.start_date = start_date unless start_date.nil? + @updated_issue.due_date = due_date unless due_date.nil? + @updated_issue.subject = subject if subject.present? + @updated_issue.description = description unless description.nil? + end + + def build_assigner_participants + if assigner_ids.blank? + @updated_issue.issue_participants.where(participant_type: "assigned").each(&:destroy!) + else + @updated_issue.issue_participants.where(participant_type: "assigned").where.not(participant_id: assigner_ids).each(&:destroy!) + assigner_ids.each do |aid| + next if @updated_issue.issue_participants.exists?(participant_type: "assigned", participant_id: aid) + @updated_issue.issue_participants.new({participant_type: "assigned", participant_id: aid}) + @add_assigner_ids << aid + end + end + end + + def build_edit_participants + @updated_issue.issue_participants.new({participant_type: "edited", participant_id: current_user.id}) unless @updated_issue.issue_participants.exists?(participant_type: "edited", participant_id: current_user.id) + end + + def build_atme_participants + @atme_receivers.each do |receiver| + next if @updated_issue.issue_participants.exists?(participant_type: "atme", participant_id: receiver.id) + @updated_issue.issue_participants.new({participant_type: "atme", participant_id: receiver.id}) + end + end + + def build_previous_issue_changes + @previous_issue_changes.merge!(@updated_issue.previous_changes.slice("status_id", "priority_id", "fixed_version_id", "issue_tags_value", "branch_name", "subject").symbolize_keys) + if @updated_issue.previous_changes[:start_date].present? + @previous_issue_changes.merge!(start_date: [@updated_issue.previous_changes[:start_date][0].to_s, @updated_issue.previous_changes[:start_date][1].to_s]) + end + if @updated_issue.previous_changes[:due_date].present? + @previous_issue_changes.merge!(due_date: [@updated_issue.previous_changes[:due_date][0].to_s, @updated_issue.previous_changes[:due_date][1].to_s]) + end + end + + def build_cirle_blockchain_token + if @updated_issue.previous_changes["blockchain_token_num"].present? + unlock_balance_on_blockchain(@updated_issue&.author_id.to_s, @updated_issue.project_id.to_s, @updated_issue.previous_changes["blockchain_token_num"][0].to_i) if @updated_issue.previous_changes["blockchain_token_num"][0].present? + lock_balance_on_blockchain(@updated_issue&.author_id.to_s, @updated_issue.project_id.to_s, @updated_issue.previous_changes["blockchain_token_num"][1].to_i) if @updated_issue.previous_changes["blockchain_token_num"][1].present? + end + end + + def build_issue_project_trends + if @updated_issue.previous_changes["status_id"].present? && @updated_issue.previous_changes["status_id"][1] == 5 + @updated_issue.project_trends.new({user_id: current_user.id, project_id: @project.id, action_type: ProjectTrend::CLOSE}) + end + if @updated_issue.previous_changes["status_id"].present? && @updated_issue.previous_changes["status_id"][0] == 5 + @updated_issue.project_trends.where(action_type: ProjectTrend::CLOSE).each(&:destroy!) + end + end + + def build_after_issue_journal_details + begin + # 更改标题 + if @updated_issue.previous_changes["subject"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "subject", old_value: @updated_issue.previous_changes["subject"][0], value: @updated_issue.previous_changes["subject"][1]}) + end + + # 更改描述 + if @updated_issue.previous_changes["description"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "description", old_value: @updated_issue.previous_changes["description"][0], value: @updated_issue.previous_changes["description"][1]}) + end + + # 修改状态 + if @updated_issue.previous_changes["status_id"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: @updated_issue.pm_issue_type_string, prop_key: "status_id", old_value: @updated_issue.previous_changes["status_id"][0], value: @updated_issue.previous_changes["status_id"][1]}) + end + + # 修改优先级 + if @updated_issue.previous_changes["priority_id"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "priority_id", old_value: @updated_issue.previous_changes["priority_id"][0], value: @updated_issue.previous_changes["priority_id"][1]}) + end + + # 修改工作项类型 + if @updated_issue.previous_changes["pm_issue_type"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "pm_issue_type", old_value: @updated_issue.previous_changes["pm_issue_type"][0], value: @updated_issue.previous_changes["pm_issue_type"][1]}) + end + + # 修改迭代 + if @updated_issue.previous_changes["pm_sprint_id"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "pm_sprint_id", old_value: @updated_issue.previous_changes["pm_sprint_id"][0], value: @updated_issue.previous_changes["pm_sprint_id"][1]}) + end + + # 修改代码库 + if @updated_issue.previous_changes["project_id"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "project_id", old_value: @updated_issue.previous_changes["project_id"][0], value: @updated_issue.previous_changes["project_id"][1]}) + end + + # 修改里程碑 + if @updated_issue.previous_changes["fixed_version_id"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "fixed_version_id", old_value: @updated_issue.previous_changes["fixed_version_id"][0], value: @updated_issue.previous_changes["fixed_version_id"][1]}) + end + + # 更改分支 + if @updated_issue.previous_changes["branch_name"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "branch_name", old_value: @updated_issue.previous_changes["branch_name"][0], value: @updated_issue.previous_changes["branch_name"][1]}) + end + + # 更改开始时间 + if @updated_issue.previous_changes["start_date"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "start_date", old_value: @updated_issue.previous_changes["start_date"][0], value: @updated_issue.previous_changes["start_date"][1]}) + end + + # 更改结束时间 + if @updated_issue.previous_changes["due_date"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "due_date", old_value: @updated_issue.previous_changes["due_date"][0], value: @updated_issue.previous_changes["due_date"][1]}) + end + + # 更改预估工时 + if @updated_issue.previous_changes["time_scale"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attr", prop_key: "time_scale", old_value: @updated_issue.previous_changes["time_scale"][0], value: @updated_issue.previous_changes["time_scale"][1]}) + end + + # 更改父工作项 + if @updated_issue.previous_changes["root_id"].present? + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: @updated_issue.pm_issue_type_string, prop_key: "root_id", old_value: @updated_issue.previous_changes["root_id"][0], value: @updated_issue.previous_changes["root_id"][1]}) + + # 更改子工作项 + before_parent_issue = Issue.find_by_id(@updated_issue.previous_changes["root_id"][0]) + if before_parent_issue.present? + journal = before_parent_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: @updated_issue.pm_issue_type_string, prop_key: "tag_leaf_issue", old_value: @updated_issue.id.to_s}) + end + + after_parent_issue = Issue.find_by_id(@updated_issue.previous_changes["root_id"][1]) + if after_parent_issue.present? + journal = after_parent_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: @updated_issue.pm_issue_type_string, prop_key: "tag_leaf_issue", value: @updated_issue.id.to_s}) + end + end + rescue + raise Error, "创建操作记录失败!" + end + end + + def build_assigner_issue_journal_details + begin + # 更改负责人 + new_assigner_ids = @assigner_ids + new_assigner_ids = [] if @assigner_ids.nil? + now_assigner_ids = @updated_issue.assigners.pluck(:id) + if !(now_assigner_ids.sort == new_assigner_ids.sort) + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "assigner", prop_key: "#{new_assigner_ids.size}", old_value: now_assigner_ids.join(","), value: new_assigner_ids.join(",")}) + end + + rescue + raise Error, "创建操作记录失败!" + end + end + + def build_issue_tag_issue_journal_details + begin + # 更改标记 + new_issue_tag_ids = @issue_tag_ids + new_issue_tag_ids = [] if @issue_tag_ids.nil? + now_issue_tag_ids = @updated_issue.issue_tags.pluck(:id) + if !(now_issue_tag_ids.sort == new_issue_tag_ids.sort) + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "issue_tag", prop_key: "#{new_issue_tag_ids.size}", old_value: now_issue_tag_ids.join(","), value: new_issue_tag_ids.join(",")}) + end + rescue + raise Error, "创建操作记录失败!" + end + end + + + def build_attachment_issue_journal_details + begin + # 更改附件 + new_attachment_ids = @attachment_ids + new_attachment_ids = [] if @attachment_ids.nil? + now_attachment_ids = @updated_issue.attachments.pluck(:id) + if !(now_attachment_ids.sort == new_attachment_ids.sort) + journal = @updated_issue.journals.create!({user_id: current_user.id}) + journal.journal_details.create!({property: "attachment", prop_key: "#{new_attachment_ids.size}", old_value: now_attachment_ids.join(","), value: new_attachment_ids.join(",")}) + end + rescue + raise Error, "创建操作记录失败!" + end + end + +end \ No newline at end of file diff --git a/app/services/api/pm/sprint_issues/list_service.rb b/app/services/api/pm/sprint_issues/list_service.rb new file mode 100644 index 000000000..5a539b195 --- /dev/null +++ b/app/services/api/pm/sprint_issues/list_service.rb @@ -0,0 +1,65 @@ +class Api::Pm::SprintIssues::ListService < ApplicationService + + include ActiveModel::Model + + attr_reader :category, :pm_project_id, :pm_issue_type, :assigner_id, :priority_id, :status_id, :keyword, :current_user + attr_reader :status_ids, :pm_issue_types + attr_reader :sort_by, :sort_direction + attr_accessor :queried_issues + + validates :category, inclusion: { in: %w[linked unlink], message: '请输入正确的Category'} + validates :sort_by, inclusion: { in: %w[issues.status_id issues.created_on issues.updated_on issue_priorities.position] , message: '请输入正确的SortBy'}, allow_blank: true + validates :sort_direction, inclusion: { in: %w[asc desc], message: '请输入正确的SortDirection'}, allow_blank: true + + validates :pm_project_id, :current_user, presence: true + + def initialize(params, current_user = nil) + @category = params[:category] || "unlink" + @pm_project_id = params[:pm_project_id] + @pm_issue_type = params[:pm_issue_type] + @assigner_id = params[:assigner_id] + @priority_id = params[:priority_id] + @status_id = params[:status_id] + @keyword = params[:keyword] + @status_ids = params[:status_ids].present? ? params[:status_ids].split(',') : [] + @pm_issue_types = params[:pm_issue_types].present? ? params[:pm_issue_types].split(',') : [] + @sort_by = params[:sort_by].present? ? params[:sort_by] : 'issues.updated_on' + @sort_direction = (params[:sort_direction].present? ? params[:sort_direction] : 'desc').downcase + @current_user = current_user + end + + def call + raise Error, errors.full_messages.join(', ') unless valid? + + issue_query_data + + @queried_issues + end + + private + def issue_query_data + issues = @category == "unlink" ? Issue.where(pm_project_id: @pm_project_id, pm_sprint_id: [nil, 0]) : Issue.where(pm_project_id: @pm_project_id).where.not(pm_sprint_id: [nil, 0]) + + issues = issues.where(pm_issue_type: @pm_issue_type) if @pm_issue_type.present? + + issues = issues.joins(:assigners).where(users: {id: @assigner_id}) if @assigner_id.present? + + issues = issues.where(priority_id: @priority_id) if @priority_id.present? + + issues = issues.where(status_id: @status_id) if @status_id.present? + + # status_ids + issues = issues.where(status_id: @status_ids) unless @status_ids.blank? + + # pm_issue_types + issues = issues.where(pm_issue_type: @pm_issue_types) unless @pm_issue_types.blank? + + issues = issues.ransack(subject_cont: @keyword).result if @keyword.present? + + scope = issues.includes(:priority, :issue_status, :user, :show_assigners, :show_issue_tags, :version, :comment_journals) + scope = scope.reorder("#{sort_by} #{sort_direction}").distinct + + @queried_issues = scope + + end +end \ No newline at end of file diff --git a/app/services/api/v1/issues/batch_delete_service.rb b/app/services/api/v1/issues/batch_delete_service.rb index 45821b373..15ebd8d0e 100644 --- a/app/services/api/v1/issues/batch_delete_service.rb +++ b/app/services/api/v1/issues/batch_delete_service.rb @@ -19,7 +19,7 @@ class Api::V1::Issues::BatchDeleteService < ApplicationService project.incre_project_issue_cache_delete_count(@issues.size) - if Site.has_notice_menu? + if Site.has_notice_menu? && !project.id.zero? @issues.each do |issue| SendTemplateMessageJob.perform_later('IssueDeleted', current_user.id, @issue&.subject, @issue.assigners.pluck(:id), @issue.author_id) end diff --git a/app/services/api/v1/issues/batch_update_service.rb b/app/services/api/v1/issues/batch_update_service.rb index ccf783dca..e826ca190 100644 --- a/app/services/api/v1/issues/batch_update_service.rb +++ b/app/services/api/v1/issues/batch_update_service.rb @@ -4,7 +4,7 @@ class Api::V1::Issues::BatchUpdateService < ApplicationService include Api::V1::Issues::Concerns::Loadable attr_reader :project, :issues, :params, :current_user - attr_reader :status_id, :priority_id, :milestone_id + attr_reader :status_id, :priority_id, :milestone_id, :project_id attr_reader :issue_tag_ids, :assigner_ids validates :project, :issues, :current_user, presence: true diff --git a/app/services/api/v1/issues/concerns/checkable.rb b/app/services/api/v1/issues/concerns/checkable.rb index 5eef81584..167c53a6c 100644 --- a/app/services/api/v1/issues/concerns/checkable.rb +++ b/app/services/api/v1/issues/concerns/checkable.rb @@ -12,6 +12,10 @@ module Api::V1::Issues::Concerns::Checkable raise ApplicationService::Error, "Milestone不存在!" unless Version.find_by_id(milestone_id).present? end + def check_root_issue(issue, root_id) + raise ApplicationService::Error, "父工作项与当前工作项已存在父子关系!" if Issue.full_children_issues(issue).map{|i| i.id}.include?(root_id) + end + def check_issue_tags(issue_tag_ids) raise ApplicationService::Error, "请输入正确的标记ID数组!" unless issue_tag_ids.is_a?(Array) raise ApplicationService::Error, "最多可选择3个标记" if issue_tag_ids.size > 3 @@ -47,6 +51,7 @@ module Api::V1::Issues::Concerns::Checkable end def check_blockchain_token_num(user_id, project_id, blockchain_token_num, now_blockchain_token_num=0) + return if project_id.zero? left_blockchain_token_num = Blockchain::BalanceQueryOneProject.call({"user_id": user_id, "project_id": project_id}) rescue 0 raise ApplicationService::Error, "用户Token不足。" if blockchain_token_num.to_i > (left_blockchain_token_num+now_blockchain_token_num).to_i end diff --git a/app/services/api/v1/issues/concerns/loadable.rb b/app/services/api/v1/issues/concerns/loadable.rb index 547ff50d7..c8f67f256 100644 --- a/app/services/api/v1/issues/concerns/loadable.rb +++ b/app/services/api/v1/issues/concerns/loadable.rb @@ -9,7 +9,7 @@ module Api::V1::Issues::Concerns::Loadable end def load_attachments(attachment_ids) - @attachments = Attachment.where("id in (?) or uuid in (?)", attachment_ids, attachment_ids) + @attachments = Attachment.where("BINARY id in (?) or uuid in (?)", attachment_ids, attachment_ids) end def load_atme_receivers(receivers_login) diff --git a/app/services/api/v1/issues/create_service.rb b/app/services/api/v1/issues/create_service.rb index c155b69d4..fdbdc71ed 100644 --- a/app/services/api/v1/issues/create_service.rb +++ b/app/services/api/v1/issues/create_service.rb @@ -4,7 +4,7 @@ class Api::V1::Issues::CreateService < ApplicationService include Api::V1::Issues::Concerns::Loadable attr_reader :project, :current_user - attr_reader :status_id, :priority_id, :milestone_id, :branch_name, :start_date, :due_date, :subject, :description, :blockchain_token_num + attr_reader :status_id, :priority_id, :milestone_id, :branch_name, :start_date, :due_date, :subject, :description, :blockchain_token_num, :root_subject attr_reader :issue_tag_ids, :assigner_ids, :attachment_ids, :receivers_login attr_accessor :created_issue @@ -29,10 +29,17 @@ class Api::V1::Issues::CreateService < ApplicationService @assigner_ids = params[:assigner_ids] @attachment_ids = params[:attachment_ids] @receivers_login = params[:receivers_login] + @pm_project_id = params[:pm_project_id] + @pm_sprint_id = params[:pm_sprint_id] + @pm_issue_type = params[:pm_issue_type] + @root_id = params[:root_id] + @time_scale = params[:time_scale] + @linkable_id = params[:link_able_id] + @root_subject = params[:root_subject] end - def call - raise Error, errors.full_messages.join(", ") unless valid? + def call + raise Error, errors.full_messages.join(', ') unless valid? ActiveRecord::Base.transaction do check_issue_status(status_id) check_issue_priority(priority_id) @@ -46,7 +53,6 @@ class Api::V1::Issues::CreateService < ApplicationService load_attachments(attachment_ids) unless attachment_ids.blank? load_issue_tags(issue_tag_ids) unless issue_tag_ids.blank? load_atme_receivers(receivers_login) unless receivers_login.blank? - try_lock("Api::V1::Issues::CreateService:#{project.id}") # 开始写数据,加锁 @created_issue = Issue.new(issue_attributes) build_author_participants @@ -57,39 +63,54 @@ class Api::V1::Issues::CreateService < ApplicationService @created_issue.assigners = @assigners unless assigner_ids.blank? @created_issue.attachments = @attachments unless attachment_ids.blank? @created_issue.issue_tags = @issue_tags unless issue_tag_ids.blank? - - @created_issue.issue_tags_value = @issue_tags.order("id asc").pluck(:id).join(",") unless issue_tag_ids.blank? + @created_issue.pm_project_id = @pm_project_id + @created_issue.pm_sprint_id = @pm_sprint_id + @created_issue.pm_issue_type = @pm_issue_type + if @root_subject.present? && @pm_issue_type.to_i == 4 + @root_issue = Issue.find_by(subject: @root_subject, pm_issue_type: 4, pm_project_id: @pm_project_id) + unless @root_issue.present? + @root_issue = Issue.create(subject: @root_subject, pm_issue_type: 4, pm_project_id: @pm_project_id, status_id: 1, priority_id: 1, tracker_id: Tracker.first.id, project_id: @project.id, author_id: current_user.id) + end + @created_issue.root_id = @root_issue.id + else + @created_issue.root_id = @root_id + end + @created_issue.time_scale = @time_scale + @created_issue.issue_tags_value = @issue_tags.order('id asc').pluck(:id).join(',') unless issue_tag_ids.blank? + @created_issue.changer_id = @current_user.id @created_issue.save! - if Site.has_blockchain? && @project.use_blockchain + PmLink.create(be_linkable_type: 'Issue', be_linkable_id: @created_issue.id, linkable_type: 'Issue', linkable_id: @linkable_id) if @linkable_id.present? + if Site.has_blockchain? && @project.use_blockchain if @created_issue.blockchain_token_num.present? && @created_issue.blockchain_token_num > 0 Blockchain::CreateIssue.call({user_id: current_user.id, project_id: @created_issue.project_id, token_num: @created_issue.blockchain_token_num}) end - - push_activity_2_blockchain("issue_create", @created_issue) + + push_activity_2_blockchain('issue_create', @created_issue) end project.del_project_issue_cache_delete_count # 把缓存里存储项目删除issue的个数清除掉 + unless @project.id.zero? + # 新增时向grimoirelab推送事件 + IssueWebhookJob.set(wait: 5.seconds).perform_later(@created_issue.id) - # 新增时向grimoirelab推送事件 - IssueWebhookJob.set(wait: 5.seconds).perform_later(@created_issue.id) + # @信息发送 + AtmeService.call(current_user, @atme_receivers, @created_issue) unless receivers_login.blank? - # @信息发送 - AtmeService.call(current_user, @atme_receivers, @created_issue) unless receivers_login.blank? + # 发消息 + if Site.has_notice_menu? + SendTemplateMessageJob.perform_later('IssueAssigned', current_user.id, @created_issue&.id, assigner_ids) unless assigner_ids.blank? + SendTemplateMessageJob.perform_later('ProjectIssue', current_user.id, @created_issue&.id) + end - # 发消息 - if Site.has_notice_menu? - SendTemplateMessageJob.perform_later('IssueAssigned', current_user.id, @created_issue&.id, assigner_ids) unless assigner_ids.blank? - SendTemplateMessageJob.perform_later('ProjectIssue', current_user.id, @created_issue&.id) + # 触发webhook + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueCreate', @created_issue&.id, current_user.id) + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueLabel', @created_issue&.id, current_user.id, {issue_tag_ids: [[], issue_tag_ids]}) unless issue_tag_ids.blank? + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueAssign', @created_issue&.id, current_user.id, {assigner_ids: [[], assigner_ids]}) unless assigner_ids.blank? end - - # 触发webhook - TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueCreate', @created_issue&.id, current_user.id) - TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueLabel', @created_issue&.id, current_user.id, {issue_tag_ids: [[], issue_tag_ids]}) unless issue_tag_ids.blank? - TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueAssign', @created_issue&.id, current_user.id, {assigner_ids: [[], assigner_ids]}) unless assigner_ids.blank? unlock("Api::V1::Issues::CreateService:#{project.id}") # 结束写数据,解锁 end - + return @created_issue end @@ -104,8 +125,8 @@ class Api::V1::Issues::CreateService < ApplicationService status_id: status_id, priority_id: priority_id, project_issues_index: (project.get_last_project_issues_index + 1), - issue_type: "1", - issue_classify: "issue" + issue_type: '1', + issue_classify: 'issue' } issue_attributes.merge!({description: description}) if description.present? @@ -119,28 +140,29 @@ class Api::V1::Issues::CreateService < ApplicationService end def build_author_participants - @created_issue.issue_participants.new({participant_type: "authored", participant_id: current_user.id}) + @created_issue.issue_participants.new({participant_type: 'authored', participant_id: current_user.id}) end def build_assigner_participants assigner_ids.each do |aid| - @created_issue.issue_participants.new({participant_type: "assigned", participant_id: aid}) + @created_issue.issue_participants.new({participant_type: 'assigned', participant_id: aid}) end end def build_atme_participants @atme_receivers.each do |receiver| - @created_issue.issue_participants.new({participant_type: "atme", participant_id: receiver.id}) + @created_issue.issue_participants.new({participant_type: 'atme', participant_id: receiver.id}) end end def build_issue_project_trends - @created_issue.project_trends.new({user_id: current_user.id, project_id: @project.id, action_type: "create"}) + return if @project.id == 0 + @created_issue.project_trends.new({user_id: current_user.id, project_id: @project.id, action_type: 'create'}) @created_issue.project_trends.new({user_id: current_user.id, project_id: @project.id, action_type: ProjectTrend::CLOSE}) if status_id.to_i == 5 end def build_issue_journal_details journal = @created_issue.journals.new({user_id: current_user.id}) - journal.journal_details.new({property: "issue", prop_key: 1, old_value: '', value: ''}) + journal.journal_details.new({property: 'issue', prop_key: 1, old_value: '', value: ''}) end end \ No newline at end of file diff --git a/app/services/api/v1/issues/delete_service.rb b/app/services/api/v1/issues/delete_service.rb index b62733181..4d7ff7cfc 100644 --- a/app/services/api/v1/issues/delete_service.rb +++ b/app/services/api/v1/issues/delete_service.rb @@ -16,14 +16,16 @@ class Api::V1::Issues::DeleteService < ApplicationService try_lock("Api::V1::Issues::DeleteService:#{project.id}") # 开始写数据,加锁 delete_issue + #删除双向关联 + PmLink.where(be_linkable_id: @issue.id, be_linkable_type: 'Issue').or(PmLink.where(linkable_id: @issue.id, linkable_type: 'Issue')).map(&:destroy) project.incre_project_issue_cache_delete_count - if Site.has_blockchain? && @project.use_blockchain + if Site.has_blockchain? && @project.use_blockchain && !project.id.zero? unlock_balance_on_blockchain(@issue.author_id.to_s, @project.id.to_s, @issue.blockchain_token_num.to_i) if @issue.blockchain_token_num.present? end - if Site.has_notice_menu? + if Site.has_notice_menu? && !project.id.zero? SendTemplateMessageJob.perform_later('IssueDeleted', current_user.id, @issue&.subject, @issue.assigners.pluck(:id), @issue.author_id) end @@ -37,5 +39,4 @@ class Api::V1::Issues::DeleteService < ApplicationService def delete_issue raise Error, "删除疑修失败!" unless issue.destroy! end - end \ No newline at end of file diff --git a/app/services/api/v1/issues/list_service.rb b/app/services/api/v1/issues/list_service.rb index b267647e7..40dc2669e 100644 --- a/app/services/api/v1/issues/list_service.rb +++ b/app/services/api/v1/issues/list_service.rb @@ -2,17 +2,18 @@ class Api::V1::Issues::ListService < ApplicationService include ActiveModel::Model attr_reader :project, :only_name, :category, :participant_category, :keyword, :author_id, :issue_tag_ids - attr_reader :begin_date, :end_date - attr_reader :milestone_id, :assigner_id, :status_id, :sort_by, :sort_direction, :current_user - attr_accessor :queried_issues, :total_issues_count, :closed_issues_count, :opened_issues_count + attr_reader :begin_date, :end_date, :update_begin_date, :update_end_date + attr_reader :milestone_id, :assigner_id, :status_id, :priority_id, :sort_by, :sort_direction, :current_user + attr_reader :pm_project_id, :pm_project_ids, :pm_sprint_id, :root_id, :pm_issue_type, :status_ids, :ids, :exclude_ids, :pm_issue_types + attr_accessor :queried_issues, :total_issues_count, :closed_issues_count, :opened_issues_count, :complete_issues_count, :participator - validates :category, inclusion: {in: %w(all opened closed), message: '请输入正确的Category'} - validates :participant_category, inclusion: {in: %w(all aboutme authoredme assignedme atme), message: '请输入正确的ParticipantCategory'} - validates :sort_by, inclusion: {in: ['issues.created_on', 'issues.updated_on', 'issues.blockchain_token_num', 'issue_priorities.position'], message: '请输入正确的SortBy'}, allow_blank: true - validates :sort_direction, inclusion: {in: %w(asc desc), message: '请输入正确的SortDirection'}, allow_blank: true + validates :category, inclusion: { in: %w[all opened closed], message: '请输入正确的Category'} + validates :participant_category, inclusion: { in: %w[all aboutme authoredme assignedme atme], message: '请输入正确的ParticipantCategory'} + validates :sort_by, inclusion: { in: %w[issues.created_on issues.updated_on issues.blockchain_token_num issue_priorities.position issues.start_date issues.due_date issues.status_id] , message: '请输入正确的SortBy'}, allow_blank: true + validates :sort_direction, inclusion: { in: %w[asc desc], message: '请输入正确的SortDirection'}, allow_blank: true validates :current_user, presence: true - def initialize(project, params, current_user=nil) + def initialize(project, params, current_user = nil) @project = project @only_name = params[:only_name] @category = params[:category] || 'all' @@ -22,20 +23,39 @@ class Api::V1::Issues::ListService < ApplicationService @issue_tag_ids = params[:issue_tag_ids].present? ? params[:issue_tag_ids].split(',') : [] @milestone_id = params[:milestone_id] @assigner_id = params[:assigner_id] + @priority_id = params[:priority_id] @status_id = params[:status_id] @begin_date = params[:begin_date] @end_date = params[:end_date] + @update_begin_date = params[:update_begin_date] + @update_end_date = params[:update_end_date] @sort_by = params[:sort_by].present? ? params[:sort_by] : 'issues.updated_on' + @pm_project_id = params[:pm_project_id] + @pm_project_ids = params[:pm_project_ids] + @pm_sprint_id = params[:pm_sprint_id] + @root_id = params[:root_id] + @pm_issue_type = params[:pm_issue_type] + @ids = params[:ids] + @exclude_ids = params[:exclude_ids] + @status_ids = params[:status_ids].present? ? params[:status_ids].split(',') : [] + @pm_issue_types = params[:pm_issue_types].present? ? params[:pm_issue_types].split(',') : [] @sort_direction = (params[:sort_direction].present? ? params[:sort_direction] : 'desc').downcase + @participator = params[:participator_id].present? ? User.find_by_id(params[:participator_id]) : current_user @current_user = current_user end def call raise Error, errors.full_messages.join(', ') unless valid? # begin - issue_query_data + issue_query_data - return {data: queried_issues, total_issues_count: @total_issues_count, closed_issues_count: @closed_issues_count, opened_issues_count: @opened_issues_count} + { + data: queried_issues, + total_issues_count: @total_issues_count, + closed_issues_count: @closed_issues_count, + opened_issues_count: @opened_issues_count, + complete_issues_count: @complete_issues_count + } # rescue # raise Error, "服务器错误,请联系系统管理员!" # end @@ -43,19 +63,18 @@ class Api::V1::Issues::ListService < ApplicationService private def issue_query_data - issues = @project.issues.issue_issue - + issues = @project&.id.zero? ? Issue.issue_issue : @project.issues.issue_issue + @total_issues_count = issues.where(pm_issue_type:[1, 2, 3]).distinct.size case participant_category when 'aboutme' # 关于我的 - issues = issues.joins(:issue_participants).where(issue_participants: {participant_type: %w(authored assigned atme), participant_id: current_user&.id}) + issues = issues.joins(:issue_participants).where(issue_participants: {participant_type: %w[authored assigned atme], participant_id: participator&.id}) when 'authoredme' # 我创建的 - issues = issues.joins(:issue_participants).where(issue_participants: {participant_type: 'authored', participant_id: current_user&.id}) + issues = issues.joins(:issue_participants).where(issue_participants: {participant_type: 'authored', participant_id: participator&.id}) when 'assignedme' # 我负责的 - issues = issues.joins(:issue_participants).where(issue_participants: {participant_type: 'assigned', participant_id: current_user&.id}) + issues = issues.joins(:issue_participants).where(issue_participants: {participant_type: 'assigned', participant_id: participator&.id}) when 'atme' # @我的 - issues = issues.joins(:issue_participants).where(issue_participants: {participant_type: 'atme', participant_id: current_user&.id}) + issues = issues.joins(:issue_participants).where(issue_participants: {participant_type: 'atme', participant_id: participator&.id}) end - # author_id issues = issues.where(author_id: author_id) if author_id.present? @@ -77,6 +96,26 @@ class Api::V1::Issues::ListService < ApplicationService end end + #pm相关 + # root_id# -1 查一级目录 + issues = if root_id.to_i == -1 + issues.where(root_id: nil) + elsif root_id.to_i.positive? + issues.where(root_id: root_id) + else + issues + end + + # pm_issue_type + issues = issues.where(pm_issue_type: pm_issue_type) if pm_issue_type.present? + + # pm_project_id + issues = issues.where(pm_project_id: pm_project_id) if pm_project_id.present? + issues = issues.where(pm_project_id: pm_project_ids.to_s.split(",")) if pm_project_ids.present? + + # pm_sprint_id + issues = issues.where(pm_sprint_id: pm_sprint_id) if pm_sprint_id.present? + # assigner_id if assigner_id.present? if assigner_id.to_i == -1 @@ -89,16 +128,36 @@ class Api::V1::Issues::ListService < ApplicationService # status_id issues = issues.where(status_id: status_id) if status_id.present? && category != 'closed' + # priority_id + issues = issues.where(priority_id: priority_id) if priority_id.present? + + # status_ids + issues = issues.where(status_id: status_ids) unless status_ids.blank? + + # pm_issue_types + issues = issues.where(pm_issue_type: pm_issue_types) unless pm_issue_types.blank? + + # ids + issues = issues.where(id: ids.to_s.split(",")) if ids.present? + + # exclude_ids + issues = issues.where.not(id: exclude_ids.to_s.split(",")) if exclude_ids.present? + if begin_date&.present? || end_date&.present? issues = issues.where('issues.created_on between ? and ?', begin_date&.present? ? begin_date.to_time : Time.now.beginning_of_day, end_date&.present? ? end_date.to_time.end_of_day : Time.now.end_of_day) end + if update_begin_date&.present? || update_end_date&.present? + issues = issues.where('issues.updated_on between ? and ?', update_begin_date&.present? ? update_begin_date.to_time : Time.now.beginning_of_day, update_end_date&.present? ? update_end_date.to_time.end_of_day : Time.now.end_of_day) + end + # keyword issues = issues.ransack(id_or_project_issues_index_eq: keyword).result.or(issues.ransack(subject_or_description_cont: keyword).result) if keyword.present? - @total_issues_count = issues.distinct.size + @closed_issues_count = issues.closed.distinct.size @opened_issues_count = issues.opened.distinct.size + @complete_issues_count = issues.closed.distinct.size + issues.where(status_id: 3).distinct.size - issues.where(pm_issue_type: 3, status_id: 3).distinct.size case category when 'closed' diff --git a/app/services/api/v1/issues/update_service.rb b/app/services/api/v1/issues/update_service.rb index c8050e6da..5f040b25f 100644 --- a/app/services/api/v1/issues/update_service.rb +++ b/app/services/api/v1/issues/update_service.rb @@ -5,7 +5,8 @@ class Api::V1::Issues::UpdateService < ApplicationService attr_reader :project, :issue, :current_user attr_reader :status_id, :priority_id, :milestone_id, :branch_name, :start_date, :due_date, :subject, :description, :blockchain_token_num - attr_reader :issue_tag_ids, :assigner_ids, :attachment_ids, :receivers_login, :before_issue_tag_ids, :before_assigner_ids + attr_reader :target_pm_project_id, :pm_sprint_id, :pm_issue_type, :root_id, :time_scale + attr_reader :issue_tag_ids, :assigner_ids, :attachment_ids, :receivers_login, :before_issue_tag_ids, :before_assigner_ids, :project_id attr_accessor :add_assigner_ids, :previous_issue_changes, :updated_issue, :atme_receivers validates :project, :issue, :current_user, presence: true @@ -30,6 +31,12 @@ class Api::V1::Issues::UpdateService < ApplicationService @before_assigner_ids = issue.assigners.pluck(:id) @attachment_ids = params[:attachment_ids] @receivers_login = params[:receivers_login] + @target_pm_project_id = params[:target_pm_project_id] + @pm_sprint_id = params[:pm_sprint_id] + @pm_issue_type = params[:pm_issue_type] + @root_id = params[:root_id] + @time_scale = params[:time_scale] + @project_id = params[:project_id] @add_assigner_ids = [] @previous_issue_changes = {} end @@ -40,6 +47,7 @@ class Api::V1::Issues::UpdateService < ApplicationService check_issue_status(status_id) if status_id.present? check_issue_priority(priority_id) if priority_id.present? check_milestone(milestone_id) if milestone_id.present? + check_root_issue(issue, root_id) if root_id.present? check_issue_tags(issue_tag_ids) unless issue_tag_ids.nil? check_assigners(assigner_ids) unless assigner_ids.nil? check_attachments(attachment_ids) unless attachment_ids.nil? @@ -68,28 +76,39 @@ class Api::V1::Issues::UpdateService < ApplicationService @updated_issue.issue_tags_relates.destroy_all & @updated_issue.issue_tags = @issue_tags || IssueTag.none unless issue_tag_ids.nil? @updated_issue.issue_tags_value = @issue_tags.order("id asc").pluck(:id).join(",") unless issue_tag_ids.nil? + #Pm相关 + @updated_issue.pm_project_id = @target_pm_project_id unless @target_pm_project_id.nil? + @updated_issue.pm_sprint_id = @pm_sprint_id unless @pm_sprint_id.nil? + if @updated_issue.children_issues.count == 0 && @updated_issue.parent_id.nil? + @updated_issue.pm_issue_type = @pm_issue_type unless @pm_issue_type.nil? + end + @updated_issue.root_id = @root_id unless @root_id.nil? #不为 nil的时候更新 + @updated_issue.root_id = nil if @root_id.try(:zero?) #为 0 的时候设置为 nil + @updated_issue.time_scale = @time_scale unless @time_scale.nil? + @updated_issue.project_id = @project_id unless @project_id.nil? @updated_issue.updated_on = Time.now + @updated_issue.changer_id = @current_user.id @updated_issue.save! build_after_issue_journal_details if @updated_issue.previous_changes.present? # 操作记录 build_previous_issue_changes build_cirle_blockchain_token if blockchain_token_num.present? - - # @信息发送 - AtmeService.call(current_user, @atme_receivers, @issue) unless receivers_login.blank? - # 消息发送 - if Site.has_notice_menu? - SendTemplateMessageJob.perform_later('IssueChanged', current_user.id, @issue&.id, previous_issue_changes) unless previous_issue_changes.blank? - SendTemplateMessageJob.perform_later('IssueAssigned', current_user.id, @issue&.id, add_assigner_ids) unless add_assigner_ids.blank? + unless @project.id.zero? + # @信息发送 + AtmeService.call(current_user, @atme_receivers, @issue) unless receivers_login.blank? + # 消息发送 + if Site.has_notice_menu? + SendTemplateMessageJob.perform_later('IssueChanged', current_user.id, @issue&.id, previous_issue_changes) unless previous_issue_changes.blank? + SendTemplateMessageJob.perform_later('IssueAssigned', current_user.id, @issue&.id, add_assigner_ids) unless add_assigner_ids.blank? + end + # 触发webhook + Rails.logger.info "################### 触发webhook" + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueUpdate', @updated_issue&.id, current_user.id, previous_issue_changes.except(:issue_tags_value, :assigned_to_id)) + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueLabel', @issue&.id, current_user.id, {issue_tag_ids: [before_issue_tag_ids, issue_tag_ids]}) unless issue_tag_ids.nil? + TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueAssign', @issue&.id, current_user.id, {assigner_ids: [before_assigner_ids, assigner_ids]}) unless assigner_ids.nil? end unlock("Api::V1::Issues::UpdateService:#{project.id}:#{issue.id}") - # 触发webhook - Rails.logger.info "################### 触发webhook" - TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueUpdate', @updated_issue&.id, current_user.id, previous_issue_changes.except(:issue_tags_value, :assigned_to_id)) - TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueLabel', @issue&.id, current_user.id, {issue_tag_ids: [before_issue_tag_ids, issue_tag_ids]}) unless issue_tag_ids.nil? - TouchWebhookJob.set(wait: 5.seconds).perform_later('IssueAssign', @issue&.id, current_user.id, {assigner_ids: [before_assigner_ids, assigner_ids]}) unless assigner_ids.nil? - return @updated_issue end end diff --git a/app/services/gitea/repository/action_secrets_service.rb b/app/services/gitea/repository/action_secrets_service.rb new file mode 100644 index 000000000..d5e782073 --- /dev/null +++ b/app/services/gitea/repository/action_secrets_service.rb @@ -0,0 +1,33 @@ +class Gitea::Repository::ActionSecretsService < Gitea::ClientService + attr_reader :owner, :repo, :secret_name, :secret + + def initialize(owner, repo, secret_name, secret) + @owner = owner + @repo = repo + @secret_name = secret_name + @secret = secret + end + + def call + response = put(url, request_params) + render_201_response(response) + end + + def destroy + response = delete(url, request_params) + render_201_response(response) + end + + + private + + def request_params + Hash.new.merge(token: owner.gitea_token, data: { data: secret } ) + end + + + + def url + "/repos/#{owner.login}/#{repo}/actions/secrets/#{secret_name}".freeze + end +end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index b83ec51d2..43ae135dc 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -10,6 +10,7 @@ class Projects::TransferService < ApplicationService def call Rails.logger.info("###### Project transfer_service begin ######") ActiveRecord::Base.transaction do + update_actions gitea_update_owner update_owner update_repo_url @@ -32,6 +33,16 @@ class Projects::TransferService < ApplicationService project.set_owner_permission(new_owner) end + def update_actions + begin + action_params = { has_actions: false } + Gitea::Repository::UpdateService.call(owner, project.identifier, action_params) + project.update action_params + rescue Exception => e + Rails.logger.info("##### Project transfer_service, gitea transfer error #{e}") + end + end + def update_repo_url project.repository.update!(user_id: new_owner.id, url: @gitea_repo["clone_url"]) end diff --git a/app/views/action/node_inputs/edit.html.erb b/app/views/action/node_inputs/edit.html.erb index 6ae9f8a78..6139eb73f 100644 --- a/app/views/action/node_inputs/edit.html.erb +++ b/app/views/action/node_inputs/edit.html.erb @@ -39,7 +39,7 @@