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 8fd3e7cff..07fb6b68b 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 @@ -141,4 +142,4 @@ gem 'doorkeeper' gem 'doorkeeper-jwt' -gem 'gitea-client', '~> 1.4.3' +gem 'gitea-client', '~> 1.5.7' diff --git a/Gemfile.lock b/Gemfile.lock index b7f060b2a..fd4d95fa4 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: @@ -99,6 +107,7 @@ GEM archive-zip (~> 0.10) nokogiri (~> 1.8) chunky_png (1.3.11) + coderay (1.1.3) concurrent-ruby (1.1.6) connection_pool (2.2.2) crass (1.0.6) @@ -135,7 +144,7 @@ GEM fugit (1.4.1) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.4) - gitea-client (1.4.2) + gitea-client (1.5.7) rest-client (~> 2.1.0) globalid (0.4.2) activesupport (>= 4.2.0) @@ -150,6 +159,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 +199,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 @@ -244,14 +256,18 @@ GEM popper_js (1.16.0) powerpack (0.1.2) prettier (0.18.2) + pry (0.12.2) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry-rails (0.3.9) + pry (>= 0.10.4) 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 +454,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 +468,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 +510,7 @@ DEPENDENCIES enumerize faraday (~> 0.15.4) font-awesome-sass (= 4.7.0) - gitea-client (~> 1.4.2) + gitea-client (~> 1.5.7) grape-entity (~> 0.7.1) groupdate (~> 4.1.0) harmonious_dictionary (~> 0.0.1) @@ -514,9 +532,9 @@ DEPENDENCIES parallel (~> 1.19, >= 1.19.1) pdfkit prettier - puma (~> 3.11) + pry-rails + puma (~> 5.6.5) rack-cors - rack-mini-profiler rails (~> 5.2.0) rails-i18n (~> 5.1) ransack @@ -538,9 +556,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/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/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/node_inputs_controller.rb b/app/controllers/action/node_inputs_controller.rb new file mode 100644 index 000000000..65227c657 --- /dev/null +++ b/app/controllers/action/node_inputs_controller.rb @@ -0,0 +1,75 @@ +class Action::NodeInputsController < ApplicationController + before_action :require_admin, except: [:index] + before_action :find_action_node + + def index + @node_inputs = @node.action_node_inputs + respond_to do |format| + format.html + format.json + end + end + + def create + @node_input = Action::NodeInput.new(node_input_params) + @node_input.action_node = @node + respond_to do |format| + if @node_input.save + format.html { redirect_to action_node_node_inputs_path(@node), notice: '创建成功.' } + format.json { render_ok(data: @node_input.as_json) } + else + format.html { render :new } + format.json { render json: @node_input.errors, status: -1 } + end + end + end + + def new + + end + + def show + + end + + def edit + + end + + def update + @node_input.update(node_input_params) + respond_to do |format| + format.html { redirect_to action_node_node_inputs_path(@node), notice: '更新成功.' } + format.json { render_ok(data: @node_input.as_json) } + end + end + + def destroy + if @node_input.destroy! + flash[:success] = '删除成功' + else + flash[:danger] = '删除失败' + end + redirect_to "api/actions/nodes" + end + + private + + def find_action_node + @node = Action::Node.find(params[:node_id]) + if params[:id].present? + @node_input = @node.action_node_inputs.find(params[:id]) + else + @node_input = Action::NodeInput.new + end + + end + + def node_input_params + if params.require(:action_node_input) + params.require(:action_node_input).permit(:name, :input_type, :description, :is_required, :sort_no) + else + params.permit(:name, :input_type, :description, :is_required, :sort_no) + end + end +end diff --git a/app/controllers/action/node_selects_controller.rb b/app/controllers/action/node_selects_controller.rb new file mode 100644 index 000000000..9acd6fc5f --- /dev/null +++ b/app/controllers/action/node_selects_controller.rb @@ -0,0 +1,76 @@ +class Action::NodeSelectsController < ApplicationController + + before_action :require_admin, except: [:index] + before_action :find_action_node + + def index + @node_selects = @node.action_node_selects + respond_to do |format| + format.html + format.json + end + end + + def create + @node_select = Action::NodeSelect.new(node_select_params) + @node_select.action_node = @node + respond_to do |format| + if @node_select.save + format.html { redirect_to action_node_node_selects_path(@node), notice: '创建成功.' } + format.json { render_ok(data: @node_select.as_json) } + else + format.html { render :new } + format.json { render json: @node_select.errors, status: -1 } + end + end + end + + def new + + end + + def show + + end + + def edit + + end + + def update + @node_select.update(node_select_params) + respond_to do |format| + format.html { redirect_to action_node_node_selects_path(@node), notice: '更新成功.' } + format.json { render_ok(data: @node_select.as_json) } + end + end + + def destroy + if @node_select.destroy! + flash[:success] = '删除成功' + else + flash[:danger] = '删除失败' + end + redirect_to "api/actions/nodes" + end + + private + + def find_action_node + @node = Action::Node.find(params[:node_id]) + if params[:id].present? + @node_select = @node.action_node_selects.find(params[:id]) + else + @node_select = Action::NodeSelect.new + end + + end + + def node_select_params + if params.require(:action_node_select) + params.require(:action_node_select).permit(:name, :val, :val_ext, :description, :sort_no) + else + params.permit(:name, :val, :val_ext, :description, :sort_no) + end + end +end diff --git a/app/controllers/action/node_types_controller.rb b/app/controllers/action/node_types_controller.rb new file mode 100644 index 000000000..32508d942 --- /dev/null +++ b/app/controllers/action/node_types_controller.rb @@ -0,0 +1,64 @@ +class Action::NodeTypesController < ApplicationController + before_action :require_admin, except: [:index] + before_action :find_node_type, except: [:index, :create, :new] + + def index + @node_types = Action::NodeType.all + end + + def create + @node_type = Action::NodeType.new(node_types_params) + respond_to do |format| + if @node_type.save + format.html { redirect_to action_node_types_path, notice: '创建成功.' } + format.json { render_ok(data: @node_type.as_json) } + else + format.html { render :new } + format.json { render json: @node_type.errors, status: -1 } + end + end + end + + def show + + end + + def new + @node_type = Action::NodeType.new + end + + def edit + + end + + def update + @node_type.update(node_types_params) + respond_to do |format| + format.html { redirect_to action_node_types_path, notice: '更新成功.' } + format.json { render_ok(data: @node_type.as_json) } + end + end + + def destroy + if @node_type.destroy! + flash[:success] = '删除成功' + else + flash[:danger] = '删除失败' + end + redirect_to action_node_types_path + end + + private + + def find_node_type + @node_type = Action::NodeType.find(params[:id]) + end + + def node_types_params + if params.require(:action_node_type) + params.require(:action_node_type).permit(:name, :description, :sort_no) + else + params.permit(:name, :description, :sort_no) + end + end +end diff --git a/app/controllers/action/nodes_controller.rb b/app/controllers/action/nodes_controller.rb new file mode 100644 index 000000000..e1e7799f4 --- /dev/null +++ b/app/controllers/action/nodes_controller.rb @@ -0,0 +1,69 @@ +class Action::NodesController < ApplicationController + before_action :require_admin, except: [:index] + before_action :find_action_node, except: [:index, :create, :new] + + def index + @node_types = Action::NodeType.all + @no_type_nodes = Action::Node.where(action_node_types_id: nil) + respond_to do |format| + format.html { @nodes = Action::Node.all } + format.json + end + end + + def create + @node = Action::Node.new(node_params) + respond_to do |format| + if @node.save + format.html { redirect_to action_nodes_path, notice: '创建成功.' } + format.json { render_ok(data: @node.as_json) } + else + format.html { render :new } + format.json { render json: @node.errors, status: -1 } + end + end + end + + def new + @node = Action::Node.new + end + + def show + + end + + def edit + + end + + def update + @node.update(node_params) + respond_to do |format| + format.html { redirect_to action_nodes_path, notice: '更新成功.' } + format.json { render_ok(data: @node.as_json) } + end + end + + def destroy + if @node.destroy! + flash[:success] = '删除成功' + else + flash[:danger] = '删除失败' + end + redirect_to action_nodes_path + end + + private + + def find_action_node + @node = Action::Node.find(params[:id]) + end + + 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) + else + params.permit(:name, :full_name, :description, :icon, :action_node_types_id, :is_local, :local_url, :yaml, :sort_no) + end + end +end diff --git a/app/controllers/action/templates_controller.rb b/app/controllers/action/templates_controller.rb new file mode 100644 index 000000000..092d38d64 --- /dev/null +++ b/app/controllers/action/templates_controller.rb @@ -0,0 +1,68 @@ +class Action::TemplatesController < ApplicationController + before_action :require_admin, except: [:index] + before_action :find_action_template, except: [:index, :create, :new] + + def index + @templates = Action::Template.all + respond_to do |format| + format.html + format.json + end + end + + def create + @template = Action::Template.new(templates_params) + respond_to do |format| + if @template.save + format.html { redirect_to action_templates_path, notice: '创建成功.' } + format.json { render_ok(data: @template.as_json) } + else + format.html { render :new } + format.json { render json: @template.errors, status: -1 } + end + end + end + + def show + + end + + def new + @template = Action::Template.new + end + + def edit + + end + + def update + @template.update(templates_params) + respond_to do |format| + format.html { redirect_to action_templates_path, notice: '更新成功.' } + format.json { render_ok(data: @template.as_json) } + end + end + + def destroy + if @template.destroy! + flash[:success] = '删除成功' + else + flash[:danger] = '删除失败' + end + redirect_to action_templates_path + end + + private + + def find_action_template + @template = Action::Template.find(params[:id]) + end + + def templates_params + if params.require(:action_template) + params.require(:action_template).permit(:name, :description, :img, :sort_no, :json, :yaml) + else + params.permit(:name, :description, :img, :sort_no, :json, :yaml) + end + end +end diff --git a/app/controllers/admins/dashboards_controller.rb b/app/controllers/admins/dashboards_controller.rb index 2c01c8bd0..37b69dc0d 100644 --- a/app/controllers/admins/dashboards_controller.rb +++ b/app/controllers/admins/dashboards_controller.rb @@ -21,13 +21,75 @@ class Admins::DashboardsController < Admins::BaseController weekly_project_ids = (CommitLog.where(created_at: current_week).pluck(:project_id).uniq + Issue.where(created_on: current_week).pluck(:project_id).uniq).uniq month_project_ids = (CommitLog.where(created_at: current_month).pluck(:project_id).uniq + Issue.where(created_on: current_month).pluck(:project_id).uniq).uniq @day_active_project_count = Project.where(updated_on: today).or(Project.where(id: day_project_ids)).count - @weekly_active_project_count = Project.where(updated_on: current_week).or(Project.where(id: weekly_project_ids)).count - @month_active_project_count = Project.where(updated_on: current_month).or(Project.where(id: month_project_ids)).count - + @weekly_active_project_count = Rails.cache.fetch("dashboardscontroller:weekly_active_project_count", expires_in: 10.minutes) do + Project.where(updated_on: current_week).or(Project.where(id: weekly_project_ids)).count + end + @month_active_project_count = Rails.cache.fetch("dashboardscontroller:month_active_project_count", expires_in: 1.hours) do + Project.where(updated_on: current_month).or(Project.where(id: month_project_ids)).count + end # 新增项目数 - @day_new_project_count = Project.where(created_on: today).count - @weekly_new_project_count = Project.where(created_on: current_week).count - @month_new_project_count = Project.where(created_on: current_month).count + @day_new_project_count = Rails.cache.fetch("dashboardscontroller:day_new_project_count", expires_in: 10.minutes) do + Project.where(created_on: today).count + end + @weekly_new_project_count = Rails.cache.fetch("dashboardscontroller:weekly_new_project_count", expires_in: 10.minutes) do + Project.where(created_on: current_week).count + end + @month_new_project_count = Rails.cache.fetch("dashboardscontroller:month_new_project_count", expires_in: 1.hours) do + Project.where(created_on: current_month).count + end + + + # 总的平台用户数 + # 总的平台项目数 + # 总的平台组织数 + # 总的平台Issue数、评论数、PR数、Commit数 + @user_count = Rails.cache.fetch("dashboardscontroller:platform:user_count", expires_in: 1.days) do + User.count + end + @project_count = Rails.cache.fetch("dashboardscontroller:platform:project_count", expires_in: 1.days) do + Project.count + end + @organization_count = Rails.cache.fetch("dashboardscontroller:platform:organization_count", expires_in: 1.days) do + Organization.count + end + @issue_count = Rails.cache.fetch("dashboardscontroller:platform:issue_count", expires_in: 1.days) do + Issue.count + end + @comment_count = Rails.cache.fetch("dashboardscontroller:platform:comment_count", expires_in: 1.days) do + Journal.count + end + @pr_count = Rails.cache.fetch("dashboardscontroller:platform:pr_count", expires_in: 1.days) do + PullRequest.count + end + @commit_count = Rails.cache.fetch("dashboardscontroller:platform:commit_count", expires_in: 1.days) do + CommitLog.count + end + + @subject_name = ["用户数", "项目数", "组织数", "Issue数", "Issue评论数", "PR数", "Commit数"] + @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] + + 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 + + end def month_active_user @@ -42,6 +104,19 @@ class Admins::DashboardsController < Admins::BaseController render_ok(data: data) end + def baidu_tongji + tongji_service = Baidu::TongjiService.new + redirect_to tongji_service.code_url + end + + def baidu_tongji_auth + if params[:code].present? + tongji_service = Baidu::TongjiService.new + tongji_service.get_access_token(params[:code]) + end + redirect_to "/admins/" + end + def evaluate names = [] data = [] @@ -63,8 +138,12 @@ class Admins::DashboardsController < Admins::BaseController Time.now.beginning_of_day..Time.now.end_of_day end - def current_week + def pre_7_days 7.days.ago.end_of_day..Time.now.end_of_day + end + + def current_week + Time.now.beginning_of_week..Time.now.end_of_day end def current_month @@ -72,6 +151,7 @@ class Admins::DashboardsController < Admins::BaseController end def pre_week - 14.days.ago.end_of_day..7.days.ago.end_of_day + # 14.days.ago.end_of_day..7.days.ago.end_of_day + Time.now.prev_week..Time.now.prev_week.end_of_week end end \ No newline at end of file diff --git a/app/controllers/admins/identity_verifications_controller.rb b/app/controllers/admins/identity_verifications_controller.rb index 51d5e423c..1db1a9883 100644 --- a/app/controllers/admins/identity_verifications_controller.rb +++ b/app/controllers/admins/identity_verifications_controller.rb @@ -14,12 +14,14 @@ class Admins::IdentityVerificationsController < Admins::BaseController end def update - if @identity_verification.update(update_params) + if update_params[:state] == "已拒绝" && update_params[:description].blank? + flash[:danger] = '拒绝理由不能为空' + render 'edit' + else + UserAction.create(action_id: @identity_verification.id, action_type: "UpdateIdentityVerifications", user_id: current_user.id, :ip => request.remote_ip, data_bank: @identity_verification.attributes.to_json) + @identity_verification.update(update_params) redirect_to admins_identity_verifications_path flash[:success] = "更新成功" - else - redirect_to admins_identity_verifications_path - flash[:danger] = "更新失败" end end diff --git a/app/controllers/admins/issues_rank_controller.rb b/app/controllers/admins/issues_rank_controller.rb new file mode 100644 index 000000000..79450fbfb --- /dev/null +++ b/app/controllers/admins/issues_rank_controller.rb @@ -0,0 +1,29 @@ +class Admins::IssuesRankController < Admins::BaseController + + def index + @statistics = DailyProjectStatistic.where('date >= ? AND date <= ?', begin_date, end_date) + @statistics = @statistics.group(:project_id).joins(:project).select("project_id, + sum(issues) as issues, + sum(closed_issues) as closed_issues, + projects.issues_count as issues_count") + @statistics = @statistics.order("#{sort_by} #{sort_direction}").limit(50) + end + + private + def begin_date + params.fetch(:begin_date, (Date.yesterday-7.days).to_s) + end + + def end_date + params.fetch(:end_date, Date.yesterday.to_s) + end + + def sort_by + DailyProjectStatistic.column_names.include?(params.fetch(:sort_by, "issues")) ? params.fetch(:sort_by, "issues") : "issues" + end + + def sort_direction + %w(desc asc).include?(params.fetch(:sort_direction, "desc")) ? params.fetch(:sort_direction, "desc") : "desc" + end + +end \ No newline at end of file diff --git a/app/controllers/admins/projects_controller.rb b/app/controllers/admins/projects_controller.rb index f1f797043..dc3f6030a 100644 --- a/app/controllers/admins/projects_controller.rb +++ b/app/controllers/admins/projects_controller.rb @@ -5,7 +5,7 @@ class Admins::ProjectsController < Admins::BaseController sort_by = Project.column_names.include?(params[:sort_by]) ? params[:sort_by] : 'created_on' sort_direction = %w(desc asc).include?(params[:sort_direction]) ? params[:sort_direction] : 'desc' search = params[:search].to_s.strip - projects = Project.where("name like ?", "%#{search}%").order("#{sort_by} #{sort_direction}") + projects = Project.where("name like ? OR identifier LIKE ?", "%#{search}%", "%#{search}%").order("#{sort_by} #{sort_direction}") @projects = paginate projects.includes(:owner, :members, :issues, :versions, :attachments, :project_score) end diff --git a/app/controllers/admins/projects_rank_controller.rb b/app/controllers/admins/projects_rank_controller.rb index e4d8046e0..8db0961b7 100644 --- a/app/controllers/admins/projects_rank_controller.rb +++ b/app/controllers/admins/projects_rank_controller.rb @@ -17,11 +17,11 @@ class Admins::ProjectsRankController < Admins::BaseController private def begin_date - params.fetch(:begin_date, (Date.today-7.days).to_s) + params.fetch(:begin_date, (Date.yesterday-7.days).to_s) end def end_date - params.fetch(:end_date, Date.today.to_s) + params.fetch(:end_date, Date.yesterday.to_s) end def sort_by diff --git a/app/controllers/admins/site_pages_controller.rb b/app/controllers/admins/site_pages_controller.rb index f0e05e71d..306c91627 100644 --- a/app/controllers/admins/site_pages_controller.rb +++ b/app/controllers/admins/site_pages_controller.rb @@ -29,8 +29,12 @@ class Admins::SitePagesController < Admins::BaseController end def update - @site_page.update(update_params) - flash[:success] = '保存成功' + if update_params[:state] == "false" && update_params[:state_description].blank? + flash[:danger] = '关闭站点理由不能为空' + else + @site_page.update(update_params) + flash[:success] = '保存成功' + end render 'edit' 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..844c01597 --- /dev/null +++ b/app/controllers/api/pm/issue_links_controller.rb @@ -0,0 +1,21 @@ +class Api::Pm::IssueLinksController < Api::Pm::BaseController + before_action :load_project + before_action :load_issue + def index + @links = @issue.pm_links.where(be_linkable_type: 'Issue') + end + + def create + params[:link_ids].map { |e| @issue.pm_links.find_or_create_by(be_linkable_type: 'Issue', be_linkable_id: e) } + render_ok + end + + def destroy + @link = @issue.pm_links.find_by(be_linkable_type: 'Issue', be_linkable_id: params[:id]) + if @link.try(:destroy) + render_ok + else + 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..b74cf29f0 --- /dev/null +++ b/app/controllers/api/pm/issues_controller.rb @@ -0,0 +1,215 @@ +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::V1::Issues::CreateService.call(@project, issue_params, current_user) + render 'api/v1/issues/create' + end + + def update + @object_result = Api::V1::Issues::UpdateService.call(@project, @issue, issue_params, current_user) + render 'api/v1/issues/update' + end + + def batch_update + @object_result = Api::V1::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::V1::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::V1::Issues::DeleteService.call(@project, @issue, current_user) + if @object_result + render_ok + else + render_error('删除疑修失败!') + 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..14f386860 --- /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/v1/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..2c343cf3a --- /dev/null +++ b/app/controllers/api/pm/projects_controller.rb @@ -0,0 +1,145 @@ +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]) + 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| + [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 bcb0c4e86..d9db4b99c 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -55,9 +55,14 @@ class Api::V1::BaseController < ApplicationController return render_forbidden if !current_user.admin? && !@project.operator?(current_user) && !(@project.fork_project.present? && @project.fork_project.operator?(current_user)) end + def require_member_above + @project = load_project + return render_forbidden if !current_user.admin? && !@project.member?(current_user) + end + # 具有对仓库的访问权限 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/gitlink_competition_applies_controller.rb b/app/controllers/api/v1/gitlink_competition_applies_controller.rb new file mode 100644 index 000000000..5f2d7ef28 --- /dev/null +++ b/app/controllers/api/v1/gitlink_competition_applies_controller.rb @@ -0,0 +1,37 @@ +class Api::V1::GitlinkCompetitionAppliesController < Api::V1::BaseController + + def create + return render_error("请输入正确的竞赛ID") unless params[:competition_id].present? + return render_error("请输入正确的队伍ID") unless params[:team_id].present? + return render_error("请输入正确的队伍成员信息") unless params[:team_members].is_a?(Array) + params[:team_members].each do |member| + apply = GitlinkCompetitionApply.find_or_create_by(competition_id: params[:competition_id], team_id: params[:team_id], educoder_login: member[:login]) + apply.competition_identifier = params[:competition_identifier] + apply.team_name = params[:team_name] + apply.school_name = member[:school_name] + apply.nickname = member[:nickname] + apply.identity = member[:identity] + apply.role = member[:role] + apply.email = member[:email] + user_info = get_user_info_by_educoder_login(member[:login]) + apply.phone = user_info["phone"] + apply.save + end + render_ok + end + + def get_user_info_by_educoder_login(edu_login) + req_params = { "login" => "#{edu_login}", "private_token" => "hriEn3UwXfJs3PmyXnqQ" } + api_url= "https://data.educoder.net" + client = Faraday.new(url: api_url) + response = client.public_send("get", "/api/sources/get_user_info_by_login", req_params) + result = JSON.parse(response.body) + + return nil if result["status"].to_s != "0" + + # login 邮箱 手机号 姓名 学校/单位 + user_info = result["data"] + + return user_info + 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/project_datasets_controller.rb b/app/controllers/api/v1/project_datasets_controller.rb new file mode 100644 index 000000000..995c1872e --- /dev/null +++ b/app/controllers/api/v1/project_datasets_controller.rb @@ -0,0 +1,10 @@ +class Api::V1::ProjectDatasetsController < Api::V1::BaseController + + def index + return render_error("请输入正确的项目id字符串") unless params[:ids].present? + ids = params[:ids].split(",") + @project_datasets = ProjectDataset.where(project_id: ids).includes(:license, :project) + @project_datasets = kaminari_unlimit_paginate(@project_datasets) + end + +end \ No newline at end of file diff --git a/app/controllers/api/v1/projects/actions/actions_controller.rb b/app/controllers/api/v1/projects/actions/actions_controller.rb new file mode 100644 index 000000000..3825b4685 --- /dev/null +++ b/app/controllers/api/v1/projects/actions/actions_controller.rb @@ -0,0 +1,31 @@ +class Api::V1::Projects::Actions::ActionsController < Api::V1::Projects::Actions::BaseController + + def index + begin + gitea_result = $gitea_hat_client.get_repos_actions_by_owner_repo(@project&.owner&.login, @project&.identifier) + @data = gitea_result[:data]["Workflows"] + rescue + @data = [] + end + end + + def disable + return render_error("请输入正确的流水线文件!") if params[:workflow].blank? + gitea_result = $gitea_hat_client.post_repos_actions_disable(@project&.owner&.login, @project&.identifier, {query: {workflow: params[:workflow]}}) rescue nil + if gitea_result + render_ok + else + render_error("禁用流水线失败") + end + end + + def enable + return render_error("请输入正确的流水线文件!") if params[:workflow].blank? + gitea_result = $gitea_hat_client.post_repos_actions_enable(@project&.owner&.login, @project&.identifier, {query: {workflow: params[:workflow]}}) rescue nil + if gitea_result + render_ok + else + render_error("取消禁用流水线失败") + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/projects/actions/base_controller.rb b/app/controllers/api/v1/projects/actions/base_controller.rb new file mode 100644 index 000000000..d76b446ad --- /dev/null +++ b/app/controllers/api/v1/projects/actions/base_controller.rb @@ -0,0 +1,4 @@ +class Api::V1::Projects::Actions::BaseController < Api::V1::BaseController + before_action :require_public_and_member_above + +end \ No newline at end of file diff --git a/app/controllers/api/v1/projects/actions/runs_controller.rb b/app/controllers/api/v1/projects/actions/runs_controller.rb new file mode 100644 index 000000000..ff7c28e85 --- /dev/null +++ b/app/controllers/api/v1/projects/actions/runs_controller.rb @@ -0,0 +1,46 @@ +class Api::V1::Projects::Actions::RunsController < Api::V1::Projects::Actions::BaseController + + def index + @result_object = Api::V1::Projects::Actions::Runs::ListService.call(@project, {workflow: params[:workflow], page: page, limit: limit}, current_user&.gitea_token) + puts @result_object + end + + def rerun + return render_error("请输入正确的流水线记录ID!") if params[:run_id].blank? + gitea_result = $gitea_hat_client.post_repos_actions_runs_rerun_by_owner_repo_run(@project&.owner&.login, @project&.identifier, params[:run_id]) rescue nil + if gitea_result + render_ok + else + render_error("重启所有流水线任务失败") + end + end + + def job_rerun + return render_error("请输入正确的流水线记录ID!") if params[:run_id].blank? + return render_error("请输入正确的流水线任务ID") if params[:job].blank? + gitea_result = $gitea_hat_client.post_repos_actions_runs_jobs_rerun_by_owner_repo_run_job(@project&.owner&.login, @project&.identifier, params[:run_id], params[:job]) rescue nil + if gitea_result + render_ok + else + render_error("重启流水线任务失败") + end + end + + def job_show + @result_object = Api::V1::Projects::Actions::Runs::JobShowService.call(@project, params[:run_id], params[:job], params[:log_cursors], current_user&.gitea_token) + end + + def job_logs + return render_error("请输入正确的流水线记录ID!") if params[:run_id].blank? + return render_error("请输入正确的流水线任务ID") if params[:job].blank? + domain = GiteaService.gitea_config[:domain] + api_url = GiteaService.gitea_config[:hat_base_url] + + url = "/repos/#{@owner.login}/#{@repository.identifier}/actions/runs/#{CGI.escape(params[:run_id])}/jobs/#{CGI.escape(params[:job])}/logs" + file_path = [domain, api_url, url].join + file_path = [file_path, "access_token=#{@owner&.gitea_token}"].join("?") + + redirect_to file_path + end + +end \ No newline at end of file diff --git a/app/controllers/api/v1/projects/branches_controller.rb b/app/controllers/api/v1/projects/branches_controller.rb index 40f44fea5..2bb7fdaf5 100644 --- a/app/controllers/api/v1/projects/branches_controller.rb +++ b/app/controllers/api/v1/projects/branches_controller.rb @@ -1,15 +1,38 @@ class Api::V1::Projects::BranchesController < Api::V1::BaseController before_action :require_public_and_member_above, only: [:index, :all] + def gitee + url = URI("https://gitee.com/api/v5/repos/#{params[:owner]}/#{params[:repo]}/branches?access_token=#{params[:token]}&page=#{page}&per_page=#{limit}") + https = Net::HTTP.new(url.host, url.port) + https.use_ssl = true + request = Net::HTTP::Get.new(url) + response = https.request(request) + render :json => response.read_body + end + + def github + url = URI("https://api.github.com/repos/#{params[:owner]}/#{params[:repo]}/branches?page=#{page}&per_page=#{limit}") + https = Net::HTTP.new(url.host, url.port) + https.use_ssl = true + + request = Net::HTTP::Get.new(url) + request["Authorization"] = "Bearer #{params[:token]}" + request["Accept"] = "application/vnd.github+json" + request["X-GitHub-Api-Version"] = "2022-11-28" + + response = https.request(request) + render :json => response.read_body + end + def index - @result_object = Api::V1::Projects::Branches::ListService.call(@project, {name: params[:keyword], page: page, limit: limit}, current_user&.gitea_token) + @result_object = Api::V1::Projects::Branches::ListService.call(@project, {name: params[:keyword], state: params[:state], page: page, limit: limit}, current_user&.gitea_token) end def all @result_object = Api::V1::Projects::Branches::AllListService.call(@project, current_user&.gitea_token) end - before_action :require_operate_above, only: [:create, :destroy] + before_action :require_operate_above, only: [:create, :destroy, :restore] def create @result_object = Api::V1::Projects::Branches::CreateService.call(@project, branch_params, current_user&.gitea_token) @@ -33,6 +56,15 @@ class Api::V1::Projects::BranchesController < Api::V1::BaseController end end + def restore + @result_object = Api::V1::Projects::Branches::RestoreService.call(@project, params[:branch_id], params[:branch_name], current_user&.gitea_token) + if @result_object + return render_ok + else + return render_error('恢复分支失败!') + end + end + before_action :require_manager_above, only: [:update_default_branch] def update_default_branch diff --git a/app/controllers/api/v1/projects/commits_controller.rb b/app/controllers/api/v1/projects/commits_controller.rb index a1545ae6f..98fdc290f 100644 --- a/app/controllers/api/v1/projects/commits_controller.rb +++ b/app/controllers/api/v1/projects/commits_controller.rb @@ -1,5 +1,5 @@ class Api::V1::Projects::CommitsController < Api::V1::BaseController - before_action :require_public_and_member_above, only: [:index, :diff] + before_action :require_public_and_member_above, only: [:index, :diff, :recent] def index @result_object = Api::V1::Projects::Commits::ListService.call(@project, {page: page, limit: limit, sha: params[:sha]}, current_user&.gitea_token) @@ -9,4 +9,11 @@ class Api::V1::Projects::CommitsController < Api::V1::BaseController def diff @result_object = Api::V1::Projects::Commits::DiffService.call(@project, params[:sha], current_user&.gitea_token) end + + def recent + hash = Api::V1::Projects::Commits::RecentService.call(@project, {keyword: params[:keyword], page: page, limit: limit}, current_user&.gitea_token) + @result_object = hash[:result] + @object_detail = hash[:detail] + puts @object_detail + end end \ No newline at end of file diff --git a/app/controllers/api/v1/projects/datasets_controller.rb b/app/controllers/api/v1/projects/datasets_controller.rb new file mode 100644 index 000000000..8690ba529 --- /dev/null +++ b/app/controllers/api/v1/projects/datasets_controller.rb @@ -0,0 +1,51 @@ +class Api::V1::Projects::DatasetsController < Api::V1::BaseController + before_action :require_public_and_member_above, only: [:show] + before_action :require_member_above, only: [:create, :update] + before_action :find_dataset, only: [:update, :show] + before_action :check_menu_authorize + + def create + ::Projects::Datasets::CreateForm.new(dataset_params).validate! + return render_error('该项目下已存在数据集!') if @project.project_dataset.present? + @project_dataset = ProjectDataset.new(dataset_params.merge!(project_id: @project.id)) + if @project_dataset.save! + render_ok + else + render_error('创建数据集失败!') + end + rescue Exception => e + uid_logger_error(e.message) + tip_exception(e.message) + end + + def update + ::Projects::Datasets::CreateForm.new(dataset_params).validate! + @project_dataset.attributes = dataset_params + if @project_dataset.save! + render_ok + else + render_error("更新数据集失败!") + end + rescue Exception => e + uid_logger_error(e.message) + tip_exception(e.message) + end + + def show + @attachments = kaminari_paginate(@project_dataset.attachments.includes(:author)) + end + + private + def dataset_params + params.permit(:title, :description, :license_id, :paper_content) + end + + def find_dataset + @project_dataset = @project.project_dataset + return render_not_found unless @project_dataset.present? + end + + def check_menu_authorize + return render_not_found unless @project.has_menu_permission("dataset") + end +end \ No newline at end of file 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..c1bcc8299 --- /dev/null +++ b/app/controllers/api/v1/projects/pipelines_controller.rb @@ -0,0 +1,460 @@ +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 + 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 + 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.cust_name = 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: Base64.encode64(@pipeline.yaml).gsub(/\n/, ''), + 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.cust_name = input_node["labelf"] 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/sync_repositories_controller.rb b/app/controllers/api/v1/projects/sync_repositories_controller.rb new file mode 100644 index 000000000..f8b466420 --- /dev/null +++ b/app/controllers/api/v1/projects/sync_repositories_controller.rb @@ -0,0 +1,148 @@ +class Api::V1::Projects::SyncRepositoriesController < Api::V1::BaseController + before_action :require_public_and_member_above, except: [:sync] + before_action :load_project, only: [:sync] + + def index + @sync_repositories = @project.sync_repositories + @group_sync_repository = @project.sync_repositories.group(:type, :external_repo_address, :sync_granularity, :external_token).count + end + + def create + @sync_repository1, @sync_repository2, @sync_repository_branch1, @sync_repository_branch2 = Api::V1::Projects::SyncRepositories::CreateService.call(@project, sync_repository_params) + rescue Exception => e + uid_logger_error(e.message) + tip_exception(e.message) + end + + def update_info + return render_error("请输入正确的同步仓库ID") unless params[:sync_repository_ids].present? + Api::V1::Projects::SyncRepositories::UpdateService.call(@project, params[:sync_repository_ids], sync_repository_update_params) + render_ok + rescue Exception => e + uid_logger_error(e.message) + tip_exception(e.message) + end + + def sync + return render_error("请输入正确的同步方向!") if params[:sync_direction].blank? + if params[:repo_type].present? + @sync_repositories = SyncRepository.where(project: @project, type: params[:repo_type], sync_direction: params[:sync_direction]) + else + @sync_repositories = SyncRepository.where(project: @project, sync_direction: params[:sync_direction]) + end + branch = params[:payload].present? ? JSON.parse(params[:payload])["ref"].split("/")[-1] : params[:ref].split("/")[-1] rescue nil + if params[:sync_direction].to_i == 1 + @sync_repository_branches = SyncRepositoryBranch.where(sync_repository_id: @sync_repositories, gitlink_branch_name: branch, enable: true) + else + @sync_repository_branches = SyncRepositoryBranch.where(sync_repository_id: @sync_repositories, external_branch_name: branch, enable: true) + end + # 全部分支同步暂时不做 + # @sync_repositories.each do |item| + # TouchSyncJob.perform_later(item) + # end + @sync_repository_branches.each do |item| + TouchSyncJob.set(wait: 5.seconds).perform_later(item) + end + rescue Exception => e + uid_logger_error(e.message) + tip_exception(e.message) + end + + def unbind + return render_error("请输入正确的同步仓库ID") unless params[:sync_repository_ids].present? + @sync_repositories = SyncRepository.where(id: params[:sync_repository_ids].split(",")) + @sync_repositories.each do |repo| + # Reposync::DeleteRepoService.call(repo.repo_name) # 解绑操作放在回调里 + Api::V1::Projects::Webhooks::DeleteService.call(@project, repo.webhook_gid) + repo.destroy + end + render_ok + rescue Exception => e + uid_logger_error(e.message) + tip_exception(e.message) + end + + def change_enable + return render_error("请输入正确的仓库类型") if params[:repo_type].blank? + return render_error("请输入正确的分支名称") if params[:gitlink_branch_name].blank? || params[:external_branch_name].blank? + # return render_error("请输入正确的状态") if params[:enable].blank? + @sync_repository_branches = SyncRepositoryBranch.joins(:sync_repository).where(sync_repositories: {project_id: @project.id, type: params[:repo_type]}, gitlink_branch_name: params[:gitlink_branch_name], external_branch_name: params[:external_branch_name]) + if @sync_repository_branches.update_all({enable: params[:enable]}) + @sync_repository_branches.each do |branch| + branch_sync_direction = branch&.sync_repository&.sync_direction.to_i + if branch_sync_direction == 1 + Reposync::UpdateBranchStatusService.call(branch&.sync_repository&.repo_name, branch.gitlink_branch_name, params[:enable]) + else + Reposync::UpdateBranchStatusService.call(branch&.sync_repository&.repo_name, branch.external_branch_name, params[:enable]) + end + TouchSyncJob.perform_later(branch) if params[:enable] && branch_sync_direction == params[:first_sync_direction].to_i + end + render_ok + else + render_error("更新失败!") + end + rescue Exception => e + uid_logger_error(e.message) + tip_exception(e.message) + end + + def create_branch + return render_error("请输入正确的同步仓库ID") unless params[:sync_repository_ids].present? + return render_error("请输入正确的Gitlink分支名称") unless params[:gitlink_branch_name].present? + return render_error("请输入正确的外部仓库分支名称") unless params[:external_branch_name].present? + return render_error("请输入正确的首次同步方向") unless params[:first_sync_direction].present? + + params[:sync_repository_ids].split(",").each do |id| + repo = SyncRepository.find_by_id id + branch = Reposync::CreateSyncBranchService.call(repo.repo_name, params[:gitlink_branch_name], params[:external_branch_name]) + return render_error(branch[2]) if branch[0].to_i !=0 + sync_branch = SyncRepositoryBranch.create!(sync_repository_id: id, gitlink_branch_name: params[:gitlink_branch_name], external_branch_name: params[:external_branch_name], reposync_branch_id: branch[1]['id']) + TouchSyncJob.perform_later(sync_branch) if params[:first_sync_direction].to_i == repo.sync_direction + end + render_ok + rescue Exception => e + uid_logger_error(e.message) + tip_exception(e.message) + end + + def branches + return render_error("请输入正确的同步仓库ID") unless params[:sync_repository_ids].present? + @sync_repository_branches = SyncRepositoryBranch.where(sync_repository_id: params[:sync_repository_ids].split(",")) + @sync_repository_branches = @sync_repository_branches.ransack(gitlink_branch_name_or_external_branch_name_cont: params[:branch_name]).result if params[:branch_name].present? + @group_sync_repository_branch = @sync_repository_branches.joins(:sync_repository).group("sync_repositories.type, sync_repository_branches.gitlink_branch_name, sync_repository_branches.external_branch_name").select("sync_repositories.type as type,max(sync_repository_branches.updated_at) as updated_at, sync_repository_branches.gitlink_branch_name, sync_repository_branches.external_branch_name").sort_by{|i|i.updated_at} + @each_json = [] + @group_sync_repository_branch.each do |item| + branches = @sync_repository_branches.joins(:sync_repository).where(sync_repositories: {type: item.type}, gitlink_branch_name: item.gitlink_branch_name, external_branch_name: item.external_branch_name).order(sync_time: :desc) + branch = branches.first + @each_json << { + gitlink_branch_name: item.gitlink_branch_name, + external_branch_name: item.external_branch_name, + type: branch&.sync_repository&.type, + sync_time: branch.sync_time.present? ? branch.sync_time.strftime("%Y-%m-%d %H:%M:%S") : nil, + sync_status: branch.sync_status, + enable: branch.enable, + enable_num: branch.enable ? 1 : 0, + created_at: branch.created_at.to_i, + reposync_branch_ids: branches.pluck(:reposync_branch_id) + } + end + @each_json = @each_json.sort_by{|h| [-h[:enable_num], h[:created_at]]} + render :json => {total_count: @group_sync_repository_branch.count, sync_repository_branches: @each_json} + end + + def history + return render_error("请输入正确的同步分支ID") unless params[:reposync_branch_ids] + @branch = SyncRepositoryBranch.find_by(reposync_branch_id: params[:reposync_branch_ids].split(",")[0]) + _, @reposync_branch_logs, @total_count, _ = Reposync::GetLogsService.call(nil, params[:reposync_branch_ids], page, limit) + end + + private + def sync_repository_params + params.permit(:type, :external_token, :external_repo_address, :sync_granularity, :external_branch_name, :gitlink_branch_name, :first_sync_direction) + end + + def sync_repository_update_params + params.permit(:external_token, :external_repo_address) + end + +end \ No newline at end of file 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..bde3913e4 --- /dev/null +++ b/app/controllers/api/v1/sonarqubes_controller.rb @@ -0,0 +1,120 @@ +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 + render_ok + end + + def insert_file + sonar_scanner_content = { + filepath: '.gitea/workflows/SonarScanner.yaml', + branch: params[:branch], + new_branch: nil, + content: 'b246CiAgIyBUcmlnZ2VyIGFuYWx5c2lzIHdoZW4gcHVzaGluZyB0byB5b3VyIG1haW4gYnJhbmNoZXMsIGFuZCB3aGVuIGNyZWF0aW5nIGEgcHVsbCByZXF1ZXN0LgogIHB1c2g6CiAgICBicmFuY2hlczoKICAgICAgLSBtYWluCiAgICAgIC0gbWFzdGVyCiAgICAgIC0gZGV2ZWxvcAogICAgICAtICdyZWxlYXNlcy8qKicKICBwdWxsX3JlcXVlc3Q6CiAgICAgIHR5cGVzOiBbb3BlbmVkLCBzeW5jaHJvbml6ZSwgcmVvcGVuZWRdCgpuYW1lOiBNYWluIFdvcmtmbG93CmpvYnM6CiAgc29uYXJxdWJlOgogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgc3RlcHM6CiAgICAtIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAdjQKICAgICAgd2l0aDoKICAgICAgICAjIERpc2FibGluZyBzaGFsbG93IGNsb25lcyBpcyByZWNvbW1lbmRlZCBmb3IgaW1wcm92aW5nIHRoZSByZWxldmFuY3kgb2YgcmVwb3J0aW5nCiAgICAgICAgZmV0Y2gtZGVwdGg6IDAKICAgIC0gbmFtZTogU29uYXJRdWJlIFNjYW4KICAgICAgdXNlczogc29uYXJzb3VyY2Uvc29uYXJxdWJlLXNjYW4tYWN0aW9uQG1hc3RlcgogICAgICBlbnY6CiAgICAgICAgU09OQVJfVE9LRU46ICR7eyBzZWNyZXRzLlNPTkFSX1RPS0VOIH19CiAgICAgICAgU09OQVJfSE9TVF9VUkw6ICAke3sgc2VjcmV0cy5TT05BUl9IT1NUX1VSTCB9fQ==', + 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? + sonar_scanner_content[:content] = Base64.decode64(sonar_scanner_content[:content]) + Gitea::UpdateFileInteractor.call(@owner.gitea_token, @owner.login, sonar_scanner_content.merge(sha:sonar_scanner_exit.result['sha'])) + else + 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]}-#{params[:repo]}\nsonar.sources=.", + "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[:components], + s: params[:s], + impactSoftwareQualities: params[:impactSoftwareQualities], + issueStatuses: params[:issueStatuses], + ps: params[:ps], + p: params[:s], + facets: params[:facets], + additionalFields: params[:additionalFields], + timeZone: params[:timeZone] + } + data = Sonarqube.client.get('/api/issues/search', params_data) + render_ok data + end + + def ce_component + params_data = { + components: params[:components] + } + data = Sonarqube.client.get('/api/ce/component', params_data) + render_ok data + end + + def sources_issue_snippet + params_data = { + issueKey: params[:issueKey] + } + data = Sonarqube.client.get('/api/sources/issue_snippets', params_data) + render_ok data + end + + def rules_show + params_data = { + key: params[:key] + } + data = Sonarqube.client.get('/api/rules/show', params_data) + render_ok data + end + + def measures_search_history + params_data = { + from: params[:form], + component: params[:component], + metrics: params[:metrics], + ps: params[:ps] + } + data = Sonarqube.client.get('/api/measures/search_history', params_data) + render_ok data + end + + def measures_component + params_data = { + component: params[:component], + additionalFields: params[:additionalFields], + metricKeys: params[:metricKeys], + } + data = Sonarqube.client.get('/api/measures/component', params_data) + render_ok data + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8981a0995..bf2fb85c1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -318,19 +318,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 @@ -363,15 +363,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 @@ -715,7 +714,7 @@ class ApplicationController < ActionController::Base end def find_user_with_id - @user = User.find_by_id params[:user_id] + @user = User.find_by(type: 'User', id: params[:user_id]) # render_not_found("未找到’#{params[:login]}’相关的用户") unless @user render_error("未找到相关的用户") unless @user end diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index ecc4760b5..c824b6ce3 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -95,6 +95,9 @@ class AttachmentsController < ApplicationController @attachment.disk_directory = month_folder @attachment.cloud_url = remote_path @attachment.uuid = SecureRandom.uuid + @attachment.description = params[:description] + @attachment.container_id = params[:container_id] + @attachment.container_type = params[:container_type] @attachment.save! else logger.info "文件已存在,id = #{@attachment.id}, filename = #{@attachment.filename}" @@ -124,7 +127,7 @@ class AttachmentsController < ApplicationController # 附件为视频时,点击播放 def preview_attachment - attachment = Attachment.find_by(id: params[:id]) + attachment = Attachment.where_id_or_uuid(params[:id]).first dir_path = "#{Rails.root}/public/preview" Dir.mkdir(dir_path) unless Dir.exist?(dir_path) if params[:status] == "preview" diff --git a/app/controllers/bind_users_controller.rb b/app/controllers/bind_users_controller.rb index f5ed33809..b8c25dd7d 100644 --- a/app/controllers/bind_users_controller.rb +++ b/app/controllers/bind_users_controller.rb @@ -8,7 +8,7 @@ class BindUsersController < ApplicationController bind_user = User.try_to_login(params[:username], params[:password]) tip_exception '用户名或者密码错误' if bind_user.blank? tip_exception '用户名或者密码错误' unless bind_user.check_password?(params[:password].to_s) - tip_exception '参数错误' unless ["qq", "wechat", "gitee", "github", "educoder"].include?(params[:type].to_s) + tip_exception '参数错误' unless ["qq", "wechat", "gitee", "github", "educoder", "acge"].include?(params[:type].to_s) tip_exception '该账号已被绑定,请更换其他账号进行绑定' if bind_user.bind_open_user?(params[:type].to_s) "OpenUsers::#{params[:type].to_s.capitalize}".constantize.create!(user: bind_user, uid: session[:unionid]) diff --git a/app/controllers/oauth/acge_controller.rb b/app/controllers/oauth/acge_controller.rb new file mode 100644 index 000000000..6f7c82039 --- /dev/null +++ b/app/controllers/oauth/acge_controller.rb @@ -0,0 +1,67 @@ +class Oauth::AcgeController < Oauth::BaseController + include RegisterHelper + + def create + begin + uid = params['uid'].to_s.strip + tip_exception("uid不能为空") if uid.blank? + redirect_uri = params['redirect_uri'].to_s.strip + tip_exception("redirect_uri不能为空") if redirect_uri.blank? + email = params['email'].to_s.strip + tip_exception("email不能为空") if email.blank? + phone = params['phone'].to_s.strip + tip_exception("phone不能为空") if phone.blank? + name = params['name'].to_s.strip + tip_exception("name不能为空") if name.blank? + + open_user = OpenUsers::Acge.find_by(uid: uid) + if open_user.present? && open_user.user.present? + successful_authentication(open_user.user) + redirect_to redirect_uri + return + else + if current_user.blank? || !current_user.logged? + session[:unionid] = uid + user = User.find_by(mail: email) || User.find_by(phone: phone) + if user.present? + OpenUsers::Acge.create!(user: user, uid: uid) + successful_authentication(user) + redirect_to redirect_uri + + return + else + username = uid + password = SecureRandom.hex(4) + reg_result = autologin_register(username, email, password, 'acge', phone, name) + existing_rows = CSV.read("public/操作系统大赛用户信息.csv") + new_row = [username, email, password, phone, name] + existing_rows << new_row + CSV.open("public/操作系统大赛用户信息.csv", 'wb') do |csv| + existing_rows.each { |row| csv << row } + end + if reg_result[:message].blank? + open_user = OpenUsers::Acge.create!(user_id: reg_result[:user][:id], uid: uid) + successful_authentication(open_user.user) + redirect_to redirect_uri + + return + else + render_error(reg_result[:message]) + end + end + else + OpenUsers::Acge.create!(user: current_user, uid: uid) + successful_authentication(current_user) + redirect_to redirect_uri + + return + end + end + + Rails.logger.info("[OAuth2] session[:unionid] -> #{session[:unionid]}") + # redirect_to "/bindlogin/acge?redirect_uri=#{redirect_uri}" + rescue Exception => ex + render_error(ex.message) + end + end +end \ No newline at end of file diff --git a/app/controllers/organizations/organization_users_controller.rb b/app/controllers/organizations/organization_users_controller.rb index d9035dc14..171e9dcae 100644 --- a/app/controllers/organizations/organization_users_controller.rb +++ b/app/controllers/organizations/organization_users_controller.rb @@ -4,17 +4,31 @@ class Organizations::OrganizationUsersController < Organizations::BaseController before_action :check_user_can_edit_org, only: [:destroy] def index - @organization_users = @organization.organization_users.includes(:user) + # @organization_users = @organization.organization_users.includes(:user) + # if params[:search].present? + # search = params[:search].to_s.downcase + # user_condition_users = User.like(search).to_sql + # team_condition_teams = User.joins(:teams).merge(@organization.teams.like(search)).to_sql + # users = User.from("( #{user_condition_users} UNION #{team_condition_teams }) AS users") + # + # @organization_users = @organization_users.where(user_id: users).distinct + # end + # + # @organization_users = kaminari_paginate(@organization_users) + + organization_user_ids = @organization.organization_users.pluck(:user_id).uniq + project_member_user_ids = @organization.projects.joins(:members).pluck("members.user_id").uniq + ids = organization_user_ids + project_member_user_ids + users = User.where(id: ids).reorder(Arel.sql("FIELD(users.id,#{ids.join(',')})")) if params[:search].present? search = params[:search].to_s.downcase user_condition_users = User.like(search).to_sql team_condition_teams = User.joins(:teams).merge(@organization.teams.like(search)).to_sql - users = User.from("( #{user_condition_users} UNION #{team_condition_teams }) AS users") + user_ids = User.from("( #{user_condition_users} UNION #{team_condition_teams }) AS users").pluck(:id) - @organization_users = @organization_users.where(user_id: users).distinct + users = users.where(id: user_ids) end - - @organization_users = kaminari_paginate(@organization_users) + @users = kaminari_paginate(users) end def pm_check_user diff --git a/app/controllers/organizations/projects_controller.rb b/app/controllers/organizations/projects_controller.rb index ab5c9ef5d..1180a1a51 100644 --- a/app/controllers/organizations/projects_controller.rb +++ b/app/controllers/organizations/projects_controller.rb @@ -10,6 +10,8 @@ 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.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/organizations/teams_controller.rb b/app/controllers/organizations/teams_controller.rb index 09f5bc3f0..56172a61e 100644 --- a/app/controllers/organizations/teams_controller.rb +++ b/app/controllers/organizations/teams_controller.rb @@ -67,7 +67,18 @@ class Organizations::TeamsController < Organizations::BaseController tip_exception("组织团队不允许被删除") if @team.owner? ActiveRecord::Base.transaction do Gitea::Organization::Team::DeleteService.call(@organization.gitea_token, @team.gtid) + other_user_ids = @organization.team_users.where.not(team_id: @team.id).pluck(:user_id) + team_user_ids = @team.team_users.pluck(:user_id) + # 当前删除团队中成员在其他组织其他团队不存在的成员需清除组织 + remove_user_ids = team_user_ids - other_user_ids + Rails.logger.info "remove_user_ids ===========> #{remove_user_ids}" @team.destroy! + if remove_user_ids.present? + User.where(id: remove_user_ids).each do |user| + @organization.organization_users.find_by(user_id: user.id).destroy! + Gitea::Organization::OrganizationUser::DeleteService.call(@organization.gitea_token, @organization.login, user.login) + end + end end render_ok rescue Exception => e diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2045eb7fd..6ddc3410a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -21,6 +21,7 @@ class ProjectsController < ApplicationController menu.append(menu_hash_by_name("issues")) if @project.has_menu_permission("issues") menu.append(menu_hash_by_name("pulls")) if @project.has_menu_permission("pulls") && @project.forge? menu.append(menu_hash_by_name("devops")) if @project.has_menu_permission("devops") && @project.forge? + menu.append(menu_hash_by_name("dataset")) if @project.has_menu_permission("dataset") && @project.forge? menu.append(menu_hash_by_name("versions")) if @project.has_menu_permission("versions") menu.append(menu_hash_by_name("wiki")) if @project.has_menu_permission("wiki") && @project.forge? menu.append(menu_hash_by_name("services")) if @project.has_menu_permission("services") && @project.forge? && (current_user.admin? || @project.member?(current_user.id)) @@ -42,7 +43,8 @@ class ProjectsController < ApplicationController @total_count = if category_id.blank? && params[:search].blank? && params[:topic_id].blank? # 默认查询时count性能问题处理 - ProjectCategory.sum("projects_count") - Project.visible.joins("left join organization_extensions on organization_extensions.organization_id = projects.user_id").where("organization_extensions.visibility =2").count + not_category_count = Project.where(project_category_id: nil).count + ProjectCategory.sum("projects_count") - Project.visible.joins("left join organization_extensions on organization_extensions.organization_id = projects.user_id").where("organization_extensions.visibility =2").count + not_category_count elsif params[:search].present? || params[:topic_id].present? @projects.total_count else @@ -58,7 +60,10 @@ class ProjectsController < ApplicationController OpenProjectDevOpsJob.set(wait: 5.seconds).perform_later(@project&.id, current_user.id) UpdateProjectTopicJob.perform_later(@project.id) if @project.id.present? end - rescue Exception => e + rescue Gitea::Api::ServerError => ex + uid_logger_error(ex.message) + tip_exception(ex.http_code, ex.message) + rescue ApplicationService::Error => e uid_logger_error(e.message) tip_exception(e.message) end @@ -193,13 +198,19 @@ class ProjectsController < ApplicationController default_branch: @project.default_branch } Gitea::Repository::UpdateService.call(@owner, @project.identifier, gitea_params) + elsif project_params.has_key?("has_actions") + gitea_params = { + has_actions: project_params[:has_actions] + } + Gitea::Repository::UpdateService.call(@owner, @project.identifier, gitea_params) else validate_params = project_params.slice(:name, :description, :project_category_id, :project_language_id, :private, :identifier) Projects::UpdateForm.new(validate_params.merge(user_id: @project.user_id, project_identifier: @project.identifier, project_name: @project.name)).validate! - private = @project.forked_from_project.present? ? !@project.forked_from_project.is_public : params[:private] || false + private = params[:private].nil? ? !@project.is_public : params[:private] + private = @project.forked_from_project.present? ? !@project.forked_from_project.is_public : private new_project_params = project_params.except(:private).merge(is_public: !private) @project.update_attributes!(new_project_params) @@ -232,15 +243,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 @@ -347,7 +349,7 @@ class ProjectsController < ApplicationController def project_params params.permit(:user_id, :name, :description, :repository_name, :website, :lesson_url, :default_branch, :identifier, - :project_category_id, :project_language_id, :license_id, :ignore_id, :private, + :project_category_id, :project_language_id, :license_id, :ignore_id, :private, :has_actions, :blockchain, :blockchain_token_all, :blockchain_init_token, :pr_view_admin) end diff --git a/app/controllers/pull_requests_controller.rb b/app/controllers/pull_requests_controller.rb index 037e201bf..1fe079f29 100644 --- a/app/controllers/pull_requests_controller.rb +++ b/app/controllers/pull_requests_controller.rb @@ -203,6 +203,7 @@ class PullRequestsController < ApplicationController def pr_merge return render_forbidden("你没有权限操作.") unless @project.operator?(current_user) + return normal_status(-1, "该分支存在冲突,无法自动合并.") unless @pull_request.conflict_files.blank? if params[:do].blank? normal_status(-1, "请选择合并方式") diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index e26559f74..a71c2ff1a 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -64,6 +64,7 @@ class RepositoriesController < ApplicationController @entries = Educoder::Repository::Entries::ListService.call(@project&.project_educoder.repo_name) else @entries = Gitea::Repository::Entries::ListService.new(@owner, @project.identifier, ref: @ref).call + return render_not_found if @entries.is_a?(Array) && @entries.blank? @entries = @entries.present? ? @entries.sort_by{ |hash| hash['type'] } : [] @path = GiteaService.gitea_config[:domain]+"/#{@project.owner.login}/#{@project.identifier}/raw/branch/#{@ref}/" end diff --git a/app/controllers/version_releases_controller.rb b/app/controllers/version_releases_controller.rb index ed608873e..2419a1fef 100644 --- a/app/controllers/version_releases_controller.rb +++ b/app/controllers/version_releases_controller.rb @@ -1,4 +1,5 @@ class VersionReleasesController < ApplicationController + include ApplicationHelper before_action :load_repository before_action :set_user before_action :require_login, except: [:index, :show] @@ -126,6 +127,16 @@ class VersionReleasesController < ApplicationController end end + def download + tip_exception(404, '您访问的页面不存在或已被删除') if params["tag_name"].blank? || params["filename"].blank? + version = @repository.version_releases.find_by(tag_name: params["tag_name"]) + attachment = version.attachments.find_by(filename: params["filename"]) + tip_exception(404, '您访问的页面不存在或已被删除') if attachment.blank? + send_file(absolute_path(local_path(attachment)), filename: attachment.title, stream: false, type: attachment.content_type.presence || 'application/octet-stream') + update_downloads(attachment) + # redirect_to "/api/attachments/#{attachment.uuid}" + end + private def set_user diff --git a/app/forms/organizations/create_form.rb b/app/forms/organizations/create_form.rb index 2163f477e..1f40350b3 100644 --- a/app/forms/organizations/create_form.rb +++ b/app/forms/organizations/create_form.rb @@ -1,12 +1,12 @@ class Organizations::CreateForm < BaseForm - NAME_REGEX = /^(?!_)(?!.*?_$)[a-zA-Z0-9_-]+$/ #只含有数字、字母、下划线不能以下划线开头和结尾 + NAME_REGEX = /^[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*$/ #只含有数字、字母、下划线不能以下划线开头和结尾 attr_accessor :name, :description, :website, :location, :repo_admin_change_team_access, :visibility, :max_repo_creation, :nickname, :original_name validates :name, :nickname, :visibility, presence: true validates :name, :nickname, length: { maximum: 100 } validates :location, length: { maximum: 50 } validates :description, length: { maximum: 200 } - validates :name, format: { with: NAME_REGEX, multiline: true, message: "只能含有数字、字母、下划线且不能以下划线开头和结尾" } + validates :name, format: { with: NAME_REGEX, multiline: true, message: "只能以数字或字母开头,仅支持横杠、下划线、点三种符号,不允许符号连续排列,长度4-50个字符" } validate do check_name(name) unless name.blank? || name == original_name diff --git a/app/forms/projects/create_form.rb b/app/forms/projects/create_form.rb index c133175c2..8ef7befac 100644 --- a/app/forms/projects/create_form.rb +++ b/app/forms/projects/create_form.rb @@ -28,6 +28,10 @@ class Projects::CreateForm < BaseForm raise "ignore_id值无效." if ignore_id && Ignore.find_by(id: ignore_id).blank? end + def check_auto_init + raise "auto_init值无效." if ignore_id && license_id && !auto_init + end + def check_owner @project_owner = Owner.find_by(id: user_id) raise "user_id值无效." if user_id && @project_owner.blank? diff --git a/app/forms/projects/datasets/create_form.rb b/app/forms/projects/datasets/create_form.rb new file mode 100644 index 000000000..c812ee17e --- /dev/null +++ b/app/forms/projects/datasets/create_form.rb @@ -0,0 +1,15 @@ +class Projects::Datasets::CreateForm < BaseForm + attr_accessor :title, :description, :license_id, :paper_content + + + validates :title, presence: true, length: { maximum: 100 } + validates :description, presence: true, length: { maximum: 500 } + validates :paper_content, length: { maximum: 500 } + + validate :check_license + + def check_license + raise "license_id值无效. " if license_id && License.find_by(id: license_id).blank? + end + +end \ No newline at end of file diff --git a/app/helpers/action/node_helper.rb b/app/helpers/action/node_helper.rb new file mode 100644 index 000000000..05d08da2f --- /dev/null +++ b/app/helpers/action/node_helper.rb @@ -0,0 +1,2 @@ +module Action::NodeHelper +end 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/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/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b86226454..93e2bde76 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -67,6 +67,7 @@ module ProjectsHelper jianmu_devops_url: jianmu_devops_url, cloud_ide_saas_url: cloud_ide_saas_url(user), open_blockchain: Site.has_blockchain? && project.use_blockchain, + has_dataset: project.project_dataset.present?, ignore_id: project.ignore_id }).compact diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index f364c11ff..360f7d03f 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -132,6 +132,8 @@ module RepositoriesHelper src_regex_3 = /src= (.*?) / src_regex_4 = /src =(.*?) / src_regex_5 = /src =(.*?) / + href_regex = /href=\"(.*?)\"/ + href_regex_1 = /href=\'(.*?)\'/ ss_c = content.to_s.scan(s_regex_c) ss = content.to_s.scan(s_regex) ss_1 = content.to_s.scan(s_regex_1) @@ -142,7 +144,9 @@ module RepositoriesHelper ss_src_3 = content.to_s.scan(src_regex_3) ss_src_4 = content.to_s.scan(src_regex_4) ss_src_5 = content.to_s.scan(src_regex_5) - total_sources = {ss_c: ss_c,ss: ss, ss_1: ss_1, ss_2: ss_2, ss_src: ss_src, ss_src_1: ss_src_1, ss_src_2: ss_src_2, ss_src_3: ss_src_3, ss_src_4: ss_src_4, ss_src_5: ss_src_5} + ss_href = content.to_s.scan(href_regex) + ss_href_1 = content.to_s.scan(href_regex_1) + total_sources = {ss_c: ss_c,ss: ss, ss_1: ss_1, ss_2: ss_2, ss_src: ss_src, ss_src_1: ss_src_1, ss_src_2: ss_src_2, ss_src_3: ss_src_3, ss_src_4: ss_src_4, ss_src_5: ss_src_5, ss_href: ss_href, ss_href_1: ss_href_1} # total_sources.uniq! total_sources.except(:ss, :ss_c).each do |k, sources| sources.each do |s| @@ -153,6 +157,7 @@ module RepositoriesHelper ext = File.extname(s_content)[1..-1] ext = ext.split("?")[0] if ext.include?("?") if (image_type?(ext) || download_type(ext)) && !ext.blank? + s_content = s_content.starts_with?("/") ? s_content[1..-1] : s_content[0..-1] s_content = File.expand_path(s_content, file_path) s_content = s_content.split("#{Rails.root}/")[1] # content = content.gsub(s[0], "/#{s_content}") @@ -173,13 +178,17 @@ module RepositoriesHelper content = content.gsub("src=#{s[0]}", "src=\'#{s_content}\'") when 'ss_2' content = content.gsub(/]:#{s[0]}/, "]: #{s_content.to_s.gsub(" ","").gsub("\r", "")}") - else + when 'ss_href' + content = content.gsub("href=\"#{s[0]}\"", "href=\"#{s_content}\"") + when 'ss_href_1' + content = content.gsub("href=\'#{s[0]}\'", "href=\'#{s_content}\'") + else content = content.gsub("(#{s[0]})", "(#{s_content})") end else path = [owner&.login, repo&.identifier, 'tree', ref, file_path].join("/") s_content = File.expand_path(s_content, path) - s_content = s_content.split("#{Rails.root}/")[1] + s_content = s_content.split("#{Rails.root}")[1] case k.to_s when 'ss_src' content = content.gsub("src=\"#{s[0]}\"", "src=\"/#{s_content}\"") @@ -187,7 +196,11 @@ module RepositoriesHelper content = content.gsub("src=\'#{s[0]}\'", "src=\'/#{s_content}\'") when 'ss_2' content = content.gsub(/]:#{s[0]}/, "]: /#{s_content.to_s.gsub(" ","").gsub("\r", "")}") - else + when 'ss_href' + content = content.gsub("href=\"#{s[0]}\"", "href=\"#{s_content}\"") + when 'ss_href_1' + content = content.gsub("href=\'#{s[0]}\'", "href=\'#{s_content}\'") + else content = content.gsub("(#{s[0]})", "(/#{s_content})") end end diff --git a/app/interactors/gitea/delete_file_interactor.rb b/app/interactors/gitea/delete_file_interactor.rb index 03ddf4230..f94b8a205 100644 --- a/app/interactors/gitea/delete_file_interactor.rb +++ b/app/interactors/gitea/delete_file_interactor.rb @@ -45,6 +45,7 @@ module Gitea else Rails.logger.error("Gitea::Repository::Entries::DeleteService error[#{response.status}]======#{response.body}") @error = "删除失败,请确认该分支是否是保护分支。" + @error = "删除失败,参数sha不匹配。" if response.body.to_s.include?("sha does not match") end end diff --git a/app/jobs/daily_platform_statistics_job.rb b/app/jobs/daily_platform_statistics_job.rb new file mode 100644 index 000000000..5610f304b --- /dev/null +++ b/app/jobs/daily_platform_statistics_job.rb @@ -0,0 +1,44 @@ +# 按天获取百度统计数据,pv,访问,ip和来源分类占比 +# 其他统计:前一周用户留存率 +class DailyPlatformStatisticsJob < ApplicationJob + queue_as :default + + def perform(*args) + Rails.logger.info("*********开始统计*********") + + tongji_service = Baidu::TongjiService.new + access_token = tongji_service.access_token + Rails.logger.info "job baidu_tongji_auth access_token ===== #{access_token}" + ActiveJob::Base.logger.info "job baidu_tongji_auth access_token ===== #{access_token}" + # 从最后一个记录日期开始,如果遗漏日期数据可以补充数据 + last_date = DailyPlatformStatistic.order(:date).last + start_date = last_date.date + end_date = Time.now + if access_token.present? + tongji_service.overview_batch_add(start_date, end_date) + + # 本周访问来源占比,每天记录一次,如果遗漏日期数据可以补充数据 + tongji_service.source_from_batch_add(start_date, end_date) + end + # 周用户留存率 + pre_week_user_ids = User.where(created_on: pre_week).pluck(:id).uniq + weekly_keep_user_count = User.where(id: pre_week_user_ids).where(last_login_on: current_week).count + weekly_keep_rate = format("%.2f", pre_week_user_ids.size > 0 ? weekly_keep_user_count.to_f / pre_week_user_ids.size : 0) + + job_date = 1.days.ago + daily_statistic = DailyPlatformStatistic.find_or_initialize_by(date: job_date) + daily_statistic.weekly_keep_rate = weekly_keep_rate + daily_statistic.save + end + + private + + def current_week + Time.now.beginning_of_week..Time.now.end_of_day + end + + def pre_week + # 7.days.ago.beginning_of_week..7.days.ago.beginning_of_week.end_of_week + Time.now.prev_week..Time.now.prev_week.end_of_week + end +end diff --git a/app/jobs/daily_project_statistics_job.rb b/app/jobs/daily_project_statistics_job.rb index ad606f137..3672d1924 100644 --- a/app/jobs/daily_project_statistics_job.rb +++ b/app/jobs/daily_project_statistics_job.rb @@ -13,6 +13,7 @@ class DailyProjectStatisticsJob < ApplicationJob praises = result["praises"].to_i forks = result["forks"].to_i issues = result["issues"].to_i + closed_issues = result["closed_issues"].to_i pullrequests = result["pullrequests"].to_i commits = result["commits"].to_i score = visits *1 + watchers *5 + praises * 5 + forks * 10 + issues *5 + pullrequests * 10 + commits * 5 @@ -25,6 +26,7 @@ class DailyProjectStatisticsJob < ApplicationJob praises: praises, forks: forks, issues: issues, + closed_issues: closed_issues, pullrequests: pullrequests, commits: commits ) diff --git a/app/jobs/delay_expired_issue_and_milestone_job.rb b/app/jobs/delay_expired_issue_and_milestone_job.rb new file mode 100644 index 000000000..4bba7aaf1 --- /dev/null +++ b/app/jobs/delay_expired_issue_and_milestone_job.rb @@ -0,0 +1,16 @@ +class DelayExpiredIssueAndMilestoneJob < ApplicationJob + queue_as :message + + def perform + Issue.where(due_date: Date.today + 1.days).find_each do |issue| + SendTemplateMessageJob.perform_later('IssueExpire', issue.id) if Site.has_notice_menu? + end + Version.where(effective_date: Date.today + 1.days).find_each do |version| + SendTemplateMessageJob.perform_later('ProjectMilestoneEarlyExpired', version.id) if Site.has_notice_menu? + end + Version.where(effective_date: Date.today - 1.days).find_each do |version| + SendTemplateMessageJob.perform_later('ProjectMilestoneExpired', version.id) if Site.has_notice_menu? + end + end + +end \ No newline at end of file diff --git a/app/jobs/delay_expired_issue_job.rb b/app/jobs/delay_expired_issue_job.rb deleted file mode 100644 index ed390e15b..000000000 --- a/app/jobs/delay_expired_issue_job.rb +++ /dev/null @@ -1,10 +0,0 @@ -class DelayExpiredIssueJob < ApplicationJob - queue_as :message - - def perform - Issue.where(due_date: Date.today + 1.days).find_each do |issue| - SendTemplateMessageJob.perform_later('IssueExpire', issue.id) if Site.has_notice_menu? - end - end - -end \ No newline at end of file diff --git a/app/jobs/send_template_message_job.rb b/app/jobs/send_template_message_job.rb index 1df1d8c7d..1b309fe35 100644 --- a/app/jobs/send_template_message_job.rb +++ b/app/jobs/send_template_message_job.rb @@ -221,6 +221,20 @@ class SendTemplateMessageJob < ApplicationJob receivers_email_string, email_title, email_content = MessageTemplate::ProjectMilestone.get_email_message_content(receiver, operator, milestone) Notice::Write::EmailCreateService.call(receivers_email_string, email_title, email_content) end + when 'ProjectMilestoneExpired' + milestone_id = args[0] + milestone = Version.find_by_id(milestone_id) + return unless milestone.present? && milestone&.project.present? + receivers = User.where(id: milestone.user_id) + receivers_string, content, notification_url = MessageTemplate::ProjectMilestoneExpired.get_message_content(receivers, milestone) + Notice::Write::CreateService.call(receivers_string, content, notification_url, source, {milestone_id: milestone_id, operator_id: operator_id}) + when 'ProjectMilestoneEarlyExpired' + milestone_id = args[0] + milestone = Version.find_by_id(milestone_id) + return unless milestone.present? && milestone&.project.present? + receivers = User.where(id: milestone.user_id) + receivers_string, content, notification_url = MessageTemplate::ProjectMilestoneEarlyExpired.get_message_content(receivers, milestone) + Notice::Write::CreateService.call(receivers_string, content, notification_url, source, {milestone_id: milestone_id, operator_id: operator_id}) when 'ProjectPraised' operator_id, project_id = args[0], args[1] operator = User.find_by_id(operator_id) diff --git a/app/jobs/touch_sync_job.rb b/app/jobs/touch_sync_job.rb new file mode 100644 index 000000000..3beaf52e5 --- /dev/null +++ b/app/jobs/touch_sync_job.rb @@ -0,0 +1,24 @@ +class TouchSyncJob < ApplicationJob + queue_as :default + + def perform(touchable) + puts "aaaa" + case touchable.class.to_s + when 'SyncRepositories::Github' || 'SyncRepositories::Gitee' + Reposync::SyncRepoService.call(touchable.repo_name) + when 'SyncRepositoryBranch' + sync_repository = touchable.sync_repository + result = [] + if sync_repository.sync_direction == 1 + result = Reposync::SyncBranchService.call(sync_repository.repo_name, touchable.gitlink_branch_name, sync_repository.sync_direction) + else + result = Reposync::SyncBranchService.call(sync_repository.repo_name, touchable.external_branch_name, sync_repository.sync_direction) + end + if result.is_a?(Array) && result[0].to_i == 0 + touchable.update_attributes!({sync_status: 1, sync_time: Time.now}) + else + touchable.update_attributes!({sync_status: 2, sync_time: Time.now}) + end + end + end +end \ No newline at end of file diff --git a/app/libs/custom_regexp.rb b/app/libs/custom_regexp.rb index b735a631b..0a59d8748 100644 --- a/app/libs/custom_regexp.rb +++ b/app/libs/custom_regexp.rb @@ -9,7 +9,7 @@ module CustomRegexp URL = /\Ahttps?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]\z/ IP = /^((\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/ - URL_REGEX = /\A(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?\z/i + URL_REGEX = /\A(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?\z/i # REPOSITORY_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9\-\_\.]+[a-zA-Z0-9]$/ #只含有数字、字母、下划线不能以下划线开头和结尾 REPOSITORY_NAME_REGEX = /^[a-zA-Z0-9\-\_\.]+[a-zA-Z0-9]$/ #只含有数字、字母、下划线不能以下划线开头和结尾 MD_REGEX = /^.+(\.[m|M][d|D])$/ diff --git a/app/models/action/node.rb b/app/models/action/node.rb new file mode 100644 index 000000000..07d3be134 --- /dev/null +++ b/app/models/action/node.rb @@ -0,0 +1,64 @@ +# == Schema Information +# +# Table name: action_nodes +# +# id :integer not null, primary key +# name :string(255) +# full_name :string(255) +# description :string(255) +# icon :string(255) +# action_node_types_id :integer +# is_local :boolean default("0") +# local_url :string(255) +# yaml :text(65535) +# sort_no :integer default("0") +# use_count :integer default("0") +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# by_name (name) +# index_action_nodes_on_action_types_id (action_node_types_id) +# index_action_nodes_on_user_id (user_id) +# + +class Action::Node < ApplicationRecord + self.table_name = 'action_nodes' + default_scope { order(sort_no: :asc) } + + has_many :action_node_inputs, :class_name => 'Action::NodeInput', foreign_key: "action_nodes_id" + has_many :action_node_selects, :class_name => 'Action::NodeSelect', foreign_key: "action_nodes_id" + belongs_to :action_node_type, :class_name => 'Action::NodeType', foreign_key: "action_node_types_id" + + 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 + + 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 new file mode 100644 index 000000000..4f3825170 --- /dev/null +++ b/app/models/action/node_input.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: action_node_inputs +# +# id :integer not null, primary key +# action_nodes_id :integer +# name :string(255) +# input_type :string(255) +# description :string(255) +# is_required :boolean default("0") +# sort_no :string(255) default("0") +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_action_node_inputs_on_action_nodes_id (action_nodes_id) +# index_action_node_inputs_on_user_id (user_id) +# + +class Action::NodeInput < ApplicationRecord + self.table_name = 'action_node_inputs' + default_scope { order(sort_no: :asc) } + + belongs_to :action_node, :class_name => 'Action::Node', foreign_key: "action_nodes_id" +end diff --git a/app/models/action/node_select.rb b/app/models/action/node_select.rb new file mode 100644 index 000000000..25be51f99 --- /dev/null +++ b/app/models/action/node_select.rb @@ -0,0 +1,39 @@ +# == Schema Information +# +# Table name: action_node_selects +# +# id :integer not null, primary key +# action_nodes_id :integer +# name :string(255) +# val :string(255) +# val_ext :string(255) +# description :string(255) +# download_url :string(255) +# sort_no :integer default("0") +# use_count :integer default("0") +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_action_node_selects_on_action_nodes_id (action_nodes_id) +# index_action_node_selects_on_name (name) +# index_action_node_selects_on_user_id (user_id) +# + +class Action::NodeSelect < ApplicationRecord + self.table_name = 'action_node_selects' + default_scope { order(sort_no: :asc) } + + belongs_to :action_node, :class_name => 'Action::Node', foreign_key: "action_nodes_id" + belongs_to :user, optional: true + + def value + if self.val_ext.blank? + self.val + else + "#{self.val}@#{self.val_ext}" + end + end +end diff --git a/app/models/action/node_type.rb b/app/models/action/node_type.rb new file mode 100644 index 000000000..7ce78b0fb --- /dev/null +++ b/app/models/action/node_type.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: action_node_types +# +# id :integer not null, primary key +# name :string(255) +# description :string(255) +# sort_no :integer +# created_at :datetime not null +# updated_at :datetime not null +# + +class Action::NodeType < ApplicationRecord + self.table_name = 'action_node_types' + default_scope { order(sort_no: :asc) } + + has_many :action_nodes, :class_name => 'Action::Node', foreign_key: "action_node_types_id" +end diff --git a/app/models/action/pipeline.rb b/app/models/action/pipeline.rb new file mode 100644 index 000000000..1dfbd0f24 --- /dev/null +++ b/app/models/action/pipeline.rb @@ -0,0 +1,37 @@ +# == 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 + + +end diff --git a/app/models/action/template.rb b/app/models/action/template.rb new file mode 100644 index 000000000..34b669f66 --- /dev/null +++ b/app/models/action/template.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: action_templates +# +# id :integer not null, primary key +# name :string(255) +# description :string(255) +# img :string(255) +# sort_no :string(255) default("0") +# json :text(65535) +# yaml :text(65535) +# created_at :datetime not null +# updated_at :datetime not null +# + +class Action::Template < ApplicationRecord + self.table_name = 'action_templates' + default_scope { order(sort_no: :asc) } + +end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 9fd858bf3..be715921b 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -1,45 +1,46 @@ -# == Schema Information -# -# Table name: attachments -# -# id :integer not null, primary key -# container_id :integer -# container_type :string(30) -# filename :string(255) default(""), not null -# disk_filename :string(255) default(""), not null -# filesize :integer default("0"), not null -# content_type :string(255) default("") -# digest :string(60) default(""), not null -# downloads :integer default("0"), not null -# author_id :integer default("0"), not null -# created_on :datetime -# description :text(65535) -# disk_directory :string(255) -# attachtype :integer default("1") -# is_public :integer default("1") -# copy_from :integer -# quotes :integer default("0") -# is_publish :integer default("1") -# publish_time :datetime -# resource_bank_id :integer -# unified_setting :boolean default("1") -# 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 -# -# index_attachments_on_author_id (author_id) -# index_attachments_on_container_id_and_container_type (container_id,container_type) -# index_attachments_on_course_second_category_id (course_second_category_id) -# index_attachments_on_created_on (created_on) -# index_attachments_on_is_public (is_public) -# index_attachments_on_quotes (quotes) -# - +# == Schema Information +# +# Table name: attachments +# +# id :integer not null, primary key +# container_id :integer +# container_type :string(30) +# filename :string(255) default(""), not null +# disk_filename :string(255) default(""), not null +# filesize :integer default("0"), not null +# content_type :string(255) default("") +# digest :string(60) default(""), not null +# downloads :integer default("0"), not null +# author_id :integer default("0"), not null +# created_on :datetime +# description :text(65535) +# disk_directory :string(255) +# attachtype :integer default("1") +# is_public :integer default("1") +# copy_from :integer +# quotes :integer default("0") +# is_publish :integer default("1") +# publish_time :datetime +# resource_bank_id :integer +# unified_setting :boolean default("1") +# 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 +# +# index_attachments_on_author_id (author_id) +# index_attachments_on_container_id_and_container_type (container_id,container_type) +# index_attachments_on_course_second_category_id (course_second_category_id) +# index_attachments_on_created_on (created_on) +# index_attachments_on_is_public (is_public) +# index_attachments_on_quotes (quotes) +# index_attachments_on_uuid (uuid) +# + @@ -72,7 +73,7 @@ class Attachment < ApplicationRecord scope :unified_setting, -> {where("unified_setting = ? ", 1)} scope :where_id_or_uuid, -> (id) { (Float(id) rescue nil).present? ? where(id: id) : where(uuid: id) } - validates_length_of :description, maximum: 100, message: "不能超过100个字符" + validates_length_of :description, maximum: 255, message: "不能超过255个字符" 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) diff --git a/app/models/concerns/project_operable.rb b/app/models/concerns/project_operable.rb index 9a5efd129..34386aae2 100644 --- a/app/models/concerns/project_operable.rb +++ b/app/models/concerns/project_operable.rb @@ -21,68 +21,69 @@ module ProjectOperable end def add_member!(user_id, role_name='Developer') - if self.owner.is_a?(Organization) - case role_name - when 'Manager' - # 构建相应的团队 - team = self.owner.teams.admin.take - if team.nil? - team = Team.build(self.user_id, 'admin', '管理员', '', 'admin', false, false) - gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil - team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? - end - - # 设置项目在团队中的访问权限 - team_project = TeamProject.build(self.user_id, team.id, self.id) - tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil - - # 新增对应的团队成员 - team_user = TeamUser.build(self.user_id, user_id, team.id) - $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 - - # 确保组织成员中有该用户 - OrganizationUser.build(self.user_id, user_id) - when 'Developer' - # 构建相应的团队 - team = self.owner.teams.write.take - if team.nil? - team = Team.build(self.user_id, 'developer', '开发者', '', 'write', false, false) - gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil - team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? - end - - # 设置项目在团队中的访问权限 - team_project = TeamProject.build(self.user_id, team.id, self.id) - tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil - - # 新增对应的团队成员 - team_user = TeamUser.build(self.user_id, user_id, team.id) - $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 - - # 确保组织成员中有该用户 - OrganizationUser.build(self.user_id, user_id) - when 'Reporter' - # 构建相应的团队 - team = self.owner.teams.read.take - if team.nil? - team = Team.build(self.user_id, 'reporter', '报告者', '', 'read', false, false) - gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil - team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? - end - - # 设置项目在团队中的访问权限 - team_project = TeamProject.build(self.user_id, team.id, self.id) - tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil - - # 新增对应的团队成员 - team_user = TeamUser.build(self.user_id, user_id, team.id) - $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 - - # 确保组织成员中有该用户 - OrganizationUser.build(self.user_id, user_id) - end - end - member = members.create!(user_id: user_id, team_user_id: team_user&.id) + # if self.owner.is_a?(Organization) + # case role_name + # when 'Manager' + # # 构建相应的团队 + # team = self.owner.teams.admin.take + # if team.nil? + # team = Team.build(self.user_id, 'admin', '管理员', '', 'admin', false, false) + # gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil + # team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? + # end + # + # # 设置项目在团队中的访问权限 + # team_project = TeamProject.build(self.user_id, team.id, self.id) + # tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil + # + # # 新增对应的团队成员 + # team_user = TeamUser.build(self.user_id, user_id, team.id) + # $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 + # + # # 确保组织成员中有该用户 + # OrganizationUser.build(self.user_id, user_id) + # when 'Developer' + # # 构建相应的团队 + # team = self.owner.teams.write.take + # if team.nil? + # team = Team.build(self.user_id, 'developer', '开发者', '', 'write', false, false) + # gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil + # team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? + # end + # + # # 设置项目在团队中的访问权限 + # team_project = TeamProject.build(self.user_id, team.id, self.id) + # tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil + # + # # 新增对应的团队成员 + # team_user = TeamUser.build(self.user_id, user_id, team.id) + # $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 + # + # # 确保组织成员中有该用户 + # OrganizationUser.build(self.user_id, user_id) + # when 'Reporter' + # # 构建相应的团队 + # team = self.owner.teams.read.take + # if team.nil? + # team = Team.build(self.user_id, 'reporter', '报告者', '', 'read', false, false) + # gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil + # team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? + # end + # + # # 设置项目在团队中的访问权限 + # team_project = TeamProject.build(self.user_id, team.id, self.id) + # tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil + # + # # 新增对应的团队成员 + # team_user = TeamUser.build(self.user_id, user_id, team.id) + # $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 + # + # # 确保组织成员中有该用户 + # OrganizationUser.build(self.user_id, user_id) + # end + # end + # member = members.create!(user_id: user_id, team_user_id: team_user&.id) + member = members.create!(user_id: user_id) set_developer_role(member, role_name) end @@ -116,71 +117,71 @@ module ProjectOperable def change_member_role!(user_id, role) member = self.member(user_id) # 所有者为组织,并且该用户属于组织成员 - if self.owner.is_a?(Organization) && member.team_user.present? - case role&.name - when 'Manager' - # 构建相应的团队 - team = self.owner.teams.admin.take - if team.nil? - team = Team.build(self.user_id, 'admin', '管理员', '', 'admin', false, false) - gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil - team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? - end - - # 设置项目在团队中的访问权限 - team_project = TeamProject.build(self.user_id, team.id, self.id) - tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil - - # 更改对应的团队成员 - team_user = member.team_user - $gitea_client.delete_teams_members_by_id_username(team_user.team.gtid, team_user.user&.login) rescue nil # 移除旧的 - $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 - team_user.update_attributes!({team_id: team.id}) unless team.team_users.exists?(user_id: member.user_id) - - # 确保组织成员中有该用户 - OrganizationUser.build(self.user_id, user_id) - when 'Developer' - # 构建相应的团队 - team = self.owner.teams.write.take - if team.nil? - team = Team.build(self.user_id, 'developer', '开发者', '', 'write', false, false) - gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil - team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? - end - # 设置项目在团队中的访问权限 - team_project = TeamProject.build(self.user_id, team.id, self.id) - $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil - - # 更改对应的团队成员 - team_user = member.team_user - $gitea_client.delete_teams_members_by_id_username(team_user.team.gtid, team_user.user&.login) rescue nil # 移除旧的 - $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 - team_user.update_attributes!({team_id: team.id}) unless team.team_users.exists?(user_id: member.user_id) - - OrganizationUser.build(self.user_id, user_id) - when 'Reporter' - # 构建相应的团队 - team = self.owner.teams.read.take - if team.nil? - team = Team.build(self.user_id, 'reporter', '报告者', '', 'read', false, false) - gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil - team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? - end - - # 设置项目在团队中的访问权限 - team_project = TeamProject.build(self.user_id, team.id, self.id) - tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil - - # 更改对应的团队成员 - team_user = member.team_user - $gitea_client.delete_teams_members_by_id_username(team_user.team.gtid, team_user.user&.login) rescue nil # 移除旧的 - $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 - team_user.update_attributes!({team_id: team.id}) unless team.team_users.exists?(user_id: member.user_id) - - # 确保组织成员中有该用户 - OrganizationUser.build(self.user_id, user_id) - end - end + # if self.owner.is_a?(Organization) && member.team_user.present? + # case role&.name + # when 'Manager' + # # 构建相应的团队 + # team = self.owner.teams.admin.take + # if team.nil? + # team = Team.build(self.user_id, 'admin', '管理员', '', 'admin', false, false) + # gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil + # team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? + # end + # + # # 设置项目在团队中的访问权限 + # team_project = TeamProject.build(self.user_id, team.id, self.id) + # tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil + # + # # 更改对应的团队成员 + # team_user = member.team_user + # $gitea_client.delete_teams_members_by_id_username(team_user.team.gtid, team_user.user&.login) rescue nil # 移除旧的 + # $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 + # team_user.update_attributes!({team_id: team.id}) unless team.team_users.exists?(user_id: member.user_id) + # + # # 确保组织成员中有该用户 + # OrganizationUser.build(self.user_id, user_id) + # when 'Developer' + # # 构建相应的团队 + # team = self.owner.teams.write.take + # if team.nil? + # team = Team.build(self.user_id, 'developer', '开发者', '', 'write', false, false) + # gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil + # team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? + # end + # # 设置项目在团队中的访问权限 + # team_project = TeamProject.build(self.user_id, team.id, self.id) + # $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil + # + # # 更改对应的团队成员 + # team_user = member.team_user + # $gitea_client.delete_teams_members_by_id_username(team_user.team.gtid, team_user.user&.login) rescue nil # 移除旧的 + # $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 + # team_user.update_attributes!({team_id: team.id}) unless team.team_users.exists?(user_id: member.user_id) + # + # OrganizationUser.build(self.user_id, user_id) + # when 'Reporter' + # # 构建相应的团队 + # team = self.owner.teams.read.take + # if team.nil? + # team = Team.build(self.user_id, 'reporter', '报告者', '', 'read', false, false) + # gteam = $gitea_client.post_orgs_teams_by_org(self.owner.login, {body: team.to_gitea_hash.to_json}) rescue nil + # team.update_attributes!({gtid: gteam["id"]}) unless gteam.nil? + # end + # + # # 设置项目在团队中的访问权限 + # team_project = TeamProject.build(self.user_id, team.id, self.id) + # tp_result = $gitea_client.put_teams_repos_by_id_org_repo(team.gtid, self.owner.login, self.identifier) rescue nil + # + # # 更改对应的团队成员 + # team_user = member.team_user + # $gitea_client.delete_teams_members_by_id_username(team_user.team.gtid, team_user.user&.login) rescue nil # 移除旧的 + # $gitea_client.put_teams_members_by_id_username(team&.gtid, team_user.user&.login) rescue nil # 新增新的 + # team_user.update_attributes!({team_id: team.id}) unless team.team_users.exists?(user_id: member.user_id) + # + # # 确保组织成员中有该用户 + # OrganizationUser.build(self.user_id, user_id) + # end + # end member.member_roles.last.update_attributes!(role: role) end diff --git a/app/models/daily_platform_statistic.rb b/app/models/daily_platform_statistic.rb new file mode 100644 index 000000000..a904d9b1e --- /dev/null +++ b/app/models/daily_platform_statistic.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: daily_platform_statistics +# +# id :integer not null, primary key +# date :date +# pv :integer default("0") +# visitor :integer default("0") +# ip :integer default("0") +# weekly_keep_rate :float(24) default("0") +# source_through :float(24) default("0") +# source_link :float(24) default("0") +# source_search :float(24) default("0") +# source_custom :float(24) default("0") +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_daily_platform_statistics_on_date (date) UNIQUE +# + +class DailyPlatformStatistic < ApplicationRecord +end diff --git a/app/models/daily_project_statistic.rb b/app/models/daily_project_statistic.rb index f7fc0aad7..ffe51bdb3 100644 --- a/app/models/daily_project_statistic.rb +++ b/app/models/daily_project_statistic.rb @@ -2,18 +2,20 @@ # # Table name: daily_project_statistics # -# id :integer not null, primary key -# project_id :integer -# date :string(255) -# visits :integer default("0") -# watchers :integer default("0") -# praises :integer default("0") -# forks :integer default("0") -# issues :integer default("0") -# pullrequests :integer default("0") -# commits :integer default("0") -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# project_id :integer +# date :date +# score :integer default("0") +# visits :integer default("0") +# watchers :integer default("0") +# praises :integer default("0") +# forks :integer default("0") +# issues :integer default("0") +# pullrequests :integer default("0") +# commits :integer default("0") +# created_at :datetime not null +# updated_at :datetime not null +# closed_issues :integer default("0") # # Indexes # diff --git a/app/models/gitlink_competition_apply.rb b/app/models/gitlink_competition_apply.rb new file mode 100644 index 000000000..f3b7d4ce1 --- /dev/null +++ b/app/models/gitlink_competition_apply.rb @@ -0,0 +1,21 @@ + # == 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) +# login :string(255) +# nickname :string(255) +# phone :string(255) +# identity :string(255) +# role :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# + +class GitlinkCompetitionApply < ApplicationRecord +end diff --git a/app/models/issue.rb b/app/models/issue.rb index f96bac8fb..cb8b98cce 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,48 +83,98 @@ 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 :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 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' + end + + def incre_or_decre_closed_issues_count + if previous_changes[:status_id].present? && is_issuely_issue? + if previous_changes[:status_id][1] == 5 + CacheAsyncSetJob.perform_later("project_common_service", {closed_issues: 1}, self.project_id) + end + if previous_changes[:status_id][0] == 5 + CacheAsyncSetJob.perform_later("project_common_service", {closed_issues: -1}, self.project_id) + end + end + end def incre_project_common - CacheAsyncSetJob.perform_later("project_common_service", {issues: 1}, self.project_id) + 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) + 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) + 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) + 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}) + 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}) + CacheAsyncSetJob.perform_later('platform_statistic_service', {issue_count: -1}) if is_issuely_issue? end def get_assign_user @@ -132,20 +183,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 @@ -153,11 +204,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 @@ -177,20 +228,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 @@ -220,7 +280,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 @@ -252,8 +312,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? @@ -275,4 +335,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/message_template.rb b/app/models/message_template.rb index 7ab7fdad8..75d64fa95 100644 --- a/app/models/message_template.rb +++ b/app/models/message_template.rb @@ -52,6 +52,8 @@ class MessageTemplate < ApplicationRecord self.create(type: 'MessageTemplate::ProjectMilestone', sys_notice: '{nickname1}在 {nickname2}/{repository} 创建了一个里程碑:{name}', notification_url: '{baseurl}/{owner}/{identifier}/milestones/{id}', email: email_html, email_title: "#{PLATFORM}: {nickname1} 在 {nickname2}/{repository} 新建了一个里程碑") email_html = File.read("#{email_template_html_dir}/project_milestone_completed.html") self.create(type: 'MessageTemplate::ProjectMilestoneCompleted', sys_notice: '在 {nickname}/{repository} 仓库,里程碑 {name} 的完成度已达到100%', notification_url: '{baseurl}/{owner}/{identifier}/milestones/{id}', email: email_html, email_title: "#{PLATFORM}: 仓库 {nickname}/{repository} 有里程碑已完成") + self.create(type: 'MessageTemplate::ProjectMilestoneEarlyExpired', sys_notice: '您创建的里程碑 {name} 已临近截止日期,请尽快处理.', notification_url: '{baseurl}/{owner}/{identifier}/milestones/{id}') + self.create(type: 'MessageTemplate::ProjectMilestoneExpired', sys_notice: '您创建的里程碑 {name} 已逾期,请及时更新进度或联系项目团队.', notification_url: '{baseurl}/{owner}/{identifier}/milestones/{id}') self.create(type: 'MessageTemplate::ProjectPraised', sys_notice: '{nickname1} 点赞了你管理的仓库 {nickname2}/{repository}', notification_url: '{baseurl}/{login}') self.create(type: 'MessageTemplate::ProjectOpenDevOps', sys_notice: '您的仓库 {repository} 已成功开通引擎服务,可通过简单的节点编排完成自动化集成与部署。欢迎体验!', notification_url: '{baseurl}/{owner}/{identifier}/devops') email_html = File.read("#{email_template_html_dir}/project_pull_request.html") diff --git a/app/models/message_template/project_milestone_early_expired.rb b/app/models/message_template/project_milestone_early_expired.rb new file mode 100644 index 000000000..3db1e1908 --- /dev/null +++ b/app/models/message_template/project_milestone_early_expired.rb @@ -0,0 +1,70 @@ +# == Schema Information +# +# Table name: message_templates +# +# id :integer not null, primary key +# type :string(255) +# sys_notice :text(65535) +# email :text(65535) +# created_at :datetime not null +# updated_at :datetime not null +# notification_url :string(255) +# email_title :string(255) +# + +# 我管理的仓库有里程碑完成 +class MessageTemplate::ProjectMilestoneEarlyExpired < MessageTemplate + + # MessageTemplate::ProjectMilestoneEarlyExpired.get_message_content(User.where(login: 'yystopf'), Version.find(7)) + def self.get_message_content(receivers, milestone) + receivers.each do |receiver| + if receiver.user_template_message_setting.present? + send_setting = receiver.user_template_message_setting.notification_body["ManageProject::MilestoneExpired"] + send_setting = send_setting.nil? ? UserTemplateMessageSetting.init_notification_body["ManageProject::MilestoneExpired"] : send_setting + receivers = receivers.where.not(id: receiver.id) unless send_setting + end + end + return '', '', '' if receivers.blank? + project = milestone&.project + owner = project&.owner + content = sys_notice.gsub('{nickname}', owner&.real_name).gsub('{repository}', project&.name).gsub('{name}', milestone&.name) + url = notification_url.gsub('{owner}', owner&.login).gsub('{identifier}', project&.identifier).gsub('{id}', milestone&.id.to_s) + + return receivers_string(receivers), content, url + rescue => e + Rails.logger.info("MessageTemplate::MilestoneEarlyExpired.get_message_content [ERROR] #{e}") + return '', '', '' + end + + def self.get_email_message_content(receiver, milestone) + if receiver.user_template_message_setting.present? + send_setting = receiver.user_template_message_setting.email_body["ManageProject::MilestoneExpired"] + send_setting = send_setting.nil? ? UserTemplateMessageSetting.init_email_body["ManageProject::MilestoneExpired"] : send_setting + return '', '', '' unless send_setting + project = milestone&.project + owner = project&.owner + title = email_title + title.gsub!('{nickname}', owner&.real_name) + title.gsub!('{repository}', project&.name) + + content = email + content.gsub!('{receiver}', receiver&.real_name) + content.gsub!('{baseurl}', base_url) + content.gsub!('{nickname}', owner&.real_name) + content.gsub!('{repository}', project&.name) + content.gsub!('{login}', owner&.login) + content.gsub!('{identifier}', project&.identifier) + content.gsub!('{id}', milestone&.id.to_s) + content.gsub!('{name}', milestone&.name) + content.gsub!('{platform}', PLATFORM) + + return receiver&.mail, title, content + else + return '', '', '' + end + + rescue => e + Rails.logger.info("MessageTemplate::MilestoneEarlyExpired.get_email_message_content [ERROR] #{e}") + return '', '', '' + end +end diff --git a/app/models/message_template/project_milestone_expired.rb b/app/models/message_template/project_milestone_expired.rb new file mode 100644 index 000000000..de6b8c10c --- /dev/null +++ b/app/models/message_template/project_milestone_expired.rb @@ -0,0 +1,70 @@ +# == Schema Information +# +# Table name: message_templates +# +# id :integer not null, primary key +# type :string(255) +# sys_notice :text(65535) +# email :text(65535) +# created_at :datetime not null +# updated_at :datetime not null +# notification_url :string(255) +# email_title :string(255) +# + +# 我管理的仓库有里程碑完成 +class MessageTemplate::ProjectMilestoneExpired < MessageTemplate + + # MessageTemplate::ProjectMilestoneExpired.get_message_content(User.where(login: 'yystopf'), Version.find(7)) + def self.get_message_content(receivers, milestone) + receivers.each do |receiver| + if receiver.user_template_message_setting.present? + send_setting = receiver.user_template_message_setting.notification_body["ManageProject::MilestoneExpired"] + send_setting = send_setting.nil? ? UserTemplateMessageSetting.init_notification_body["ManageProject::MilestoneExpired"] : send_setting + receivers = receivers.where.not(id: receiver.id) unless send_setting + end + end + return '', '', '' if receivers.blank? + project = milestone&.project + owner = project&.owner + content = sys_notice.gsub('{nickname}', owner&.real_name).gsub('{repository}', project&.name).gsub('{name}', milestone&.name) + url = notification_url.gsub('{owner}', owner&.login).gsub('{identifier}', project&.identifier).gsub('{id}', milestone&.id.to_s) + + return receivers_string(receivers), content, url + rescue => e + Rails.logger.info("MessageTemplate::ProjectMilestoneExpired.get_message_content [ERROR] #{e}") + return '', '', '' + end + + def self.get_email_message_content(receiver, milestone) + if receiver.user_template_message_setting.present? + send_setting = receiver.user_template_message_setting.email_body["ManageProject::MilestoneExpired"] + send_setting = send_setting.nil? ? UserTemplateMessageSetting.init_email_body["ManageProject::MilestoneExpired"] : send_setting + return '', '', '' unless send_setting + project = milestone&.project + owner = project&.owner + title = email_title + title.gsub!('{nickname}', owner&.real_name) + title.gsub!('{repository}', project&.name) + + content = email + content.gsub!('{receiver}', receiver&.real_name) + content.gsub!('{baseurl}', base_url) + content.gsub!('{nickname}', owner&.real_name) + content.gsub!('{repository}', project&.name) + content.gsub!('{login}', owner&.login) + content.gsub!('{identifier}', project&.identifier) + content.gsub!('{id}', milestone&.id.to_s) + content.gsub!('{name}', milestone&.name) + content.gsub!('{platform}', PLATFORM) + + return receiver&.mail, title, content + else + return '', '', '' + end + + rescue => e + Rails.logger.info("MessageTemplate::ProjectMilestoneExpired.get_email_message_content [ERROR] #{e}") + return '', '', '' + end +end diff --git a/app/models/open_users/acge.rb b/app/models/open_users/acge.rb new file mode 100644 index 000000000..59963b91f --- /dev/null +++ b/app/models/open_users/acge.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: open_users +# +# id :integer not null, primary key +# user_id :integer +# type :string(255) +# uid :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# extra :text(65535) +# +# Indexes +# +# index_open_users_on_type_and_uid (type,uid) UNIQUE +# index_open_users_on_user_id (user_id) +# + +class OpenUsers::Acge < OpenUser + def nickname + extra&.[]('nickname') + end + + def en_type + 'acge' + end +end diff --git a/app/models/organization.rb b/app/models/organization.rb index cb829626e..f978611b1 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -64,7 +64,7 @@ class Organization < Owner alias_attribute :name, :login - NAME_REGEX = /^(?!_)(?!.*?_$)[a-zA-Z0-9_-]+$/ #只含有数字、字母、下划线不能以下划线开头和结尾 + NAME_REGEX = /^[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*$/ #只含有数字、字母、下划线不能以下划线开头和结尾 default_scope { where(type: "Organization") } @@ -79,7 +79,7 @@ class Organization < Owner validates :login, presence: true validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, case_sensitive: false - validates :login, format: { with: NAME_REGEX, multiline: true, message: "只能含有数字、字母、下划线且不能以下划线开头和结尾" } + validates :login, format: { with: NAME_REGEX, multiline: true, message: "只能以数字或字母开头,仅支持横杠、下划线、点三种符号,不允许符号连续排列,长度4-50个字符" } delegate :description, :website, :location, :repo_admin_change_team_access, :recommend, :visibility, :max_repo_creation, :num_projects, :num_users, :num_teams, @@ -182,14 +182,6 @@ class Organization < Owner organization_users.count end - def teams_count - teams.count - end - - def organization_users_count - organization_users.count - end - def real_name name = lastname + firstname name = name.blank? ? (nickname.blank? ? login : nickname) : name @@ -217,4 +209,11 @@ class Organization < Owner enabling_cla == true end + def num_users + organization_user_ids = self.organization_users.pluck(:user_id).uniq + project_member_user_ids = self.projects.joins(:members).pluck("members.user_id").uniq + ids = organization_user_ids + project_member_user_ids + ids.uniq.size + end + end diff --git a/app/models/page.rb b/app/models/page.rb index 1c606760e..4b55c99b6 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -28,7 +28,7 @@ class Page < ApplicationRecord belongs_to :project # language_frame 前端语言框架 - enum language_frame: { hugo: 0, jekyll: 1, hexo: 2} + enum language_frame: { hugo: 0, jekyll: 1, hexo: 2, files: 3} after_create do PageService.genernate_user(user_id) diff --git a/app/models/page_theme.rb b/app/models/page_theme.rb index bce3d5f70..e5830d06c 100644 --- a/app/models/page_theme.rb +++ b/app/models/page_theme.rb @@ -13,7 +13,7 @@ # class PageTheme < ApplicationRecord - enum language_frame: { hugo: 0, jeklly: 1, hexo: 2} + enum language_frame: { hugo: 0, jeklly: 1, hexo: 2, files:3} validates :name, presence: {message: "主题名不能为空"}, uniqueness: {message: "主题名已存在",scope: :language_frame},length: {maximum: 255} def image diff --git a/app/models/pm_link.rb b/app/models/pm_link.rb new file mode 100644 index 000000000..91962bf7b --- /dev/null +++ b/app/models/pm_link.rb @@ -0,0 +1,25 @@ +# == 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 + + 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 34e981508..245537408 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -55,14 +55,13 @@ # default_branch :string(255) default("master") # website :string(255) # lesson_url :string(255) +# use_blockchain :boolean default("0") # is_pinned :boolean default("0") # recommend_index :integer default("0") -# use_blockchain :boolean default("0") # pr_view_admin :boolean default("0") # # Indexes # -# index_projects_on_forked_count (forked_count) # index_projects_on_forked_from_project_id (forked_from_project_id) # index_projects_on_identifier (identifier) # index_projects_on_invite_code (invite_code) @@ -72,7 +71,6 @@ # index_projects_on_license_id (license_id) # index_projects_on_name (name) # index_projects_on_platform (platform) -# index_projects_on_praises_count (praises_count) # index_projects_on_project_category_id (project_category_id) # index_projects_on_project_language_id (project_language_id) # index_projects_on_project_type (project_type) @@ -80,7 +78,6 @@ # index_projects_on_rgt (rgt) # index_projects_on_status (status) # index_projects_on_updated_on (updated_on) -# index_projects_on_user_id (user_id) # class Project < ApplicationRecord @@ -90,6 +87,8 @@ class Project < ApplicationRecord include ProjectOperable include Dcodes + default_scope {where.not(id: 0)} + # common:开源托管项目 # mirror:普通镜像项目,没有定时同步功能 # sync_mirror:同步镜像项目,有系统定时同步功能,且用户可手动同步操作 @@ -137,6 +136,8 @@ class Project < ApplicationRecord has_many :project_topics, through: :project_topic_ralates has_many :commit_logs, dependent: :destroy has_many :daily_project_statistics, dependent: :destroy + has_one :project_dataset, dependent: :destroy + has_many :sync_repositories, dependent: :destroy after_create :incre_user_statistic, :incre_platform_statistic after_save :check_project_members before_save :set_invite_code, :reset_unmember_followed, :set_recommend_and_is_pinned, :reset_cache_data @@ -486,6 +487,11 @@ class Project < ApplicationRecord return JSON.parse(cache_result) end end + + def full_url + Rails.application.config_for(:configuration)['platform_url'].to_s + '/' + self.owner&.try(:login).to_s + '/' + self.identifier.to_s + end + def to_builder Jbuilder.new do |project| project.id self.id diff --git a/app/models/project_dataset.rb b/app/models/project_dataset.rb new file mode 100644 index 000000000..ae4bb5789 --- /dev/null +++ b/app/models/project_dataset.rb @@ -0,0 +1,26 @@ +# == Schema Information +# +# Table name: project_datasets +# +# id :integer not null, primary key +# title :string(255) +# description :text(65535) +# project_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# license_id :integer +# paper_content :text(65535) +# +# Indexes +# +# index_project_datasets_on_license_id (license_id) +# index_project_datasets_on_project_id (project_id) +# + +class ProjectDataset < ApplicationRecord + + belongs_to :project + belongs_to :license, optional: true + has_many :attachments, as: :container, dependent: :destroy + +end diff --git a/app/models/project_unit.rb b/app/models/project_unit.rb index 93e3668eb..d2b6d2085 100644 --- a/app/models/project_unit.rb +++ b/app/models/project_unit.rb @@ -17,12 +17,13 @@ class ProjectUnit < ApplicationRecord belongs_to :project - enum unit_type: {code: 1, issues: 2, pulls: 3, wiki:4, devops: 5, versions: 6, resources: 7, services: 8} + enum unit_type: {code: 1, issues: 2, pulls: 3, wiki:4, devops: 5, versions: 6, resources: 7, services: 8, dataset: 9} validates :unit_type, uniqueness: { scope: :project_id} def self.init_types(project_id, project_type='common') unit_types = project_type == 'sync_mirror' ? ProjectUnit::unit_types.except("pulls") : ProjectUnit::unit_types + unit_types = unit_types.except("dataset") unit_types.each do |_, v| self.create!(project_id: project_id, unit_type: v) end diff --git a/app/models/sync_repositories/gitee.rb b/app/models/sync_repositories/gitee.rb new file mode 100644 index 000000000..0a51b21c8 --- /dev/null +++ b/app/models/sync_repositories/gitee.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: sync_repositories +# +# id :integer not null, primary key +# project_id :integer +# type :string(255) +# repo_name :string(255) +# external_repo_address :string(255) +# sync_granularity :integer +# sync_direction :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_sync_repositories_on_project_id (project_id) +# + +class SyncRepositories::Gitee < SyncRepository + +end diff --git a/app/models/sync_repositories/github.rb b/app/models/sync_repositories/github.rb new file mode 100644 index 000000000..1ef413a54 --- /dev/null +++ b/app/models/sync_repositories/github.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: sync_repositories +# +# id :integer not null, primary key +# project_id :integer +# type :string(255) +# repo_name :string(255) +# external_repo_address :string(255) +# sync_granularity :integer +# sync_direction :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_sync_repositories_on_project_id (project_id) +# + +class SyncRepositories::Github < SyncRepository +end diff --git a/app/models/sync_repository.rb b/app/models/sync_repository.rb new file mode 100644 index 000000000..94f7c6630 --- /dev/null +++ b/app/models/sync_repository.rb @@ -0,0 +1,35 @@ +# == Schema Information +# +# Table name: sync_repositories +# +# id :integer not null, primary key +# project_id :integer +# type :string(255) +# repo_name :string(255) +# external_repo_address :string(255) +# sync_granularity :integer +# sync_direction :integer +# created_at :datetime not null +# updated_at :datetime not null +# external_token :string(255) +# webhook_gid :integer +# +# Indexes +# +# index_sync_repositories_on_project_id (project_id) +# + +class SyncRepository < ApplicationRecord + + belongs_to :project + has_many :sync_repository_branches, dependent: :destroy + + before_destroy :unbind_reposyncer + + validates :repo_name, uniqueness: { message: "已存在" } + + def unbind_reposyncer + Reposync::DeleteRepoService.call(self.repo_name) rescue nil + end + +end diff --git a/app/models/sync_repository_branch.rb b/app/models/sync_repository_branch.rb new file mode 100644 index 000000000..1940fd116 --- /dev/null +++ b/app/models/sync_repository_branch.rb @@ -0,0 +1,38 @@ +# == Schema Information +# +# Table name: sync_repository_branches +# +# id :integer not null, primary key +# sync_repository_id :integer +# gitlink_branch_name :string(255) +# external_branch_name :string(255) +# sync_time :datetime +# sync_status :integer default("0") +# reposync_branch_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# enable :boolean default("1") +# +# Indexes +# +# index_sync_repository_branches_on_sync_repository_id (sync_repository_id) +# + +class SyncRepositoryBranch < ApplicationRecord + + belongs_to :sync_repository + + before_destroy :unbind_reposyncer + + enum sync_status: {success: 1, failure: 2} + + + def unbind_reposyncer + if self.sync_repository.sync_direction.to_i == 1 + Reposync::DeleteBranchService.call(self.sync_repository&.repo_name, self.gitlink_branch_name) rescue nil + else + Reposync::DeleteBranchService.call(self.sync_repository&.repo_name, self.external_branch_name) rescue nil + end + end + +end diff --git a/app/models/template_message_setting/create_or_assign.rb b/app/models/template_message_setting/create_or_assign.rb index 629051305..8f0817070 100644 --- a/app/models/template_message_setting/create_or_assign.rb +++ b/app/models/template_message_setting/create_or_assign.rb @@ -28,5 +28,6 @@ class TemplateMessageSetting::CreateOrAssign < TemplateMessageSetting self.find_or_create_by(name: "疑修状态变更", key: "IssueChanged") self.find_or_create_by(name: "合并请求状态变更", key: "PullRequestChanged") self.find_or_create_by(name: "疑修截止日期到达最后一天", key: "IssueExpire", notification_disabled: false) + self.find_or_create_by(name: "里程碑逾期提醒", key: "MilestoneExpired", notification_disabled: false, email_disabled: true) end end diff --git a/app/models/token.rb b/app/models/token.rb index fac516eb8..7d65f32a3 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,19 +1,19 @@ -# == Schema Information -# -# Table name: tokens -# -# id :integer not null, primary key -# user_id :integer default("0"), not null -# action :string(30) default(""), not null -# value :string(40) default(""), not null -# created_on :datetime not null -# -# Indexes -# -# index_tokens_on_user_id (user_id) -# tokens_value (value) UNIQUE -# - +# == Schema Information +# +# Table name: tokens +# +# id :integer not null, primary key +# user_id :integer default("0"), not null +# action :string(30) default(""), not null +# value :string(40) default(""), not null +# created_on :datetime not null +# +# Indexes +# +# index_tokens_on_user_id (user_id) +# tokens_value (value) UNIQUE +# + # # This program is free software; you can redistribute it and/or @@ -44,7 +44,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 +117,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.rb b/app/models/user.rb index 8ffa5b181..9d623f949 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -115,7 +115,7 @@ class User < Owner # trustie: 来自Trustie平台 # forge: 平台本身注册的用户 # military: 军科的用户 - enumerize :platform, in: [:forge, :educoder, :trustie, :military, :github, :gitee, :qq, :wechat, :bot], default: :forge, scope: :shallow + enumerize :platform, in: [:forge, :educoder, :trustie, :military, :github, :gitee, :qq, :wechat, :bot, :acge], default: :forge, scope: :shallow belongs_to :laboratory, optional: true has_one :user_extension, dependent: :destroy @@ -465,7 +465,7 @@ class User < Owner $gitea_client.delete_users_tokens_by_username_token(self.login, e["name"], {query: {sudo: self.login} }) } end - new_result = $gitea_client.post_users_tokens_by_username(self.login, { query: {sudo: self.login}, body:{ name: self.login} }) + new_result = $gitea_client.post_users_tokens_by_username(self.login, { query: {sudo: self.login}, body:{ name: "#{self.login}-#{SecureRandom.hex(6)}", scopes: ["all"]}.to_json }) if new_result["sha1"].present? update(gitea_token: new_result["sha1"]) end diff --git a/app/models/user_template_message_setting.rb b/app/models/user_template_message_setting.rb index 49db51d4d..befd2be87 100644 --- a/app/models/user_template_message_setting.rb +++ b/app/models/user_template_message_setting.rb @@ -36,6 +36,7 @@ class UserTemplateMessageSetting < ApplicationRecord "CreateOrAssign::IssueChanged": true, "CreateOrAssign::PullRequestChanged": true, "CreateOrAssign::IssueExpire": true, + "CreateOrAssign::MilestoneExpired": true, "ManageProject::Issue": true, "ManageProject::PullRequest": true, "ManageProject::Member": true, @@ -44,6 +45,8 @@ class UserTemplateMessageSetting < ApplicationRecord "ManageProject::Forked": true, "ManageProject::Milestone": true, "ManageProject::MilestoneCompleted": true, + "ManageProject::MilestoneExpired": true, + "ManageProject::MilestoneEarlyExpired": true, }.stringify_keys! end @@ -57,6 +60,7 @@ class UserTemplateMessageSetting < ApplicationRecord "CreateOrAssign::IssueChanged": false, "CreateOrAssign::PullRequestChanged": false, "CreateOrAssign::IssueExpire": false, + "CreateOrAssign::MilestoneExpired": false, "ManageProject::Issue": false, "ManageProject::PullRequest": false, "ManageProject::Member": false, @@ -65,6 +69,8 @@ class UserTemplateMessageSetting < ApplicationRecord "ManageProject::Forked": false, "ManageProject::Milestone": false, "ManageProject::MilestoneCompleted": false, + "ManageProject::MilestoneExpired": false, + "ManageProject::MilestoneEarlyExpired": false, }.stringify_keys! end diff --git a/app/models/version.rb b/app/models/version.rb index ec2ad68cd..871f63dfd 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -68,5 +68,7 @@ class Version < ApplicationRecord def send_update_message_to_notice_system SendTemplateMessageJob.perform_later('ProjectMilestoneCompleted', self.id) if Site.has_notice_menu? && self.issue_percent == 1.0 + SendTemplateMessageJob.perform_later('ProjectMilestoneEarlyExpired', self.id) if Site.has_notice_menu? && self.effective_date == Date.today + 1.days + SendTemplateMessageJob.perform_later('ProjectMilestoneExpired', self.id) if Site.has_notice_menu? && self.effective_date == Date.today - 1.days end end 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..3f92963a3 --- /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.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/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..02b2b533c 100644 --- a/app/services/api/v1/issues/delete_service.rb +++ b/app/services/api/v1/issues/delete_service.rb @@ -19,11 +19,11 @@ class Api::V1::Issues::DeleteService < ApplicationService 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 +37,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..c27dcd29f 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] , 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.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 d55c0f586..895c98c5c 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,37 @@ 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? + @updated_issue.pm_issue_type = @pm_issue_type unless @pm_issue_type.nil? + @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 @@ -135,7 +152,7 @@ class Api::V1::Issues::UpdateService < ApplicationService 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", "description").symbolize_keys) + @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 diff --git a/app/services/api/v1/projects/actions/runs/job_show_service.rb b/app/services/api/v1/projects/actions/runs/job_show_service.rb new file mode 100644 index 000000000..e80e882be --- /dev/null +++ b/app/services/api/v1/projects/actions/runs/job_show_service.rb @@ -0,0 +1,42 @@ +class Api::V1::Projects::Actions::Runs::JobShowService < ApplicationService + include ActiveModel::Model + + attr_reader :project, :token, :owner, :repo, :run, :job, :log_cursors + attr_accessor :gitea_data + + validates :run, :job, :log_cursors, presence: true + + def initialize(project, run, job, log_cursors, token = nil) + @project = project + @owner = project&.owner.login + @repo = project&.identifier + @run = run + @job = job + @log_cursors = log_cursors + @token = token + end + + def call + raise Error, errors.full_messages.join(",") unless valid? + load_gitea_data + + @gitea_data + end + + private + def request_params + { + access_token: token + } + end + + def request_body + { + logCursors: log_cursors + } + end + + def load_gitea_data + @gitea_data = $gitea_hat_client.post_repos_actions_runs_jobs_by_owner_repo_run_job(owner, repo, run, job, {query: request_params, body: request_body.to_json}) + end +end \ No newline at end of file diff --git a/app/services/api/v1/projects/actions/runs/list_service.rb b/app/services/api/v1/projects/actions/runs/list_service.rb new file mode 100644 index 000000000..5889518ce --- /dev/null +++ b/app/services/api/v1/projects/actions/runs/list_service.rb @@ -0,0 +1,40 @@ +class Api::V1::Projects::Actions::Runs::ListService < ApplicationService + include ActiveModel::Model + + attr_reader :project, :token, :owner, :repo, :workflow, :page, :limit + attr_accessor :gitea_data + + validates :workflow, presence: true + + def initialize(project, params, token =nil) + @project = project + @owner = project&.owner.login + @repo = project&.identifier + @workflow = params[:workflow] + @page = params[:page] || 1 + @limit = params[:limit] || 15 + @token = token + end + + def call + raise Error, errors.full_messages.join(",") unless valid? + load_gitea_data + + @gitea_data + end + + private + def request_params + { + access_token: token, + workflow: workflow, + page: page, + limit: limit + } + end + + def load_gitea_data + @gitea_data = $gitea_hat_client.get_repos_actions_by_owner_repo(owner, repo, {query: request_params}) rescue nil + raise Error, '获取流水线执行记录失败!' unless @gitea_data.is_a?(Hash) + end +end \ No newline at end of file diff --git a/app/services/api/v1/projects/branches/delete_service.rb b/app/services/api/v1/projects/branches/delete_service.rb index 28836c797..341079273 100644 --- a/app/services/api/v1/projects/branches/delete_service.rb +++ b/app/services/api/v1/projects/branches/delete_service.rb @@ -32,7 +32,7 @@ class Api::V1::Projects::Branches::DeleteService < ApplicationService def excute_data_to_gitea begin - @gitea_data = $gitea_client.delete_repos_branches_by_owner_repo_branch(owner, repo, branch_name, {query: request_params}) + @gitea_data = $gitea_client.delete_repos_branches_by_owner_repo_branch(owner, repo, CGI.escape(branch_name), {query: request_params}) rescue => e raise Error, '保护分支无法删除!' if e.to_s.include?("branch protected") raise Error, '删除分支失败!' diff --git a/app/services/api/v1/projects/branches/list_service.rb b/app/services/api/v1/projects/branches/list_service.rb index 590c4884f..6980b71ea 100644 --- a/app/services/api/v1/projects/branches/list_service.rb +++ b/app/services/api/v1/projects/branches/list_service.rb @@ -1,6 +1,6 @@ class Api::V1::Projects::Branches::ListService < ApplicationService - attr_accessor :project, :token, :owner, :repo, :name, :page, :limit + attr_accessor :project, :token, :owner, :repo, :name, :state, :page, :limit attr_accessor :gitea_data, :gitea_repo_data def initialize(project, params, token=nil) @@ -9,6 +9,7 @@ class Api::V1::Projects::Branches::ListService < ApplicationService @repo = project&.identifier @token = token @name = params[:name] + @state = params[:state] @page = params[:page] @limit = params[:limit] end @@ -18,7 +19,6 @@ class Api::V1::Projects::Branches::ListService < ApplicationService load_default_branch @gitea_data[:default_branch] = @gitea_repo_data["default_branch"] - @gitea_data end @@ -30,7 +30,8 @@ class Api::V1::Projects::Branches::ListService < ApplicationService limit: limit } params.merge!({name: name}) if name.present? - + params.merge!({state: state}) if state.present? + params end diff --git a/app/services/api/v1/projects/branches/restore_service.rb b/app/services/api/v1/projects/branches/restore_service.rb new file mode 100644 index 000000000..fbd6220ed --- /dev/null +++ b/app/services/api/v1/projects/branches/restore_service.rb @@ -0,0 +1,47 @@ +class Api::V1::Projects::Branches::RestoreService < ApplicationService + + include ActiveModel::Model + + attr_accessor :project, :token, :owner, :repo, :branch_id, :branch_name + attr_accessor :gitea_data + + validates :branch_id, :branch_name, presence: true + + def initialize(project, branch_id, branch_name, token= nil) + @project = project + @owner = project&.owner&.login + @repo = project&.identifier + @branch_id = branch_id + @branch_name = branch_name + @token = token + end + + def call + raise Error, errors.full_messages.join(",") unless valid? + excute_data_to_gitea + + true + end + + private + def request_params + { + access_token: token + } + end + + def request_body + { + branch_id: branch_id, + name: branch_name, + } + end + + def excute_data_to_gitea + begin + @gitea_data = $gitea_hat_client.post_repos_branches_restore_by_owner_repo(owner, repo, {query: request_params, body: request_body.to_json}) + rescue => e + raise Error, '恢复分支失败!' + end + end +end \ No newline at end of file diff --git a/app/services/api/v1/projects/commits/recent_service.rb b/app/services/api/v1/projects/commits/recent_service.rb new file mode 100644 index 000000000..9a226d9f2 --- /dev/null +++ b/app/services/api/v1/projects/commits/recent_service.rb @@ -0,0 +1,44 @@ +class Api::V1::Projects::Commits::RecentService < ApplicationService + + attr_reader :project, :page, :limit, :keyword, :owner, :repo, :token + attr_accessor :gitea_data, :gitea_repo_detail + + def initialize(project, params, token=nil) + @project = project + @page = params[:page] || 1 + @limit = params[:limit] || 15 + @keyword = params[:keyword] + @owner = project&.owner&.login + @repo = project&.identifier + @token = token + end + + def call + load_gitea_data + load_gitea_repo_detail + + {result: gitea_data, detail:gitea_repo_detail} + end + + private + def request_params + param = { + access_token: token, + page: page, + limit: limit + } + param.merge!(keyword: keyword) if keyword.present? + + param + end + + def load_gitea_data + @gitea_data = $gitea_hat_client.get_repos_recent_commits_by_owner_repo(owner, repo, {query: request_params}) rescue nil + raise Error, "获取最近提交列表失败" unless @gitea_data.is_a?(Hash) + end + + def load_gitea_repo_detail + @gitea_repo_detail = $gitea_client.get_repos_by_owner_repo(owner, repo, {query: {access_token: token}}) + raise Error, "获取项目详情失败" unless @gitea_repo_detail.is_a?(Hash) + end +end \ No newline at end of file diff --git a/app/services/api/v1/projects/compare_service.rb b/app/services/api/v1/projects/compare_service.rb index 23a248305..9e646326f 100644 --- a/app/services/api/v1/projects/compare_service.rb +++ b/app/services/api/v1/projects/compare_service.rb @@ -29,6 +29,6 @@ class Api::V1::Projects::CompareService < ApplicationService end def load_gitea_data - @gitea_data = $gitea_client.get_repos_compare_by_owner_repo_from_to(owner, repo, from, to, {query: request_params}) rescue nil + @gitea_data = $gitea_hat_client.get_repos_compare_by_owner_repo_baseref_headref(owner, repo, to, from, {query: request_params}) rescue nil end end \ No newline at end of file diff --git a/app/services/api/v1/projects/sync_repositories/create_service.rb b/app/services/api/v1/projects/sync_repositories/create_service.rb new file mode 100644 index 000000000..4a0dc5091 --- /dev/null +++ b/app/services/api/v1/projects/sync_repositories/create_service.rb @@ -0,0 +1,109 @@ +class Api::V1::Projects::SyncRepositories::CreateService < ApplicationService + + include ActiveModel::Model + + attr_reader :project, :type, :external_token, :external_repo_address, :sync_granularity, :external_branch_name, :gitlink_branch_name, :first_sync_direction + attr_accessor :sync_repository1, :sync_repository2, :sync_repository_branch1, :sync_repository_branch2, :gitea_webhook + + validates :type, inclusion: {in: %w(SyncRepositories::Gitee SyncRepositories::Github)} + validates :external_repo_address, format: { with: CustomRegexp::URL_REGEX, multiline: true, message: "地址格式不正确" } + validates :sync_granularity, :first_sync_direction, inclusion: {in: [1,2]} + # validate :check_gitlink_branch_name + + def initialize(project, params) + @project = project + @type = params[:type] + @external_token = params[:external_token] + @external_repo_address = params[:external_repo_address] + @sync_granularity = params[:sync_granularity].to_i + @external_branch_name = params[:external_branch_name] + @gitlink_branch_name = params[:gitlink_branch_name] + @first_sync_direction = params[:first_sync_direction].to_i + end + + def call + raise Error, errors.full_messages.join(",") unless valid? + + create_webhook + if sync_granularity == 2 + # 创建两个不同方向的同步仓库 + create_sync_repository + # 创建两个不同方向的同步分支 + create_sync_repository_branch + # 第一次同步 + touch_first_sync_branch + else + create_sync_repository + touch_first_sync + end + + [@sync_repository1, @sync_repository2, @sync_repository_branch1, @sync_repository_branch2] + end + + # def check_gitlink_branch_name + # if sync_granularity == 2 + # result = $gitea_hat_client.get_repos_branch_name_set_by_owner_repo(project&.owner&.login, project&.identifier) rescue nil + # raise Error, '分支不存在' if !result.include?(gitlink_branch_name) + # end + # end + + private + def create_sync_repository + repository1 = Reposync::CreateSyncRepoService.call(repo_name(1), gitlink_repo_address, gitlink_token, external_repo_address, external_token, sync_granularity, 1) + repository2 = Reposync::CreateSyncRepoService.call(repo_name(2), gitlink_repo_address, gitlink_token, external_repo_address, external_token, sync_granularity, 2) + raise Error, '创建同步仓库失败' if repository1[0].to_i > 0 || repository2[0].to_i > 0 + @sync_repository1 = SyncRepository.create!(project: project, type: type, repo_name: repo_name(1), external_repo_address: external_repo_address, external_token: external_token, sync_granularity: sync_granularity, sync_direction: 1, webhook_gid: @gitea_webhook["id"]) + @sync_repository2 = SyncRepository.create!(project: project, type: type, repo_name: repo_name(2), external_repo_address: external_repo_address, external_token: external_token, sync_granularity: sync_granularity, sync_direction: 2, webhook_gid: @gitea_webhook["id"]) + end + + def create_sync_repository_branch + branch1 = Reposync::CreateSyncBranchService.call(repo_name(1),gitlink_branch_name, external_branch_name) + branch2 = Reposync::CreateSyncBranchService.call(repo_name(2),gitlink_branch_name, external_branch_name) + raise Error, '创建同步仓库分支失败' if branch1[0].to_i > 0 || branch2[0].to_i > 0 + @sync_repository_branch1 = SyncRepositoryBranch.create!(sync_repository: @sync_repository1, gitlink_branch_name: gitlink_branch_name, external_branch_name: external_branch_name, reposync_branch_id: branch1[1]["id"]) + @sync_repository_branch2 = SyncRepositoryBranch.create!(sync_repository: @sync_repository2, gitlink_branch_name: gitlink_branch_name, external_branch_name: external_branch_name, reposync_branch_id: branch2[1]["id"]) + end + + def touch_first_sync + first_sync_direction == 1 ? TouchSyncJob.perform_later(@sync_repository1) : TouchSyncJob.perform_later(@sync_repository2) + end + + def touch_first_sync_branch + first_sync_direction == 1 ? TouchSyncJob.perform_later(@sync_repository_branch1) : TouchSyncJob.perform_later(@sync_repository_branch2) + end + + def create_webhook + url = "" + if type == "SyncRepositories::Gitee" + url = "#{Rails.application.config_for(:configuration)['platform_url']}/api/v1/#{project&.owner&.login}/#{project&.identifier}/sync_repositories/sync?sync_direction=1&repo_type=SyncRepositories::Gitee" + else + url = "#{Rails.application.config_for(:configuration)['platform_url']}/api/v1/#{project&.owner&.login}/#{project&.identifier}/sync_repositories/sync?sync_direction=1&repo_type=SyncRepositories::Github" + end + webhook_params = { + active: true, + branch_filter: '*', + http_method: 'POST', + url: url, + content_type: 'json', + type: 'reposync', + events: ["push"] + } + @gitea_webhook = Api::V1::Projects::Webhooks::CreateService.call(project, webhook_params) + end + + def repo_name(sync_direction) + if type == "SyncRepositories::Gitee" + return "gitee:#{project.id}:#{sync_granularity}:#{sync_direction}" + else + return "github:#{project.id}:#{sync_granularity}:#{sync_direction}" + end + end + + def gitlink_repo_address + "#{EduSetting.get("gitlink_repo_domain")}/#{project.owner&.login}/#{project.identifier}.git" + end + + def gitlink_token + EduSetting.get("gitlink_admin_token") + end +end \ No newline at end of file diff --git a/app/services/api/v1/projects/sync_repositories/update_service.rb b/app/services/api/v1/projects/sync_repositories/update_service.rb new file mode 100644 index 000000000..9b4fec3ec --- /dev/null +++ b/app/services/api/v1/projects/sync_repositories/update_service.rb @@ -0,0 +1,40 @@ +class Api::V1::Projects::SyncRepositories::UpdateService < ApplicationService + + include ActiveModel::Model + attr_reader :project, :external_token, :external_repo_address, :sync_repositories + attr_accessor :sync_repository1, :sync_repository2 + + validates :external_repo_address, format: { with: CustomRegexp::URL_REGEX, multiline: true, message: "地址格式不正确" } + validates :external_token, presence: true + + #Api::V1::Projects::SyncRepositories::UpdateService.call(Project.last, "21,22", {external_repo_address: "https://github.com/viletyy/testdevops.git", external_token:"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"}) + def initialize(project, sync_repository_ids, params) + @project = project + @sync_repositories = SyncRepository.where(project_id: project.id, id: sync_repository_ids.split(",")) + @external_token = params[:external_token] + @external_repo_address = params[:external_repo_address] + end + + def call + raise Error, errors.full_messages.join(",") unless valid? + + update_sync_repository + + end + + private + def update_sync_repository + @sync_repositories.each do |repo| + Reposync::UpdateRepoAddrService.call(repo&.repo_name, internal_repo_address, internal_token, external_repo_address, external_token) + repo.update_attributes!({external_repo_address: external_repo_address, external_token: external_token}) + end + end + + def internal_repo_address + "#{EduSetting.get("gitlink_repo_domain")}/#{project.owner&.login}/#{project.identifier}.git" + end + + def internal_token + EduSetting.get("gitlink_admin_token") + end +end \ No newline at end of file diff --git a/app/services/api/v1/projects/tags/delete_service.rb b/app/services/api/v1/projects/tags/delete_service.rb index d0d317aa8..8f898bf1a 100644 --- a/app/services/api/v1/projects/tags/delete_service.rb +++ b/app/services/api/v1/projects/tags/delete_service.rb @@ -32,7 +32,7 @@ class Api::V1::Projects::Tags::DeleteService < ApplicationService def excute_data_to_gitea begin - @gitea_data = $gitea_client.delete_repos_tags_by_owner_repo_tag(owner, repo, tag_name, {query: request_params}) + @gitea_data = $gitea_client.delete_repos_tags_by_owner_repo_tag(owner, repo, CGI.escape(tag_name), {query: request_params}) rescue => e raise Error, '请先删除发行版!' if e.to_s.include?("409") raise Error, '删除标签失败!' diff --git a/app/services/api/v1/projects/webhooks/create_service.rb b/app/services/api/v1/projects/webhooks/create_service.rb index 303f3b39f..829710cbe 100644 --- a/app/services/api/v1/projects/webhooks/create_service.rb +++ b/app/services/api/v1/projects/webhooks/create_service.rb @@ -8,7 +8,7 @@ class Api::V1::Projects::Webhooks::CreateService < ApplicationService validates :active, inclusion: {in: [true, false]} validates :http_method, inclusion: { in: %w(POST GET), message: "请输入正确的请求方式"} validates :content_type, inclusion: { in: %w(json form), message: "请输入正确的Content Type"} - validates :type, inclusion: {in: %w(gitea slack discord dingtalk telegram msteams feishu matrix jianmu softbot), message: "请输入正确的Webhook Type"} + validates :type, inclusion: {in: %w(gitea slack discord dingtalk telegram msteams feishu matrix jianmu softbot reposync), message: "请输入正确的Webhook Type"} def initialize(project, params, token=nil) @project = project @owner = project&.owner.login diff --git a/app/services/baidu/tongji_service.rb b/app/services/baidu/tongji_service.rb new file mode 100644 index 000000000..a2a4f28fc --- /dev/null +++ b/app/services/baidu/tongji_service.rb @@ -0,0 +1,221 @@ +module Baidu + class TongjiService < ApplicationService + attr_reader :client_id, :client_secret, :site_id + # login、code、password、password_confirmation + def initialize + @client_id = "6dMO2kqKUaMZkBrMaUMxQSNAT49v0Mjq" + @client_secret = "qvWqF33AOmGs1tPCgsROvis9EQCuNmd3" + @site_id = 18657013 + end + + def call + + end + + + def init_overview_data_by(start_date = nil, end_date = nil) + start_date = Time.now.prev_year.beginning_of_year if start_date.nil? + end_date = Time.now + Rails.logger.info("*********开始百度统计-概览:#{start_date}-#{end_date}*********") + sql_connection = ActiveRecord::Base.connection + sql_connection.begin_db_transaction + + # 如果存在数据 先清空 + # sql_connection.execute("delete from daily_platform_statistics where date between '#{start_date}' and '#{end_date}'") + multiple_days_data = overview_multiple_days_data(start_date, end_date) + if multiple_days_data.present? + sql = "replace into daily_platform_statistics (date,pv,visitor,ip,created_at,updated_at) values #{multiple_days_data.join(",")}" + sql_connection.execute(sql) + end + sql_connection.commit_db_transaction + Rails.logger.info("*********结束百度统计-概览:#{start_date}-#{end_date}*********") + end + + def init_source_from_data_by(start_date = nil, end_date = nil) + start_date = Time.now.prev_year.beginning_of_year if start_date.nil? + end_date = Time.now + Rails.logger.info("*********开始百度统计-来源:#{start_date}-#{end_date}*********") + source_from_batch_add(start_date, end_date) + Rails.logger.info("*********结束百度统计-来源:#{start_date}-#{end_date}*********") + end + + # 按日期获取来源数据 + def source_from_batch_add(start_date,end_date) + # 补充更新开始时间的当天数据 + source_from_by_date(start_date) + diff_days(start_date, end_date).times.each do |t| + new_start_date = start_date + (t + 1).days + source_from_by_date(new_start_date) + end + # 补充更新最后时间一天数据 + source_from_by_date(end_date) + end + + # 按天获取来源数据 + def source_from_by_date(start_date) + return [] unless access_token.present? && start_date.present? + source_from_data = api("source/all/a", start_date, start_date, "pv_count,visitor_count,ip_count") + source_from = [] + source_from_data['items'][1].each_with_index do |source, index| + source_from.push(((source[0].to_f / source_from_data['sum'][0][0].to_f) * 100).round(2)) + end + daily_statistic = DailyPlatformStatistic.find_or_initialize_by(date: start_date) + daily_statistic.source_through = source_from[0] + daily_statistic.source_link = source_from[1] + daily_statistic.source_search = source_from[2] + daily_statistic.source_custom = source_from[3] + daily_statistic.save + end + + def diff_days(start_date, end_date) + (end_date.beginning_of_day.to_i - start_date.beginning_of_day.to_i) / (24 * 3600) + end + + def overview_batch_add(start_date, end_date) + return [] unless access_token.present? && start_date.present? && end_date.present? + start_date = Time.now - 1.days if start_date.strftime("%Y%m%d") == end_date.strftime("%Y%m%d") + overview_data = api("overview/getTimeTrendRpt", start_date, end_date, "pv_count,visitor_count,ip_count") + overview_data['items'][0].each_with_index do |date, index| + pv = overview_data['items'][1][index][0] + visitor = overview_data['items'][1][index][1] + ip = overview_data['items'][1][index][2] + job_date = date[0].to_s.gsub("/", "-") + daily_statistic = DailyPlatformStatistic.find_or_initialize_by(date: job_date) + daily_statistic.date = job_date + daily_statistic.pv = pv + daily_statistic.visitor = visitor + daily_statistic.ip = ip + daily_statistic.save + end + overview_data + end + + def overview_multiple_days_data(start_date, end_date) + return [] unless access_token.present? && start_date.present? && end_date.present? + overview_data = api("overview/getTimeTrendRpt", start_date, end_date, "pv_count,visitor_count,ip_count") + data = [] + created_at = Time.now.strftime("%Y-%m-%d 00:00:00") + overview_data['items'][0].each_with_index do |date, index| + pv = overview_data['items'][1][index][0] + visitor = overview_data['items'][1][index][1] + ip = overview_data['items'][1][index][2] + data.push("('#{date[0].to_s.gsub("/", "-")}', #{pv.to_s.gsub("--","0")}, #{visitor.to_s.gsub("--","0")}, #{ip.to_s.gsub("--","0")},\"#{created_at}\",\"#{created_at}\")") + end + data + end + + def code_url + "http://openapi.baidu.com/oauth/2.0/authorize?response_type=code&client_id=#{client_id}&redirect_uri=oob&scope=basic&display=popup" + end + + def oauth_url(code) + "http://openapi.baidu.com/oauth/2.0/token?grant_type=authorization_code&code=#{code}&client_id=#{client_id}&client_secret=#{client_secret}&redirect_uri=oob" + end + + def get_access_token(code) + uri = URI.parse(oauth_url(code)) + response = Net::HTTP.get_response(uri) + Rails.logger.info "baidu_tongji_auth response.body ===== #{response.body}" + if response.code.to_i == 200 + data = JSON.parse(response.body) + access_token = data['access_token'] + refresh_token = data['refresh_token'] + expires_in = data['expires_in'] + if access_token.present? + Rails.cache.write("baidu_tongji_auth/access_token", access_token, expires_in: expires_in) + Rails.cache.write("baidu_tongji_auth/refresh_token", refresh_token, expires_in: 1.year) + end + end + end + + def refresh_access_token + url = "http://openapi.baidu.com/oauth/2.0/token?grant_type=refresh_token&refresh_token=#{refresh_token}&client_id=#{client_id}&client_secret=#{client_secret}" + uri = URI.parse(url) + response = Net::HTTP.get_response(uri) + Rails.logger.info "baidu_tongji_auth response.body ===== #{response.body}" + if response.code.to_i == 200 + data = JSON.parse(response.body) + access_token = data['access_token'] + refresh_token = data['refresh_token'] + expires_in = data['expires_in'] + if access_token.present? + Rails.cache.write("baidu_tongji_auth/access_token", access_token, expires_in: expires_in) + Rails.cache.write("baidu_tongji_auth/refresh_token", refresh_token, expires_in: 1.year) + end + end + end + + def access_token + access_token = Rails.cache.read("baidu_tongji_auth/access_token") + if access_token.blank? && refresh_token.present? + refresh_access_token + access_token = Rails.cache.read("baidu_tongji_auth/access_token") + end + access_token + end + + def refresh_token + refresh_token = Rails.cache.read("baidu_tongji_auth/refresh_token") + # 如果刷新token失效,access_token也重置 + if refresh_token.blank? + Rails.cache.delete("baidu_tongji_auth/access_token") + end + refresh_token + end + + # 网站概况(趋势数据) + def api_overview + start_date = Time.now.beginning_of_week + end_date = Time.now + start_date = Time.now - 1.days if start_date.strftime("%Y%m%d") == end_date.strftime("%Y%m%d") + api("overview/getTimeTrendRpt", start_date, end_date, "pv_count,visitor_count,ip_count") + end + + # 网站概况(来源网站、搜索词、入口页面、受访页面) + def api_overview_getCommonTrackRpt + start_date = Time.now.beginning_of_week + end_date = Time.now + api("overview/getCommonTrackRpt", start_date, end_date, "pv_count") + end + + # 全部来源 + def source_from + start_date = Time.now.beginning_of_week + end_date = Time.now + api("source/all/a", start_date, end_date, "pv_count,visitor_count,ip_count") + end + + def api(api_method, start_date, end_date, metrics = nil) + start_date_fmt = start_date.strftime("%Y%m%d") + end_date_fmt = end_date.strftime("%Y%m%d") + api_url = "https://openapi.baidu.com/rest/2.0/tongji/report/getData?access_token=#{access_token}&site_id=#{site_id}&method=#{api_method}&start_date=#{start_date_fmt}&end_date=#{end_date_fmt}&metrics=#{metrics}" + data = url_http_post(api_url, {}) + data['result'] + end + + def url_http_post(api_url, params) + Rails.logger.info "api_url==#{api_url}" + uri = URI.parse(api_url) + http = Net::HTTP.new uri.host, uri.port + http.open_timeout = 60 + http.read_timeout = 60 + if uri.scheme == 'https' + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http.use_ssl = true + end + begin + request = Net::HTTP::Post.new(uri) + request.set_form_data(params) if params.present? + request['Content-Type'] = 'application/json;charset=utf-8' + # request['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8' + response = http.start { |http| http.request(request) } + Rails.logger.info "api response.body==#{response.body}" + JSON.parse response.body + rescue => err + Rails.logger.error("#############api_url:#{api_url},error:#{err.message.size}") + # Rails.logger.error("#############api_url:#{api_url},error:#{err.message}") + return {} + end + end + end +end diff --git a/app/services/cache/v2/project_common_service.rb b/app/services/cache/v2/project_common_service.rb index 0d167c2a7..1e85f3e08 100644 --- a/app/services/cache/v2/project_common_service.rb +++ b/app/services/cache/v2/project_common_service.rb @@ -1,5 +1,5 @@ class Cache::V2::ProjectCommonService < ApplicationService - attr_reader :project_id, :owner_id, :name, :identifier, :description, :visits, :watchers, :praises, :forks, :issues, :pullrequests, :commits + attr_reader :project_id, :owner_id, :name, :identifier, :description, :visits, :watchers, :praises, :forks, :issues, :closed_issues, :pullrequests, :commits attr_accessor :project def initialize(project_id, params={}) @@ -13,6 +13,7 @@ class Cache::V2::ProjectCommonService < ApplicationService @praises = params[:praises] @forks = params[:forks] @issues = params[:issues] + @closed_issues = params[:closed_issues] @pullrequests = params[:pullrequests] @commits = params[:commits] end @@ -78,6 +79,10 @@ class Cache::V2::ProjectCommonService < ApplicationService "issues" end + def closed_issues_key + "closed_issues" + end + def pullrequests_key "pullrequests" end @@ -151,6 +156,10 @@ class Cache::V2::ProjectCommonService < ApplicationService Cache::V2::ProjectRankService.call(@project_id, {issues: @issues}) Cache::V2::ProjectDateRankService.call(@project_id, Date.today, {issues: @issues}) end + if @closed_issues.present? + $redis_cache.hincrby(project_common_key, closed_issues_key, @closed_issues) + Cache::V2::ProjectDateRankService.call(@project_id, Date.today, {closed_issues: @closed_issues}) + end if @pullrequests.present? $redis_cache.hincrby(project_common_key, pullrequests_key, @pullrequests) Cache::V2::ProjectRankService.call(@project_id, {pullrequests: @pullrequests}) @@ -202,6 +211,10 @@ class Cache::V2::ProjectCommonService < ApplicationService $redis_cache.hset(project_common_key, issues_key, Issue.issue_issue.where(project_id: @project_id).count) end + def reset_project_closed_issues + $redis_cache.hset(project_common_key, closed_issues_key, Issue.issue_issue.closed.where(project_id: @project_id).count) + end + def reset_project_pullrequests $redis_cache.hset(project_common_key, pullrequests_key, PullRequest.where(project_id: @project_id).count) end @@ -224,6 +237,7 @@ class Cache::V2::ProjectCommonService < ApplicationService reset_project_praises reset_project_forks reset_project_issues + reset_project_closed_issues reset_project_pullrequests reset_project_commits diff --git a/app/services/cache/v2/project_date_rank_service.rb b/app/services/cache/v2/project_date_rank_service.rb index 9df69bbb4..bf661f0a2 100644 --- a/app/services/cache/v2/project_date_rank_service.rb +++ b/app/services/cache/v2/project_date_rank_service.rb @@ -1,6 +1,6 @@ # 项目日活跃度计算存储 class Cache::V2::ProjectDateRankService < ApplicationService - attr_reader :project_id, :rank_date, :visits, :praises, :forks, :issues, :pullrequests, :commits + attr_reader :project_id, :rank_date, :visits, :praises, :forks, :issues, :closed_issues, :pullrequests, :commits attr_accessor :project_common def initialize(project_id, rank_date=Date.today, params={}) @@ -11,6 +11,7 @@ class Cache::V2::ProjectDateRankService < ApplicationService @praises = params[:praises] @forks = params[:forks] @issues = params[:issues] + @closed_issues = params[:closed_issues] @pullrequests = params[:pullrequests] @commits = params[:commits] end @@ -57,6 +58,9 @@ class Cache::V2::ProjectDateRankService < ApplicationService $redis_cache.zincrby(project_rank_key, @issues.to_i * 5, @project_id) $redis_cache.hincrby(project_rank_statistic_key, "issues", @issues.to_i) end + if @closed_issues.present? + $redis_cache.hincrby(project_rank_statistic_key, "closed_issues", @closed_issues.to_i) + end if @pullrequests.present? $redis_cache.zincrby(project_rank_key, @pullrequests.to_i * 10, @project_id) $redis_cache.hincrby(project_rank_statistic_key, "pullrequests", @pullrequests.to_i) diff --git a/app/services/getway/cms/get_service.rb b/app/services/getway/cms/get_service.rb index 6b7050e84..76c5f27e8 100644 --- a/app/services/getway/cms/get_service.rb +++ b/app/services/getway/cms/get_service.rb @@ -16,6 +16,6 @@ class Getway::Cms::GetService < Getway::ClientService end def url - "/cms/doc/open/#{doc_id}".freeze + "/cms/doc/open/baseInfo/#{doc_id}".freeze end end \ No newline at end of file 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/gitea/user/generate_token_service.rb b/app/services/gitea/user/generate_token_service.rb index 946bd68ce..e2dd6b0f4 100644 --- a/app/services/gitea/user/generate_token_service.rb +++ b/app/services/gitea/user/generate_token_service.rb @@ -29,7 +29,7 @@ class Gitea::User::GenerateTokenService < Gitea::ClientService end def request_params - { name: "#{@username}-#{token_name}" } + { name: "#{@username}-#{token_name}", scopes: ["all"] } end def token_name diff --git a/app/services/page_service.rb b/app/services/page_service.rb index 5c166e82c..c85fa394c 100644 --- a/app/services/page_service.rb +++ b/app/services/page_service.rb @@ -47,7 +47,7 @@ class PageService repo_link = project.repository.url repo = project.repository.identifier branch = branch - script_path =page.build_script_path + script_path = branch == "gh-pages" ? "files_build" : page.build_script_path if script_path.present? uri = URI.parse("http://gitlink.#{@deploy_domain}/gitlink_execute_script?key=#{@deploy_key}&script_path=#{script_path}&project_dir=#{project_dir}&repo=#{repo}&repo_link=#{repo_link}&branch=#{branch}&owner=#{owner}") response = Net::HTTP.get_response(uri) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index c4f892f7f..a727cf916 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -26,9 +26,6 @@ class Projects::CreateService < ApplicationService end end @project - rescue => e - puts "create project service error: #{e.message}" - raise Error, e.message end private diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 37edf56ee..63475f2b4 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -18,6 +18,8 @@ class Projects::ForkService < ApplicationService :license_id, :ignore_id, {repository: [:identifier, :hidden]}] result = Gitea::Repository::ForkService.new(@project.owner, @target_owner, @project.identifier, @organization, @new_identifier).call + Rails.logger.info("##### ForkService #{@project.identifier} result======#{result}") + raise Error, 'fork失败' if result.blank? or result['id'].blank? clone_project.owner = @target_owner clone_project.forked_from_project_id = @project.id clone_project.gpid = result['id'] @@ -41,7 +43,7 @@ class Projects::ForkService < ApplicationService clone_project end rescue => e - puts "clone project service error: #{e.message}" + Rails.logger.info "fork project service error: #{e.message}" raise Error, e.message end diff --git a/app/services/repositories/create_service.rb b/app/services/repositories/create_service.rb index e7ff8bd1d..800d5f420 100644 --- a/app/services/repositories/create_service.rb +++ b/app/services/repositories/create_service.rb @@ -33,18 +33,17 @@ class Repositories::CreateService < ApplicationService end repository end - rescue => e - puts "create repository service error: #{e.message}" - raise Error, e.message end private def create_gitea_repository if project.owner.is_a?(User) - @gitea_repository = Gitea::Repository::CreateService.new(user.gitea_token, gitea_repository_params).call + # @gitea_repository = Gitea::Repository::CreateService.new(user.gitea_token, gitea_repository_params).call + @gitea_repository = $gitea_client.post_user_repos({query: {token: user.gitea_token}, body: gitea_repository_params.to_json}) elsif project.owner.is_a?(Organization) - @gitea_repository = Gitea::Organization::Repository::CreateService.call(user.gitea_token, project.owner.login, gitea_repository_params) + # @gitea_repository = Gitea::Organization::Repository::CreateService.call(user.gitea_token, project.owner.login, gitea_repository_params) + @gitea_repository = $gitea_client.post_orgs_repos_by_org(project.owner.login, {query: {token: user.gitea_token}, body: gitea_repository_params.to_json}) end end @@ -67,7 +66,7 @@ class Repositories::CreateService < ApplicationService end def repository_params - params.merge(project_id: project.id) + params.merge(project_id: project.id).except(:auto_init) end def gitea_repository_params diff --git a/app/services/reposync/client_service.rb b/app/services/reposync/client_service.rb new file mode 100644 index 000000000..a8ddc18bd --- /dev/null +++ b/app/services/reposync/client_service.rb @@ -0,0 +1,115 @@ +class Reposync::ClientService < ApplicationService + attr_reader :url, :params + + def initialize(options={}) + @url = options[:url] + @params = options[:params] + end + + def post(url, params={}) + puts "[reposync][POST] request params: #{params}" + conn.post do |req| + req.url full_url(url) + req.body = params[:data].to_json + end + end + + def get(url, params={}) + puts "[reposync][GET] request params: #{params}" + conn.get do |req| + req.url full_url(url, 'get') + params.each_pair do |key, value| + req.params["#{key}"] = value + end + end + end + + def delete(url, params={}) + puts "[reposync][DELETE] request params: #{params}" + conn.delete do |req| + req.url full_url(url) + req.body = params[:data].to_json + end + end + + def patch(url, params={}) + puts "[reposync][PATCH] request params: #{params}" + conn.patch do |req| + req.url full_url(url) + req.body = params[:data].to_json + end + end + + def put(url, params={}) + puts "[reposync][PUT] request params: #{params}" + conn.put do |req| + req.url full_url(url) + req.body = params[:data].to_json + end + end + + def conn + @client ||= begin + Faraday.new(url: domain) do |req| + req.request :url_encoded + req.headers['Content-Type'] = 'application/json' + req.adapter Faraday.default_adapter + req.options.timeout = 100 # open/read timeout in seconds + req.options.open_timeout = 10 # connection open timeout in seconds + end + end + + @client + end + + def domain + EduSetting.get("reposync_api_domain") || "http://106.75.110.152:50087" + end + + def full_url(api_rest, action='post') + url = [domain, api_rest].join('').freeze + url = action === 'get' ? url : URI.escape(url) + url = URI.escape(url) unless url.ascii_only? + puts "[reposync] request url: #{url}" + return url + end + + def log_error(status, body) + puts "[reposync] status: #{status}" + puts "[reposync] body: #{body}" + end + + def render_response(response) + status = response.status + body = JSON.parse(response&.body) + + log_error(status, body) + + if status == 200 + if body["code_status"].to_i == 0 + return [body["code_status"], body["data"], body["msg"]] + else + puts "[reposync][ERROR] code: #{body["code_status"]}" + puts "[reposync][ERROR] message: #{body["msg"]}" + return [body["code_status"], body["data"], body["msg"]] + end + end + end + + def render_list_response(response) + status = response.status + body = JSON.parse(response&.body) + + log_error(status, body) + + if status == 200 + if body["code_status"].to_i == 0 + return [body["code_status"], body["data"], body["total"], body["msg"]] + else + puts "[reposync][ERROR] code: #{body["code_status"]}" + puts "[reposync][ERROR] message: #{body["msg"]}" + return [body["code_status"], body["data"], body["total"], body["msg"]] + end + end + end +end \ No newline at end of file diff --git a/app/services/reposync/create_sync_branch_service.rb b/app/services/reposync/create_sync_branch_service.rb new file mode 100644 index 000000000..2b417a7e5 --- /dev/null +++ b/app/services/reposync/create_sync_branch_service.rb @@ -0,0 +1,30 @@ +class Reposync::CreateSyncBranchService < Reposync::ClientService + + attr_accessor :repo_name, :internal_branch_name, :external_branch_name, :enable + + def initialize(repo_name, internal_branch_name, external_branch_name, enable=true) + @repo_name = repo_name + @internal_branch_name = internal_branch_name + @external_branch_name = external_branch_name + @enable = enable + end + + def call + result = post(url, request_params) + response = render_response(result) + end + + private + def request_params + Hash.new.merge(data: { + internal_branch_name: internal_branch_name, + external_branch_name: external_branch_name, + enable: enable + }.stringify_keys) + end + + def url + "/cerobot/sync/#{repo_name}/branch".freeze + end + +end \ No newline at end of file diff --git a/app/services/reposync/create_sync_repo_service.rb b/app/services/reposync/create_sync_repo_service.rb new file mode 100644 index 000000000..0f0cc43ee --- /dev/null +++ b/app/services/reposync/create_sync_repo_service.rb @@ -0,0 +1,38 @@ +class Reposync::CreateSyncRepoService < Reposync::ClientService + + attr_accessor :repo_name, :internal_repo_address, :inter_token, :external_repo_address, :exter_token, :sync_granularity, :sync_direction, :enable + + def initialize(repo_name, internal_repo_address, inter_token, external_repo_address, exter_token, sync_granularity, sync_direction, enable=true) + @repo_name = repo_name + @internal_repo_address = internal_repo_address + @inter_token = inter_token + @external_repo_address = external_repo_address + @exter_token = exter_token + @sync_granularity = sync_granularity + @sync_direction = sync_direction + @enable = enable + end + + def call + result = post(url, request_params) + response = render_response(result) + end + + private + def request_params + Hash.new.merge(data: { + repo_name: repo_name, + enable: enable, + internal_repo_address: internal_repo_address, + inter_token: inter_token, + external_repo_address: external_repo_address, + exter_token: exter_token, + sync_granularity: sync_granularity, + sync_direction: sync_direction + }.stringify_keys) + end + + def url + "/cerobot/sync/repo".freeze + end +end \ No newline at end of file diff --git a/app/services/reposync/delete_branch_service.rb b/app/services/reposync/delete_branch_service.rb new file mode 100644 index 000000000..6d3d7c328 --- /dev/null +++ b/app/services/reposync/delete_branch_service.rb @@ -0,0 +1,19 @@ +class Reposync::DeleteBranchService < Reposync::ClientService + + attr_accessor :repo_name, :branch_name + + def initialize(repo_name, branch_name) + @repo_name = repo_name + @branch_name = branch_name + end + + def call + result = delete(url) + response = render_response(result) + end + + private + def url + "/cerobot/sync/#{repo_name}/branch/#{branch_name}" + end +end \ No newline at end of file diff --git a/app/services/reposync/delete_repo_service.rb b/app/services/reposync/delete_repo_service.rb new file mode 100644 index 000000000..420a2d60f --- /dev/null +++ b/app/services/reposync/delete_repo_service.rb @@ -0,0 +1,18 @@ +class Reposync::DeleteRepoService < Reposync::ClientService + + attr_accessor :repo_name + + def initialize(repo_name) + @repo_name = repo_name + end + + def call + result = delete(url) + response = render_response(result) + end + + private + def url + "/cerobot/sync/repo/#{repo_name}" + end +end \ No newline at end of file diff --git a/app/services/reposync/get_logs_service.rb b/app/services/reposync/get_logs_service.rb new file mode 100644 index 000000000..1288de6bf --- /dev/null +++ b/app/services/reposync/get_logs_service.rb @@ -0,0 +1,33 @@ +class Reposync::GetLogsService < Reposync::ClientService + + attr_accessor :repo_name, :branch_id, :page_num, :page_size + + def initialize(repo_name=nil, branch_id=nil, page_num=1, page_size=10) + @repo_name = repo_name + @branch_id = branch_id + @page_num = page_num + @page_size = page_size + end + + def call + result = get(url, request_params) + response = render_list_response(result) + end + + private + def request_params + params = { + page_num: page_num, + page_size: page_size, + create_sort: true + } + params.merge!(repo_name: repo_name) if repo_name.present? + params.merge!(branch_id: branch_id) if branch_id.present? + + return params.stringify_keys + end + + def url + "/cerobot/sync/repo/logs" + end +end \ No newline at end of file diff --git a/app/services/reposync/get_sync_branches_service.rb b/app/services/reposync/get_sync_branches_service.rb new file mode 100644 index 000000000..92c9d8565 --- /dev/null +++ b/app/services/reposync/get_sync_branches_service.rb @@ -0,0 +1,30 @@ +class Reposync::GetSyncBranchesService < Reposync::ClientService + + attr_accessor :repo_name, :page, :limit, :create_sort + + def initialize(repo_name, page=1, limit=10, create_sort=false) + @repo_name = repo_name + @page = page + @limit = limit + @create_sort = create_sort + end + + def call + result = get(url, request_params) + response = render_response(result) + end + + private + def request_params + { + page: page, + limit: limit, + create_sort: create_sort + }.stringify_keys + end + + def url + "/cerobot/sync/#{repo_name}/branch".freeze + end + +end \ No newline at end of file diff --git a/app/services/reposync/get_sync_repos_service.rb b/app/services/reposync/get_sync_repos_service.rb new file mode 100644 index 000000000..3fc39d89f --- /dev/null +++ b/app/services/reposync/get_sync_repos_service.rb @@ -0,0 +1,28 @@ +class Reposync::GetSyncReposService < Reposync::ClientService + attr_accessor :page, :limit, :create_sort + + def initialize(page=1, limit=10, create_sort=false) + @page = page + @limit = limit + @create_sort = create_sort + end + + def call + result = get(url, request_params) + response = render_response(result) + end + + private + def request_params + { + page: page, + limit: limit, + create_sort: create_sort + }.stringify_keys + end + + def url + "/cerobot/sync/repo".freeze + end + +end \ No newline at end of file diff --git a/app/services/reposync/sync_branch_service.rb b/app/services/reposync/sync_branch_service.rb new file mode 100644 index 000000000..9ca71b3fe --- /dev/null +++ b/app/services/reposync/sync_branch_service.rb @@ -0,0 +1,22 @@ +class Reposync::SyncBranchService < Reposync::ClientService + + attr_accessor :repo_name, :branch_name, :sync_direct + + def initialize(repo_name, branch_name, sync_direct) + @repo_name = repo_name + @branch_name = branch_name + @sync_direct = sync_direct + end + + def call + result = post(url) + response = render_response(result) + end + + private + + def url + "/cerobot/sync/#{repo_name}/branch/#{branch_name}?sync_direct=#{sync_direct}" + end + +end \ No newline at end of file diff --git a/app/services/reposync/sync_repo_service.rb b/app/services/reposync/sync_repo_service.rb new file mode 100644 index 000000000..8b9e6c247 --- /dev/null +++ b/app/services/reposync/sync_repo_service.rb @@ -0,0 +1,18 @@ +class Reposync::SyncRepoService < Reposync::ClientService + + attr_accessor :repo_name + + def initialize(repo_name) + @repo_name = repo_name + end + + def call + result = post(url) + response = render_response(result) + end + + private + def url + "/cerobot/sync/repo/#{repo_name}" + end +end \ No newline at end of file diff --git a/app/services/reposync/update_branch_status_service.rb b/app/services/reposync/update_branch_status_service.rb new file mode 100644 index 000000000..98ebcd5b5 --- /dev/null +++ b/app/services/reposync/update_branch_status_service.rb @@ -0,0 +1,21 @@ +class Reposync::UpdateBranchStatusService < Reposync::ClientService + + attr_accessor :repo_name, :branch_name, :enable + + def initialize(repo_name, branch_name, enable) + @repo_name = repo_name + @branch_name = branch_name + @enable = enable + end + + def call + result = put(url) + response = render_response(result) + end + + private + + def url + "/cerobot/sync/#{repo_name}/branch/#{branch_name}?enable=#{enable}" + end +end \ No newline at end of file diff --git a/app/services/reposync/update_repo_addr_service.rb b/app/services/reposync/update_repo_addr_service.rb new file mode 100644 index 000000000..aa34f4fb3 --- /dev/null +++ b/app/services/reposync/update_repo_addr_service.rb @@ -0,0 +1,31 @@ +class Reposync::UpdateRepoAddrService < Reposync::ClientService + + attr_accessor :repo_name, :internal_repo_address, :inter_token, :external_repo_address, :exter_token + + def initialize(repo_name, internal_repo_address, inter_token, external_repo_address, exter_token) + @repo_name = repo_name + @internal_repo_address = internal_repo_address + @inter_token = inter_token + @external_repo_address = external_repo_address + @exter_token = exter_token + end + + def call + result = put(url, request_params) + response = render_response(result) + end + + private + def request_params + Hash.new.merge(data: { + internal_repo_address: internal_repo_address, + inter_token: inter_token, + external_repo_address: external_repo_address, + exter_token: exter_token + }.stringify_keys) + end + + def url + "/cerobot/sync/repo/#{repo_name}/repo_addr".freeze + end +end \ No newline at end of file diff --git a/app/services/reposync/update_repo_status_service.rb b/app/services/reposync/update_repo_status_service.rb new file mode 100644 index 000000000..db7065d85 --- /dev/null +++ b/app/services/reposync/update_repo_status_service.rb @@ -0,0 +1,19 @@ +class Reposync::UpdateRepoStatusService < Reposync::ClientService + + attr_accessor :repo_name, :enable + + def initialize(repo_name, enable) + @repo_name = repo_name + @enable = enable + end + + def call + result = put(url) + response = render_response(result) + end + + private + def url + "/cerobot/sync/repo/#{repo_name}?enable=#{enable}".freeze + end +end \ No newline at end of file diff --git a/app/views/action/node_inputs/_form.html.erb b/app/views/action/node_inputs/_form.html.erb new file mode 100644 index 000000000..deccfa69e --- /dev/null +++ b/app/views/action/node_inputs/_form.html.erb @@ -0,0 +1,39 @@ +<%= form_with(model: node_input, url: action_node_node_inputs_path(@node), local: true) do |form| %> + <% if node_input.errors.any? %> +
+

