diff --git a/.gitignore b/.gitignore
index 836852ea..77104d20 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,7 @@ public/react/yarn.lock
/.idea/*
# Ignore react node_modules
+public/react/*
/public/react/.cache
/public/react/node_modules/
/public/react/config/stats.json
diff --git a/README.md b/README.md
index 8f5a897e..b75fcd64 100644
--- a/README.md
+++ b/README.md
@@ -113,6 +113,8 @@ http://localhost:3000/
### API
- [API](api_document.md)
+- [API](showdoc.com.cn)
+ 账号:forgeplus@admin.com 密码:forge123
## 贡献代码
diff --git a/api_document.md b/api_document.md
index a8b7ac79..49604731 100644
--- a/api_document.md
+++ b/api_document.md
@@ -3536,13 +3536,13 @@ curl -X GET http://localhost:3000/api/ci/languages/114.json | jq
#### 获取构建列表
```
-GET /api/:owner/:repo/builds
+GET /api/:owner/:repo/builds??branch={branch}
```
*示例*
```bash
curl -X GET \
-http://localhost:3000/api/Jason/forge/builds | jq
+http://localhost:3000/api/Jason/forge/builds?branch=develop | jq
```
*请求参数说明:*
@@ -3554,6 +3554,7 @@ http://localhost:3000/api/Jason/forge/builds | jq
|page |否|string |页数,第几页 |
|limit |否|string |每页多少条数据,默认20条 |
|search |是|string |构建状态条件过滤; 值说明:pending: 准备中,failure: 构建失败,running: 运行中,error:构建失败(.trustie-pipeline.yml文件错误),success: 构建成功,killed: 撤销构建 |
+|branch |是|string |分支 |
*返回参数说明:*
@@ -4076,13 +4077,15 @@ http://localhost:3000/api/ci/pipelines/list.json?identifier="xxx" | jq
*返回参数说明:*
-| 参数名 | 类型 | 说明 |
-| ------------- | ------ | --------------- |
-| id | int | 流水线id |
-| pipeline_name | string | 流水线名称 |
-| file_name | string | 流水线文件名 |
-| created_at | string | 创建时间 |
-| sync | int | 是否同步到gitea |
+| 参数名 | 类型 | 说明 |
+| ----------------- | ------ | ------------ |
+| id | int | 流水线id |
+| pipeline_name | string | 流水线名称 |
+| file_name | string | 流水线文件名 |
+| branch | string | 触发分支 |
+| event | string | 触发事件 |
+| last_build_time | string | 上次构建时间 |
+| last_build_status | string | 上次构建状态 |
返回值
@@ -4090,11 +4093,15 @@ http://localhost:3000/api/ci/pipelines/list.json?identifier="xxx" | jq
{
"pipelines": [
{
- "id": 1,
- "pipeline_name": "2020-01-08 流水线",
- "file_name": ".trustie.pipeline.yaml",
- "created_at": "2021-01-08 04:16:24",
- "updated_at": "2021-01-08 04:16:24"
+ "id": 65,
+ "pipeline_name": "流水线 2021-01-25",
+ "file_name": ".drone.yml",
+ "branch": "develop",
+ "event": "push",
+ "sha": "19fb5eb28603a4a1ec799ad44c1a3ef69d5c2cd0",
+ "identifier": "trustieTest",
+ "last_build_status": "success",
+ "last_build_time": "2021-01-25 15:54:22"
}
]
}
@@ -4116,7 +4123,10 @@ curl --location --request POST 'http://localhost:3000/api/ci/pipelines' \
--data-raw ' {
"pipeline_name": "流水线 2021-01-12",
"file_name": ".trustie.pipeline.yaml",
- "identifier": "xxx"
+ "repo": "xxx",
+ "owner": "xxx",
+ "branch": "master",
+ "event": "push"
}'
```
@@ -4126,7 +4136,10 @@ curl --location --request POST 'http://localhost:3000/api/ci/pipelines' \
| ------------- | ---- | ------ | ---------------------------------------------- |
| pipeline_name | 是 | string | 流水线名称 |
| file_name | 是 | string | 文件名称(默认初始值:.trustie.pipeline.yaml) |
-| identifier | 是 | string | 项目identifier |
+| repo | 是 | string | 项目identifier |
+| owner | 是 | string | 项目的owner |
+| branch | 是 | string | 分支名称, branch必须存在一个 |
+| event | 是 | string | 触发事件,可多选,多个逗号隔开 |
*返回参数说明:*
@@ -4667,6 +4680,192 @@ http://localhost:3000/api/ci/templates/templates_by_stage.json?stage_type=build
------
+#### 模板列表查询
+
+```
+GET /api/ci/templates/list.json?limit=10&page=1&name=&stage_type=customize
+```
+
+*示例*
+
+```bash
+curl --location --request GET 'http://localhost:3000/api/ci/templates/list.json?limit=10&page=1&name=&stage_type=customize'
+```
+
+*请求参数说明:*
+
+| 参数名 | 必选 | 类型 | 说明 |
+| ---------- | ---- | ------ | ----------------------------------------- |
+| stage_type | 是 | string | 阶段类型:init/build/deploy/customize/all |
+| limit | 是 | int | 每页条数 |
+| page | 是 | int | 页码 |
+| name | 否 | string | 模糊查询参数 |
+
+*返回参数说明:*
+
+| 参数名 | 类型 | 说明 |
+| ------------- | ------ | ---------------- |
+| category | string | 分类名称 |
+| templates | arr | 分类下的模板列表 |
+| id | int | 模板id |
+| template_name | string | 模板名称 |
+| content | String | 模板内容 |
+
+返回值
+
+```json
+{
+ "total_count": 1,
+ "templates": [
+ {
+ "id": 19,
+ "template_name": "工具1",
+ "stage_type": "customize",
+ "category": "其他",
+ "content": "xxxxxxxxxxxxxxxxxxxxxx",
+ "login": "victor",
+ "created_at": "2021-01-26T15:51:30.000+08:00",
+ "updated_at": "2021-01-26T15:51:30.000+08:00"
+ }
+ ]
+}
+```
+
+
+
+------
+
+#### 模板详情查询
+
+```
+GET /api/ci/templates/id
+```
+
+*示例*
+
+```bash
+curl --location --request GET 'http://localhost:3000/api/ci/templates/17.json'
+```
+
+*请求参数说明:*
+
+| 参数名 | 必选 | 类型 | 说明 |
+| ------ | ---- | ---- | ------ |
+| id | 是 | int | 模板id |
+
+*返回参数说明:*
+
+| 参数名 | 类型 | 说明 |
+| ------------- | ------ | -------- |
+| category | string | 分类名称 |
+| id | int | 模板id |
+| template_name | string | 模板名称 |
+| content | String | 模板内容 |
+
+返回值
+
+```json
+{
+ "id": 17,
+ "template_name": "win/x86_64",
+ "stage_type": "init",
+ "category": "初始化",
+ "content": "kind: pipeline\r\ntype: docker\r\nname: default\r\nplatform:\r\n os: linux\r\n arch: amd64",
+ "login": "victor",
+ "created_at": "2021-01-26T15:29:27.000+08:00",
+ "updated_at": "2021-01-26T15:29:27.000+08:00"
+}
+```
+
+
+
+------
+
+#### 模板新增/更新
+
+```
+POST /api/ci/templates
+```
+
+*示例*
+
+```bash
+curl --location --request POST 'http://localhost:3000/api/ci/templates' \
+--data-raw ' {
+ "template_name": "java++",
+ "stage_type": "build",
+ "category": "java",
+ "content": "xxxxxxxxxxxxxxxxxxxxxx",
+ "id": 21
+}'
+```
+
+*请求参数说明:*
+
+| 参数名 | 必选 | 类型 | 说明 |
+| ------------- | ---- | ------ | ---------------- |
+| template_name | 是 | string | 模板名称 |
+| stage_type | 是 | string | 阶段类型 |
+| category | 是 | string | 分类 |
+| content | 是 | string | 模板内容 |
+| id | 否 | int | 模板id,更新时传 |
+
+*返回参数说明:*
+
+| 参数名 | 类型 | 说明 |
+| ------- | ------ | ------------ |
+| status | int | 状态码 0成功 |
+| message | string | 消息 |
+
+返回值
+
+```json
+{
+ "status": 0,
+ "message": "success"
+}
+```
+
+------
+
+#### 模板删除
+
+```
+DELETE /api/ci/templates/{id}
+```
+
+*示例*
+
+```bash
+curl --location --request DELETE 'http://localhost:3000/api/ci/templates/10'
+```
+
+*请求参数说明:*
+
+| 参数名 | 必选 | 类型 | 说明 |
+| ------ | ---- | ---- | -------- |
+| id | 是 | int | 流水线id |
+
+*返回参数说明:*
+
+| 参数名 | 类型 | 说明 |
+| ------- | ------ | ------------ |
+| status | int | 状态码 0成功 |
+| message | string | 返回消息 |
+
+返回值
+
+```json
+{
+ "status": 0,
+ "message": "success"
+}
+```
+
+------
+
+------
+
#### 解除CI服务器绑定
```
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 8c552512..29595dc6 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -680,6 +680,14 @@ class ApplicationController < ActionController::Base
relation.page(page).per(limit)
end
+ def kaminari_array_paginate(relation)
+ limit = params[:limit] || params[:per_page]
+ limit = (limit.to_i.zero? || limit.to_i > 15) ? 15 : limit.to_i
+ page = params[:page].to_i.zero? ? 1 : params[:page].to_i
+
+ Kaminari.paginate_array(relation).page(page).per(limit)
+ end
+
def strf_time(time)
time.blank? ? '' : time.strftime("%Y-%m-%d %H:%M:%S")
end
diff --git a/app/controllers/ci/base_controller.rb b/app/controllers/ci/base_controller.rb
index 5aedbddb..1649721c 100644
--- a/app/controllers/ci/base_controller.rb
+++ b/app/controllers/ci/base_controller.rb
@@ -16,6 +16,10 @@ class Ci::BaseController < ApplicationController
@repos = Ci::Repo.find_all_with_namespace(namespace)
end
+ def load_repo_by_repo_slug(slug)
+ @repo_slug = Ci::Repo.load_repo_by_repo_slug(slug)
+ end
+
private
def authorize_access_project!
unless @project.manager?(current_user)
diff --git a/app/controllers/ci/cloud_accounts_controller.rb b/app/controllers/ci/cloud_accounts_controller.rb
index 043d24fe..7d829dbd 100644
--- a/app/controllers/ci/cloud_accounts_controller.rb
+++ b/app/controllers/ci/cloud_accounts_controller.rb
@@ -38,7 +38,7 @@ class Ci::CloudAccountsController < Ci::BaseController
ActiveRecord::Base.transaction do
if @repo
return render_error('该项目已经激活') if @repo.repo_active?
- @repo.activate!(@ci_user.user_id)
+ @repo.activate!(@project)
else
@repo = Ci::Repo.auto_create!(@ci_user, @project)
@user.update_column(:user_syncing, false)
diff --git a/app/controllers/ci/pipelines_controller.rb b/app/controllers/ci/pipelines_controller.rb
index 19460fde..ea8b25f7 100644
--- a/app/controllers/ci/pipelines_controller.rb
+++ b/app/controllers/ci/pipelines_controller.rb
@@ -1,18 +1,40 @@
class Ci::PipelinesController < Ci::BaseController
- before_action :require_login, only: %i[list create]
- skip_before_action :connect_to_ci_db
- before_action :load_project, only: %i[content create_trustie_pipeline]
- before_action :load_repository, only: %i[create_trustie_pipeline]
+ before_action :require_login, only: %i[list create content]
+ skip_before_action :connect_to_ci_db, except: %i[list create destroy content]
+ before_action :load_project, only: %i[create content]
+ before_action :load_repo, only: %i[create content]
# ======流水线相关接口========== #
def list
- @pipelines = Ci::Pipeline.where('identifier=?', params[:identifier])
+ @result = Array.new
+ list = Ci::Pipeline.where('identifier=? and owner=?', params[:identifier], params[:owner])
+ # 查询build状态
+ list.collect do |pipeline|
+ pipeline.last_build_time = nil
+ repo = load_repo_by_repo_slug("#{pipeline.owner}/#{pipeline.identifier}")
+ if repo
+ build = repo.builds.order("build_created desc").find_by(build_target: pipeline.branch)
+ if build
+ pipeline.pipeline_status = build.build_status
+ pipeline.last_build_time = Time.at(build.build_created)
+ end
+ end
+ @result.push(pipeline)
+ end
+ @total_count = @result.size
+ @pipelines = paginate @result
end
def create
ActiveRecord::Base.transaction do
- pipeline = Ci::Pipeline.new(pipeline_name: params[:pipeline_name], file_name: params[:file_name], login: current_user.login, identifier: params[:identifier])
+ size = Ci::Pipeline.where('branch=? and identifier=? and owner=?', params[:branch], params[:repo], params[:owner]).size
+ if size > 0
+ render_error("#{params[:branch]}分支已经存在流水线!")
+ return
+ end
+ pipeline = Ci::Pipeline.new(pipeline_name: params[:pipeline_name], file_name: params[:file_name],owner: params[:owner],
+ login: current_user.login, identifier: params[:repo], branch: params[:branch], event: params[:event])
pipeline.save!
# 默认创建四个初始阶段
@@ -26,16 +48,73 @@ class Ci::PipelinesController < Ci::BaseController
).save!
index += 1
end
+ create_pipeline_file(pipeline)
+ create_ci_repo(pipeline)
render_ok({id: pipeline.id})
end
rescue Exception => ex
render_error(ex.message)
end
+ # 在代码库创建文件
+ def create_pipeline_file(pipeline)
+ sha = get_pipeline_file_sha(pipeline.file_name, pipeline.branch)
+ if sha
+ logger.info "#{pipeline.file_name}已存在"
+ else
+ interactor = Gitea::CreateFileInteractor.call(current_user.gitea_token, @owner.login, content_params)
+ if interactor.success?
+ logger.info "#{pipeline.file_name}创建成功"
+ end
+ end
+ end
+
+ # 在drone数据库repo表新增一条repo记录
+ def create_ci_repo(pipeline)
+ if pipeline.branch != 'master'
+ create_params = {
+ repo_user_id: @ci_user.user_id,
+ repo_namespace: @project.owner.login,
+ repo_name: @project.identifier,
+ repo_slug: "#{@project.owner.login}/#{@project.identifier}-" + pipeline.id.to_s,
+ repo_clone_url: @project.repository.url,
+ repo_branch: pipeline.branch,
+ repo_config: pipeline.file_name
+ }
+ repo = Ci::Repo.create_repo(create_params)
+ repo
+ end
+ nil
+ end
+
+ def get_pipeline_file_sha(file_name, branch)
+ file_path_uri = URI.parse(file_name)
+ interactor = Repositories::EntriesInteractor.call(@project.owner, @project.identifier, file_path_uri, ref: branch || 'master')
+ if interactor.success?
+ file = interactor.result
+ file['sha']
+ end
+ end
+
+ def content_params
+ {
+ filepath: params[:file_name],
+ branch: params[:branch],
+ new_branch: params[:new_branch],
+ content: "#pipeline \n",
+ message: 'create pipeline',
+ committer: {
+ email: current_user.mail,
+ name: current_user.login
+ },
+ identifier: params[:repo]
+ }
+ end
+
def update
pipeline = Ci::Pipeline.find(params[:id])
if pipeline
- pipeline.update!(pipeline_name: params[:pipeline_name])
+ pipeline.update!(pipeline_name: params[:pipeline_name],branch: params[:branch], event: params[:event])
end
render_ok
rescue Exception => ex
@@ -45,6 +124,10 @@ class Ci::PipelinesController < Ci::BaseController
def destroy
pipeline = Ci::Pipeline.find(params[:id])
if pipeline
+ repo = load_repo_by_repo_slug("#{pipeline.login}/#{pipeline.identifier}-" + pipeline.id.to_s)
+ if repo
+ repo.destroy!
+ end
pipeline.destroy!
end
render_ok
@@ -53,10 +136,8 @@ class Ci::PipelinesController < Ci::BaseController
end
def content
- @yaml = "#pipeline \n"
+ @yaml = "\n"
pipeline = Ci::Pipeline.find(params[:id])
- @sync = pipeline.sync
- @sha = ''
stages = pipeline.pipeline_stages
if stages && !stages.empty?
init_step = stages.first.pipeline_stage_steps.first
@@ -72,55 +153,15 @@ class Ci::PipelinesController < Ci::BaseController
end
end
end
- if @sync == 1
- @sha = get_pipeline_file_sha(pipeline.file_name)
+ @sha = get_pipeline_file_sha(pipeline.file_name, pipeline.branch)
+ trigger = ''
+ trigger += " branch:\r\n - #{pipeline.branch}\r\n" unless pipeline.branch.blank?
+ unless pipeline.event.blank?
+ trigger += " event:\r\n"
+ pipeline.event.split(',').each { |event| trigger += " - #{event}\r\n"}
end
- end
-
- def get_pipeline_file_sha(file_name)
- file_path_uri = URI.parse(file_name)
- interactor = Repositories::EntriesInteractor.call(@project.owner, @project.identifier, file_path_uri, ref: params[:ref] || "master")
- if interactor.success?
- file = interactor.result
- return file['sha']
- end
- end
-
- def create_trustie_pipeline
- pipeline = Ci::Pipeline.find(params[:id])
- sha = get_pipeline_file_sha(pipeline.file_name)
- if sha
- pipeline.update!(sync: 1)
- interactor = Gitea::UpdateFileInteractor.call(current_user.gitea_token, params[:owner], params.merge(identifier: @project.identifier,sha: sha))
- if interactor.success?
- render_ok
- else
- render_error(interactor.error)
- end
- else
- interactor = Gitea::CreateFileInteractor.call(current_user.gitea_token, @owner.login, content_params)
- if interactor.success?
- pipeline.update!(sync: 1)
- render_ok
- else
- render_error(interactor.error)
- end
- end
- end
-
- def content_params
- {
- filepath: params[:filepath],
- branch: params[:branch],
- new_branch: params[:new_branch],
- content: params[:content],
- message: params[:message],
- committer: {
- email: current_user.mail,
- name: current_user.login
- },
- identifier: @project.identifier
- }
+ @yaml += "trigger:\r\n" + trigger unless trigger.blank?
+ @branch = pipeline.branch
end
# =========阶段相关接口========= #
@@ -135,8 +176,8 @@ class Ci::PipelinesController < Ci::BaseController
# 修改stage排序
update_stage_index(params[:id], params[:show_index], 1)
pipeline_stage = Ci::PipelineStage.new(stage_name: params[:stage_name],
- stage_type: params[:stage_type].blank? ? 'customize' : params[:stage_type],
- pipeline_id: params[:id], show_index: params[:show_index])
+ stage_type: params[:stage_type].blank? ? 'customize' : params[:stage_type],
+ pipeline_id: params[:id], show_index: params[:show_index])
pipeline_stage.save!
render_ok
end
@@ -188,7 +229,7 @@ class Ci::PipelinesController < Ci::BaseController
unless steps.empty?
steps.each do |step|
unless step[:template_id]
- render_error("请选择模板!")
+ render_error('请选择模板!')
return
end
if !step[:id]
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index c7b7338f..69644e7a 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -41,11 +41,8 @@ class Ci::ProjectsController < Ci::BaseController
ActiveRecord::Base.transaction do
if @repo
return render_error('该项目已经激活') if @repo.repo_active?
- if @project.ci_reactivate?
- @project.ci_reactivate!(@repo)
- return render_ok
- end
- @repo.activate!(@ci_user.user_id)
+ @repo.activate!(@project)
+ return render_ok
else
@repo = Ci::Repo.auto_create!(@ci_user, @project)
@ci_user.update_column(:user_syncing, false)
@@ -66,7 +63,7 @@ class Ci::ProjectsController < Ci::BaseController
return render_error('该项目已经取消激活') if !@repo.repo_active?
@project.update_column(:open_devops, false)
- @repo.deactivate!
+ @repo.deactivate_repos!
render_ok
end
diff --git a/app/controllers/ci/templates_controller.rb b/app/controllers/ci/templates_controller.rb
index c77a9144..cb49590d 100644
--- a/app/controllers/ci/templates_controller.rb
+++ b/app/controllers/ci/templates_controller.rb
@@ -1,30 +1,53 @@
-class Ci::TemplatesController < ApplicationController
+class Ci::TemplatesController < Ci::BaseController
+ before_action :require_login, only: %i[list create]
+ skip_before_action :connect_to_ci_db
+
+ #======模板管理======#
def list
- @templates = Ci::Template.all
+ stage_type = params[:stage_type]
+ template_name = params[:name]
+ templates = template_name.blank? ? Ci::Template.all : Ci::Template.where("template_name like ?", "%#{template_name}%")
+ templates = templates.select{ |template| template.login == current_user.login} unless current_user.admin?
+ if !stage_type.blank? && stage_type != 'all'
+ templates = templates.select{ |template| template.stage_type == stage_type}
+ end
+ @total_count = templates.map(&:id).count
+ @templates = paginate templates
end
- def templates_by_stage
- stage_type = params[:stage_type]
- if stage_type != Ci::PipelineStage::CUSTOMIZE_STAGE_TYPE
- @templates = Ci::Template.where("stage_type = ?", stage_type)
- # 根据模板类别分组
- @category_templates = @templates.group_by{ |template| template.category }
- else
- # 自定义阶段,按阶段分类分类返回模板列表
- @templates = Ci::Template.where("stage_type != ?", Ci::PipelineStage::INIT_STAGE_TYPE)
- @category_templates = @templates.group_by{ |template| template.parent_category }
- end
+ def show
+ @template = Ci::Template.find(params[:id])
end
def create
- template = Ci::Template.new(template_name: params[:template_name],
- stage_type: params[:stage_type],
- category: params[:category],
- parent_category: params[:parent_category],
- content: params[:content]
- )
- template.save!
+ stage_type = params[:stage_type]
+ category = params[:category]
+ if category.blank?
+ category = Ci::Template::STAGE_TYPES[:"#{stage_type}"]
+ end
+
+ if params[:id]
+ template = Ci::Template.find(params[:id])
+ if template
+ template.update!(template_name: params[:template_name],
+ stage_type: stage_type,
+ category: category,
+ parent_category: Ci::Template::STAGE_TYPES[:"#{stage_type}"],
+ content: params[:content],
+ login: current_user.admin? ? 'admin' : current_user.login
+ )
+ end
+ else
+ template = Ci::Template.new(template_name: params[:template_name],
+ stage_type: stage_type,
+ category: category,
+ parent_category: Ci::Template::STAGE_TYPES[:"#{stage_type}"],
+ content: params[:content],
+ login: current_user.admin? ? 'admin' : current_user.login
+ )
+ template.save!
+ end
render_ok
rescue Exception => ex
render_error(ex.message)
@@ -53,4 +76,29 @@ class Ci::TemplatesController < ApplicationController
render_error(ex.message)
end
+ #======流水线模板查询=====#
+ def templates_by_stage
+ stage_type = params[:stage_type]
+ if stage_type != Ci::PipelineStage::CUSTOMIZE_STAGE_TYPE
+ @templates = Ci::Template.where("stage_type = ?", stage_type)
+ @templates = @templates.select{ |template| template.login == current_user.login || template.login == 'admin'} unless current_user.admin?
+ if stage_type == Ci::PipelineStage::INIT_STAGE_TYPE && !@templates.nil?
+ @templates.each do |template|
+ content = template.content
+ unless content.blank?
+ pipeline = Ci::Pipeline.find(params[:id])
+ template.content = content.gsub(/{name}/, pipeline.pipeline_name) unless pipeline.nil?
+ end
+ end
+ end
+ # 根据模板类别分组
+ @category_templates = @templates.group_by{ |template| template.category }
+ else
+ # 自定义阶段,按阶段分类分类返回模板列表
+ @templates = Ci::Template.where("stage_type != ?", Ci::PipelineStage::INIT_STAGE_TYPE)
+ @templates = @templates.select{ |template| template.login == current_user.login || template.login == 'admin'} unless current_user.admin?
+ @category_templates = @templates.group_by{ |template| template.parent_category }
+ end
+ end
+
end
diff --git a/app/controllers/concerns/ci/cloud_account_manageable.rb b/app/controllers/concerns/ci/cloud_account_manageable.rb
index 00c42305..13edb09c 100644
--- a/app/controllers/concerns/ci/cloud_account_manageable.rb
+++ b/app/controllers/concerns/ci/cloud_account_manageable.rb
@@ -101,9 +101,6 @@ module Ci::CloudAccountManageable
if cloud_account.server_type == Ci::CloudAccount::SERVER_TYPE_SELF
@connection.execute("DROP DATABASE IF EXISTS #{current_user.login}_drone") # TOTO drop drone database
- else
- #删除drone用户
- @trustie_db_connection.execute("DELETE FROM users WHERE user_login = '#{cloud_account.account}'")
end
cloud_account.destroy! unless cloud_account.blank?
diff --git a/app/controllers/concerns/paginate_helper.rb b/app/controllers/concerns/paginate_helper.rb
index 7233adeb..638a3242 100644
--- a/app/controllers/concerns/paginate_helper.rb
+++ b/app/controllers/concerns/paginate_helper.rb
@@ -1,12 +1,14 @@
module PaginateHelper
- def paginate(objs, **opts)
- page = params[:page].to_i <= 0 ? 1 : params[:page].to_i
- per_page = params[:per_page].to_i > 0 && params[:per_page].to_i < 50 ? params[:per_page].to_i : opts[:per_page] || 20
- if objs.is_a?(Array)
- Kaminari.paginate_array(objs).page(page).per(per_page)
+ def paginate(relation)
+ limit = params[:limit] || params[:per_page]
+ limit = (limit.to_i.zero? || limit.to_i > 15) ? 15 : limit.to_i
+ page = params[:page].to_i.zero? ? 1 : params[:page].to_i
+
+ if relation.is_a?(Array)
+ Kaminari.paginate_array(relation).page(page).per(limit)
else
- objs.page(page).per(per_page)
+ relation.page(page).per(limit)
end
end
end
\ No newline at end of file
diff --git a/app/controllers/organizations/base_controller.rb b/app/controllers/organizations/base_controller.rb
new file mode 100644
index 00000000..2e8ae0cf
--- /dev/null
+++ b/app/controllers/organizations/base_controller.rb
@@ -0,0 +1,34 @@
+class Organizations::BaseController < ApplicationController
+ include ApplicationHelper
+ include PaginateHelper
+
+ protected
+
+ def can_edit_org?
+ current_user.admin? || @organization.is_owner?(current_user.id)
+ end
+
+ def check_user_can_edit_org
+ tip_exception("您没有权限进行该操作") unless can_edit_org?
+ end
+
+ def org_limited_condition
+ @organization.organization_extension.limited? && !current_user.logged?
+ end
+
+ def org_privacy_condition
+ @organization.organization_extension.privacy? && @organization.organization_users.where(user_id: current_user.id).blank?
+ end
+
+ def team_not_found_condition
+ @team.team_users.where(user_id: current_user.id).blank? && !@organization.is_owner?(current_user.id)
+ end
+
+ def user_mark
+ params[:username] || params[:id]
+ end
+
+ def project_mark
+ params[:repo_name] || params[:id]
+ 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
new file mode 100644
index 00000000..8a7755a1
--- /dev/null
+++ b/app/controllers/organizations/organization_users_controller.rb
@@ -0,0 +1,57 @@
+class Organizations::OrganizationUsersController < Organizations::BaseController
+ before_action :load_organization
+ before_action :load_operate_user, :load_organization_user, :check_user_can_edit_org, only: [:destroy]
+
+ def index
+ @organization_users = @organization.organization_users.includes(:user)
+ search = params[:search].to_s.downcase
+ @organization_users = @organization_users.joins(:user).where("LOWER(CONCAT_WS(users.lastname, users.firstname, users.login, users.mail)) LIKE ?", "%#{search.split(" ").join('|')}%") if search.present?
+
+ @organization_users = kaminari_paginate(@organization_users)
+ end
+
+ def destroy
+ tip_exception("您不能从所有者团队中删除最后一个用户") if @organization.is_owner_team_last_one?(@operate_user.id)
+ ActiveRecord::Base.transaction do
+ @organization_user.destroy!
+ TeamUser.where(organization_id: @organization.id, user_id: @operate_user.id).map{|u| u.destroy!}
+ Gitea::Organization::OrganizationUser::DeleteService.call(@organization.gitea_token, @organization.login, @operate_user.login)
+ render_ok
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def quit
+ @organization_user = @organization.organization_users.find_by(user_id: current_user.id)
+ tip_exception("您不在该组织中") if @organization_user.nil?
+ tip_exception("您不能从所有者团队中删除最后一个用户") if @organization.is_owner_team_last_one?(current_user.id)
+ ActiveRecord::Base.transaction do
+ @organization_user.destroy!
+ TeamUser.where(organization_id: @organization.id, user_id: current_user.id).map{|u| u.destroy!}
+ Gitea::Organization::OrganizationUser::DeleteService.call(@organization.gitea_token, @organization.login, current_user.login)
+ render_ok
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ private
+ def load_organization
+ @organization = Organization.find_by(login: params[:organization_id]) || Organization.find_by(id: params[:organization_id])
+ return render_not_found("组织不存在") if @organization.nil?
+ return render_forbidden("没有查看组织的权限") if org_limited_condition || org_privacy_condition
+ end
+
+ def load_operate_user
+ @operate_user = User.find_by(login: user_mark) if user_mark.present?
+ tip_exception("平台用户不存在") if @operate_user.nil?
+ end
+
+ def load_organization_user
+ @organization_user = OrganizationUser.find_by(organization_id: @organization.id, user_id: @operate_user.id)
+ tip_exception("组织成员不存在") if @organization_user.nil?
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/organizations/organizations_controller.rb b/app/controllers/organizations/organizations_controller.rb
new file mode 100644
index 00000000..e03ddf12
--- /dev/null
+++ b/app/controllers/organizations/organizations_controller.rb
@@ -0,0 +1,106 @@
+class Organizations::OrganizationsController < Organizations::BaseController
+ before_action :require_login, except: [:index, :show]
+ before_action :convert_image!, only: [:create, :update]
+ before_action :load_organization, only: [:show, :update, :destroy]
+ before_action :check_user_can_edit_org, only: [:update, :destroy]
+
+ def index
+ if current_user.logged?
+ logged_organizations_sql = Organization.with_visibility(%w(common limited)).to_sql
+ privacy_organizations_sql = Organization.with_visibility("privacy").joins(:organization_users).where(organization_users: {user_id: current_user.id}).to_sql
+ @organizations = Organization.from("( #{ logged_organizations_sql } UNION #{ privacy_organizations_sql } ) AS users")
+ else
+ @organizations = Organization.with_visibility("common")
+ end
+ @organizations = @organizations.ransack(login_cont: params[:search]).result if params[:search].present?
+ @organizations = @organizations.includes(:organization_extension).order("organization_extensions.#{sort_by} #{sort_direction}")
+ @organizations = kaminari_paginate(@organizations)
+ end
+
+ def show
+ @can_create_project = @organization.can_create_project?(current_user.id)
+ @is_admin = can_edit_org?
+ @is_member = @organization.is_member?(current_user.id)
+ end
+
+ def create
+ ActiveRecord::Base.transaction do
+ @organization = Organizations::CreateService.call(current_user, organization_params)
+ Util.write_file(@image, avatar_path(@organization)) if params[:image].present?
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def update
+ ActiveRecord::Base.transaction do
+ login = @organization.login
+ @organization.update!(login: organization_params[:name]) if organization_params[:name].present?
+ @organization.organization_extension.update_attributes!(organization_params.except(:name))
+ Gitea::Organization::UpdateService.call(@organization.gitea_token, login, @organization.reload)
+ Util.write_file(@image, avatar_path(@organization)) if params[:image].present?
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def destroy
+ tip_exception("密码不正确") unless current_user.check_password?(password)
+ ActiveRecord::Base.transaction do
+ Gitea::Organization::DeleteService.call(@organization.gitea_token, @organization.login)
+ @organization.destroy!
+ end
+ render_ok
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ private
+ def convert_image!
+ return unless params[:image].present?
+ max_size = EduSetting.get('upload_avatar_max_size') || 2 * 1024 * 1024 # 2M
+ if params[:image].class == ActionDispatch::Http::UploadedFile
+ @image = params[:image]
+ render_error('请上传文件') if @image.size.zero?
+ render_error('文件大小超过限制') if @image.size > max_size.to_i
+ else
+ image = params[:image].to_s.strip
+ return render_error('请上传正确的图片') if image.blank?
+ @image = Util.convert_base64_image(image, max_size: max_size.to_i)
+ end
+ rescue Base64ImageConverter::Error => ex
+ render_error(ex.message)
+ end
+
+ def avatar_path(organization)
+ ApplicationController.helpers.disk_filename(organization.class, organization.id)
+ end
+
+ def organization_params
+ params.permit(:name, :description, :website, :location,
+ :repo_admin_change_team_access, :visibility,
+ :max_repo_creation)
+ end
+
+ def password
+ params.fetch(:password, "")
+ end
+
+ def load_organization
+ @organization = Organization.find_by(login: params[:id]) || Organization.find_by(id: params[:id])
+ return render_not_found("组织不存在") if @organization.nil?
+ return render_forbidden("没有查看组织的权限") if org_limited_condition || org_privacy_condition
+ end
+
+ def sort_by
+ params.fetch(:sort_by, "created_at")
+ end
+
+ def sort_direction
+ params.fetch(:sort_direction, "desc")
+ end
+
+end
\ No newline at end of file
diff --git a/app/controllers/organizations/projects_controller.rb b/app/controllers/organizations/projects_controller.rb
new file mode 100644
index 00000000..cc275b09
--- /dev/null
+++ b/app/controllers/organizations/projects_controller.rb
@@ -0,0 +1,45 @@
+class Organizations::ProjectsController < Organizations::BaseController
+ before_action :load_organization
+
+ def index
+ public_projects_sql = @organization.projects.where(is_public: true).to_sql
+ private_projects_sql = @organization.projects
+ .where(is_public: false)
+ .joins(team_projects: {team: :team_users})
+ .where(team_users: {user_id: current_user.id}).to_sql
+ @projects = Project.from("( #{ public_projects_sql} UNION #{ private_projects_sql } ) AS projects")
+
+ @projects = @projects.ransack(name_or_identifier_cont: params[:search]).result if params[:search].present?
+ @projects = @projects.includes(:owner).order("projects.#{sort} #{sort_direction}")
+ @projects = paginate(@projects)
+ end
+
+ def search
+ tip_exception("请输入搜索关键词") if params[:search].nil?
+ public_projects_sql = @organization.projects.where(is_public: true).to_sql
+ private_projects_sql = @organization.projects
+ .where(is_public: false)
+ .joins(team_projects: {team: :team_users})
+ .where(team_users: {user_id: current_user.id}).to_sql
+ @projects = Project.from("( #{ public_projects_sql} UNION #{ private_projects_sql } ) AS projects")
+
+ @projects = @projects.ransack(name_or_identifier_cont: params[:search]).result
+ @projects = @projects.includes(:owner).order("projects.#{sort} #{sort_direction}")
+ end
+
+ private
+
+ def load_organization
+ @organization = Organization.find_by(login: params[:organization_id]) || Organization.find_by(id: params[:organization_id])
+ return render_not_found("组织不存在") if @organization.nil?
+ return render_forbidden("没有查看组织的权限") if org_limited_condition || org_privacy_condition
+ end
+
+ def sort
+ params.fetch(:sort_by, "updated_on")
+ end
+
+ def sort_direction
+ params.fetch(:sort_direction, "desc")
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/organizations/team_projects_controller.rb b/app/controllers/organizations/team_projects_controller.rb
new file mode 100644
index 00000000..2badca17
--- /dev/null
+++ b/app/controllers/organizations/team_projects_controller.rb
@@ -0,0 +1,58 @@
+class Organizations::TeamProjectsController < Organizations::BaseController
+ before_action :load_organization
+ before_action :load_team
+ before_action :load_operate_project, :check_user_can_edit_org, only: [:create, :destroy]
+ before_action :load_team_project, only: [:destroy]
+
+ def index
+ @team_projects = @team.team_projects
+
+ @team_projects = paginate(@team_projects)
+ end
+
+ def create
+ tip_exception("该组织团队项目包括组织所有项目,不允许更改") if @team.includes_all_project
+ ActiveRecord::Base.transaction do
+ @team_project = TeamProject.build(@organization.id, @team.id, @operate_project.id)
+ Gitea::Organization::TeamProject::CreateService.call(@organization.gitea_token, @team.gtid, @organization.login, @operate_project.identifier)
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def destroy
+ tip_exception("该组织团队项目包括组织所有项目,不允许更改") if @team.includes_all_project
+ ActiveRecord::Base.transaction do
+ @team_project.destroy!
+ Gitea::Organization::TeamProject::DeleteService.call(@organization.gitea_token, @team.gtid, @organization.login, @operate_project.identifier)
+ render_ok
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ private
+ def load_organization
+ @organization = Organization.find_by(login: params[:organization_id]) || Organization.find_by(id: params[:organization_id])
+ return render_not_found("组织不存在") if @organization.nil?
+ return render_forbidden("没有查看组织的权限") if org_limited_condition || org_privacy_condition
+ end
+
+ def load_team
+ @team = Team.find_by_id(params[:team_id])
+ return render_not_found("组织团队不存在") if @team.nil?
+ return render_forbidden("没有查看组织团队的权限") if team_not_found_condition
+ end
+
+ def load_operate_project
+ @operate_project = Project.find_by(id: project_mark) || Project.find_by(identifier: project_mark)
+ tip_exception("项目不存在") if @operate_project.nil?
+ end
+
+ def load_team_project
+ @team_project = TeamProject.find_by(organization_id: @organization.id, team_id: @team.id, project_id: @operate_project.id)
+ tip_exception("组织团队项目不存在") if @team_project.nil?
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/organizations/team_users_controller.rb b/app/controllers/organizations/team_users_controller.rb
new file mode 100644
index 00000000..d8694a6c
--- /dev/null
+++ b/app/controllers/organizations/team_users_controller.rb
@@ -0,0 +1,76 @@
+class Organizations::TeamUsersController < Organizations::BaseController
+ before_action :load_organization, :load_team
+ before_action :load_operate_user, only: [:create, :destroy]
+ before_action :load_team_user, only: [:destroy]
+ before_action :check_user_can_edit_org, only: [:create, :destroy]
+
+ def index
+ @team_users = @team.team_users.includes(:user)
+
+ search = params[:search].to_s.downcase
+ @team_users = @team_users.joins(:user).where("LOWER(CONCAT_WS(users.lastname, users.firstname, users.login, users.mail, users.nickname)) LIKE ?", "%#{search.split(" ").join('|')}%") if search.present?
+
+ @team_users = kaminari_paginate(@team_users)
+ end
+
+ def create
+ ActiveRecord::Base.transaction do
+ @team_user = TeamUser.build(@organization.id, @operate_user.id, @team.id)
+ @organization_user = OrganizationUser.build(@organization.id, @operate_user.id)
+ Gitea::Organization::TeamUser::CreateService.call(@organization.gitea_token, @team.gtid, @operate_user.login)
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def destroy
+ tip_exception("您不能从所有者团队中删除最后一个用户") if @team.owner? && @organization.is_owner_team_last_one?(@operate_user.id)
+ ActiveRecord::Base.transaction do
+ @team_user.destroy!
+ Gitea::Organization::TeamUser::DeleteService.call(@organization.gitea_token, @team.gtid, @operate_user.login)
+ render_ok
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def quit
+ @team_user = @team.team_users.find_by(user_id: current_user.id)
+ tip_exception("您不在该组织团队中") if @team_user.nil?
+ tip_exception("您不能从所有者团队中删除最后一个用户") if @team.owner? && @organization.is_owner_team_last_one?(current_user.id)
+ ActiveRecord::Base.transaction do
+ @team_user.destroy!
+ Gitea::Organization::TeamUser::DeleteService.call(@organization.gitea_token, @team.gtid, current_user.login)
+ render_ok
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ private
+ def load_organization
+ @organization = Organization.find_by(login: params[:organization_id]) || Organization.find_by(id: params[:organization_id])
+ return render_not_found("组织不存在") if @organization.nil?
+ return render_forbidden("没有查看组织的权限") if org_limited_condition || org_privacy_condition
+ end
+
+ def load_team
+ @team = Team.find_by_id(params[:team_id])
+ return render_not_found("组织团队不存在") if @team.nil?
+ return render_forbidden("没有查看组织团队的权限") if team_not_found_condition
+ end
+
+ def load_operate_user
+ @operate_user = User.find_by(login: user_mark) if user_mark.present?
+ tip_exception("平台用户不存在") if @operate_user.nil?
+ end
+
+ def load_team_user
+ @team_user = TeamUser.find_by(team_id: @team.id, user_id: @operate_user.id)
+ tip_exception("组织团队成员不存在") if @team_user.nil?
+ end
+
+end
\ No newline at end of file
diff --git a/app/controllers/organizations/teams_controller.rb b/app/controllers/organizations/teams_controller.rb
new file mode 100644
index 00000000..5bc01fe3
--- /dev/null
+++ b/app/controllers/organizations/teams_controller.rb
@@ -0,0 +1,77 @@
+class Organizations::TeamsController < Organizations::BaseController
+ before_action :load_organization
+ before_action :load_team, only: [:show, :update, :destroy]
+ before_action :check_user_can_edit_org, only: [:create, :update, :destroy]
+
+ def index
+ #if @organization.is_owner?(current_user) || current_user.admin?
+ @teams = @organization.teams
+ #else
+ # @teams = @organization.teams.joins(:team_users).where(team_users: {user_id: current_user.id})
+ #end
+ @is_admin = can_edit_org?
+ @teams = @teams.includes(:team_units, :team_users)
+
+ @teams = kaminari_paginate(@teams)
+ end
+
+ def search
+ tip_exception("请输入搜索关键词") if params[:search].nil?
+ if @organization.is_owner?(current_user) || current_user.admin?
+ @teams = @organization.teams
+ else
+ @teams = @organization.teams.joins(:team_users).where(team_users: {user_id: current_user.id})
+ end
+ @is_admin = can_edit_org?
+ @teams = @teams.ransack(name_cont: params[:search]).result if params[:search].present?
+ @teams = @teams.includes(:team_units, :team_users)
+ end
+
+ def show
+ @is_admin = can_edit_org?
+ @is_member = @team.is_member?(current_user.id)
+ end
+
+ def create
+ @team = Organizations::Teams::CreateService.call(current_user, @organization, team_params)
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def update
+ @team = Organizations::Teams::UpdateService.call(current_user, @team, team_params)
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def destroy
+ tip_exception("组织团队不允许被删除") if @team.owner?
+ ActiveRecord::Base.transaction do
+ Gitea::Organization::Team::DeleteService.call(@organization.gitea_token, @team.gtid)
+ @team.destroy!
+ end
+ render_ok
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ private
+ def team_params
+ params.permit(:name, :description, :authorize, :includes_all_project, :can_create_org_project, :unit_types => [])
+ end
+
+ def load_organization
+ @organization = Organization.find_by(login: params[:organization_id]) || Organization.find_by(id: params[:organization_id])
+ return render_not_found("组织不存在") if @organization.nil?
+ return render_forbidden("没有查看组织的权限") if org_limited_condition || org_privacy_condition
+ end
+
+ def load_team
+ @team = Team.find_by_id(params[:id])
+ return render_not_found("组织团队不存在") if @team.nil?
+ return render_forbidden("没有查看组织团队的权限") if team_not_found_condition
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/owners_controller.rb b/app/controllers/owners_controller.rb
new file mode 100644
index 00000000..97444f7a
--- /dev/null
+++ b/app/controllers/owners_controller.rb
@@ -0,0 +1,12 @@
+class OwnersController < ApplicationController
+ before_action :require_login
+
+ def index
+ @owners = []
+ @owners += [current_user]
+ @owners += Organization.joins(team_users: :team)
+ .where(team_users: {user_id: current_user.id},
+ teams: {can_create_org_project: true})
+ .distinct
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/projects/teams_controller.rb b/app/controllers/projects/teams_controller.rb
new file mode 100644
index 00000000..b6ea3218
--- /dev/null
+++ b/app/controllers/projects/teams_controller.rb
@@ -0,0 +1,47 @@
+class Projects::TeamsController < Projects::BaseController
+ before_action :load_operate_team, only: [:create, :destroy]
+ before_action :load_team_project, only: :destroy
+
+ def index
+ if @project.owner.is_a?(Organization)
+ @teams = Team.joins(:team_projects).where(team_projects: {project_id: @project.id})
+ else
+ @teams = Team.none
+ end
+ @teams = paginate(@teams)
+ end
+
+ def create
+ ActiveRecord::Base.transaction do
+ @team_project = TeamProject.build(@owner.id, @operate_team.id, @project.id)
+ Gitea::Organization::TeamProject::CreateService.call(@owner.gitea_token, @operate_team.gtid, @owner.login, @project.identifier)
+ render_ok
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ def destroy
+ ActiveRecord::Base.transaction do
+ @team_project.destroy!
+ Gitea::Organization::TeamProject::DeleteService.call(@owner.gitea_token, @operate_team.gtid, @owner.login, @project.identifier)
+ render_ok
+ end
+ rescue Exception => e
+ uid_logger_error(e.message)
+ tip_exception(e.message)
+ end
+
+ private
+ def load_operate_team
+ @operate_team = Team.find_by(id: params[:team_id]) || Team.find_by(id: params[:id])
+ tip_exception("项目不存在") if @operate_team.nil?
+ tip_exception("该组织团队拥有组织所有项目,无法进行操作") if @operate_team.includes_all_project
+ end
+
+ def load_team_project
+ @team_project = TeamProject.find_by(organization_id: @owner.id, team_id: @operate_team.id, project_id: @project.id)
+ tip_exception("组织团队项目不存在") if @team_project.nil?
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3965c1f5..fbb681c0 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -11,7 +11,7 @@ class ProjectsController < ApplicationController
scope = Projects::ListQuery.call(params)
# @projects = kaminari_paginate(scope)
- @projects = kaminari_paginate scope.includes(:project_category, :project_language, :repository, :project_educoder, :apply_signatures, :members, owner: :user_extension)
+ @projects = kaminari_paginate scope.includes(:project_category, :project_language, :repository, :project_educoder, :apply_signatures, :members, :owner)
category_id = params[:category_id]
@total_count = @projects.total_count
@@ -67,7 +67,7 @@ class ProjectsController < ApplicationController
default_branch: params[:default_branch]
}
if [true, false].include? private
- new_project_params = project_params.merge(is_public: !private)
+ new_project_params = project_params.except(:private).merge(is_public: !private)
Gitea::Repository::UpdateService.call(@owner, @project.identifier, gitea_params)
@project.repository.update_column(:hidden, private)
end
@@ -119,7 +119,7 @@ class ProjectsController < ApplicationController
end
def recommend
- @projects = Project.recommend.includes(:repository, :project_category, owner: :user_extension).limit(5)
+ @projects = Project.recommend.includes(:repository, :project_category, :owner).limit(5)
end
def about
diff --git a/app/controllers/pull_requests_controller.rb b/app/controllers/pull_requests_controller.rb
index 4a18eb21..b4003455 100644
--- a/app/controllers/pull_requests_controller.rb
+++ b/app/controllers/pull_requests_controller.rb
@@ -230,7 +230,8 @@ class PullRequestsController < ApplicationController
}
@requests_params = @local_params.merge({
assignee: current_user.try(:login),
- assignees: ["#{params[:assigned_login].to_s}"],
+ # assignees: ["#{params[:assigned_login].to_s}"],
+ assignees: ["#{current_user.try(:login).to_s}"],
labels: params[:issue_tag_ids],
due_date: Time.now
})
diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb
index 519a63bd..6683dde4 100644
--- a/app/controllers/repositories_controller.rb
+++ b/app/controllers/repositories_controller.rb
@@ -88,7 +88,7 @@ class RepositoriesController < ApplicationController
end
def edit
- return render_forbidden if !@project.manager?(current_user)
+ return render_forbidden if !@project.manager?(current_user) && !current_user.admin?
end
def create_file
diff --git a/app/controllers/statistic_controller.rb b/app/controllers/statistic_controller.rb
new file mode 100644
index 00000000..6eaa5bc9
--- /dev/null
+++ b/app/controllers/statistic_controller.rb
@@ -0,0 +1,25 @@
+class StatisticController < ApplicationController
+
+ # 平台概况
+ def platform_profile
+ @platform_user_query = Statistic::PlatformUserQuery.new(params).call
+ @platform_project_query = Statistic::PlatformProjectQuery.new(params).call
+ @platform_course_query = Statistic::PlatformCourseQuery.new(params).call
+ end
+
+ # 平台代码提交数据
+ def platform_code
+ @platform_pull_request_query = Statistic::PlatformPullRequestQuery.new(params).call
+ @platform_commit_query = Statistic::PlatformCommitQuery.new(params,current_user).call
+ end
+
+ # 项目案例活跃度排行榜
+ def active_project_rank
+ @active_project_rank_query = Statistic::ActiveProjectRankQuery.new(params, current_user).call
+ end
+
+ # 开发者活跃度排行榜
+ def active_developer_rank
+ @active_developer_rank_query = Statistic::ActiveDeveloperRankQuery.new(params, current_user).call
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/users/organizations_controller.rb b/app/controllers/users/organizations_controller.rb
new file mode 100644
index 00000000..721339e8
--- /dev/null
+++ b/app/controllers/users/organizations_controller.rb
@@ -0,0 +1,25 @@
+class Users::OrganizationsController < Users::BaseController
+
+ def index
+ if current_user.logged?
+ logged_organizations_sql = observed_user.organizations.with_visibility(%w(common limited)).to_sql
+ privacy_organizations_sql = observed_user.organizations.with_visibility("privacy").joins(:organization_users).where(organization_users: {user_id: current_user.id}).to_sql
+ @organizations = Organization.from("( #{ logged_organizations_sql } UNION #{ privacy_organizations_sql } ) AS users")
+ else
+ @organizations = observed_user.organizations.with_visibility("common")
+ end
+
+ @organizations = @organizations.ransack(login_cont: params[:search]).result if params[:search].present?
+ @organizations = @organizations.includes(:organization_extension).order("organization_extensions.#{sort_by} #{sort_direction}")
+ @organizations = kaminari_paginate(@organizations)
+ end
+
+ private
+ def sort_by
+ params.fetch(:sort_by, "created_at")
+ end
+
+ def sort_direction
+ params.fetch(:sort_direction, "desc")
+ end
+end
\ No newline at end of file
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index f0985d54..6b6b8fcb 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -31,6 +31,7 @@ class UsersController < ApplicationController
#用户的组织数量
# @user_composes_count = @user.composes.size
@user_composes_count = 0
+ @user_org_count = User.current.logged? ? @user.organizations.with_visibility(%w(common limited)).size + @user.organizations.with_visibility("privacy").joins(:organization_users).where(organization_users: {user_id: current_user.id}).size : @user.organizations.with_visibility("common").size
user_projects = User.current.logged? && (User.current.admin? || User.current.login == @user.login) ? @user.projects : @user.projects.visible
@projects_common_count = user_projects.common.size
@projects_mirrior_count = user_projects.mirror.size
diff --git a/app/controllers/version_releases_controller.rb b/app/controllers/version_releases_controller.rb
index a9e7fa63..1b56236a 100644
--- a/app/controllers/version_releases_controller.rb
+++ b/app/controllers/version_releases_controller.rb
@@ -124,7 +124,7 @@ class VersionReleasesController < ApplicationController
private
def set_user
- @user = @repository.user
+ @user = @repository.owner
end
def find_version
diff --git a/app/docs/slate/.dockerignore b/app/docs/slate/.dockerignore
new file mode 100644
index 00000000..1cf06112
--- /dev/null
+++ b/app/docs/slate/.dockerignore
@@ -0,0 +1,2 @@
+build/
+.github/
diff --git a/app/docs/slate/.editorconfig b/app/docs/slate/.editorconfig
new file mode 100644
index 00000000..1692977c
--- /dev/null
+++ b/app/docs/slate/.editorconfig
@@ -0,0 +1,18 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# Top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+
+[*.rb]
+charset = utf-8
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/app/docs/slate/.gitattributes b/app/docs/slate/.gitattributes
new file mode 100644
index 00000000..3069c432
--- /dev/null
+++ b/app/docs/slate/.gitattributes
@@ -0,0 +1 @@
+source/javascripts/lib/* linguist-vendored
diff --git a/app/docs/slate/.github/ISSUE_TEMPLATE/bug.md b/app/docs/slate/.github/ISSUE_TEMPLATE/bug.md
new file mode 100644
index 00000000..25fcdb1d
--- /dev/null
+++ b/app/docs/slate/.github/ISSUE_TEMPLATE/bug.md
@@ -0,0 +1,22 @@
+---
+name: Report a Bug
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Bug Description**
+A clear and concise description of what the bug is and how to reproduce it.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Browser (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Last upstream Slate commit (run `git log --author="Robert Lord" | head -n 1`):**
+Put the commit hash here
diff --git a/app/docs/slate/.github/ISSUE_TEMPLATE/config.yml b/app/docs/slate/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..16f4beed
--- /dev/null
+++ b/app/docs/slate/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Questions, Ideas, Discussions
+ url: https://github.com/slatedocs/slate/discussions
+ about: Ask and answer questions, and propose new features.
diff --git a/app/docs/slate/.github/PULL_REQUEST_TEMPLATE.md b/app/docs/slate/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..151e45d7
--- /dev/null
+++ b/app/docs/slate/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/app/docs/slate/.github/workflows/build.yml b/app/docs/slate/.github/workflows/build.yml
new file mode 100644
index 00000000..c9ec0ecc
--- /dev/null
+++ b/app/docs/slate/.github/workflows/build.yml
@@ -0,0 +1,42 @@
+name: Build
+
+on:
+ push:
+ branches: [ '*' ]
+ pull_request:
+ branches: [ '*' ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ ruby-version: [2.3, 2.4, 2.5, 2.6, 2.7]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby-version }}
+
+ - uses: actions/cache@v2
+ with:
+ path: vendor/bundle
+ key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }}
+ restore-keys: |
+ gems-${{ runner.os }}-${{ matrix.ruby-version }}-
+ gems-${{ runner.os }}-
+
+ # necessary to get ruby 2.3 to work nicely with bundler vendor/bundle cache
+ # can remove once ruby 2.3 is no longer supported
+ - run: gem update --system
+
+ - run: bundle config set deployment 'true'
+ - name: bundle install
+ run: |
+ bundle config path vendor/bundle
+ bundle install --jobs 4 --retry 3
+
+ - run: bundle exec middleman build
diff --git a/app/docs/slate/.github/workflows/deploy.yml b/app/docs/slate/.github/workflows/deploy.yml
new file mode 100644
index 00000000..341cd5f7
--- /dev/null
+++ b/app/docs/slate/.github/workflows/deploy.yml
@@ -0,0 +1,41 @@
+name: Deploy
+
+on:
+ push:
+ branches: [ 'main' ]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ env:
+ ruby-version: 2.5
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ env.ruby-version }}
+
+ - uses: actions/cache@v2
+ with:
+ path: vendor/bundle
+ key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }}
+ restore-keys: |
+ gems-${{ runner.os }}-${{ matrix.ruby-version }}-
+ gems-${{ runner.os }}-
+
+ - run: bundle config set deployment 'true'
+ - name: bundle install
+ run: |
+ bundle config path vendor/bundle
+ bundle install --jobs 4 --retry 3
+
+ - run: bundle exec middleman build
+
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./build
+ keep_files: true
diff --git a/app/docs/slate/.github/workflows/dev_deploy.yml b/app/docs/slate/.github/workflows/dev_deploy.yml
new file mode 100644
index 00000000..7f1a40ab
--- /dev/null
+++ b/app/docs/slate/.github/workflows/dev_deploy.yml
@@ -0,0 +1,50 @@
+name: Dev Deploy
+
+on:
+ push:
+ branches: [ 'dev' ]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ env:
+ ruby-version: 2.5
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ env.ruby-version }}
+
+ - uses: actions/cache@v2
+ with:
+ path: vendor/bundle
+ key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }}
+ restore-keys: |
+ gems-${{ runner.os }}-${{ matrix.ruby-version }}-
+ gems-${{ runner.os }}-
+
+ - run: bundle config set deployment 'true'
+ - name: bundle install
+ run: |
+ bundle config path vendor/bundle
+ bundle install --jobs 4 --retry 3
+
+ - run: bundle exec middleman build
+
+ - name: Push to Docker Hub
+ uses: docker/build-push-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_KEY }}
+ repository: slatedocs/slate
+ tag_with_ref: true
+
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v3.7.0-8
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ destination_dir: dev
+ publish_dir: ./build
+ keep_files: true
diff --git a/app/docs/slate/.github/workflows/publish.yml b/app/docs/slate/.github/workflows/publish.yml
new file mode 100644
index 00000000..d57930a5
--- /dev/null
+++ b/app/docs/slate/.github/workflows/publish.yml
@@ -0,0 +1,22 @@
+name: Publish Docker image
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ push_to_registry:
+ name: Push Docker image to Docker Hub
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v2
+
+ - name: Push to Docker Hub
+ uses: docker/build-push-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_KEY }}
+ repository: slatedocs/slate
+ tag_with_ref: true
+ tags: latest
diff --git a/app/docs/slate/.gitignore b/app/docs/slate/.gitignore
new file mode 100644
index 00000000..1d5d08dd
--- /dev/null
+++ b/app/docs/slate/.gitignore
@@ -0,0 +1,27 @@
+*.gem
+*.rbc
+.bundle
+.config
+coverage
+InstalledFiles
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+*.DS_STORE
+build/
+.cache
+.vagrant
+.sass-cache
+
+# YARD artifacts
+.yardoc
+_yardoc
+doc/
+.idea/
+
+# Vagrant artifacts
+ubuntu-*-console.log
diff --git a/app/docs/slate/CHANGELOG.md b/app/docs/slate/CHANGELOG.md
new file mode 100644
index 00000000..1ffe3eb8
--- /dev/null
+++ b/app/docs/slate/CHANGELOG.md
@@ -0,0 +1,254 @@
+# Changelog
+
+## Version 2.8.0
+
+*October 27, 2020*
+
+* Remove last trailing newline when using the copy code button
+* Rework docker image and make available at slatedocs/slate
+* Improve Dockerfile layout to improve caching (thanks @micvbang)
+* Bump rouge from 3.20 to 3.24
+* Bump nokogiri from 1.10.9 to 1.10.10
+* Bump middleman from 4.3.8 to 4.3.11
+* Bump lunr.js from 2.3.8 to 2.3.9
+
+## Version 2.7.1
+
+*August 13, 2020*
+
+* __[security]__ Bumped middleman from 4.3.7 to 4.3.8
+
+_Note_: Slate uses redcarpet, not kramdown, for rendering markdown to HTML, and so was unaffected by the security vulnerability in middleman.
+If you have changed slate to use kramdown, and with GFM, you may need to install the `kramdown-parser-gfm` gem.
+
+## Version 2.7.0
+
+*June 21, 2020*
+
+* __[security]__ Bumped rack in Gemfile.lock from 2.2.2 to 2.2.3
+* Bumped bundled jQuery from 3.2.1 to 3.5.1
+* Bumped bundled lunr from 0.5.7 to 2.3.8
+* Bumped imagesloaded from 3.1.8 to 4.1.4
+* Bumped rouge from 3.17.0 to 3.20.0
+* Bumped redcarpet from 3.4.0 to 3.5.0
+* Fix color of highlighted code being unreadable when printing page
+* Add clipboard icon for "Copy to Clipboard" functionality to code boxes (see note below)
+* Fix handling of ToC selectors that contain punctutation (thanks @gruis)
+* Fix language bar truncating languages that overflow screen width
+* Strip HTML tags from ToC title before displaying it in title bar in JS (backup to stripping done in Ruby code) (thanks @atic)
+
+To enable the new clipboard icon, you need to add `code_clipboard: true` to the frontmatter of source/index.html.md.
+See [this line](https://github.com/slatedocs/slate/blame/main/source/index.html.md#L19) for an example of usage.
+
+## Version 2.6.1
+
+*May 30, 2020*
+
+* __[security]__ update child dependency activesupport in Gemfile.lock to 5.4.2.3
+* Update Middleman in Gemfile.lock to 4.3.7
+* Replace Travis-CI with GitHub actions for continuous integration
+* Replace Spectrum with GitHub discussions
+
+## Version 2.6.0
+
+*May 18, 2020*
+
+__Note__: 2.5.0 was "pulled" due to a breaking bug discovered after release. It is recommended to skip it, and move straight to 2.6.0.
+
+* Fix large whitespace gap in middle column for sections with codeblocks
+* Fix highlighted code elements having a different background than rest of code block
+* Change JSON keys to have a different font color than their values
+* Disable asset hashing for woff and woff2 elements due to middleman bug breaking woff2 asset hashing in general
+* Move Dockerfile to Debian from Alpine
+* Converted repo to a [GitHub template](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-template-repository)
+* Update sassc to 2.3.0 in Gemfile.lock
+
+## Version 2.5.0
+
+*May 8, 2020*
+
+* __[security]__ update nokogiri to ~> 1.10.8
+* Update links in example docs to https://github.com/slatedocs/slate from https://github.com/lord/slate
+* Update LICENSE to include full Apache 2.0 text
+* Test slate against Ruby 2.5 and 2.6 on Travis-CI
+* Update Vagrantfile to use Ubuntu 18.04 (thanks @bradthurber)
+* Parse arguments and flags for deploy.sh on script start, instead of potentially after building source files
+* Install nodejs inside Vagrantfile (thanks @fernandoaguilar)
+* Add Dockerfile for running slate (thanks @redhatxl)
+* update middleman-syntax and rouge to ~>3.2
+* update middleman to 4.3.6
+
+## Version 2.4.0
+
+*October 19, 2019*
+
+- Move repository from lord/slate to slatedocs/slate
+- Fix documentation to point at new repo link, thanks to [Arun](https://github.com/slash-arun), [Gustavo Gawryszewski](https://github.com/gawry), and [Daniel Korbit](https://github.com/danielkorbit)
+- Update `nokogiri` to 1.10.4
+- Update `ffi` in `Gemfile.lock` to fix security warnings, thanks to [Grey Baker](https://github.com/greysteil) and [jakemack](https://github.com/jakemack)
+- Update `rack` to 2.0.7 in `Gemfile.lock` to fix security warnings, thanks to [Grey Baker](https://github.com/greysteil) and [jakemack](https://github.com/jakemack)
+- Update middleman to `4.3` and relax constraints on middleman related gems, thanks to [jakemack](https://github.com/jakemack)
+- Add sass gem, thanks to [jakemack](https://github.com/jakemack)
+- Activate `asset_cache` in middleman to improve cacheability of static files, thanks to [Sam Gilman](https://github.com/thenengah)
+- Update to using bundler 2 for `Gemfile.lock`, thanks to [jakemack](https://github.com/jakemack)
+
+## Version 2.3.1
+
+*July 5, 2018*
+
+- Update `sprockets` in `Gemfile.lock` to fix security warnings
+
+## Version 2.3
+
+*July 5, 2018*
+
+- Allows strikethrough in markdown by default.
+- Upgrades jQuery to 3.2.1, thanks to [Tomi Takussaari](https://github.com/TomiTakussaari)
+- Fixes invalid HTML in `layout.erb`, thanks to [Eric Scouten](https://github.com/scouten) for pointing out
+- Hopefully fixes Vagrant memory issues, thanks to [Petter Blomberg](https://github.com/p-blomberg) for the suggestion
+- Cleans HTML in headers before setting `document.title`, thanks to [Dan Levy](https://github.com/justsml)
+- Allows trailing whitespace in markdown files, thanks to [Samuel Cousin](https://github.com/kuzyn)
+- Fixes pushState/replaceState problems with scrolling not changing the document hash, thanks to [Andrey Fedorov](https://github.com/anfedorov)
+- Removes some outdated examples, thanks [@al-tr](https://github.com/al-tr), [Jerome Dahdah](https://github.com/jdahdah), and [Ricardo Castro](https://github.com/mccricardo)
+- Fixes `nav-padding` bug, thanks [Jerome Dahdah](https://github.com/jdahdah)
+- Code style fixes thanks to [Sebastian Zaremba](https://github.com/vassyz)
+- Nokogiri version bump thanks to [Grey Baker](https://github.com/greysteil)
+- Fix to default `index.md` text thanks to [Nick Busey](https://github.com/NickBusey)
+
+Thanks to everyone who contributed to this release!
+
+## Version 2.2
+
+*January 19, 2018*
+
+- Fixes bugs with some non-roman languages not generating unique headers
+- Adds editorconfig, thanks to [Jay Thomas](https://github.com/jaythomas)
+- Adds optional `NestingUniqueHeadCounter`, thanks to [Vladimir Morozov](https://github.com/greenhost87)
+- Small fixes to typos and language, thx [Emir Ribić](https://github.com/ribice), [Gregor Martynus](https://github.com/gr2m), and [Martius](https://github.com/martiuslim)!
+- Adds links to Spectrum chat for questions in README and ISSUE_TEMPLATE
+
+## Version 2.1
+
+*October 30, 2017*
+
+- Right-to-left text stylesheet option, thanks to [Mohammad Hossein Rabiee](https://github.com/mhrabiee)
+- Fix for HTML5 history state bug, thanks to [Zach Toolson](https://github.com/ztoolson)
+- Small styling changes, typo fixes, small bug fixes from [Marian Friedmann](https://github.com/rnarian), [Ben Wilhelm](https://github.com/benwilhelm), [Fouad Matin](https://github.com/fouad), [Nicolas Bonduel](https://github.com/NicolasBonduel), [Christian Oliff](https://github.com/coliff)
+
+Thanks to everyone who submitted PRs for this version!
+
+## Version 2.0
+
+*July 17, 2017*
+
+- All-new statically generated table of contents
+ - Should be much faster loading and scrolling for large pages
+ - Smaller Javascript file sizes
+ - Avoids the problem with the last link in the ToC not ever highlighting if the section was shorter than the page
+ - Fixes control-click not opening in a new page
+ - Automatically updates the HTML title as you scroll
+- Updated design
+ - New default colors!
+ - New spacings and sizes!
+ - System-default typefaces, just like GitHub
+- Added search input delay on large corpuses to reduce lag
+- We even bumped the major version cause hey, why not?
+- Various small bug fixes
+
+Thanks to everyone who helped debug or wrote code for this version! It was a serious community effort, and I couldn't have done it alone.
+
+## Version 1.5
+
+*February 23, 2017*
+
+- Add [multiple tabs per programming language](https://github.com/lord/slate/wiki/Multiple-language-tabs-per-programming-language) feature
+- Upgrade Middleman to add Ruby 1.4.0 compatibility
+- Switch default code highlighting color scheme to better highlight JSON
+- Various small typo and bug fixes
+
+## Version 1.4
+
+*November 24, 2016*
+
+- Upgrade Middleman and Rouge gems, should hopefully solve a number of bugs
+- Update some links in README
+- Fix broken Vagrant startup script
+- Fix some problems with deploy.sh help message
+- Fix bug with language tabs not hiding properly if no error
+- Add `!default` to SASS variables
+- Fix bug with logo margin
+- Bump tested Ruby versions in .travis.yml
+
+## Version 1.3.3
+
+*June 11, 2016*
+
+Documentation and example changes.
+
+## Version 1.3.2
+
+*February 3, 2016*
+
+A small bugfix for slightly incorrect background colors on code samples in some cases.
+
+## Version 1.3.1
+
+*January 31, 2016*
+
+A small bugfix for incorrect whitespace in code blocks.
+
+## Version 1.3
+
+*January 27, 2016*
+
+We've upgraded Middleman and a number of other dependencies, which should fix quite a few bugs.
+
+Instead of `rake build` and `rake deploy`, you should now run `bundle exec middleman build --clean` to build your server, and `./deploy.sh` to deploy it to Github Pages.
+
+## Version 1.2
+
+*June 20, 2015*
+
+**Fixes:**
+
+- Remove crash on invalid languages
+- Update Tocify to scroll to the highlighted header in the Table of Contents
+- Fix variable leak and update search algorithms
+- Update Python examples to be valid Python
+- Update gems
+- More misc. bugfixes of Javascript errors
+- Add Dockerfile
+- Remove unused gems
+- Optimize images, fonts, and generated asset files
+- Add chinese font support
+- Remove RedCarpet header ID patch
+- Update language tabs to not disturb existing query strings
+
+## Version 1.1
+
+*July 27, 2014*
+
+**Fixes:**
+
+- Finally, a fix for the redcarpet upgrade bug
+
+## Version 1.0
+
+*July 2, 2014*
+
+[View Issues](https://github.com/tripit/slate/issues?milestone=1&state=closed)
+
+**Features:**
+
+- Responsive designs for phones and tablets
+- Started tagging versions
+
+**Fixes:**
+
+- Fixed 'unrecognized expression' error
+- Fixed #undefined hash bug
+- Fixed bug where the current language tab would be unselected
+- Fixed bug where tocify wouldn't highlight the current section while searching
+- Fixed bug where ids of header tags would have special characters that caused problems
+- Updated layout so that pages with disabled search wouldn't load search.js
+- Cleaned up Javascript
diff --git a/app/docs/slate/CODE_OF_CONDUCT.md b/app/docs/slate/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..cc17fd98
--- /dev/null
+++ b/app/docs/slate/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@lord.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/app/docs/slate/Gemfile b/app/docs/slate/Gemfile
new file mode 100644
index 00000000..9a318414
--- /dev/null
+++ b/app/docs/slate/Gemfile
@@ -0,0 +1,12 @@
+ruby '>=2.3.1'
+source 'https://gems.ruby-china.com'
+
+# Middleman
+gem 'middleman', '~>4.3'
+gem 'middleman-syntax', '~> 3.2'
+gem 'middleman-autoprefixer', '~> 2.7'
+gem 'middleman-sprockets', '~> 4.1'
+gem 'rouge', '~> 3.21'
+gem 'redcarpet', '~> 3.5.0'
+gem 'nokogiri', '~> 1.10.8'
+gem 'sass'
diff --git a/app/docs/slate/Gemfile.lock b/app/docs/slate/Gemfile.lock
new file mode 100644
index 00000000..44028888
--- /dev/null
+++ b/app/docs/slate/Gemfile.lock
@@ -0,0 +1,136 @@
+GEM
+ remote: https://gems.ruby-china.com/
+ specs:
+ activesupport (5.2.4.4)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 0.7, < 2)
+ minitest (~> 5.1)
+ tzinfo (~> 1.1)
+ addressable (2.7.0)
+ public_suffix (>= 2.0.2, < 5.0)
+ autoprefixer-rails (9.5.1.1)
+ execjs
+ backports (3.18.2)
+ coffee-script (2.4.1)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.12.2)
+ concurrent-ruby (1.1.7)
+ contracts (0.13.0)
+ dotenv (2.7.6)
+ erubis (2.7.0)
+ execjs (2.7.0)
+ fast_blank (1.0.0)
+ fastimage (2.2.0)
+ ffi (1.13.1)
+ haml (5.1.2)
+ temple (>= 0.8.0)
+ tilt
+ hamster (3.0.0)
+ concurrent-ruby (~> 1.0)
+ hashie (3.6.0)
+ i18n (0.9.5)
+ concurrent-ruby (~> 1.0)
+ kramdown (2.3.0)
+ rexml
+ listen (3.0.8)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ memoist (0.16.2)
+ middleman (4.3.11)
+ coffee-script (~> 2.2)
+ haml (>= 4.0.5)
+ kramdown (>= 2.3.0)
+ middleman-cli (= 4.3.11)
+ middleman-core (= 4.3.11)
+ middleman-autoprefixer (2.10.1)
+ autoprefixer-rails (~> 9.1)
+ middleman-core (>= 3.3.3)
+ middleman-cli (4.3.11)
+ thor (>= 0.17.0, < 2.0)
+ middleman-core (4.3.11)
+ activesupport (>= 4.2, < 6.0)
+ addressable (~> 2.3)
+ backports (~> 3.6)
+ bundler
+ contracts (~> 0.13.0)
+ dotenv
+ erubis
+ execjs (~> 2.0)
+ fast_blank
+ fastimage (~> 2.0)
+ hamster (~> 3.0)
+ hashie (~> 3.4)
+ i18n (~> 0.9.0)
+ listen (~> 3.0.0)
+ memoist (~> 0.14)
+ padrino-helpers (~> 0.13.0)
+ parallel
+ rack (>= 1.4.5, < 3)
+ sassc (~> 2.0)
+ servolux
+ tilt (~> 2.0.9)
+ uglifier (~> 3.0)
+ middleman-sprockets (4.1.1)
+ middleman-core (~> 4.0)
+ sprockets (>= 3.0)
+ middleman-syntax (3.2.0)
+ middleman-core (>= 3.2)
+ rouge (~> 3.2)
+ mini_portile2 (2.4.0)
+ minitest (5.14.2)
+ nokogiri (1.10.10)
+ mini_portile2 (~> 2.4.0)
+ padrino-helpers (0.13.3.4)
+ i18n (~> 0.6, >= 0.6.7)
+ padrino-support (= 0.13.3.4)
+ tilt (>= 1.4.1, < 3)
+ padrino-support (0.13.3.4)
+ activesupport (>= 3.1)
+ parallel (1.19.2)
+ public_suffix (4.0.6)
+ rack (2.2.3)
+ rb-fsevent (0.10.4)
+ rb-inotify (0.10.1)
+ ffi (~> 1.0)
+ redcarpet (3.5.0)
+ rexml (3.2.4)
+ rouge (3.24.0)
+ sass (3.7.4)
+ sass-listen (~> 4.0.0)
+ sass-listen (4.0.0)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ sassc (2.4.0)
+ ffi (~> 1.9)
+ servolux (0.13.0)
+ sprockets (3.7.2)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ temple (0.8.2)
+ thor (1.0.1)
+ thread_safe (0.3.6)
+ tilt (2.0.10)
+ tzinfo (1.2.7)
+ thread_safe (~> 0.1)
+ uglifier (3.2.0)
+ execjs (>= 0.3.0, < 3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ middleman (~> 4.3)
+ middleman-autoprefixer (~> 2.7)
+ middleman-sprockets (~> 4.1)
+ middleman-syntax (~> 3.2)
+ nokogiri (~> 1.10.8)
+ redcarpet (~> 3.5.0)
+ rouge (~> 3.21)
+ sass
+
+RUBY VERSION
+ ruby 2.3.3p222
+
+BUNDLED WITH
+ 2.1.4
diff --git a/app/docs/slate/LICENSE b/app/docs/slate/LICENSE
new file mode 100644
index 00000000..261eeb9e
--- /dev/null
+++ b/app/docs/slate/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/app/docs/slate/README.md b/app/docs/slate/README.md
new file mode 100644
index 00000000..7d15f864
--- /dev/null
+++ b/app/docs/slate/README.md
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
Slate helps you create beautiful, intelligent, responsive API documentation.