<%= pluralize(node_input.errors.count, "error") %> prohibited this node_input from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name, "参数名称" %> + <%= form.text_field :name %> +
+
+ <%= form.label :input_type, "参数类型" %> + <%= form.text_field :input_type %> +
+
+ <%= form.label :description, "描述" %> + <%= form.text_area :description, rows: 5, :style => 'width:800px;' %> +
+
+ <%= form.label :is_required, "是否必填项" %> + <%= form.check_box("is_required", {}, "true", "false") %> +
+
+ <%= form.label :sort_no, "排序号" %> + <%= form.text_field :sort_no %> +
+ +
+ + <%= form.submit("保存") %> +
+<% end %> diff --git a/app/views/action/node_inputs/_node_input.json.jbuilder b/app/views/action/node_inputs/_node_input.json.jbuilder new file mode 100644 index 000000000..9f0a6074b --- /dev/null +++ b/app/views/action/node_inputs/_node_input.json.jbuilder @@ -0,0 +1,6 @@ +json.extract! node_input, :id, :name, :input_type, :description +if node_input.input_type.to_s == "select" + json.select node.action_node_selects do |node_select| + json.partial! "node_select", locals: { node_select: node_select, node: node } + end +end diff --git a/app/views/action/node_inputs/_node_select.json.jbuilder b/app/views/action/node_inputs/_node_select.json.jbuilder new file mode 100644 index 000000000..4e90508e9 --- /dev/null +++ b/app/views/action/node_inputs/_node_select.json.jbuilder @@ -0,0 +1,4 @@ +json.extract! node_select, :id, :version +if node.is_local? + json.local_url node.local_url +end diff --git a/app/views/action/node_inputs/edit.html.erb b/app/views/action/node_inputs/edit.html.erb new file mode 100644 index 000000000..6ae9f8a78 --- /dev/null +++ b/app/views/action/node_inputs/edit.html.erb @@ -0,0 +1,47 @@ +

编辑 + +

+ +<%= form_with(model: @node_input, url: action_node_node_input_path(@node,@node_input), local: true) do |form| %> + <% if @node_input.errors.any? %> +
+

<%= pluralize(@node_input.errors.count, "error") %> prohibited this node_input from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name, "参数名称" %> + <%= form.text_field :name %> +
+
+ <%= form.label :input_type, "参数类型" %> + <%= form.text_field :input_type %> +
+
+ <%= form.label :description, "描述" %> + <%= form.text_area :description, rows: 5, :style => 'width:800px;' %> +
+
+ <%= form.label :is_required, "是否必填项" %> + <%= form.check_box("is_required", {}, "true", "false") %> +
+ +
+ <%= form.label :sort_no, "排序号" %> + <%= form.text_field :sort_no %> +
+ +
+ + <%= form.submit("保存赛事") %> +
+<% end %> + + +<%= link_to 'Back', action_node_node_inputs_path(@node) %> diff --git a/app/views/action/node_inputs/index.html.erb b/app/views/action/node_inputs/index.html.erb new file mode 100644 index 000000000..f070b3dd7 --- /dev/null +++ b/app/views/action/node_inputs/index.html.erb @@ -0,0 +1,46 @@ +<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + +

action 节点参数配置<%= link_to '>>>Back action节点首页', action_nodes_path %> + +

+

说明:该界面适用于action 节点参数配置

+ + + + + + + + + + + + + + + + + <% @node_inputs.each do |info| %> + + + + + + + + + + + + <% end %> + +
ID参数名称参数输入类型参数描述是否必填项排序号更新时间操作
<%= info.id %><%= info.name %><%= info.input_type %> + <% if info.input_type == "select" %> + <%= select_tag(:version, options_for_select(@node.action_node_selects.map(&:value)), class: 'form-control') %> + <%= link_to '修改选择项', edit_action_node_node_input_path(@node.id, info) %> + <% end %> + <%= info.description %><%= info.is_required %><%= info.sort_no %><%= info.updated_at&.strftime('%Y-%m-%d %H:%M') %><%= link_to '编辑', edit_action_node_node_input_path(@node.id, info) %><%= link_to 'Destroy', action_node_node_input_path(@node, info), method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to '新增', new_action_node_node_input_path(@node) %> diff --git a/app/views/action/node_inputs/index.json.jbuilder b/app/views/action/node_inputs/index.json.jbuilder new file mode 100644 index 000000000..3909639ce --- /dev/null +++ b/app/views/action/node_inputs/index.json.jbuilder @@ -0,0 +1,20 @@ +json.types @node_types.each do |node_type| + if node_type.name.to_s == "未分类" + json.extract! node_type, :id, :name + json.nodes @no_type_nodes do |node| + json.extract! node, :id, :name, :full_name, :description, :action_node_types_id, :yaml, :sort_no, :use_count + json.inputs node.action_node_inputs do |node_input| + json.partial! "node_input", locals: { node_input: node_input, node: node } + end + end + else + json.extract! node_type, :id, :name + json.nodes node_type.action_nodes do |node| + json.extract! node, :id, :name, :full_name, :description, :action_node_types_id, :yaml, :sort_no, :use_count + json.inputs node.action_node_inputs do |node_input| + json.partial! "node_input", locals: { node_input: node_input, node: node } + end + end + end + +end diff --git a/app/views/action/node_inputs/new.html.erb b/app/views/action/node_inputs/new.html.erb new file mode 100644 index 000000000..965ae9378 --- /dev/null +++ b/app/views/action/node_inputs/new.html.erb @@ -0,0 +1,5 @@ +

新增

+ +<%= render 'form', node_input: @node_input %> + +<%= link_to 'Back', action_node_node_inputs_path(@node) %> \ No newline at end of file diff --git a/app/views/action/node_inputs/show.json.jbuilder b/app/views/action/node_inputs/show.json.jbuilder new file mode 100644 index 000000000..64548544d --- /dev/null +++ b/app/views/action/node_inputs/show.json.jbuilder @@ -0,0 +1,7 @@ +json.status 0 +json.message "success" + +json.extract! @node, :id, :name, :full_name, :description, :action_node_types_id, :is_local, :local_url, :yaml, :sort_no, :use_count +json.inputs @node.action_node_inputs do |node_input| + json.partial! "node_input", locals: { node_input: node_input, node: @node } +end \ No newline at end of file diff --git a/app/views/action/node_selects/_form.html.erb b/app/views/action/node_selects/_form.html.erb new file mode 100644 index 000000000..7dd4a6637 --- /dev/null +++ b/app/views/action/node_selects/_form.html.erb @@ -0,0 +1,39 @@ +<%= form_with(model: node_select, url: action_node_node_selects_path(@node), local: true) do |form| %> + <% if node_select.errors.any? %> +
+

<%= pluralize(node_select.errors.count, "error") %> prohibited this node select from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name, "选择项名称" %> + <%= form.text_field :name %> +
+
+ <%= form.label :val, "选择项值" %> + <%= form.text_field :val %> +
+
+ <%= form.label :val_ext, "选择项值扩展" %> + <%= form.text_field :val_ext %> +
+
+ <%= form.label :description, "描述" %> + <%= form.text_area :description, rows: 5, :style => 'width:800px;' %> +
+
+ <%= form.label :sort_no, "排序号" %> + <%= form.text_field :sort_no %> +
+ +
+ + <%= form.submit("保存") %> +
+<% end %> diff --git a/app/views/action/node_selects/edit.html.erb b/app/views/action/node_selects/edit.html.erb new file mode 100644 index 000000000..fca5b3ce7 --- /dev/null +++ b/app/views/action/node_selects/edit.html.erb @@ -0,0 +1,46 @@ +

编辑 + +

+ +<%= form_with(model: @node_select, url: action_node_node_select_path(@node,@node_select), local: true) do |form| %> + <% if @node_select.errors.any? %> +
+

<%= pluralize(@node_select.errors.count, "error") %> prohibited this node select from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name, "选择项名称" %> + <%= form.text_field :name %> +
+
+ <%= form.label :val, "选择项值" %> + <%= form.text_field :val %> +
+
+ <%= form.label :val_ext, "选择项值扩展" %> + <%= form.text_field :val_ext %> +
+
+ <%= form.label :description, "描述" %> + <%= form.text_area :description, rows: 5, :style => 'width:800px;' %> +
+
+ <%= form.label :sort_no, "排序号" %> + <%= form.text_field :sort_no %> +
+ +
+ + <%= form.submit("保存") %> +
+<% end %> + + +<%= link_to 'Back', action_node_node_inputs_path(@node) %> diff --git a/app/views/action/node_selects/index.html.erb b/app/views/action/node_selects/index.html.erb new file mode 100644 index 000000000..09d427d97 --- /dev/null +++ b/app/views/action/node_selects/index.html.erb @@ -0,0 +1,43 @@ +<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + +

action 节点参数配置<%= link_to '>>>Back action节点首页', action_nodes_path %> + +

+

说明:该界面适用于action 节点参数配置

+ + + + + + + + + + + + + + + + + + <% @node_selects.each do |info| %> + + + + + + + + + + + + + <% end %> + +
ID选择项名称选择项值选择项值扩展描述下载地址排序号更新时间操作
<%= info.id %><%= info.name %><%= info.val %><%= info.val_ext %><%= info.description %><%= info.download_url %><%= info.sort_no %><%= info.updated_at&.strftime('%Y-%m-%d %H:%M') %><%= link_to '编辑', edit_action_node_node_select_path(@node.id, info) %><%= link_to 'Destroy', action_node_node_select_path(@node, info), method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to '新增', new_action_node_node_input_path(@node) %> diff --git a/app/views/action/node_selects/new.html.erb b/app/views/action/node_selects/new.html.erb new file mode 100644 index 000000000..c2fcaf123 --- /dev/null +++ b/app/views/action/node_selects/new.html.erb @@ -0,0 +1,5 @@ +

新增

+ +<%= render 'form', node_select: @node_select %> + +<%= link_to 'Back', action_node_node_selects_path(@node) %> \ No newline at end of file diff --git a/app/views/action/node_types/_form.html.erb b/app/views/action/node_types/_form.html.erb new file mode 100644 index 000000000..9a6a5d693 --- /dev/null +++ b/app/views/action/node_types/_form.html.erb @@ -0,0 +1,31 @@ +<%= form_with(model: node_type, local: true) do |form| %> + <% if node_type.errors.any? %> +
+

<%= pluralize(node_type.errors.count, "error") %> prohibited this node type from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name, "分类名称" %> + <%= form.text_field :name %> +
+
+ <%= form.label :description, "描述" %> + <%= form.text_area :description, rows: 5, :style => 'width:800px;' %> +
+
+ <%= form.label :sort_no, "排序号" %> + <%= form.text_field :sort_no %> +
+ +
+ + <%= form.submit("保存") %> +
+<% end %> diff --git a/app/views/action/node_types/edit.html.erb b/app/views/action/node_types/edit.html.erb new file mode 100644 index 000000000..9a44930a4 --- /dev/null +++ b/app/views/action/node_types/edit.html.erb @@ -0,0 +1,7 @@ +

编辑 + +

+<%= render 'form', node_type: @node_type %> + + +<%= link_to 'Back', action_node_types_path %> diff --git a/app/views/action/node_types/index.html.erb b/app/views/action/node_types/index.html.erb new file mode 100644 index 000000000..812d06a24 --- /dev/null +++ b/app/views/action/node_types/index.html.erb @@ -0,0 +1,37 @@ +<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + +

action 节点分类配置<%= link_to '>>>Back action节点首页', action_nodes_path %> + +

+

说明:该界面适用于action 节点分类配置

+ + + + + + + + + + + + + + + <% @node_types.each do |info| %> + + + + + + + + + + <% end %> + +
ID分类名称描述排序号更新时间操作
<%= info.id %><%= info.name %><%= info.description %><%= info.sort_no %><%= info.updated_at&.strftime('%Y-%m-%d %H:%M') %><%= link_to '编辑', edit_action_node_type_path(info) %><%= link_to 'Destroy', action_node_type_path(info), method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to '新增', new_action_node_type_path %> diff --git a/app/views/action/node_types/new.html.erb b/app/views/action/node_types/new.html.erb new file mode 100644 index 000000000..1e053b9da --- /dev/null +++ b/app/views/action/node_types/new.html.erb @@ -0,0 +1,5 @@ +

新增

+ +<%= render 'form', node_type: @node_type %> + +<%= link_to 'Back', action_node_types_path %> \ No newline at end of file diff --git a/app/views/action/nodes/_form.html.erb b/app/views/action/nodes/_form.html.erb new file mode 100644 index 000000000..4e40f0ff0 --- /dev/null +++ b/app/views/action/nodes/_form.html.erb @@ -0,0 +1,63 @@ +<%= form_with(model: node, local: true) do |form| %> + <%# if node.errors.any? %> + + + + + <%# node.errors.full_messages.each do |message| %> + + <%# end %> + + + <%# end %> +
+ + <%= form.select :action_node_types_id, options_for_select(Action::NodeType.all.map { |key| [key.name, key.id]}, node.action_node_types_id), {}, class: "form-control" %> +
+ +
+ <%= form.label :name, "节点名称" %> + <%= form.text_field :name %> +
+
+ <%= form.label :full_name, "节点全称" %> + <%= form.text_field :full_name %> +
+ +
+ <%= form.label :description, "描述" %> + <%= form.text_area :description, rows: 5, :style => 'width:800px;' %> +
+ +
+ <%= form.label :icon, "Icon图标" %> + <%= form.text_field :icon %> +
+ +
+ <%= form.label :is_local, "是否本地化" %> + <%= form.check_box("is_local", {}, "true", "false") %> +
+ +
+ <%= form.label :local_url, "本地化地址" %> + <%= form.text_field :local_url, :style => 'width:1200px;' %> +
+ + + +
+ <%= form.label :sort_no, "排序号" %> + <%= form.text_field :sort_no %> +
+ +
+ <%= form.label :yaml, "yaml语法代码" %> + <%= form.text_area :yaml, rows: 5, :style => 'width:1200px;' %> +
+ +
+ + <%= form.submit("保存") %> +
+<% end %> diff --git a/app/views/action/nodes/_node_input.json.jbuilder b/app/views/action/nodes/_node_input.json.jbuilder new file mode 100644 index 000000000..c41f93c74 --- /dev/null +++ b/app/views/action/nodes/_node_input.json.jbuilder @@ -0,0 +1,6 @@ +json.extract! node_input, :id, :name, :input_type, :description, :is_required +if node_input.input_type.to_s == "select" + json.select node.action_node_selects do |node_select| + json.partial! "node_select", locals: { node_select: node_select, node: node } + end +end diff --git a/app/views/action/nodes/_node_select.json.jbuilder b/app/views/action/nodes/_node_select.json.jbuilder new file mode 100644 index 000000000..897e3f8ef --- /dev/null +++ b/app/views/action/nodes/_node_select.json.jbuilder @@ -0,0 +1,4 @@ +json.extract! node_select, :id, :val +if node.is_local? + json.local_url node.local_url +end diff --git a/app/views/action/nodes/edit.html.erb b/app/views/action/nodes/edit.html.erb new file mode 100644 index 000000000..ee9cb0947 --- /dev/null +++ b/app/views/action/nodes/edit.html.erb @@ -0,0 +1,7 @@ +

编辑 + +

+ +<%= render 'form', node: @node %> + +<%= link_to 'Back', action_nodes_path %> diff --git a/app/views/action/nodes/index.html.erb b/app/views/action/nodes/index.html.erb new file mode 100644 index 000000000..763a8e467 --- /dev/null +++ b/app/views/action/nodes/index.html.erb @@ -0,0 +1,49 @@ +<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + +

action 节点配置

+

>>前往节点分类配置

+

>>前往模板配置

+

说明:该界面适用于action 节点配置参数配置

+ + + + + + + + + + + + + + + + + + + + + <% @nodes.each do |info| %> + + + + + + + + + + + + + + + + <% end %> + +
ID节点名称节点全称节点描述分类排序号更新时间操作
<%= info.id %><%= info.name %><%= info.full_name %><%= info.description %><%= info.action_node_type&.name %><%= info.sort_no %><%= info.updated_at&.strftime('%Y-%m-%d %H:%M') %><%= link_to '编辑', edit_action_node_path(info) %><%= link_to '参数列表', action_node_node_inputs_path(info) %><%= link_to 'Destroy', info, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to '新增', new_action_node_path %> diff --git a/app/views/action/nodes/index.json.jbuilder b/app/views/action/nodes/index.json.jbuilder new file mode 100644 index 000000000..b81879e20 --- /dev/null +++ b/app/views/action/nodes/index.json.jbuilder @@ -0,0 +1,20 @@ +json.types @node_types.each do |node_type| + if node_type.name.to_s == "未分类" + json.extract! node_type, :id, :name + json.nodes @no_type_nodes do |node| + json.extract! node, :id, :name, :full_name, :description, :icon, :action_node_types_id, :yaml, :sort_no, :use_count + json.inputs node.action_node_inputs do |node_input| + json.partial! "node_input", locals: { node_input: node_input, node: node } + end + end + else + json.extract! node_type, :id, :name + json.nodes node_type.action_nodes do |node| + json.extract! node, :id, :name, :full_name, :description, :icon, :action_node_types_id, :yaml, :sort_no, :use_count + json.inputs node.action_node_inputs do |node_input| + json.partial! "node_input", locals: { node_input: node_input, node: node } + end + end + end + +end diff --git a/app/views/action/nodes/new.html.erb b/app/views/action/nodes/new.html.erb new file mode 100644 index 000000000..4a5d2fae9 --- /dev/null +++ b/app/views/action/nodes/new.html.erb @@ -0,0 +1,5 @@ +

新增

+ +<%= render 'form', node: @node %> + +<%= link_to 'Back', action_nodes_path %> \ No newline at end of file diff --git a/app/views/action/nodes/show.json.jbuilder b/app/views/action/nodes/show.json.jbuilder new file mode 100644 index 000000000..64548544d --- /dev/null +++ b/app/views/action/nodes/show.json.jbuilder @@ -0,0 +1,7 @@ +json.status 0 +json.message "success" + +json.extract! @node, :id, :name, :full_name, :description, :action_node_types_id, :is_local, :local_url, :yaml, :sort_no, :use_count +json.inputs @node.action_node_inputs do |node_input| + json.partial! "node_input", locals: { node_input: node_input, node: @node } +end \ No newline at end of file diff --git a/app/views/action/templates/_form.html.erb b/app/views/action/templates/_form.html.erb new file mode 100644 index 000000000..7d93cb76d --- /dev/null +++ b/app/views/action/templates/_form.html.erb @@ -0,0 +1,43 @@ +<%= form_with(model: template, local: true) do |form| %> + <% if template.errors.any? %> +
+

<%= pluralize(template.errors.count, "error") %> prohibited this node type from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name, "模板名称" %> + <%= form.text_field :name %> +
+
+ <%= form.label :description, "描述" %> + <%= form.text_area :description, rows: 5, :style => 'width:800px;' %> +
+
+ <%= form.label :img, "配图" %> + <%= form.text_field :img %> +
+
+ <%= form.label :sort_no, "排序号" %> + <%= form.text_field :sort_no %> +
+
+ <%= form.label :yaml, "yaml语法代码" %> + <%= form.text_area :yaml, rows: 5, :style => 'width:1200px;' %> +
+
+ <%= form.label :json, "json语法代码" %> + <%= form.text_area :json, rows: 5, :style => 'width:1200px;' %> +
+ +
+ + <%= form.submit("保存") %> +
+<% end %> diff --git a/app/views/action/templates/edit.html.erb b/app/views/action/templates/edit.html.erb new file mode 100644 index 000000000..f9c1effc1 --- /dev/null +++ b/app/views/action/templates/edit.html.erb @@ -0,0 +1,6 @@ +

编辑 + +

+<%= render 'form', template: @template %> + +<%= link_to 'Back', action_templates_path %> diff --git a/app/views/action/templates/index.html.erb b/app/views/action/templates/index.html.erb new file mode 100644 index 000000000..b71aaede9 --- /dev/null +++ b/app/views/action/templates/index.html.erb @@ -0,0 +1,37 @@ +<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> + +

action 模板配置<%= link_to '>>>Back action节点首页', action_nodes_path %> + +

+

说明:该界面适用于action 模板配置

+ + + + + + + + + + + + + + + <% @templates.each do |info| %> + + + + + + + + + + <% end %> + +
ID模板名称描述排序号更新时间操作
<%= info.id %><%= info.name %><%= info.description %><%= info.sort_no %><%= info.updated_at&.strftime('%Y-%m-%d %H:%M') %><%= link_to '编辑', edit_action_template_path(info) %><%= link_to 'Destroy', action_template_path(info), method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to '新增', new_action_template_path %> diff --git a/app/views/action/templates/index.json.jbuilder b/app/views/action/templates/index.json.jbuilder new file mode 100644 index 000000000..da534c842 --- /dev/null +++ b/app/views/action/templates/index.json.jbuilder @@ -0,0 +1,3 @@ +json.templates @templates.each do |tpl| + json.extract! tpl, :id, :name, :description, :img, :sort_no, :json, :yaml +end diff --git a/app/views/action/templates/new.html.erb b/app/views/action/templates/new.html.erb new file mode 100644 index 000000000..e67c6a674 --- /dev/null +++ b/app/views/action/templates/new.html.erb @@ -0,0 +1,5 @@ +

新增

+ +<%= render 'form', template: @template %> + +<%= link_to 'Back', action_templates_path %> \ No newline at end of file diff --git a/app/views/action/templates/show.json.jbuilder b/app/views/action/templates/show.json.jbuilder new file mode 100644 index 000000000..5db67424b --- /dev/null +++ b/app/views/action/templates/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @template, :id, :name, :description, :img, :sort_no, :json, :yaml \ No newline at end of file diff --git a/app/views/admins/dashboards/_baidu_tongji.html.erb b/app/views/admins/dashboards/_baidu_tongji.html.erb new file mode 100644 index 000000000..75fd1cde3 --- /dev/null +++ b/app/views/admins/dashboards/_baidu_tongji.html.erb @@ -0,0 +1,68 @@ +
+ 数据来源百度统计,本周 [<%= @current_week_statistic.first&.date %> / <%= @current_week_statistic.last&.date %>] +
+ + + + + + + + + + + + + + + <% if @current_week_statistic.size ==1 && @pre_week_statistic.present? %> + + + + + + + + + + + + <% end %> + <% @current_week_statistic.each_with_index do |week, index| %> + + + + + + + + + + + + <% end %> + + <% current_week_size = @current_week_statistic.size %> + + + + + + + + + + + +
日期访问量访客数IP数直接访问占比外部链接占比搜索引擎占比自定义
上周合计<%= @pre_week_statistic.map(&:pv).sum %><%= @pre_week_statistic.map(&:visitor).sum %><%= @pre_week_statistic.map(&:ip).sum %><%= (@pre_week_statistic.map(&:source_through).sum.to_f / 7).round(2) %>%<%= (@pre_week_statistic.map(&:source_link).sum.to_f / 7).round(2) %>%<%= (@pre_week_statistic.map(&:source_search).sum.to_f / 7).round(2) %>%<%= ((@pre_week_statistic.map(&:source_custom) - [nil]).sum.to_f / 7).round(2) %>%
<%= week.date %> <%= week.pv %><%= week.visitor %><%= week.ip %><%= week.source_through %>%<%= week.source_link %>%<%= week.source_search %>%<%= week.source_custom.to_f %>%
本周合计<%= @current_week_statistic.map(&:pv).sum %><%= @current_week_statistic.map(&:visitor).sum %><%= @current_week_statistic.map(&:ip).sum %><%= (@current_week_statistic.map(&:source_through).sum.to_f / current_week_size).round(2) %>%<%= (@current_week_statistic.map(&:source_link).sum.to_f / current_week_size).round(2) %>%<%= (@current_week_statistic.map(&:source_search).sum.to_f / current_week_size).round(2) %>%<%= ((@current_week_statistic.map(&:source_custom) - [nil]).sum.to_f / current_week_size).round(2) %>%
+ + +<% unless @access_token.present? && @overview_data.present? %> +
+ 1.百度统计需人工授权,点击去授权 +
+
+ 2.获取百度统计授权码 + +
+<% end %> \ No newline at end of file diff --git a/app/views/admins/dashboards/_baidu_tongji_api.html.erb b/app/views/admins/dashboards/_baidu_tongji_api.html.erb new file mode 100644 index 000000000..3a18dc405 --- /dev/null +++ b/app/views/admins/dashboards/_baidu_tongji_api.html.erb @@ -0,0 +1,67 @@ +<% if @access_token.present? && @overview_data.present? %> +
+ 数据来源百度统计,本周 <%= @overview_data['timeSpan'] %> +
+ + + + + + + + + + + <% pv_count = [] %> + <% visitor_count = [] %> + <% ip_count = [] %> + <% @overview_data['items'][0].each_with_index do |date, index| %> + <% pv = @overview_data['items'][1][index][0] %> + <% visitor = @overview_data['items'][1][index][1] %> + <% ip = @overview_data['items'][1][index][2] %> + + + + + + + + <% pv_count.push(pv) %> + <% visitor_count.push(visitor) %> + <% ip_count.push(ip) %> + <% end %> + + + + + + + + +
日期访问量访客数IP数
<%= date[0] %> <%= pv %><%= visitor %><%= ip %>
合计<%= pv_count %><%= visitor_count %><%= ip_count %>
+ + + + + + + + + + + + <% @source_from_data['items'][1].each_with_index do |source, index| %> + + <% end %> + + +
直接访问占比外部链接占比搜索引擎占比自定义
<%= ((source[0].to_f / @source_from_data['sum'][0][0].to_f) * 100).round(2).to_s %>%
+<% else %> +
+ 1.百度统计需人工授权,点击去授权 +
+
+ 2.获取百度统计授权码 + +
+<% end %> \ No newline at end of file diff --git a/app/views/admins/dashboards/index.html.erb b/app/views/admins/dashboards/index.html.erb index 2d86b4b6c..a55ef55e4 100644 --- a/app/views/admins/dashboards/index.html.erb +++ b/app/views/admins/dashboards/index.html.erb @@ -2,6 +2,38 @@ <% add_admin_breadcrumb('概览', admins_path) %> <% end %> +<% cache "/admin/dashboards/#{Time.now.strftime('%Y-%m-%d')}", :expires_in => 1.days do %> +
+
+
+ +
+ <%@subject_name.each_with_index do |subject, index| %> +
+
+
+
+
+
<%=subject %>
+ <%= @subject_data[index] %> +
+
+
+ +
+
+
+
+
+
+ <% end %> + +
+
+
+
+<%end %> +
@@ -53,6 +85,9 @@
+ <% if EduSetting.get("open_baidu_tongji").to_s == "true" %> + <%= render partial: 'admins/dashboards/baidu_tongji' %> + <% end %>
\ No newline at end of file diff --git a/app/views/admins/identity_verifications/edit.html.erb b/app/views/admins/identity_verifications/edit.html.erb index 580c86f3f..28289e74f 100644 --- a/app/views/admins/identity_verifications/edit.html.erb +++ b/app/views/admins/identity_verifications/edit.html.erb @@ -106,7 +106,7 @@
- <%= f.input :description, as: :text,label: '拒绝理由:(拒绝时请填写拒绝理由,可以为空)', wrapper_html: { class: 'col-md-12' }, input_html: { maxlength: 100, size: 40, class: 'col-md-11' , value: @identity_verification.description } %> + <%= f.input :description, as: :text,label: '拒绝理由:(拒绝时请填写拒绝理由,不可以为空)', wrapper_html: { class: 'col-md-12' }, input_html: { maxlength: 100, size: 40, class: 'col-md-11' , value: @identity_verification.description } %>
<%= f.button :submit, value: '保存', class: 'btn-primary mr-3 px-4' %> diff --git a/app/views/admins/issues_rank/index.html.erb b/app/views/admins/issues_rank/index.html.erb new file mode 100644 index 000000000..45ef1e6d2 --- /dev/null +++ b/app/views/admins/issues_rank/index.html.erb @@ -0,0 +1,37 @@ +<% define_admin_breadcrumbs do %> + <% add_admin_breadcrumb('疑修排行榜', admins_path) %> +<% end %> + + +
+ <%= form_tag(admins_issues_rank_index_path, method: :get, class: 'form-inline search-form flex-1', id: 'issue-rank-date-form') do %> +
+ 开始日期 + +
+
+ 截止日期 + +
+ <% end %> +
+ +
+ <%= render partial: 'admins/issues_rank/shared/data_list', locals: { statistics: @statistics } %> +
+ \ No newline at end of file diff --git a/app/views/admins/issues_rank/index.js.erb b/app/views/admins/issues_rank/index.js.erb new file mode 100644 index 000000000..189206df0 --- /dev/null +++ b/app/views/admins/issues_rank/index.js.erb @@ -0,0 +1 @@ +$('.issue-rank-list-container').html("<%= j( render partial: 'admins/issues_rank/shared/data_list', locals: { statistics: @statistics } ) %>"); \ No newline at end of file diff --git a/app/views/admins/issues_rank/shared/_data_list.html.erb b/app/views/admins/issues_rank/shared/_data_list.html.erb new file mode 100644 index 000000000..9512028c3 --- /dev/null +++ b/app/views/admins/issues_rank/shared/_data_list.html.erb @@ -0,0 +1,26 @@ + + + + + + + + + + + + <% statistics.each_with_index do |item, index| %> + + + + + + + + <% end %> + +
排名项目新增疑修数关闭疑修数当前疑修数量
<%= index + 1%> + "> + <%= "#{item&.project&.owner&.real_name}/#{item&.project&.name}" %> + + <%= item&.issues %><%= item&.closed_issues %><%= item&.project&.issues&.issue_issue.count %>
\ No newline at end of file diff --git a/app/views/admins/page_themes/_form_modal.html.erb b/app/views/admins/page_themes/_form_modal.html.erb index 3d0a97588..5a89bf2bd 100644 --- a/app/views/admins/page_themes/_form_modal.html.erb +++ b/app/views/admins/page_themes/_form_modal.html.erb @@ -14,7 +14,7 @@ - <% state_options = [['hugo', "hugo"], ['jeklly', "jeklly"],['hexo',"hexo"]] %> + <% state_options = [['hugo', "hugo"], ['jeklly', "jeklly"],['hexo',"hexo"],['files',"files"]] %> <%= select_tag('page_theme[language_frame]', options_for_select(state_options), class: 'form-control') %>
<% end%> diff --git a/app/views/admins/page_themes/index.html.erb b/app/views/admins/page_themes/index.html.erb index 4b4392d88..91dd0e34e 100644 --- a/app/views/admins/page_themes/index.html.erb +++ b/app/views/admins/page_themes/index.html.erb @@ -6,7 +6,7 @@ <%= form_tag(admins_page_themes_path, method: :get, class: 'form-inline search-form flex-1', remote: true) do %>
- <% state_options = [['全部',nil], ['hugo', 0], ['jeklly', 1],['hexo',2]] %> + <% state_options = [['全部',nil], ['hugo', 0], ['jeklly', 1],['hexo',2],['files',3]] %> <%= select_tag(:language_frame, options_for_select(state_options), class: 'form-control') %>
<%= submit_tag('搜索', class: 'btn btn-primary ml-3', 'data-disable-with': '搜索中...') %> diff --git a/app/views/admins/projects/index.html.erb b/app/views/admins/projects/index.html.erb index af93598c9..35d931b02 100644 --- a/app/views/admins/projects/index.html.erb +++ b/app/views/admins/projects/index.html.erb @@ -4,7 +4,7 @@
<%= form_tag(admins_projects_path, method: :get, class: 'form-inline search-form flex-1', remote: true) do %> - <%= text_field_tag(:search, params[:search], class: 'form-control col-12 col-md-2 mr-3', placeholder: '项目名称检索') %> + <%= text_field_tag(:search, params[:search], class: 'form-control col-12 col-md-2 mr-3', placeholder: '项目名称/标识检索') %> <%= submit_tag('搜索', class: 'btn btn-primary ml-3', 'data-disable-with': '搜索中...') %> <% end %> diff --git a/app/views/admins/projects_rank/index.html.erb b/app/views/admins/projects_rank/index.html.erb index e8f334f05..2e33a5335 100644 --- a/app/views/admins/projects_rank/index.html.erb +++ b/app/views/admins/projects_rank/index.html.erb @@ -7,11 +7,11 @@ <%= form_tag(admins_projects_rank_index_path, method: :get, class: 'form-inline search-form flex-1', id: 'project-rank-date-form') do %>
开始日期 - +
截止日期 - +
<% end %> <%= link_to '导出', "/项目活跃度排行.xls", class: 'btn btn-primary mr-3' %> diff --git a/app/views/admins/shared/_sidebar.html.erb b/app/views/admins/shared/_sidebar.html.erb index 4db545074..f9bf9ae48 100644 --- a/app/views/admins/shared/_sidebar.html.erb +++ b/app/views/admins/shared/_sidebar.html.erb @@ -81,6 +81,7 @@ <%= sidebar_item_group('#rank-submenu', '活跃度排行', icon: 'calendar') do %>
  • <%= sidebar_item(admins_users_rank_index_path, '用户活跃度排行', icon: 'user', controller: 'admins-users_rank') %>
  • <%= sidebar_item(admins_projects_rank_index_path, '项目活跃度排行', icon: 'database', controller: 'admins-projects_rank') %>
  • +
  • <%= sidebar_item(admins_issues_rank_index_path, '疑修活跃度排行', icon: 'calendar', controller: 'admins-issues_rank') %>
  • <% end %> diff --git a/app/views/api/pm/issue_links/index.json.jbuilder b/app/views/api/pm/issue_links/index.json.jbuilder new file mode 100644 index 000000000..5a563feba --- /dev/null +++ b/app/views/api/pm/issue_links/index.json.jbuilder @@ -0,0 +1,4 @@ + +json.issues @links.each do |link| + json.partial! "api/v1/issues/simple_detail", locals: { issue: link.be_linkable } +end \ No newline at end of file diff --git a/app/views/api/pm/issues/parent_issues.json.jbuilder b/app/views/api/pm/issues/parent_issues.json.jbuilder new file mode 100644 index 000000000..a9a930d6a --- /dev/null +++ b/app/views/api/pm/issues/parent_issues.json.jbuilder @@ -0,0 +1,8 @@ +json.total_count @issues.total_count +json.issues @issues.each do |issue| + if params[:only_name].present? + json.(issue, :id, :subject, :project_issues_index) + else + json.partial! "api/v1/issues/simple_detail", locals: {issue: issue} + end +end \ No newline at end of file diff --git a/app/views/api/v1/attachments/_detail.json.jbuilder b/app/views/api/v1/attachments/_detail.json.jbuilder new file mode 100644 index 000000000..7998812f4 --- /dev/null +++ b/app/views/api/v1/attachments/_detail.json.jbuilder @@ -0,0 +1,11 @@ +json.id attachment.uuid +json.title attachment.title +json.description attachment.description +json.filesize number_to_human_size(attachment.filesize) +json.is_pdf attachment.is_pdf? +json.url attachment.is_pdf? ? download_url(attachment,disposition:"inline") : download_url(attachment) +json.created_on attachment.created_on.strftime("%Y-%m-%d %H:%M:%S") +json.content_type attachment.content_type +json.creator do + json.partial! "api/v1/users/simple_user", locals: {user: attachment.author} +end \ No newline at end of file diff --git a/app/views/api/v1/attachments/_simple_detail.json.jbuilder b/app/views/api/v1/attachments/_simple_detail.json.jbuilder index 3d56eb82f..20ebe5687 100644 --- a/app/views/api/v1/attachments/_simple_detail.json.jbuilder +++ b/app/views/api/v1/attachments/_simple_detail.json.jbuilder @@ -1,7 +1,8 @@ -json.id attachment.id +json.id attachment.uuid json.title attachment.title +json.description attachment.description json.filesize number_to_human_size(attachment.filesize) json.is_pdf attachment.is_pdf? -json.url attachment.is_pdf? ? download_url(attachment,disposition:"inline") : download_url(attachment) +json.url Rails.application.config_for(:configuration)['platform_url'] + (attachment.is_pdf? ? download_url(attachment,disposition:"inline") : download_url(attachment)).to_s json.created_on attachment.created_on.strftime("%Y-%m-%d %H:%M") json.content_type attachment.content_type diff --git a/app/views/api/v1/issues/_detail.json.jbuilder b/app/views/api/v1/issues/_detail.json.jbuilder index 7c3adecc3..8b5ff5949 100644 --- a/app/views/api/v1/issues/_detail.json.jbuilder +++ b/app/views/api/v1/issues/_detail.json.jbuilder @@ -33,6 +33,13 @@ json.author do json.nil! end end +json.changer do + if issue.changer.present? + json.partial! "api/v1/users/simple_user", locals: {user: issue.changer} + else + json.nil! + end +end json.assigners issue.show_assigners.each do |assigner| json.partial! "api/v1/users/simple_user", locals: {user: assigner} end @@ -44,4 +51,17 @@ json.operate_journals_count issue.operate_journals.size json.attachments issue.attachments.each do |attachment| json.partial! "api/v1/attachments/simple_detail", locals: {attachment: attachment} end -json.pull_fixed issue.pull_attached_issues.where(fixed: true).present? \ No newline at end of file +json.pull_fixed issue.pull_attached_issues.where(fixed: true).present? +json.root_id issue.root_id +json.pm_issue_type issue.pm_issue_type +json.pm_sprint_id issue.pm_sprint_id +json.pm_project_id issue.pm_project_id +json.time_scale issue.time_scale +json.child_count issue.child_count +json.project do + if issue.project.present? && issue.project&.owner.present? + json.partial! "api/v1/projects/detail", locals: {project: issue.project} + else + json.nil! + end +end \ No newline at end of file diff --git a/app/views/api/v1/issues/_simple_detail.json.jbuilder b/app/views/api/v1/issues/_simple_detail.json.jbuilder index 4a5a433be..15259baa5 100644 --- a/app/views/api/v1/issues/_simple_detail.json.jbuilder +++ b/app/views/api/v1/issues/_simple_detail.json.jbuilder @@ -6,9 +6,32 @@ json.tags issue.show_issue_tags.each do |tag| json.partial! "api/v1/issues/issue_tags/simple_detail", locals: {tag: tag} end json.status_name issue.issue_status&.name +json.status_id issue.status_id +json.status do + if issue.issue_status.present? + json.partial! "api/v1/issues/statues/simple_detail", locals: {status: issue.issue_status} + else + json.nil! + end +end json.priority_name issue.priority&.name +json.priority_id issue.priority_id +json.priority do + if issue.priority.present? + json.partial! "api/v1/issues/issue_priorities/simple_detail", locals: {priority: issue.priority} + else + json.nil! + end +end json.milestone_name issue.version&.name json.milestone_id issue.fixed_version_id +json.root_id issue.root_id +json.pm_issue_type issue.pm_issue_type +json.pm_sprint_id issue.pm_sprint_id +json.pm_project_id issue.pm_project_id +json.time_scale issue.time_scale +json.child_count issue.child_count + json.author do if issue.user.present? json.partial! "api/v1/users/simple_user", locals: {user: issue.user} diff --git a/app/views/api/v1/issues/index.json.jbuilder b/app/views/api/v1/issues/index.json.jbuilder index cde117fdc..30e26d2ca 100644 --- a/app/views/api/v1/issues/index.json.jbuilder +++ b/app/views/api/v1/issues/index.json.jbuilder @@ -1,12 +1,13 @@ json.total_issues_count @total_issues_count json.opened_count @opened_issues_count json.closed_count @closed_issues_count +json.complete_count @complete_issues_count json.total_count @issues.total_count -json.has_created_issues @project.issues.size > 0 +json.has_created_issues @project.present? ? @project.issues.size > 0 : 0 json.issues @issues.each do |issue| if params[:only_name].present? json.(issue, :id, :subject, :project_issues_index) else - json.partial! "simple_detail", locals: {issue: issue} + json.partial! "api/v1/issues/simple_detail", locals: {issue: issue} end end \ No newline at end of file diff --git a/app/views/api/v1/issues/issue_priorities/_simple_detail.json.jbuilder b/app/views/api/v1/issues/issue_priorities/_simple_detail.json.jbuilder index b7c37147a..f5cf659d8 100644 --- a/app/views/api/v1/issues/issue_priorities/_simple_detail.json.jbuilder +++ b/app/views/api/v1/issues/issue_priorities/_simple_detail.json.jbuilder @@ -1 +1 @@ -json.(priority, :id, :name) +json.(priority, :id, :name,:pm_color) diff --git a/app/views/api/v1/issues/issue_priorities/index.json.jbuilder b/app/views/api/v1/issues/issue_priorities/index.json.jbuilder index c1b8ebb25..04bcd96ff 100644 --- a/app/views/api/v1/issues/issue_priorities/index.json.jbuilder +++ b/app/views/api/v1/issues/issue_priorities/index.json.jbuilder @@ -1,4 +1,4 @@ json.total_count @priorities.total_count json.priorities @priorities.each do |priority| - json.partial! "simple_detail", locals: {priority: priority} + json.partial! "api/v1/issues/issue_priorities/simple_detail", locals: {priority: priority} end \ No newline at end of file diff --git a/app/views/api/v1/issues/issue_tags/index.json.jbuilder b/app/views/api/v1/issues/issue_tags/index.json.jbuilder index 0bd055b57..83fee7f6b 100644 --- a/app/views/api/v1/issues/issue_tags/index.json.jbuilder +++ b/app/views/api/v1/issues/issue_tags/index.json.jbuilder @@ -1,8 +1,8 @@ json.total_count @issue_tags.total_count json.issue_tags @issue_tags.each do |tag| if params[:only_name] - json.partial! "simple_detail", locals: {tag: tag} + json.partial! "api/v1/issues/issue_tags/simple_detail", locals: {tag: tag} else - json.partial! "detail", locals: {tag: tag} + json.partial! "api/v1/issues/issue_tags/detail", locals: {tag: tag} end end \ No newline at end of file diff --git a/app/views/api/v1/issues/journals/_detail.json.jbuilder b/app/views/api/v1/issues/journals/_detail.json.jbuilder index 264997bbd..21040bc0a 100644 --- a/app/views/api/v1/issues/journals/_detail.json.jbuilder +++ b/app/views/api/v1/issues/journals/_detail.json.jbuilder @@ -17,7 +17,7 @@ else json.notes journal.notes json.comments_count journal.comments_count json.children_journals journal.first_ten_children_journals.each do |journal| - json.partial! "children_detail", journal: journal + json.partial! "api/v1/issues/journals/children_detail", journal: journal end json.attachments journal.attachments do |attachment| json.partial! "api/v1/attachments/simple_detail", locals: {attachment: attachment} diff --git a/app/views/api/v1/issues/journals/children_journals.json.jbuilder b/app/views/api/v1/issues/journals/children_journals.json.jbuilder index c0cd04501..ddea195d7 100644 --- a/app/views/api/v1/issues/journals/children_journals.json.jbuilder +++ b/app/views/api/v1/issues/journals/children_journals.json.jbuilder @@ -1,4 +1,4 @@ json.total_count @journals.total_count json.journals @journals do |journal| - json.partial! "children_detail", journal: journal + json.partial! "api/v1/issues/journals/children_detail", journal: journal end \ No newline at end of file diff --git a/app/views/api/v1/issues/journals/create.json.jbuilder b/app/views/api/v1/issues/journals/create.json.jbuilder index 91f3f3174..a28523db1 100644 --- a/app/views/api/v1/issues/journals/create.json.jbuilder +++ b/app/views/api/v1/issues/journals/create.json.jbuilder @@ -1 +1 @@ -json.partial! "detail", journal: @object_result \ No newline at end of file +json.partial! "api/v1/issues/journals/detail", journal: @object_result \ No newline at end of file diff --git a/app/views/api/v1/issues/journals/index.json.jbuilder b/app/views/api/v1/issues/journals/index.json.jbuilder index 453c39c59..b113f39a0 100644 --- a/app/views/api/v1/issues/journals/index.json.jbuilder +++ b/app/views/api/v1/issues/journals/index.json.jbuilder @@ -4,5 +4,5 @@ json.total_comment_journals_count @total_comment_journals_count json.total_count @journals.total_count json.journals @journals do |journal| journal.associate_attachment_container - json.partial! "detail", journal: journal + json.partial! "api/v1/issues/journals/detail", journal: journal end \ No newline at end of file diff --git a/app/views/api/v1/issues/journals/update.json.jbuilder b/app/views/api/v1/issues/journals/update.json.jbuilder index 91f3f3174..a28523db1 100644 --- a/app/views/api/v1/issues/journals/update.json.jbuilder +++ b/app/views/api/v1/issues/journals/update.json.jbuilder @@ -1 +1 @@ -json.partial! "detail", journal: @object_result \ No newline at end of file +json.partial! "api/v1/issues/journals/detail", journal: @object_result \ No newline at end of file diff --git a/app/views/api/v1/issues/statues/_simple_detail.json.jbuilder b/app/views/api/v1/issues/statues/_simple_detail.json.jbuilder index f66b6c95a..c649fc37a 100644 --- a/app/views/api/v1/issues/statues/_simple_detail.json.jbuilder +++ b/app/views/api/v1/issues/statues/_simple_detail.json.jbuilder @@ -1 +1 @@ -json.(status, :id, :name) +json.(status, :id, :name, :pm_color) diff --git a/app/views/api/v1/issues/statues/index.json.jbuilder b/app/views/api/v1/issues/statues/index.json.jbuilder index 9fb60acc2..bff73bd8d 100644 --- a/app/views/api/v1/issues/statues/index.json.jbuilder +++ b/app/views/api/v1/issues/statues/index.json.jbuilder @@ -1,4 +1,4 @@ json.total_count @statues.total_count json.statues @statues.each do |status| - json.partial! "simple_detail", locals: {status: status} + json.partial! "api/v1/issues/statues/simple_detail", locals: {status: status} end \ No newline at end of file diff --git a/app/views/api/v1/project_datasets/index.json.jbuilder b/app/views/api/v1/project_datasets/index.json.jbuilder new file mode 100644 index 000000000..29a84708d --- /dev/null +++ b/app/views/api/v1/project_datasets/index.json.jbuilder @@ -0,0 +1,14 @@ +json.total_count @project_datasets.total_count +json.project_datasets @project_datasets.each do |dataset| + json.(dataset, :id, :title, :description, :paper_content) + json.project do + json.partial! "api/v1/projects/simple_detail", project: dataset.project + end + if dataset.license.present? + json.license do + json.(dataset.license, :name, :content) + end + else + json.license nil + end +end \ No newline at end of file diff --git a/app/views/api/v1/projects/_detail.json.jbuilder b/app/views/api/v1/projects/_detail.json.jbuilder new file mode 100644 index 000000000..03219f822 --- /dev/null +++ b/app/views/api/v1/projects/_detail.json.jbuilder @@ -0,0 +1,26 @@ +json.(project, :id, :name, :identifier, :description, :forked_count, :praises_count, :forked_from_project_id, :is_public) +json.mirror_url project.repository&.mirror_url +json.type project.numerical_for_project_type +json.praised project.praised_by?(current_user) +json.last_update_time render_unix_time(project.updated_on) +json.time_ago time_from_now(project.updated_on) +json.language do + if project.project_language.blank? + json.nil! + else + json.id project.project_language.id + json.name project.project_language.name + end +end +json.category do + if project.project_category.blank? + json.nil! + else + json.id project.project_category.id + json.name project.project_category.name + end +end +json.topics project.project_topics.each do |topic| + json.(topic, :id, :name) +end +json.url project.full_url \ No newline at end of file diff --git a/app/views/api/v1/projects/_simple_detail.json.jbuilder b/app/views/api/v1/projects/_simple_detail.json.jbuilder index 3eadaaf8f..15799aa13 100644 --- a/app/views/api/v1/projects/_simple_detail.json.jbuilder +++ b/app/views/api/v1/projects/_simple_detail.json.jbuilder @@ -1,7 +1,7 @@ if project.present? json.type project.project_type json.(project, - :description, :forked_count, :forked_from_project_id, :identifier, + :id, :description, :forked_count, :forked_from_project_id, :identifier, :issues_count, :pull_requests_count, :invite_code, :website, :platform, :name, :open_devops, :praises_count, :is_public, :status, :watchers_count, :ignore_id, :license_id, :project_category_id, :project_language_id) diff --git a/app/views/api/v1/projects/actions/actions/index.json.jbuilder b/app/views/api/v1/projects/actions/actions/index.json.jbuilder new file mode 100644 index 000000000..530cfc6af --- /dev/null +++ b/app/views/api/v1/projects/actions/actions/index.json.jbuilder @@ -0,0 +1,4 @@ +json.total_count @data.size +json.files @data.each do |file| + json.name file["Name"] +end \ No newline at end of file diff --git a/app/views/api/v1/projects/actions/runs/index.json.jbuilder b/app/views/api/v1/projects/actions/runs/index.json.jbuilder new file mode 100644 index 000000000..9122ef9cf --- /dev/null +++ b/app/views/api/v1/projects/actions/runs/index.json.jbuilder @@ -0,0 +1,23 @@ +json.total_data @result_object[:total_data].to_i +if @result_object[:data]["Runs"].present? + json.runs @result_object[:data]["Runs"].each do |run| + json.workflow run["WorkflowID"] + json.index run["Index"] + json.title run["Title"] + json.trigger_user do + json.partial! 'api/v1/users/commit_user', locals: { user: render_cache_commit_author(run['TriggerUser']), name: run['TriggerUser']['Name'] } + end + + if run["Ref"].starts_with?("refs/tags") + json.ref run["Ref"].gsub!("/refs/tags/", "") + else + json.ref run["Ref"].gsub!("refs/heads/", "") + end + + json.status run["Status"] + json.time_ago time_from_now(run["Created"]) + json.holding_time run["Status"] == 6 ? Time.now.to_i - run["Created"] : run["Stopped"] - run["Created"] + end +else + json.runs [] +end \ No newline at end of file diff --git a/app/views/api/v1/projects/actions/runs/job_show.json.jbuilder b/app/views/api/v1/projects/actions/runs/job_show.json.jbuilder new file mode 100644 index 000000000..bf5ce048a --- /dev/null +++ b/app/views/api/v1/projects/actions/runs/job_show.json.jbuilder @@ -0,0 +1,16 @@ +json.state do + json.run do + json.title @result_object["state"]["run"]["title"] + json.status @result_object["state"]["run"]["status"] + json.done @result_object["state"]["run"]["done"] + json.jobs @result_object["state"]["run"]["jobs"] + json.current_job do + json.title @result_object["state"]["currentJob"]["title"] + json.detail @result_object["state"]["currentJob"]["detail"] + json.steps @result_object["state"]["currentJob"]["steps"] + end + end +end +json.logs do + json.steps_log @result_object["logs"]["stepsLog"] +end \ No newline at end of file diff --git a/app/views/api/v1/projects/branches/_simple_gitea_detail.json.jbuilder b/app/views/api/v1/projects/branches/_simple_gitea_detail.json.jbuilder index c9235bdb4..ff0f0618a 100644 --- a/app/views/api/v1/projects/branches/_simple_gitea_detail.json.jbuilder +++ b/app/views/api/v1/projects/branches/_simple_gitea_detail.json.jbuilder @@ -23,4 +23,14 @@ json.commit_time branch['commit']['timestamp'] json.default_branch default_branch || nil json.http_url render_http_url(@project) json.zip_url render_zip_url(@owner, @project.repository, branch['name']) -json.tar_url render_tar_url(@owner, @project.repository, branch['name']) \ No newline at end of file +json.tar_url render_tar_url(@owner, @project.repository, branch['name']) +json.branch_id branch['id'] +json.is_deleted branch['is_deleted'] +json.deleted_unix branch['deleted_unix'] +json.deleted_by do + if branch['is_deleted'] + json.partial! 'api/v1/users/commit_user', locals: { user: render_cache_commit_author(branch['deleted_by']), name: branch['deleted_by']['name'] } + else + json.nil! + end +end \ No newline at end of file diff --git a/app/views/api/v1/projects/commits/recent.json.jbuilder b/app/views/api/v1/projects/commits/recent.json.jbuilder new file mode 100644 index 000000000..38a367d4c --- /dev/null +++ b/app/views/api/v1/projects/commits/recent.json.jbuilder @@ -0,0 +1,16 @@ +json.total_count @result_object[:total_data].to_i +json.ssh_url @object_detail['ssh_url'] +json.clone_url @object_detail['clone_url'] +json.commits @result_object[:data].each do |commit| + json.sha commit['sha'] + json.author do + json.partial! 'api/v1/users/commit_user', locals: { user: render_cache_commit_author(commit['commit']['author']), name: commit['commit']['author']['name'] } + end + + json.committer do + json.partial! 'api/v1/users/commit_user', locals: { user: render_cache_commit_author(commit['commit']['committer']), name: commit['commit']['committer']['name'] } + end + json.commit_message commit['commit']['message'] + json.time_from_now time_from_now(commit['created']) + json.parent_shas commit['parents'].map{|x|x['sha']} +end \ No newline at end of file diff --git a/app/views/api/v1/projects/compare.json.jbuilder b/app/views/api/v1/projects/compare.json.jbuilder index 2d20c51e0..f45bceb86 100644 --- a/app/views/api/v1/projects/compare.json.jbuilder +++ b/app/views/api/v1/projects/compare.json.jbuilder @@ -14,5 +14,9 @@ json.commits @result_object['Commits'] do |commit| json.parent_shas commit['Sha']['ParentShas'] end json.diff do - json.partial! "api/v1/projects/simple_gitea_diff_detail", diff: @result_object['Diff'] + if @result_object['Diff'].present? + json.partial! "api/v1/projects/simple_gitea_diff_detail", diff: @result_object['Diff'] + else + json.nil! + end end \ No newline at end of file diff --git a/app/views/api/v1/projects/datasets/show.json.jbuilder b/app/views/api/v1/projects/datasets/show.json.jbuilder new file mode 100644 index 000000000..6b18d8015 --- /dev/null +++ b/app/views/api/v1/projects/datasets/show.json.jbuilder @@ -0,0 +1,6 @@ +json.(@project_dataset, :id, :title, :description, :license_id, :paper_content) +json.license_name @project_dataset&.license&.name +json.attachment_total_count @attachments.total_count +json.attachments @attachments do |at| + json.partial! "api/v1/attachments/detail", locals: {attachment: at} +end \ No newline at end of file diff --git a/app/views/api/v1/projects/git/trees.json.jbuilder b/app/views/api/v1/projects/git/trees.json.jbuilder index 0eb08f048..b560fff2b 100644 --- a/app/views/api/v1/projects/git/trees.json.jbuilder +++ b/app/views/api/v1/projects/git/trees.json.jbuilder @@ -1,6 +1,6 @@ -json.total_count @result_object['total_count'] -json.sha @result_object['sha'] -json.entries @result_object['tree'].each do |entry| +json.total_count @result_object[:data]['total_count'].to_i +json.sha @result_object[:data]['sha'] +json.entries @result_object[:data]['tree'].each do |entry| json.name entry['path'] json.mode entry['mode'] json.type entry['type'] === 'blob' ? 'file' : 'dir' diff --git a/app/views/api/v1/projects/pipelines/build_node.yaml.erb b/app/views/api/v1/projects/pipelines/build_node.yaml.erb new file mode 100644 index 000000000..ded111099 --- /dev/null +++ b/app/views/api/v1/projects/pipelines/build_node.yaml.erb @@ -0,0 +1,9 @@ +steps: + - name: <%=self.node.name %> + uses: <%=self.full_name %> + <%if self.action_node_inputs.present? %> + with: + <% self.action_node_inputs.each do |input| %> + <%=input.name %>: '' + <%end %> + <%end %> \ No newline at end of file diff --git a/app/views/api/v1/projects/pipelines/build_pipeline.yaml.erb b/app/views/api/v1/projects/pipelines/build_pipeline.yaml.erb new file mode 100644 index 000000000..149d7a6be --- /dev/null +++ b/app/views/api/v1/projects/pipelines/build_pipeline.yaml.erb @@ -0,0 +1,62 @@ +# action name +name: <%=@pipeline_name %> + +# 什么时候触发这个workflow +on: + <%@on_nodes.each do |node| %> + <%if node.name.to_s.include?("on-push") %> + push: + <% node.input_values.each_key do |key| %> + <%=key %>: + <% if node.input_values[key].blank? %> + - * + <% else %> + <% node.input_values[key].to_s.split(",").each do |val| %> + - <%=val %> + <% end %> + <% end %> + <% end %> + <% end %> + <%if node.name.to_s.include?("on-pull_request") %> + pull_request: + <% node.input_values.each_key do |key| %> + <%=key %>: + <% if node.input_values[key].blank? %> + - * + <% else %> + <% node.input_values[key].to_s.split(",").each do |val| %> + - <%=val %> + <% end %> + <% end %> + <% end %> + <% end %> + <%if node.name.to_s.include?("on-schedule") %> + schedule: + <% node.input_values.each_key do |key| %> + - <%=key %>: "<%=node.input_values[key] %>" + <% end %> + <% end %> + <% end %> + +jobs: + job1: + # 运行环境 + runs-on: 'ubuntu-latest' + steps: + <%@steps_nodes.each do |node| %> + - name: <%=node.cust_name || node.name %> + <% if node.name !="shell" %> + uses: <%=node.full_name %> + <% end %> + <%if node.input_values.present? %> + with: + <% node.input_values.each_key do |key| %> + <%=key %>: '<%=node.input_values[key] %>' + <%end %> + <%end %> + <%if node.run_values.present? %> + <% node.run_values.each_key do |key| %> + <%=key %>: '<%=node.run_values[key] %>' + <%end %> + <%end %> + <% end %> diff --git a/app/views/api/v1/projects/pipelines/index.json.jbuilder b/app/views/api/v1/projects/pipelines/index.json.jbuilder new file mode 100644 index 000000000..7c3f2a405 --- /dev/null +++ b/app/views/api/v1/projects/pipelines/index.json.jbuilder @@ -0,0 +1,8 @@ +json.status 0 +json.message "success" + +json.pipelines @pipelines.each do |pip| + json.extract! pip, :id, :pipeline_name, :pipeline_status, :description, :file_name, :is_graphic_design, + :repo_name, :repo_identifier, :branch, :event, :sha, :disable, :json, :yaml, :created_at, :updated_at + # json.project +end diff --git a/app/views/api/v1/projects/pipelines/show.json.jbuilder b/app/views/api/v1/projects/pipelines/show.json.jbuilder new file mode 100644 index 000000000..d82015ade --- /dev/null +++ b/app/views/api/v1/projects/pipelines/show.json.jbuilder @@ -0,0 +1,5 @@ +json.status 0 +json.message "success" +json.extract! @pipeline, :id, :pipeline_name, :pipeline_status, :description, :file_name, :is_graphic_design, + :repo_name, :repo_identifier, :branch, :event, :sha, :disable, :json, :yaml, :created_at, :updated_at +# json.project \ No newline at end of file diff --git a/app/views/api/v1/projects/pipelines/test.yaml.erb b/app/views/api/v1/projects/pipelines/test.yaml.erb new file mode 100644 index 000000000..3ec890230 --- /dev/null +++ b/app/views/api/v1/projects/pipelines/test.yaml.erb @@ -0,0 +1,89 @@ +name: Check dist<%= @name %> + +# action name +name: Test with Junit + +# 什么时候触发这个workflow +on: + # push 到master分之的时候 这里可以指定多个 + #push: + # branches: + # - master +# paths-ignore: +# - '**.md' + # pull request 到master分之的时候, 这里可以指定多个 + #pull_request: + # branches: + # - master +# paths-ignore: +# - '**.md' + # 定时调度执行 + schedule: + - cron: '26 10,11 * * *' +env: + https_proxy: http://172.20.32.253:3128 + http_proxy: http://172.20.32.253:3128 + +# 一个workflow可以由多个job组成,多个job可以并行运行 +jobs: + junit: + strategy: + matrix: + # 指定jdk 版本。可以指定多个版本 比如[8,11,17] + java: [11] + # 指定运行 os 版本 也是多个 + os: [ 'ubuntu-latest' ] + # 运行环境,这里就是上面定义的多个 os + runs-on: 'ubuntu-latest' + + steps: + # 将job的工作目录指向$GITHUB_WORSPACES checkout@v2比较旧不推荐使用 + - name: Checkout codes + uses: actions/checkout@v3 + #- name: Install Java and Maven + # uses: actions/setup-java@v3 + # with: + # java-version: '11' + # distribution: 'temurin' + # 设置jdk环境 + - name: download latest temurin JDK + id: download_latest_jdk + env: + HTTPS_PROXY: http://172.20.32.253:3128 + HTTP_PROXY: http://172.20.32.253:3128 + #run: curl -o https://testgitea2.trustie.net/xxq250/licensee-identify-api-sss/raw/branch/master/openlogic-openjdk-11.0.22+7-linux-x64.tar.gz + run: wget -O $RUNNER_TEMP/java_package.tar.gz "http://172.20.32.202:10082/xxq250/licensee-identify-api-sss/raw/branch/master/OpenJDK11U-jdk_x64_linux_hotspot_11.0.22_7.tar.gz" + - uses: actions/setup-java@v4 + with: + distribution: 'jdkfile' + jdkFile: ${{ runner.temp }}/java_package.tar.gz + java-version: '11.0.0' + architecture: x64 + mvn-toolchain-vendor: 'Oracle' + # 设置maven 仓库缓存 避免每次构建时都重新下载依赖 + - name: Cache local Maven repository + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Test java version + run: java -version + - name: Set up Maven + uses: stCarolas/setup-maven@v5 + with: + maven-version: 3.8.2 + - name: Setup Maven mirrors + uses: s4u/maven-settings-action@v3.0.0 + with: + mirrors: '[{"id": "alimaven", "name": "aliyun maven", "mirrorOf": "central", "url": "http://172.20.32.181:30005/repository/aliyun-maven/"}]' + - name: env show + run: env + - name: cat maven-settings.xml + run: cat /root/.m2/settings.xml + - name: Test with Maven + run: mvn clean test -B -U + env: + https_proxy: http://172.20.32.253:3128 + http_proxy: http://172.20.32.253:3128 diff --git a/app/views/api/v1/projects/sync_repositories/branches.json.jbuilder b/app/views/api/v1/projects/sync_repositories/branches.json.jbuilder new file mode 100644 index 000000000..f040c6476 --- /dev/null +++ b/app/views/api/v1/projects/sync_repositories/branches.json.jbuilder @@ -0,0 +1,12 @@ +json.total_count @group_sync_repository_branch.count +json.sync_repository_branches @group_sync_repository_branch.each do |item| + json.gitlink_branch_name item.gitlink_branch_name + json.external_branch_name item.external_branch_name + branches = @sync_repository_branches.joins(:sync_repository).where(sync_repositories: {type: item.type}, gitlink_branch_name: item.gitlink_branch_name, external_branch_name: item.external_branch_name).order(updated_at: :desc) + branch = branches.first + json.type branch&.sync_repository&.type + json.sync_time branch.sync_time.present? ? branch.sync_time.strftime("%Y-%m-%d %H:%M:%S") : nil + json.sync_status branch.sync_status + json.enable branch.enable + json.reposync_branch_ids branches.pluck(:reposync_branch_id) +end \ No newline at end of file diff --git a/app/views/api/v1/projects/sync_repositories/create.json.jbuilder b/app/views/api/v1/projects/sync_repositories/create.json.jbuilder new file mode 100644 index 000000000..20bb70c66 --- /dev/null +++ b/app/views/api/v1/projects/sync_repositories/create.json.jbuilder @@ -0,0 +1,5 @@ +json.gitlink_repo_address "#{EduSetting.get("gitlink_repo_domain")}/#{@project.owner&.login}/#{@project.identifier}.git" +json.external_repo_address @sync_repository1.external_repo_address +json.sync_granularity @sync_repository1.sync_granularity +json.gitlink_branch_name @sync_repository_branch1&.gitlink_branch_name +json.external_branch_name @sync_repository_branch1&.external_branch_name \ No newline at end of file diff --git a/app/views/api/v1/projects/sync_repositories/history.json.jbuilder b/app/views/api/v1/projects/sync_repositories/history.json.jbuilder new file mode 100644 index 000000000..01546b292 --- /dev/null +++ b/app/views/api/v1/projects/sync_repositories/history.json.jbuilder @@ -0,0 +1,12 @@ +json.total_count @total_count +json.gitlink_branch_name @branch&.gitlink_branch_name +json.external_type @branch&.sync_repository&.type +json.external_branch_name @branch&.external_branch_name +json.logs @reposync_branch_logs.each do |log| + type = log['repo_name'].start_with?('gitee') ? 'gitee' : 'github' + json.change_from log['sync_direct'] == "to_inter" ? type : 'gitlink' + json.commit_id log['commit_id'] + json.sync_time log['update_at'] + json.log log['log'] + json.sync_status log['log'].include?("************ 分支同步完成 ************") ? 'success' : "failure" +end \ No newline at end of file diff --git a/app/views/api/v1/projects/sync_repositories/index.json.jbuilder b/app/views/api/v1/projects/sync_repositories/index.json.jbuilder new file mode 100644 index 000000000..84bbdde5e --- /dev/null +++ b/app/views/api/v1/projects/sync_repositories/index.json.jbuilder @@ -0,0 +1,8 @@ +json.total_count @group_sync_repository.keys.count +json.sync_repositories @group_sync_repository.each do |key| + json.type key[0][0] + json.external_repo_address key[0][1] + json.sync_granularity key[0][2] + json.external_token key[0][3] + json.sync_repository_ids @sync_repositories.where(type: key[0][0], external_repo_address: key[0][1], sync_granularity: key[0][2]).pluck(:id) +end \ No newline at end of file diff --git a/app/views/api/v1/users/_simple_user.json.jbuilder b/app/views/api/v1/users/_simple_user.json.jbuilder index ef45bcd94..5e8e970e7 100644 --- a/app/views/api/v1/users/_simple_user.json.jbuilder +++ b/app/views/api/v1/users/_simple_user.json.jbuilder @@ -3,7 +3,7 @@ if user.present? json.type user.type json.name user.real_name json.login user.login - json.image_url url_to_avatar(user) + json.image_url Rails.application.config_for(:configuration)['platform_url'] + "/" + url_to_avatar(user).to_s else json.nil! end \ No newline at end of file diff --git a/app/views/organizations/organization_users/_detail.json.jbuilder b/app/views/organizations/organization_users/_detail.json.jbuilder index c7572971b..7590da02e 100644 --- a/app/views/organizations/organization_users/_detail.json.jbuilder +++ b/app/views/organizations/organization_users/_detail.json.jbuilder @@ -1,7 +1,14 @@ -json.id org_user.id +json.id user&.id json.user do - json.partial! "organizations/user_detail", user: org_user.user + json.partial! "organizations/user_detail", user: user end -json.team_names org_user.teams.pluck(:nickname) -json.created_at org_user.created_at.strftime("%Y-%m-%d") +json.team_names user.teams.where("teams.organization_id=?", organization.id).pluck(:nickname) +join_date = if user.organization_users.find_by(:organization_id => organization.id).present? + user.organization_users.find_by(:organization_id => organization.id).created_at.strftime("%Y-%m-%d") + elsif user.members.joins(:project).find_by(project: organization.projects).present? + user.members.joins(:project).find_by(project: organization.projects).created_on.strftime("%Y-%m-%d") + else + user.created_on.strftime("%Y-%m-%d") + end +json.created_at join_date diff --git a/app/views/organizations/organization_users/index.json.jbuilder b/app/views/organizations/organization_users/index.json.jbuilder index 9f1f278bc..9241af184 100644 --- a/app/views/organizations/organization_users/index.json.jbuilder +++ b/app/views/organizations/organization_users/index.json.jbuilder @@ -1,5 +1,5 @@ -json.total_count @organization_users.total_count -json.organization_users @organization_users do |org_user| - next if org_user.user.blank? - json.partial! "detail", org_user: org_user, organization: @organization +json.total_count @users.total_count +json.organization_users @users do |user| + next if user.blank? + json.partial! "detail", user: user, organization: @organization end diff --git a/app/views/organizations/projects/index.json.jbuilder b/app/views/organizations/projects/index.json.jbuilder index 5a4615fc7..c395732f1 100644 --- a/app/views/organizations/projects/index.json.jbuilder +++ b/app/views/organizations/projects/index.json.jbuilder @@ -6,4 +6,24 @@ json.projects @projects.each do |project| json.praised project.praised_by?(current_user) json.last_update_time render_unix_time(project.updated_on) json.time_ago time_from_now(project.updated_on) + json.language do + if project.project_language.blank? + json.nil! + else + json.id project.project_language.id + json.name project.project_language.name + end + end + json.category do + if project.project_category.blank? + json.nil! + else + json.id project.project_category.id + json.name project.project_category.name + end + end + json.topics project.project_topics.each do |topic| + json.(topic, :id, :name) + end + json.url project.full_url end \ No newline at end of file diff --git a/app/views/projects/index.json.jbuilder b/app/views/projects/index.json.jbuilder index d3b96ab18..a07dd6c1a 100644 --- a/app/views/projects/index.json.jbuilder +++ b/app/views/projects/index.json.jbuilder @@ -17,6 +17,7 @@ json.projects @projects do |project| json.forked_from_project_id project.forked_from_project_id json.open_devops project.open_devops? json.platform project.platform + json.has_dataset project.has_menu_permission("dataset") && project.project_dataset.present? json.author do if project.educoder? project_educoder = project.project_educoder diff --git a/app/views/repositories/detail.json.jbuilder b/app/views/repositories/detail.json.jbuilder index d4299a1ff..0e5d850a5 100644 --- a/app/views/repositories/detail.json.jbuilder +++ b/app/views/repositories/detail.json.jbuilder @@ -1,3 +1,4 @@ +json.empty @result[:repo]["empty"] json.content @project.content json.website @project.website json.lesson_url @project.lesson_url diff --git a/app/views/settings/show.json.jbuilder b/app/views/settings/show.json.jbuilder index 1027be670..c3e515fd2 100644 --- a/app/views/settings/show.json.jbuilder +++ b/app/views/settings/show.json.jbuilder @@ -36,7 +36,8 @@ json.setting do json.subject_banner_url default_setting.subject_banner_url&.[](1..-1) json.course_banner_url default_setting.course_banner_url&.[](1..-1) - json.competition_banner_url default_setting.competition_banner_url&.[](1..-1) + json.competition_banner_url EduSetting.get("competition_banner_url").to_s + json.competition_banner_href EduSetting.get("competition_banner_href").to_s json.moop_cases_banner_url default_setting.moop_cases_banner_url&.[](1..-1) json.oj_banner_url default_setting.oj_banner_url&.[](1..-1) diff --git a/app/views/template_message_settings/_detail.json.jbuilder b/app/views/template_message_settings/_detail.json.jbuilder index d85a4c4ea..5f6a0bf62 100644 --- a/app/views/template_message_settings/_detail.json.jbuilder +++ b/app/views/template_message_settings/_detail.json.jbuilder @@ -3,6 +3,7 @@ json.type_name type.constantize.type_name json.total_settings_count count json.settings do json.array! type.constantize.openning.limit(100).each do |setting| - json.(setting, :name, :key, :notification_disabled, :email_disabled) + json.(setting, :name, :key, :email_disabled) + json.notification_disabled false end end \ No newline at end of file diff --git a/app/views/version_releases/_version_release.json.jbuilder b/app/views/version_releases/_version_release.json.jbuilder index 1ccdbe617..9835db152 100644 --- a/app/views/version_releases/_version_release.json.jbuilder +++ b/app/views/version_releases/_version_release.json.jbuilder @@ -16,6 +16,12 @@ json.user_login user&.login json.image_url user.present? ? url_to_avatar(user) : "" json.attachments do json.array! version.try(:attachments) do |attachment| - json.partial! "attachments/attachment_simple", locals: {attachment: attachment} + # json.partial! "attachments/attachment_simple", locals: {attachment: attachment} + json.id attachment.id + json.title attachment.title + json.filesize number_to_human_size attachment.filesize + json.description attachment.description + json.is_pdf attachment.is_pdf? + json.url "/#{@owner.login}/#{@repository.identifier}/releases/download/#{version&.tag_name}/#{attachment.filename}" end end \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index c36279e38..4cbefed14 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,7 +41,7 @@ module Gitlink config.middleware.insert_before 0, Rack::Cors do allow do # origins '*' - origins /http:\/\/localhost(:\d+)?\z/, /^(http|https):\/\/(.*(gitlink.org.cn))$/, /^(http|https):\/\/(.*(trustie.net))$/ + origins /http:\/\/localhost(:\d+)?\z/, /http:\/\/172.20.32.201(:\d+)?\z/, /http:\/\/172.20.32.202(:\d+)?\z/, /^(http|https):\/\/(.*(gitlink.org.cn))$/, /^(http|https):\/\/(.*(trustie.net))$/ # location of your api resource '/*', :headers => :any, :methods => [:get, :post, :delete, :options, :put, :patch], credentials: true end diff --git a/config/environments/development.rb b/config/environments/development.rb index 1adbdde7c..199481259 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -62,8 +62,8 @@ Rails.application.configure do # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. - config.file_watcher = ActiveSupport::EventedFileUpdateChecker - + config.file_watcher = ActiveSupport::FileUpdateChecker + config.reload_classes_only_on_change = false config.action_controller.perform_caching = true config.action_mailer.delivery_method = :smtp diff --git a/config/initializers/sonarqube.rb b/config/initializers/sonarqube.rb new file mode 100644 index 000000000..30754885c --- /dev/null +++ b/config/initializers/sonarqube.rb @@ -0,0 +1,8 @@ +sonarqube_config = Rails.application.config_for(:configuration)['sonarqube'] + +Sonarqube.configure do |config| + config.endpoint = sonarqube_config["url"] # API endpoint URL, default: ENV['SONARQUBE_API_ENDPOINT'] + config.private_token = sonarqube_config["secret"] # user's private token, default: ENV['SONARQUBE_API_PRIVATE_TOKEN'] + # Optional + # config.user_agent = 'Custom User Agent' # user agent, default: 'Sonarqube Ruby Gem [version]' +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 7694a77c0..8c7a59b53 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,11 +19,13 @@ Rails.application.routes.draw do get 'attachments/entries/get_file', to: 'attachments#get_file' get 'attachments/download/:id', to: 'attachments#show' get 'attachments/download/:id/:filename', to: 'attachments#show' + get ':owner/:repo/releases/download/:tag_name/:filename', to: 'version_releases#download', constraints: { repo: /[^\/]+/, tag_name: /[^\/]+/, filename: /[^\/]+/ } get 'check_pr_url',to: "settings#check_url" # get 'auth/qq/callback', to: 'oauth/qq#create' get 'auth/failure', to: 'oauth/base#auth_failure' get 'auth/cas/callback', to: 'oauth/cas#create' + get 'auth/acge/callback', to: "oauth/acge#create" get 'auth/:provider/callback', to: 'oauth/callbacks#create' get 'oauth/bind', to: 'oauth/educoder#bind' @@ -127,7 +129,6 @@ Rails.application.routes.draw do # blockchain related routes get 'users/blockchain/balance', to: 'users#blockchain_balance' - get 'projects/mp_show', to: 'projects#mp_show' post 'users/blockchain/balance_project', to: 'users#blockchain_balance_one_project' post 'users/blockchain/transfer', to: 'users#blockchain_transfer' post 'users/blockchain/exchange', to: 'users#blockchain_exchange' @@ -509,6 +510,19 @@ Rails.application.routes.draw do end end + # scope module: :action do + # + # end + + namespace :action do + resources :nodes do + resources :node_inputs + resources :node_selects + end + resources :node_types + resources :templates + end + # Project Area START scope "/:owner/:repo",constraints: { repo: /[^\/]+/ } do scope do @@ -795,6 +809,8 @@ Rails.application.routes.draw do namespace :admins do mount Sidekiq::Web => '/sidekiq' get '/', to: 'dashboards#index' + get '/baidu_tongji', to: 'dashboards#baidu_tongji' + get '/baidu_tongji_auth', to: 'dashboards#baidu_tongji_auth' namespace :topic do resources :activity_forums resources :banners @@ -820,6 +836,7 @@ Rails.application.routes.draw do resources :identity_verifications resources :site_pages resources :page_themes + resources :issues_rank, only: [:index] resources :projects_rank, only: [:index] resources :sites resources :edu_settings @@ -1091,15 +1108,15 @@ Rails.application.routes.draw do resources :sub_repertoires, only: [:index, :create, :edit, :update, :destroy] resources :tag_repertoires, only: [:index, :create, :edit, :update, :destroy] - resources :salesmans, only: [:index, :create, :edit, :update, :destroy] do - post :batch_add, on: :collection - end - resources :salesman_channels, only: [:index, :create, :edit, :update, :destroy] do - post :batch_add, on: :collection - end - resources :salesman_customers, only: [:index, :create, :edit, :update, :destroy] do - post :batch_add, on: :collection - end + # resources :salesmans, only: [:index, :create, :edit, :update, :destroy] do + # post :batch_add, on: :collection + # end + # resources :salesman_channels, only: [:index, :create, :edit, :update, :destroy] do + # post :batch_add, on: :collection + # end + # resources :salesman_customers, only: [:index, :create, :edit, :update, :destroy] do + # post :batch_add, on: :collection + # end end diff --git a/config/routes/api.rb b/config/routes/api.rb index baa66412e..998d699bd 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -1,15 +1,54 @@ defaults format: :json do - namespace :api do - namespace :v1 do + namespace :api do + namespace :pm do + resources :issues do + collection do + patch :batch_update + delete :batch_destroy + get :priorities + get :tags + get :statues + end + member do + get :link_index + get :parent_issues + end + resources :issue_links - resources :users, only: [:index] do + resources :journals do + member do + get :children_journals + end + end + end + resources :issue_tags + resources :sprint_issues, only: [:index] do + collection do + get :statistics + + get :burndown_charts + post :complete + end + end + resources :projects do + collection do + get :convert + get :issues_count + get :statistics + get :polyline + end + end + end + + namespace :v1 do + resources :users, only: [:index] do collection do post :check_user_id post :check_user_login end end - scope ':owner' do + scope ':owner' do resource :users, path: '/', only: [:update, :edit, :destroy] do collection do get :send_email_vefify_code @@ -37,6 +76,21 @@ defaults format: :json do collection do get :compare get :blame + + end + end + resource :sonarqubes, only: [:index] do + collection do + get :issues_search + get :ce_component + get :sources_issue_snippet + get :rules_show + get :measures_search_history + get :measures_component + + post :sonar_initialize + post :insert_file + end end @@ -44,6 +98,7 @@ defaults format: :json do collection do patch :batch_update delete :batch_destroy + post :pm_create end member do @@ -55,30 +110,48 @@ defaults format: :json do end end end + + scope module: :issues do - resources :issue_tags, except: [:new, :edit] do - collection do - get :pm_index - end - end + resources :issue_tags, except: [:new, :edit] resources :milestones, except: [:new, :edit] - resources :issue_statues, only: [:index], controller: '/api/v1/issues/statues' do - collection do - get :pm_index - end - end + resources :issue_statues, only: [:index], controller: '/api/v1/issues/statues' resources :issue_authors, only: [:index], controller: '/api/v1/issues/authors' resources :issue_assigners, only: [:index], controller: '/api/v1/issues/assigners' - resources :issue_priorities, only: [:index] do - collection do - get :pm_index - end - end + resources :issue_priorities, only: [:index] end # projects文件夹下的 scope module: :projects do - resources :pulls, module: 'pulls' do + resources :sync_repositories, only: [:create, :index] do + collection do + post :update_info + post :sync + get :branches + post :change_enable + post :unbind + get :history + post :create_branch + end + end + resource :dataset, only: [:create, :update, :show] + resources :actions, module: 'actions' do + collection do + post :disable + post :enable + resources :runs, only: [:index] do + post '/jobs/:job', to: 'runs#job_show' + post '/rerun', to: 'runs#rerun' + post '/jobs/:job/rerun', to: 'runs#job_rerun' + get '/jobs/:job/logs', to: 'runs#job_logs' + end + end + end + resources :pipelines do + post :build_yaml, on: :collection + post :save_yaml, on: :collection + end + resources :pulls, module: 'pulls' do resources :versions, only: [:index] do member do get :diff @@ -97,7 +170,10 @@ defaults format: :json do end resources :branches, param: :name, only:[:index, :create, :destroy] do collection do + get :gitee + get :github get :all + post :restore patch :update_default_branch end end @@ -107,7 +183,11 @@ defaults format: :json do delete 'tags/*name', to: "tags#destroy", via: :all get 'tags/*name', to: "tags#show", via: :all - resources :commits, only: [:index] + resources :commits, only: [:index] do + collection do + get :recent + end + end resources :code_stats, only: [:index] resources :contributors, only: [:index] do collection do @@ -130,7 +210,8 @@ defaults format: :json do resources :projects, only: [:index] resources :project_topics, only: [:index, :create, :destroy] - + resources :project_datasets, only: [:index] + resources :gitlink_competition_applies, only: [:create] end end diff --git a/config/sidekiq_cron.yml b/config/sidekiq_cron.yml index 0ec8f997e..72a69bbad 100644 --- a/config/sidekiq_cron.yml +++ b/config/sidekiq_cron.yml @@ -5,10 +5,15 @@ sync_gitea_repo_update_time: delay_expired_issue: cron: "0 0 * * *" - class: "DelayExpiredIssueJob" + class: "DelayExpiredIssueAndMilestoneJob" queue: message create_daily_project_statistics: cron: "0 1 * * *" class: "DailyProjectStatisticsJob" - queue: cache \ No newline at end of file + queue: cache + +daily_platform_statistics: + cron: "0 1 * * *" + class: "DailyPlatformStatisticsJob" + queue: default \ No newline at end of file diff --git a/db/migrate/20231031070603_add_pm_issue_type_to_issues.rb b/db/migrate/20231031070603_add_pm_issue_type_to_issues.rb new file mode 100644 index 000000000..846a760f1 --- /dev/null +++ b/db/migrate/20231031070603_add_pm_issue_type_to_issues.rb @@ -0,0 +1,5 @@ +class AddPmIssueTypeToIssues < ActiveRecord::Migration[5.2] + def change + add_column :issues, :pm_issue_type, :integer + end +end diff --git a/db/migrate/20231101090823_add_pm_project_id.rb b/db/migrate/20231101090823_add_pm_project_id.rb new file mode 100644 index 000000000..81ba7130b --- /dev/null +++ b/db/migrate/20231101090823_add_pm_project_id.rb @@ -0,0 +1,5 @@ +class AddPmProjectId < ActiveRecord::Migration[5.2] + def change + add_column :issue_tags, :pm_project_id, :integer + end +end diff --git a/db/migrate/20231107003833_add_hour_to_issues.rb b/db/migrate/20231107003833_add_hour_to_issues.rb new file mode 100644 index 000000000..d0a3e4856 --- /dev/null +++ b/db/migrate/20231107003833_add_hour_to_issues.rb @@ -0,0 +1,5 @@ +class AddHourToIssues < ActiveRecord::Migration[5.2] + def change + add_column :issues, :time_scale, :decimal, precision: 10, scale: 2, default: 0.00 + end +end diff --git a/db/migrate/20231107072541_create_pm_links.rb b/db/migrate/20231107072541_create_pm_links.rb new file mode 100644 index 000000000..84cdb00e8 --- /dev/null +++ b/db/migrate/20231107072541_create_pm_links.rb @@ -0,0 +1,12 @@ +class CreatePmLinks < ActiveRecord::Migration[5.2] + def change + create_table :pm_links do |t| + t.string :be_linkable_type, null: false + t.integer :be_linkable_id, null: false + + t.string :linkable_type, null: false, index: true + t.integer :linkable_id, null: false, index: true + t.timestamps + end + end +end diff --git a/db/migrate/20231108024716_add_child_count_to_issues.rb b/db/migrate/20231108024716_add_child_count_to_issues.rb new file mode 100644 index 000000000..73560b81e --- /dev/null +++ b/db/migrate/20231108024716_add_child_count_to_issues.rb @@ -0,0 +1,5 @@ +class AddChildCountToIssues < ActiveRecord::Migration[5.2] + def change + add_column :issues, :child_count, :integer, default:0 + end +end diff --git a/db/migrate/20231114023928_add_changer_to_issues.rb b/db/migrate/20231114023928_add_changer_to_issues.rb new file mode 100644 index 000000000..4dff74ed1 --- /dev/null +++ b/db/migrate/20231114023928_add_changer_to_issues.rb @@ -0,0 +1,5 @@ +class AddChangerToIssues < ActiveRecord::Migration[5.2] + def change + add_reference :issues, :changer + end +end diff --git a/db/migrate/20240104053819_add_organization_id_to_issue_tags.rb b/db/migrate/20240104053819_add_organization_id_to_issue_tags.rb new file mode 100644 index 000000000..04cc02d0e --- /dev/null +++ b/db/migrate/20240104053819_add_organization_id_to_issue_tags.rb @@ -0,0 +1,5 @@ +class AddOrganizationIdToIssueTags < ActiveRecord::Migration[5.2] + def change + add_reference :issue_tags, :organization + end +end diff --git a/db/migrate/202401111014309_add_index_uuid_to_attachments.rb b/db/migrate/202401111014309_add_index_uuid_to_attachments.rb new file mode 100644 index 000000000..8635dbd4b --- /dev/null +++ b/db/migrate/202401111014309_add_index_uuid_to_attachments.rb @@ -0,0 +1,5 @@ +class AddIndexUuidToAttachments < ActiveRecord::Migration[5.2] + def change + add_index :attachments, :uuid + end +end diff --git a/db/migrate/202401321314370_create_daily_platform_statistics.rb b/db/migrate/202401321314370_create_daily_platform_statistics.rb new file mode 100644 index 000000000..d658979c5 --- /dev/null +++ b/db/migrate/202401321314370_create_daily_platform_statistics.rb @@ -0,0 +1,18 @@ +class CreateDailyPlatformStatistics < ActiveRecord::Migration[5.2] + def change + create_table :daily_platform_statistics do |t| + t.date :date + t.index :date, unique: true + t.bigint :pv, default: 0 + t.bigint :visitor, default: 0 + t.bigint :ip, default: 0 + t.float :weekly_keep_rate, default: 0 + t.float :source_through, default: 0 + t.float :source_link, default: 0 + t.float :source_search, default: 0 + t.float :source_custom, default: 0 + + t.timestamps + end + end +end diff --git a/db/migrate/202401321314371_add_closed_issues_to_daily_project_statistics.rb b/db/migrate/202401321314371_add_closed_issues_to_daily_project_statistics.rb new file mode 100644 index 000000000..2b38b18b0 --- /dev/null +++ b/db/migrate/202401321314371_add_closed_issues_to_daily_project_statistics.rb @@ -0,0 +1,5 @@ +class AddClosedIssuesToDailyProjectStatistics < ActiveRecord::Migration[5.2] + def change + add_column :daily_project_statistics, :closed_issues, :integer, default: 0 + end +end diff --git a/db/migrate/20240401030707_create_project_datasets.rb b/db/migrate/20240401030707_create_project_datasets.rb new file mode 100644 index 000000000..7a2cfffcc --- /dev/null +++ b/db/migrate/20240401030707_create_project_datasets.rb @@ -0,0 +1,11 @@ +class CreateProjectDatasets < ActiveRecord::Migration[5.2] + def change + create_table :project_datasets do |t| + t.string :title + t.text :description + t.references :project + + t.timestamps + end + end +end diff --git a/db/migrate/20240403015938_add_license_and_paper_content_to_project_dataset.rb b/db/migrate/20240403015938_add_license_and_paper_content_to_project_dataset.rb new file mode 100644 index 000000000..4a2a72a2d --- /dev/null +++ b/db/migrate/20240403015938_add_license_and_paper_content_to_project_dataset.rb @@ -0,0 +1,6 @@ +class AddLicenseAndPaperContentToProjectDataset < ActiveRecord::Migration[5.2] + def change + add_reference :project_datasets, :license + add_column :project_datasets, :paper_content, :text + end +end diff --git a/db/migrate/20240408010101_create_action_node_types.rb b/db/migrate/20240408010101_create_action_node_types.rb new file mode 100644 index 000000000..b9c530784 --- /dev/null +++ b/db/migrate/20240408010101_create_action_node_types.rb @@ -0,0 +1,10 @@ +class CreateActionNodeTypes < ActiveRecord::Migration[5.2] + def change + create_table :action_node_types do |t| + t.string :name + t.string :description + t.integer :sort_no, default: 0 + t.timestamps + end + end +end diff --git a/db/migrate/20240408010102_create_action_nodes.rb b/db/migrate/20240408010102_create_action_nodes.rb new file mode 100644 index 000000000..67331e4db --- /dev/null +++ b/db/migrate/20240408010102_create_action_nodes.rb @@ -0,0 +1,18 @@ +class CreateActionNodes < ActiveRecord::Migration[5.2] + def change + create_table :action_nodes do |t| + t.string :name + t.string :full_name + t.string :description + t.string :icon + t.references :action_node_types + t.boolean :is_local, default: false + t.string :local_url + t.text :yaml + t.integer :sort_no, default: 0 + t.integer :use_count, default: 0 + t.references :user + t.timestamps + end + end +end diff --git a/db/migrate/20240408010213_create_action_node_selects.rb b/db/migrate/20240408010213_create_action_node_selects.rb new file mode 100644 index 000000000..9d07fe4ba --- /dev/null +++ b/db/migrate/20240408010213_create_action_node_selects.rb @@ -0,0 +1,17 @@ +class CreateActionNodeSelects < ActiveRecord::Migration[5.2] + def change + create_table :action_node_selects do |t| + t.references :action_nodes + t.string :name + t.string :val + t.string :val_ext + t.string :description + t.string :download_url + t.integer :sort_no, default: 0 + t.integer :use_count, default: 0 + t.references :user + t.timestamps + t.index :name, length: 191 + end + end +end diff --git a/db/migrate/20240408010227_create_action_node_inputs.rb b/db/migrate/20240408010227_create_action_node_inputs.rb new file mode 100644 index 000000000..a710271cd --- /dev/null +++ b/db/migrate/20240408010227_create_action_node_inputs.rb @@ -0,0 +1,14 @@ +class CreateActionNodeInputs < ActiveRecord::Migration[5.2] + def change + create_table :action_node_inputs do |t| + t.references :action_nodes + t.string :name + t.string :input_type + t.string :description + t.boolean :is_required, default: false + t.integer :sort_no, default: 0 + t.references :user + t.timestamps + end + end +end diff --git a/db/migrate/20240408010233_create_action_templates.rb b/db/migrate/20240408010233_create_action_templates.rb new file mode 100644 index 000000000..1b4c985d2 --- /dev/null +++ b/db/migrate/20240408010233_create_action_templates.rb @@ -0,0 +1,13 @@ +class CreateActionTemplates < ActiveRecord::Migration[5.2] + def change + create_table :action_templates do |t| + t.string :name + t.string :description + t.string :img + t.integer :sort_no, default: 0 + t.text :json + t.text :yaml + t.timestamps + end + end +end diff --git a/db/migrate/20240415014011_create_sync_repositories.rb b/db/migrate/20240415014011_create_sync_repositories.rb new file mode 100644 index 000000000..d2908bfe9 --- /dev/null +++ b/db/migrate/20240415014011_create_sync_repositories.rb @@ -0,0 +1,14 @@ +class CreateSyncRepositories < ActiveRecord::Migration[5.2] + def change + create_table :sync_repositories do |t| + t.references :project + t.string :type + t.string :repo_name + t.string :external_repo_address + t.integer :sync_granularity + t.integer :sync_direction, comment: "1表示从gitlink到外部2表示从外部到gitlink" + + t.timestamps + end + end +end diff --git a/db/migrate/20240415015216_create_sync_repository_branches.rb b/db/migrate/20240415015216_create_sync_repository_branches.rb new file mode 100644 index 000000000..b44a6f396 --- /dev/null +++ b/db/migrate/20240415015216_create_sync_repository_branches.rb @@ -0,0 +1,14 @@ +class CreateSyncRepositoryBranches < ActiveRecord::Migration[5.2] + def change + create_table :sync_repository_branches do |t| + t.references :sync_repository + t.string :gitlink_branch_name, comment: 'gitlink分支' + t.string :external_branch_name, comment: '外部仓库分支' + t.datetime :sync_time + t.integer :sync_status, default: 0 + t.integer :reposync_branch_id + + t.timestamps + end + end +end diff --git a/db/migrate/20240417025003_add_enable_to_sync_repository_branch.rb b/db/migrate/20240417025003_add_enable_to_sync_repository_branch.rb new file mode 100644 index 000000000..fdbd7b7ee --- /dev/null +++ b/db/migrate/20240417025003_add_enable_to_sync_repository_branch.rb @@ -0,0 +1,5 @@ +class AddEnableToSyncRepositoryBranch < ActiveRecord::Migration[5.2] + def change + add_column :sync_repository_branches, :enable, :boolean, default: true + end +end diff --git a/db/migrate/20240424085125_add_external_token_to_sync_repository.rb b/db/migrate/20240424085125_add_external_token_to_sync_repository.rb new file mode 100644 index 000000000..fde46cab0 --- /dev/null +++ b/db/migrate/20240424085125_add_external_token_to_sync_repository.rb @@ -0,0 +1,5 @@ +class AddExternalTokenToSyncRepository < ActiveRecord::Migration[5.2] + def change + add_column :sync_repositories, :external_token, :string + end +end diff --git a/db/migrate/20240429070012_add_webhook_gid_to_sync_repository.rb b/db/migrate/20240429070012_add_webhook_gid_to_sync_repository.rb new file mode 100644 index 000000000..dfd3e6c72 --- /dev/null +++ b/db/migrate/20240429070012_add_webhook_gid_to_sync_repository.rb @@ -0,0 +1,5 @@ +class AddWebhookGidToSyncRepository < ActiveRecord::Migration[5.2] + def change + add_column :sync_repositories, :webhook_gid, :integer + end +end diff --git a/db/migrate/20240514121788_create_action_pipelines.rb b/db/migrate/20240514121788_create_action_pipelines.rb new file mode 100644 index 000000000..b453c3943 --- /dev/null +++ b/db/migrate/20240514121788_create_action_pipelines.rb @@ -0,0 +1,23 @@ +class CreateActionPipelines < ActiveRecord::Migration[5.2] + def change + create_table :action_pipelines do |t| + t.references :project + t.references :user + t.string :pipeline_name + t.string :pipeline_status + t.string :description + t.string :file_name + t.boolean :is_graphic_design, default: false + t.string :repo_name + t.string :repo_identifier + t.string :repo_owner + t.string :branch + t.string :event + t.string :sha + t.text :json + t.text :yaml + t.boolean :disable, default: false + t.timestamps + end + end +end diff --git a/db/migrate/20240522015212_create_gitlink_competition_applies.rb b/db/migrate/20240522015212_create_gitlink_competition_applies.rb new file mode 100644 index 000000000..6f24a0922 --- /dev/null +++ b/db/migrate/20240522015212_create_gitlink_competition_applies.rb @@ -0,0 +1,19 @@ +class CreateGitlinkCompetitionApplies < ActiveRecord::Migration[5.2] + def change + create_table :gitlink_competition_applies do |t| + t.integer :competition_id + t.string :competition_identifier + t.integer :team_id + t.string :team_name + t.string :school_name + t.string :educoder_login + t.string :nickname + t.string :phone + t.string :email + t.string :identity + t.string :role + + t.timestamps + end + end +end diff --git a/lib/tasks/actions_download.rake b/lib/tasks/actions_download.rake new file mode 100644 index 000000000..bad2d535e --- /dev/null +++ b/lib/tasks/actions_download.rake @@ -0,0 +1,82 @@ +# actions 下载包 +# node go java +namespace :actions_download do + + task go: :environment do + # curl -X GET --header 'Content-Type: application/json;charset=UTF-8' 'https://gitee.com/api/v5/repos/mindspore/mindspore/issues?access_token=5ccebd935915fb6cfcae634b161047a2&state=open&sort=created&direction=desc&page=1&per_page=10' + # api_url = "https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json" + api_url = "https://testgitea2.trustie.net/actions/go-versions/raw/branch/main/versions-manifest.json" + uri = URI.parse(api_url) + response = Net::HTTP.get_response(uri) + puts "gitee api response.code ===== #{response.code}" + lists = JSON.parse(response.body) + puts "lists.size =====#{lists.size}" + lists.each do |data| + version_arr = data['version'].to_s.split(".") + if version_arr[0].to_i == 1 && version_arr[1].to_i >= 18 + action_node_select = Action::NodeSelect.find_or_initialize_by(name: "go-version", val: data["version"]) + puts data["version"] + data['files'].each do |file| + if file['platform'] == "linux" + puts "download_url==#{file['download_url']}" + action_node_select.download_url = file['download_url'] + end + end + action_node_select.action_nodes_id=1 + action_node_select.save + end + end + end + + task node: :environment do + # curl -X GET --header 'Content-Type: application/json;charset=UTF-8' 'https://gitee.com/api/v5/repos/mindspore/mindspore/issues?access_token=5ccebd935915fb6cfcae634b161047a2&state=open&sort=created&direction=desc&page=1&per_page=10' + # api_url = "https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json" + api_url = "https://testgitea2.trustie.net/actions/node-versions/raw/branch/main/versions-manifest.json" + uri = URI.parse(api_url) + response = Net::HTTP.get_response(uri) + puts "gitee api response.code ===== #{response.code}" + lists = JSON.parse(response.body) + puts "lists.size =====#{lists.size}" + lists.each do |data| + version_arr = data['version'].to_s.split(".") + if version_arr[0].to_i >= 16 + puts data["version"] + action_node_select = Action::NodeSelect.find_or_initialize_by(name: "node-version", val: data["version"]) + data['files'].each do |file| + if file['platform'] == "linux" + puts "download_url==#{file['download_url']}" + action_node_select.download_url = file['download_url'] + end + end + action_node_select.action_nodes_id=2 + action_node_select.save + end + end + end + + task java: :environment do + # curl -X GET --header 'Content-Type: application/json;charset=UTF-8' 'https://gitee.com/api/v5/repos/mindspore/mindspore/issues?access_token=5ccebd935915fb6cfcae634b161047a2&state=open&sort=created&direction=desc&page=1&per_page=10' + # api_url = "https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json" + [0, 1, 2].each do |page| + api_url = "https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?project=jdk&vendor=adoptium&heap_size=normal&sort_method=DEFAULT&sort_order=DESC&os=linux&architecture=x64&image_type=jdk&release_type=ga&jvm_impl=hotspot&page_size=20&page=#{page}" + uri = URI.parse(api_url) + response = Net::HTTP.get_response(uri) + puts "gitee api response.code ===== #{response.code}" + lists = JSON.parse(response.body) + puts "lists.size =====#{lists.size}" + lists.each do |data| + puts data["release_name"] + puts "#{data['version_data']['major']}@#{data['version_data']['openjdk_version']}" + action_node_select = Action::NodeSelect.find_or_initialize_by(name: "java-version", val: "#{data['version_data']['major']}", val_ext: "#{data['version_data']['openjdk_version']}") + data['binaries'].each do |file| + puts "download_url==#{file['package']['link']}" + action_node_select.download_url = file['package']['link'] + end + action_node_select.action_nodes_id=5 + action_node_select.save + end + end + + end + +end \ No newline at end of file diff --git a/lib/tasks/import_issues_from_chandao.rake b/lib/tasks/import_issues_from_chandao.rake new file mode 100644 index 000000000..41bbc3356 --- /dev/null +++ b/lib/tasks/import_issues_from_chandao.rake @@ -0,0 +1,106 @@ + +desc "导入禅道数据" +namespace :import_from_chandao do + desc "bug数据" + # 执行示例 bundle exec rake "import_from_chandao:bugs[企业内部工时管理系统.csv, 3]" + # RAILS_ENV=production bundle exec rake "import_from_chandao:bugs[企业内部工时管理系统.csv, 3]" + task :bugs, [:name, :pm_project_id] => :environment do |t, args| + name = args.name + CSV.foreach("#{Rails.root}/#{args.name}", headers: true) do | row | + randd_field_hash = row.to_hash + issue = Issue.new(issue_classify: "issue") + author = User.like(randd_field_hash['由谁创建']).take + issue.author_id = author&.id + assigner = randd_field_hash['指派给'].present? ? User.like(randd_field_hash['指派给']).take : nil + if assigner.present? + issue.assigners << assigner + end + issue.status_id = IssueStatus.first.id + issue.tracker_id = Tracker.first.id + issue.priority_id = randd_field_hash['优先级'].to_i + issue.subject = randd_field_hash['Bug标题'] + issue.description = randd_field_hash['重现步骤'] + issue.created_on = randd_field_hash['创建日期'].to_time + issue.due_date = randd_field_hash['截止日期'] + issue.project_id = 0 + issue.pm_project_id = args.pm_project_id + issue.pm_issue_type = 3 + issue.save! + end + end + + # 执行示例 bundle exec rake "import_from_chandao:tasks[复杂智能软件项目-所有任务.csv, 365]" + # RAILS_ENV=production bundle exec rake "import_from_chandao:tasks[复杂智能软件项目-所有任务.csv, 365]" + task :tasks, [:name, :pm_project_id] => :environment do |t, args| + def trans_status(str) + h={ + "未开始" => 1, + "进行中" => 2, + "已完成" => 3, + "已关闭" => 5 + } + h[str] + end + + name = args.name + pm_project_id = args.pm_project_id + CSV.foreach("#{Rails.root}/#{name}", headers: true) do | row | + randd_field_hash = row.to_hash + issue = Issue.new(issue_classify: "issue") + author = User.like(randd_field_hash['由谁创建']).take + issue.author_id = author&.id + assigner = randd_field_hash['指派给'].present? ? User.like(randd_field_hash['指派给']).take : nil + if assigner.present? + issue.assigners << assigner + end + issue.status_id = trans_status(randd_field_hash['任务状态']) || IssueStatus.first.id + issue.tracker_id = Tracker.first.id + issue.priority_id = randd_field_hash['优先级'].to_i + issue.subject = randd_field_hash['任务名称'] + issue.description = randd_field_hash['任务描述'] + issue.created_on = randd_field_hash['创建日期'].to_time rescue nil + issue.updated_on = randd_field_hash['最后修改日期'].to_time rescue issue.created_on + issue.time_scale = randd_field_hash['最初预计'].to_i + issue.start_date = randd_field_hash['预计开始'].to_time rescue nil + issue.due_date = randd_field_hash['截止日期'].to_time rescue nil + issue.project_id = 0 + issue.pm_project_id = pm_project_id + issue.pm_issue_type = 2 + issue.save! + requirement_issue = Issue.find_by(project_issues_index: randd_field_hash['相关需求'].split('(#')[1].split(')')[0], pm_project_id: pm_project_id, pm_issue_type: 1) rescue nil + if requirement_issue.present? + requirement_issue.pm_links.find_or_create_by(be_linkable_type: 'Issue', be_linkable_id: issue.id) + end + end + end + + # 执行示例 bundle exec rake "import_from_chandao:requirements[企业网站第二期.csv, 3]" + # RAILS_ENV=production bundle exec rake "import_from_chandao:requirements[企业网站第二期.csv, 3]" + task :requirements, [:name, :pm_project_id] => :environment do |t, args| + name = args.name + pm_project_id = args.pm_project_id + CSV.foreach("#{Rails.root}/#{name}", headers: true) do | row | + randd_field_hash = row.to_hash + issue = Issue.new(issue_classify: "issue") + author = User.like(randd_field_hash['由谁创建']).take + issue.author_id = author&.id + assigner = randd_field_hash['指派给'].present? ? User.like(randd_field_hash['指派给']).take : nil + if assigner.present? + issue.assigners << assigner + end + issue.project_issues_index = randd_field_hash['编号'].to_i + issue.status_id = IssueStatus.first.id + issue.tracker_id = Tracker.first.id + issue.priority_id = randd_field_hash['优先级'].to_i + issue.subject = randd_field_hash['需求名称'] + issue.description = randd_field_hash['需求描述'] + issue.created_on = randd_field_hash['创建日期'].to_time + issue.updated_on = randd_field_hash['最后修改日期'].to_time rescue randd_field_hash['创建日期'].to_time + issue.time_scale = randd_field_hash['预计工时'].to_i + issue.project_id = 0 + issue.pm_project_id = pm_project_id + issue.pm_issue_type = 1 + issue.save! + end + end +end \ No newline at end of file diff --git a/public/MP_verify_lvSp9mqhewuv8zRT.txt b/public/MP_verify_lvSp9mqhewuv8zRT.txt new file mode 100644 index 000000000..e23f48cc0 --- /dev/null +++ b/public/MP_verify_lvSp9mqhewuv8zRT.txt @@ -0,0 +1 @@ +lvSp9mqhewuv8zRT \ No newline at end of file diff --git a/public/editormd/lib/marked.min.js b/public/editormd/lib/marked.min.js index eb14b2545..9fdbc8481 100644 --- a/public/editormd/lib/marked.min.js +++ b/public/editormd/lib/marked.min.js @@ -1,22 +1,6 @@ /** - * marked v0.3.3 - a markdown parser - * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) - * https://github.com/chjj/marked - - 备注,这个改动没启用,只是做个记录: - br不转成br - 加了个 if (cap[0] != '
    ' && cap[0] != '
    ') { out+=this.options.sanitize?escape(cap[0]):cap[0]; } - - out+=this.renderer.em(this.output(cap[2]||cap[1])) --> - out+=this.renderer.em(this.output(cap[2]||cap[1]), cap.input) + * marked - a markdown parser + * Copyright (c) 2011-2021, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked */ -// 0.4.0 /^ *(#{1,6}) ——》/^ *(#{1,6}) 去掉了一个空格 -/*if("string"!=typeof e)throw new Error("marked(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected");*/ -// 说明:左边 --> 右边 左边被替换成了右边的内容 -// b(i[1].replace(/^ *| *\| *$/g,"")) --> i[1].replace(/^ *| *\| *$/g, "").split(/ *\| */) table没识别的问题 -// header.length===a.align.length --> header.length table没识别的问题 -// 2个table: b(a.cells[p],a.header.length) -> a.cells[p].replace(/^ *\| *| *\| *$/g, "").split(/ *\| */) -// .replace(/(?: *\| *)?\n$/,"") --> .replace(/\n$/, "") -// /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/ --> /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ -!function(e){"use strict";var t={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:d,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ *(#{1,6})*([^\n]+?) *(?:#+ *)?(?:\n+|$)/,nptable:d,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?\\?>\\n*|\\n*|\\n*|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)|(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,table:d,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading| {0,3}>|<\/?(?:tag)(?: +|\n|\/?>)|<(?:script|pre|style|!--))[^\n]+)*)/,text:/^[^\n]+/};function n(e){this.tokens=[],this.tokens.links={},this.options=e||m.defaults,this.rules=t.normal,this.options.pedantic?this.rules=t.pedantic:this.options.gfm&&(this.options.tables?this.rules=t.tables:this.rules=t.gfm)}t._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,t._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,t.def=p(t.def).replace("label",t._label).replace("title",t._title).getRegex(),t.bullet=/(?:[*+-]|\d+\.)/,t.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,t.item=p(t.item,"gm").replace(/bull/g,t.bullet).getRegex(),t.list=p(t.list).replace(/bull/g,t.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+t.def.source+")").getRegex(),t._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",t._comment=//,t.html=p(t.html,"i").replace("comment",t._comment).replace("tag",t._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),t.paragraph=p(t.paragraph).replace("hr",t.hr).replace("heading",t.heading).replace("lheading",t.lheading).replace("tag",t._tag).getRegex(),t.blockquote=p(t.blockquote).replace("paragraph",t.paragraph).getRegex(),t.normal=f({},t),t.gfm=f({},t.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\n? *\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6})+([^\n]+?) *#* *(?:\n+|$)/}),t.gfm.paragraph=p(t.paragraph).replace("(?!","(?!"+t.gfm.fences.source.replace("\\1","\\2")+"|"+t.list.source.replace("\\1","\\3")+"|").getRegex(),t.tables=f({},t.gfm,{nptable:/^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),t.pedantic=f({},t.normal,{html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",t._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/}),n.rules=t,n.lex=function(e,t){return new n(t).lex(e)},n.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},n.prototype.token=function(e,n){var r,s,i,l,o,a,h,p,u,c,g,d,f;for(e=e.replace(/^ +$/gm,"");e;)if((i=this.rules.newline.exec(e))&&(e=e.substring(i[0].length),i[0].length>1&&this.tokens.push({type:"space"})),i=this.rules.code.exec(e))e=e.substring(i[0].length),i=i[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?i:i.replace(/\n+$/,"")});else if(i=this.rules.fences.exec(e))e=e.substring(i[0].length),this.tokens.push({type:"code",lang:i[2],text:i[3]||""});else if(i=this.rules.heading.exec(e))e=e.substring(i[0].length),this.tokens.push({type:"heading",depth:i[1].length,text:i[2]});else if(n&&(i=this.rules.nptable.exec(e))&&(a={type:"table",header:i[1].replace(/^ *| *\| *$/g, "").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3]?i[3].replace(/\n$/,"").split("\n"):[]}).header.length){for(e=e.substring(i[0].length),p=0;p ?/gm,""),this.token(i,n),this.tokens.push({type:"blockquote_end"});else if(i=this.rules.list.exec(e)){for(e=e.substring(i[0].length),g=(l=i[2]).length>1,this.tokens.push({type:"list_start",ordered:g,start:g?+l:""}),r=!1,c=(i=i[0].match(this.rules.item)).length,p=0;p1&&o.length>1||(e=i.slice(p+1).join("\n")+e,p=c-1)),s=r||/\n\n(?!\s*$)/.test(a),p!==c-1&&(r="\n"===a.charAt(a.length-1),s||(s=r)),f=void 0,(d=/^\[[ xX]\] /.test(a))&&(f=" "!==a[1],a=a.replace(/^\[[ xX]\] +/,"")),this.tokens.push({type:s?"loose_item_start":"list_item_start",task:d,checked:f}),this.token(a,!1),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(i=this.rules.html.exec(e))e=e.substring(i[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===i[1]||"script"===i[1]||"style"===i[1]),text:i[0]});else if(n&&(i=this.rules.def.exec(e)))e=e.substring(i[0].length),i[3]&&(i[3]=i[3].substring(1,i[3].length-1)),u=i[1].toLowerCase().replace(/\s+/g," "),this.tokens.links[u]||(this.tokens.links[u]={href:i[2],title:i[3]});else if(n&&(i=this.rules.table.exec(e))&&(a={type:"table",header:i[1].replace(/^ *| *\| *$/g, "").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3]?i[3].replace(/\n$/, "").split("\n"):[]}).header.length){for(e=e.substring(i[0].length),p=0;p?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:d,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(href(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,strong:/^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)|^__([^\s])__(?!_)|^\*\*([^\s])\*\*(?!\*)/,em:/^_([^\s][\s\S]*?[^\s_])_(?!_)|^_([^\s_][\s\S]*?[^\s])_(?!_)|^\*([^\s][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*][\s\S]*?[^\s])\*(?!\*)|^_([^\s_])_(?!_)|^\*([^\s*])\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`]?)\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:d,text:/^[\s\S]+?(?=[\\/g,">").replace(/"/g,""").replace(/'/g,"'")}function h(e){return e.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function p(e,t){return e=e.source||e,t=t||"",{replace:function(t,n){return n=(n=n.source||n).replace(/(^|[^\[])\^/g,"$1"),e=e.replace(t,n),this},getRegex:function(){return new RegExp(e,t)}}}function u(e,t){return c[" "+e]||(/^[^:]+:\/*[^/]*$/.test(e)?c[" "+e]=e+"/":c[" "+e]=e.replace(/[^/]*$/,"")),e=c[" "+e],"//"===t.slice(0,2)?e.replace(/:[\s\S]*/,":")+t:"/"===t.charAt(0)?e.replace(/(:\/*[^/]*)[\s\S]*/,"$1")+t:e+t}r._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,r._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,r._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,r.autolink=p(r.autolink).replace("scheme",r._scheme).replace("email",r._email).getRegex(),r._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,r.tag=p(r.tag).replace("comment",t._comment).replace("attribute",r._attribute).getRegex(),r._label=/(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?/,r._href=/\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?)/,r._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,r.link=p(r.link).replace("label",r._label).replace("href",r._href).replace("title",r._title).getRegex(),r.reflink=p(r.reflink).replace("label",r._label).getRegex(),r.normal=f({},r),r.pedantic=f({},r.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/,link:p(/^!?\[(label)\]\((.*?)\)/).replace("label",r._label).getRegex(),reflink:p(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",r._label).getRegex()}),r.gfm=f({},r.normal,{escape:p(r.escape).replace("])","~|])").getRegex(),url:p(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("email",r._email).getRegex(),_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:p(r.text).replace("]|","~]|").replace("|","|https?://|ftp://|www\\.|[a-zA-Z0-9.!#$%&'*+/=?^_`{\\|}~-]+@|").getRegex()}),r.breaks=f({},r.gfm,{br:p(r.br).replace("{2,}","*").getRegex(),text:p(r.gfm.text).replace("{2,}","*").getRegex()}),s.rules=r,s.output=function(e,t,n){return new s(t,n).output(e)},s.prototype.output=function(e){for(var t,n,r,i,l,o="";e;)if(l=this.rules.escape.exec(e))e=e.substring(l[0].length),o+=l[1];else if(l=this.rules.autolink.exec(e))e=e.substring(l[0].length),r="@"===l[2]?"mailto:"+(n=a(this.mangle(l[1]))):n=a(l[1]),o+=this.renderer.link(r,null,n);else if(this.inLink||!(l=this.rules.url.exec(e))){if(l=this.rules.tag.exec(e))!this.inLink&&/^/i.test(l[0])&&(this.inLink=!1),e=e.substring(l[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(l[0]):a(l[0]):l[0];else if(l=this.rules.link.exec(e))e=e.substring(l[0].length),this.inLink=!0,r=l[2],this.options.pedantic?(t=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r))?(r=t[1],i=t[3]):i="":i=l[3]?l[3].slice(1,-1):"",r=r.trim().replace(/^<([\s\S]*)>$/,"$1"),o+=this.outputLink(l,{href:s.escapes(r),title:s.escapes(i)}),this.inLink=!1;else if((l=this.rules.reflink.exec(e))||(l=this.rules.nolink.exec(e))){if(e=e.substring(l[0].length),t=(l[2]||l[1]).replace(/\s+/g," "),!(t=this.links[t.toLowerCase()])||!t.href){o+=l[0].charAt(0),e=l[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(l,t),this.inLink=!1}else if(l=this.rules.strong.exec(e))e=e.substring(l[0].length),o+=this.renderer.strong(this.output(l[4]||l[3]||l[2]||l[1]));else if(l=this.rules.em.exec(e))e=e.substring(l[0].length),o+=this.renderer.em(this.output(l[6]||l[5]||l[4]||l[3]||l[2]||l[1]));else if(l=this.rules.code.exec(e))e=e.substring(l[0].length),o+=this.renderer.codespan(a(l[2].trim(),!0));else if(l=this.rules.br.exec(e))e=e.substring(l[0].length),o+=this.renderer.br();else if(l=this.rules.del.exec(e))e=e.substring(l[0].length),o+=this.renderer.del(this.output(l[1]));else if(l=this.rules.text.exec(e))e=e.substring(l[0].length),o+=this.renderer.text(a(this.smartypants(l[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else l[0]=this.rules._backpedal.exec(l[0])[0],e=e.substring(l[0].length),"@"===l[2]?r="mailto:"+(n=a(l[0])):(n=a(l[0]),r="www."===l[1]?"http://"+n:n),o+=this.renderer.link(r,null,n);return o},s.escapes=function(e){return e?e.replace(s.rules._escapes,"$1"):e},s.prototype.outputLink=function(e,t){var n=t.href,r=t.title?a(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,a(e[1]))},s.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},s.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,s=0;s.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},i.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'
    '+(n?e:a(e,!0))+"
    \n":"
    "+(n?e:a(e,!0))+"
    "},i.prototype.blockquote=function(e){return"
    \n"+e+"
    \n"},i.prototype.html=function(e){return e},i.prototype.heading=function(e,t,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"},i.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},i.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"},i.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},i.prototype.checkbox=function(e){return" "},i.prototype.paragraph=function(e){return"

    "+e+"

    \n"},i.prototype.table=function(e,t){return t&&(t=""+t+""),"\n\n"+e+"\n"+t+"
    \n"},i.prototype.tablerow=function(e){return"\n"+e+"\n"},i.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"\n"},i.prototype.strong=function(e){return""+e+""},i.prototype.em=function(e){return""+e+""},i.prototype.codespan=function(e){return""+e+""},i.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},i.prototype.del=function(e){return""+e+""},i.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(h(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return n}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:")||0===r.indexOf("data:"))return n}this.options.baseUrl&&!g.test(e)&&(e=u(this.options.baseUrl,e));try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return n}var s='
    "},i.prototype.image=function(e,t,n){this.options.baseUrl&&!g.test(e)&&(e=u(this.options.baseUrl,e));var r=''+n+'":">"},i.prototype.text=function(e){return e},l.prototype.strong=l.prototype.em=l.prototype.codespan=l.prototype.del=l.prototype.text=function(e){return e},l.prototype.link=l.prototype.image=function(e,t,n){return""+n},l.prototype.br=function(){return""},o.parse=function(e,t){return new o(t).parse(e)},o.prototype.parse=function(e){this.inline=new s(e.links,this.options),this.inlineText=new s(e.links,f({},this.options,{renderer:new l})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},o.prototype.next=function(){return this.token=this.tokens.pop()},o.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},o.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},o.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,h(this.inlineText.output(this.token.text)));case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,s="",i="";for(n="",e=0;et)n.splice(t);else for(;n.lengthAn error occurred:

    "+a(e.message+"",!0)+"
    ";throw e}}d.exec=d,m.options=m.setOptions=function(e){return f(m.defaults,e),m},m.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new i,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tables:!0,xhtml:!1}},m.defaults=m.getDefaults(),m.Parser=o,m.parser=o.parse,m.Renderer=i,m.TextRenderer=l,m.Lexer=n,m.lexer=n.lex,m.InlineLexer=s,m.inlineLexer=s.output,m.parse=m,"undefined"!=typeof module&&"object"==typeof exports?module.exports=m:"function"==typeof define&&define.amd?define(function(){return m}):e.marked=m}(this||("undefined"!=typeof window?window:global)); - +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).marked=t()}(this,function(){"use strict";function r(e,t){for(var u=0;ue.length)&&(t=e.length);for(var u=0,n=new Array(t);u=e.length?{done:!0}:{done:!1,value:e[n++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var t={exports:{}};function e(){return{baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}t.exports={defaults:e(),getDefaults:e,changeDefaults:function(e){t.exports.defaults=e}};function u(e){return D[e]}var n=/[&<>"']/,s=/[&<>"']/g,l=/[<>"']|&(?!#?\w+;)/,a=/[<>"']|&(?!#?\w+;)/g,D={"&":"&","<":"<",">":">",'"':""","'":"'"};var c=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function h(e){return e.replace(c,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}var p=/(^|[^\[])\^/g;var g=/[^\w:]/g,f=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;var F={},A=/^[^:]+:\/*[^/]*$/,d=/^([^:]+:)[\s\S]*$/,C=/^([^:]+:\/*[^/]*)[\s\S]*$/;function k(e,t){F[" "+e]||(A.test(e)?F[" "+e]=e+"/":F[" "+e]=E(e,"/",!0));var u=-1===(e=F[" "+e]).indexOf(":");return"//"===t.substring(0,2)?u?t:e.replace(d,"$1")+t:"/"===t.charAt(0)?u?t:e.replace(C,"$1")+t:e+t}function E(e,t,u){var n=e.length;if(0===n)return"";for(var r=0;rt)u.splice(t);else for(;u.length>=1,e+=e;return u+e},T=t.exports.defaults,I=_,R=y,q=x,Z=z;function O(e,t,u){var n=t.href,r=t.title?q(t.title):null,t=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?{type:"link",raw:u,href:n,title:r,text:t}:{type:"image",raw:u,href:n,title:r,text:q(t)}}_=function(){function e(e){this.options=e||T}var t=e.prototype;return t.space=function(e){e=this.rules.block.newline.exec(e);if(e)return 1=u.length?e.slice(u.length):e}).join("\n")}(u,t[3]||"");return{type:"code",raw:u,lang:t[2]&&t[2].trim(),text:e}}},t.heading=function(e){var t=this.rules.block.heading.exec(e);if(t){var u=t[2].trim();return/#$/.test(u)&&(e=I(u,"#"),!this.options.pedantic&&e&&!/ $/.test(e)||(u=e.trim())),{type:"heading",raw:t[0],depth:t[1].length,text:u}}},t.nptable=function(e){e=this.rules.block.nptable.exec(e);if(e){var t={type:"table",header:R(e[1].replace(/^ *| *\| *$/g,"")),align:e[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:e[3]?e[3].replace(/\n$/,"").split("\n"):[],raw:e[0]};if(t.header.length===t.align.length){for(var u=t.align.length,n=0;n ?/gm,"");return{type:"blockquote",raw:t[0],text:e}}},t.list=function(e){e=this.rules.block.list.exec(e);if(e){for(var t,u,n,r,i,s,l=e[0],a=e[2],o=1g[1].length:n[1].length>=g[0].length||3/i.test(e[0])&&(t=!1),!u&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?u=!0:u&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(u=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:t,inRawBlock:u,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):q(e[0]):e[0]}},t.link=function(e){var t=this.rules.inline.link.exec(e);if(t){var u=t[2].trim();if(!this.options.pedantic&&/^$/.test(u))return;e=I(u.slice(0,-1),"\\");if((u.length-e.length)%2==0)return}else{var n=Z(t[2],"()");-1$/.test(u)?n.slice(1):n.slice(1,-1):n)&&n.replace(this.rules.inline._escapes,"$1"),title:i&&i.replace(this.rules.inline._escapes,"$1")},t[0])}},t.reflink=function(e,t){if((u=this.rules.inline.reflink.exec(e))||(u=this.rules.inline.nolink.exec(e))){e=(u[2]||u[1]).replace(/\s+/g," ");if((e=t[e.toLowerCase()])&&e.href)return O(u,e,u[0]);var u=u[0].charAt(0);return{type:"text",raw:u,text:u}}},t.emStrong=function(e,t,u){void 0===u&&(u="");var n=this.rules.inline.emStrong.lDelim.exec(e);if(n&&(!n[3]||!u.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08C7\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\u9FFC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7BF\uA7C2-\uA7CA\uA7F5-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82C[\uDC00-\uDD1E\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDD\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var r=n[1]||n[2]||"";if(!r||r&&(""===u||this.rules.inline.punctuation.exec(u))){var i,s=n[0].length-1,l=s,a=0,o="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(o.lastIndex=0,t=t.slice(-1*e.length+s);null!=(n=o.exec(t));)if(i=n[1]||n[2]||n[3]||n[4]||n[5]||n[6])if(i=i.length,n[3]||n[4])l+=i;else if(!((n[5]||n[6])&&s%3)||(s+i)%3){if(!(0<(l-=i)))return i=Math.min(i,i+l+a),Math.min(s,i)%2?{type:"em",raw:e.slice(0,s+n.index+i+1),text:e.slice(1,s+n.index+i)}:{type:"strong",raw:e.slice(0,s+n.index+i+1),text:e.slice(2,s+n.index+i-1)}}else a+=i}}},t.codespan=function(e){var t=this.rules.inline.code.exec(e);if(t){var u=t[2].replace(/\n/g," "),n=/[^ ]/.test(u),e=/^ /.test(u)&&/ $/.test(u);return n&&e&&(u=u.substring(1,u.length-1)),u=q(u,!0),{type:"codespan",raw:t[0],text:u}}},t.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},t.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2]}},t.autolink=function(e,t){e=this.rules.inline.autolink.exec(e);if(e){var u,t="@"===e[2]?"mailto:"+(u=q(this.options.mangle?t(e[1]):e[1])):u=q(e[1]);return{type:"link",raw:e[0],text:u,href:t,tokens:[{type:"text",raw:u,text:u}]}}},t.url=function(e,t){var u,n,r,i;if(u=this.rules.inline.url.exec(e)){if("@"===u[2])r="mailto:"+(n=q(this.options.mangle?t(u[0]):u[0]));else{for(;i=u[0],u[0]=this.rules.inline._backpedal.exec(u[0])[0],i!==u[0];);n=q(u[0]),r="www."===u[1]?"http://"+n:n}return{type:"link",raw:u[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}},t.inlineText=function(e,t,u){e=this.rules.inline.text.exec(e);if(e){u=t?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):q(e[0]):e[0]:q(this.options.smartypants?u(e[0]):e[0]);return{type:"text",raw:e[0],text:u}}},e}(),y=w,z=b,w=v,b={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?! {0,3}bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:y,table:y,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};b.def=z(b.def).replace("label",b._label).replace("title",b._title).getRegex(),b.bullet=/(?:[*+-]|\d{1,9}[.)])/,b.item=/^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/,b.item=z(b.item,"gm").replace(/bull/g,b.bullet).getRegex(),b.listItemStart=z(/^( *)(bull) */).replace("bull",b.bullet).getRegex(),b.list=z(b.list).replace(/bull/g,b.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+b.def.source+")").getRegex(),b._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",b._comment=/|$)/,b.html=z(b.html,"i").replace("comment",b._comment).replace("tag",b._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),b.paragraph=z(b._paragraph).replace("hr",b.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",b._tag).getRegex(),b.blockquote=z(b.blockquote).replace("paragraph",b.paragraph).getRegex(),b.normal=w({},b),b.gfm=w({},b.normal,{nptable:"^ *([^|\\n ].*\\|.*)\\n {0,3}([-:]+ *\\|[-| :]*)(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)",table:"^ *\\|(.+)\\n {0,3}\\|?( *[-:]+[-| :]*)(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),b.gfm.nptable=z(b.gfm.nptable).replace("hr",b.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",b._tag).getRegex(),b.gfm.table=z(b.gfm.table).replace("hr",b.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",b._tag).getRegex(),b.pedantic=w({},b.normal,{html:z("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",b._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:y,paragraph:z(b.normal._paragraph).replace("hr",b.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",b.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});y={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:y,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/\_\_[^_*]*?\*[^_*]*?\_\_|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/\*\*[^_*]*?\_[^_*]*?\*\*|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:y,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~"};y.punctuation=z(y.punctuation).replace(/punctuation/g,y._punctuation).getRegex(),y.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,y.escapedEmSt=/\\\*|\\_/g,y._comment=z(b._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),y.emStrong.lDelim=z(y.emStrong.lDelim).replace(/punct/g,y._punctuation).getRegex(),y.emStrong.rDelimAst=z(y.emStrong.rDelimAst,"g").replace(/punct/g,y._punctuation).getRegex(),y.emStrong.rDelimUnd=z(y.emStrong.rDelimUnd,"g").replace(/punct/g,y._punctuation).getRegex(),y._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,y._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,y._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,y.autolink=z(y.autolink).replace("scheme",y._scheme).replace("email",y._email).getRegex(),y._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,y.tag=z(y.tag).replace("comment",y._comment).replace("attribute",y._attribute).getRegex(),y._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,y._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,y._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,y.link=z(y.link).replace("label",y._label).replace("href",y._href).replace("title",y._title).getRegex(),y.reflink=z(y.reflink).replace("label",y._label).getRegex(),y.reflinkSearch=z(y.reflinkSearch,"g").replace("reflink",y.reflink).replace("nolink",y.nolink).getRegex(),y.normal=w({},y),y.pedantic=w({},y.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:z(/^!?\[(label)\]\((.*?)\)/).replace("label",y._label).getRegex(),reflink:z(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",y._label).getRegex()}),y.gfm=w({},y.normal,{escape:z(y.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\'+(u?e:H(e,!0))+"\n":"
    "+(u?e:H(e,!0))+"
    \n"},t.blockquote=function(e){return"
    \n"+e+"
    \n"},t.html=function(e){return e},t.heading=function(e,t,u,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"},t.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},t.list=function(e,t,u){var n=t?"ol":"ul";return"<"+n+(t&&1!==u?' start="'+u+'"':"")+">\n"+e+"\n"},t.listitem=function(e){return"
  • "+e+"
  • \n"},t.checkbox=function(e){return" "},t.paragraph=function(e){return"

    "+e+"

    \n"},t.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},t.tablerow=function(e){return"\n"+e+"\n"},t.tablecell=function(e,t){var u=t.header?"th":"td";return(t.align?"<"+u+' align="'+t.align+'">':"<"+u+">")+e+"\n"},t.strong=function(e){return""+e+""},t.em=function(e){return""+e+""},t.codespan=function(e){return""+e+""},t.br=function(){return this.options.xhtml?"
    ":"
    "},t.del=function(e){return""+e+""},t.link=function(e,t,u){if(null===(e=V(this.options.sanitize,this.options.baseUrl,e)))return u;e='
    "},t.image=function(e,t,u){if(null===(e=V(this.options.sanitize,this.options.baseUrl,e)))return u;u=''+u+'":">"},t.text=function(e){return e},e}(),S=function(){function e(){}var t=e.prototype;return t.strong=function(e){return e},t.em=function(e){return e},t.codespan=function(e){return e},t.del=function(e){return e},t.html=function(e){return e},t.text=function(e){return e},t.link=function(e,t,u){return""+u},t.image=function(e,t,u){return""+u},t.br=function(){return""},e}(),B=function(){function e(){this.seen={}}var t=e.prototype;return t.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},t.getNextSafeSlug=function(e,t){var u=e,n=0;if(this.seen.hasOwnProperty(u))for(n=this.seen[e];u=e+"-"+ ++n,this.seen.hasOwnProperty(u););return t||(this.seen[e]=n,this.seen[u]=0),u},t.slug=function(e,t){void 0===t&&(t={});var u=this.serialize(e);return this.getNextSafeSlug(u,t.dryrun)},e}(),J=b,K=S,Q=B,W=t.exports.defaults,Y=m,ee=y,te=function(){function u(e){this.options=e||W,this.options.renderer=this.options.renderer||new J,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new K,this.slugger=new Q}u.parse=function(e,t){return new u(t).parse(e)},u.parseInline=function(e,t){return new u(t).parseInline(e)};var e=u.prototype;return e.parse=function(e,t){void 0===t&&(t=!0);for(var u,n,r,i,s,l,a,o,D,c,h,p,g,f,F,A,d="",C=e.length,k=0;kAn error occurred:

    "+se(e.message+"",!0)+"
    ";throw e}}return ae.options=ae.setOptions=function(e){return re(ae.defaults,e),le(ae.defaults),ae},ae.getDefaults=$,ae.defaults=x,ae.use=function(){for(var u=this,e=arguments.length,t=new Array(e),n=0;nAn error occurred:

    "+se(e.message+"",!0)+"
    ";throw e}},ae.Parser=te,ae.parser=te.parse,ae.Renderer=ne,ae.TextRenderer=S,ae.Lexer=ee,ae.lexer=ee.lex,ae.Tokenizer=ue,ae.Slugger=B,ae.parse=ae}); \ No newline at end of file diff --git a/public/操作系统大赛用户信息.csv b/public/操作系统大赛用户信息.csv new file mode 100644 index 000000000..7365072b2 --- /dev/null +++ b/public/操作系统大赛用户信息.csv @@ -0,0 +1,2 @@ +用户名,邮箱,密码,手机号,昵称 +123456789,yystopf1@163.com,9b653a7d,15386415122,何慧 diff --git a/spec/controllers/api/pm/issue_links_controller_spec.rb b/spec/controllers/api/pm/issue_links_controller_spec.rb new file mode 100644 index 000000000..a80df2c77 --- /dev/null +++ b/spec/controllers/api/pm/issue_links_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Api::Pm::IssueLinksController, type: :controller do + +end diff --git a/spec/controllers/api/pm/projects_controller_spec.rb b/spec/controllers/api/pm/projects_controller_spec.rb new file mode 100644 index 000000000..8b9e3c903 --- /dev/null +++ b/spec/controllers/api/pm/projects_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Api::Pm::ProjectsController, type: :controller do + +end diff --git a/spec/controllers/api/v1/pm_issues_controller_spec.rb b/spec/controllers/api/v1/pm_issues_controller_spec.rb new file mode 100644 index 000000000..f68abf45f --- /dev/null +++ b/spec/controllers/api/v1/pm_issues_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Api::V1::PmIssuesController, type: :controller do + +end diff --git a/spec/controllers/api/v1/sonarqubes_controller_spec.rb b/spec/controllers/api/v1/sonarqubes_controller_spec.rb new file mode 100644 index 000000000..ce71590c4 --- /dev/null +++ b/spec/controllers/api/v1/sonarqubes_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Api::V1::SonarqubesController, type: :controller do + +end diff --git a/spec/controllers/pm/journals_controller_spec.rb b/spec/controllers/pm/journals_controller_spec.rb new file mode 100644 index 000000000..0a0ced0ba --- /dev/null +++ b/spec/controllers/pm/journals_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Pm::JournalsController, type: :controller do + +end diff --git a/spec/helpers/api/pm/issue_links_helper_spec.rb b/spec/helpers/api/pm/issue_links_helper_spec.rb new file mode 100644 index 000000000..924962a6c --- /dev/null +++ b/spec/helpers/api/pm/issue_links_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Api::Pm::IssueLinksHelper. For example: +# +# describe Api::Pm::IssueLinksHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe Api::Pm::IssueLinksHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/api/pm/projects_helper_spec.rb b/spec/helpers/api/pm/projects_helper_spec.rb new file mode 100644 index 000000000..b93449cc2 --- /dev/null +++ b/spec/helpers/api/pm/projects_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Api::Pm::ProjectsHelper. For example: +# +# describe Api::Pm::ProjectsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe Api::Pm::ProjectsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/api/v1/pm_issues_helper_spec.rb b/spec/helpers/api/v1/pm_issues_helper_spec.rb new file mode 100644 index 000000000..4b67710fb --- /dev/null +++ b/spec/helpers/api/v1/pm_issues_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Api::V1::PmIssuesHelper. For example: +# +# describe Api::V1::PmIssuesHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe Api::V1::PmIssuesHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/api/v1/sonarqubes_helper_spec.rb b/spec/helpers/api/v1/sonarqubes_helper_spec.rb new file mode 100644 index 000000000..9021add1e --- /dev/null +++ b/spec/helpers/api/v1/sonarqubes_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Api::V1::SonarqubesHelper. For example: +# +# describe Api::V1::SonarqubesHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe Api::V1::SonarqubesHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/pm/journals_helper_spec.rb b/spec/helpers/pm/journals_helper_spec.rb new file mode 100644 index 000000000..8dd212826 --- /dev/null +++ b/spec/helpers/pm/journals_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Pm::JournalsHelper. For example: +# +# describe Pm::JournalsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe Pm::JournalsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/daily_platform_statistic_spec.rb b/spec/models/daily_platform_statistic_spec.rb new file mode 100644 index 000000000..0f6d5fcdd --- /dev/null +++ b/spec/models/daily_platform_statistic_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe DailyPlatformStatistic, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/gitlink_competition_apply_spec.rb b/spec/models/gitlink_competition_apply_spec.rb new file mode 100644 index 000000000..8b05ff4ad --- /dev/null +++ b/spec/models/gitlink_competition_apply_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe GitlinkCompetitionApply, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/pm_link_spec.rb b/spec/models/pm_link_spec.rb new file mode 100644 index 000000000..d911d5b7e --- /dev/null +++ b/spec/models/pm_link_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe PmLink, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end