diff --git a/.gitignore b/.gitignore index 836852ea0..77104d20a 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 8f5a897e0..b75fcd647 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/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9d6efdcb6..ee81765a5 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/concerns/paginate_helper.rb b/app/controllers/concerns/paginate_helper.rb index 7233adebf..638a32426 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 000000000..2e8ae0cf9 --- /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 000000000..8a7755a1b --- /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 000000000..e03ddf121 --- /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 000000000..cc275b090 --- /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 000000000..2badca177 --- /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 000000000..d8694a6c5 --- /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 000000000..5bc01fe39 --- /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 000000000..97444f7a4 --- /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 000000000..b6ea32185 --- /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 0a26110d6..2939e7fb8 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 = paginate scope.includes(:project_category, :project_language, :repository, :project_educoder, owner: :user_extension) + @projects = paginate scope.includes(:project_category, :project_language, :repository, :project_educoder, :owner) category_id = params[:category_id] @total_count = @@ -128,7 +128,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 @@ -163,7 +163,7 @@ class ProjectsController < ApplicationController private def project_params params.permit(:user_id, :name, :description, :repository_name, - :project_category_id, :project_language_id, :license_id, :ignore_id) + :project_category_id, :project_language_id, :license_id, :ignore_id, :private) end def mirror_params diff --git a/app/controllers/users/organizations_controller.rb b/app/controllers/users/organizations_controller.rb new file mode 100644 index 000000000..721339e84 --- /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 d5ca638a6..6c5c10fb3 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 a9e7fa63a..1b56236a4 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/forms/projects/create_form.rb b/app/forms/projects/create_form.rb index 39c372309..2838996b6 100644 --- a/app/forms/projects/create_form.rb +++ b/app/forms/projects/create_form.rb @@ -1,13 +1,13 @@ class Projects::CreateForm < BaseForm REPOSITORY_NAME_REGEX = /^(?!_)(?!.*?_$)[a-zA-Z0-9_-]+$/ #只含有数字、字母、下划线不能以下划线开头和结尾 attr_accessor :user_id, :name, :description, :repository_name, :project_category_id, - :project_language_id, :ignore_id, :license_id, :private + :project_language_id, :ignore_id, :license_id, :private, :owner validates :user_id, :name, :description,:repository_name, :project_category_id, :project_language_id, presence: true validates :repository_name, format: { with: REPOSITORY_NAME_REGEX, multiline: true, message: "只能含有数字、字母、下划线且不能以下划线开头和结尾" } - validate :check_ignore, :check_license + validate :check_ignore, :check_license, :check_owner, :check_max_repo_creation validate do check_project_category(project_category_id) check_project_language(project_language_id) @@ -20,4 +20,15 @@ class Projects::CreateForm < BaseForm def check_ignore raise "ignore_id值无效." if ignore_id && Ignore.find_by(id: ignore_id).blank? end + + def check_owner + @owner = Owner.find_by(id: user_id) + raise "user_id值无效." if user_id && owner.blank? + end + + def check_max_repo_creation + return unless owner.is_a?(Organization) + return if owner.max_repo_creation <= -1 + raise "已超过组织设置最大仓库数" if owner.max_repo_creation == owner.num_projects + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 75532188e..30953bf57 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -143,7 +143,7 @@ module ApplicationHelper def url_to_avatar(source) if File.exist?(disk_filename(source&.class, source&.id)) ctime = File.ctime(disk_filename(source.class, source.id)).to_i - if source.class.to_s == 'User' + if %w(User Organization).include?(source.class.to_s) File.join(relative_path, ["#{source.class}", "#{source.id}"]) + "?t=#{ctime}" else File.join("images/avatars", ["#{source.class}", "#{source.id}"]) + "?t=#{ctime}" diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c00010b2b..6fbc22eb5 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -61,12 +61,14 @@ module ProjectsHelper { login: project.project_educoder.owner, name: project.project_educoder.owner, + type: 'Educoder', image_url: project.project_educoder.image_url } else { login: @owner.login, name: @owner.real_name, + type: @owner.type, image_url: url_to_avatar(@owner) } end diff --git a/app/jobs/sync_mirrored_repository_job.rb b/app/jobs/sync_mirrored_repository_job.rb index 8184ddee5..f0617146a 100644 --- a/app/jobs/sync_mirrored_repository_job.rb +++ b/app/jobs/sync_mirrored_repository_job.rb @@ -5,7 +5,7 @@ class SyncMirroredRepositoryJob < ApplicationJob repo = Repository.find_by(id: repo_id) current_user = User.find_by(id: user_id) return if repo.blank? || current_user.blank? - result = Gitea::Repository::SyncMirroredService.new(repo.user.login, repo.identifier, token: current_user.gitea_token).call + result = Gitea::Repository::SyncMirroredService.new(repo.owner.login, repo.identifier, token: current_user.gitea_token).call repo&.mirror.set_status! if result[:status] === 200 end end diff --git a/app/models/concerns/project_operable.rb b/app/models/concerns/project_operable.rb index f05ed164e..5e8bf43d9 100644 --- a/app/models/concerns/project_operable.rb +++ b/app/models/concerns/project_operable.rb @@ -8,11 +8,12 @@ module ProjectOperable has_many :developers, -> { joins(:roles).where(roles: { name: 'Developer' }) }, class_name: 'Member' has_many :reporters, -> { joins(:roles).where(roles: { name: 'Reporter' }) }, class_name: 'Member' has_many :writable_members, -> { joins(:roles).where.not(roles: {name: 'Reporter'}) }, class_name: 'Member' + has_many :team_projects, dependent: :destroy end def add_member!(user_id, role_name='Developer') member = members.create!(user_id: user_id) - set_developer_role(member) + set_developer_role(member, role_name) end def remove_member!(user_id) @@ -21,7 +22,13 @@ module ProjectOperable end def member?(user_id) - members.exists?(user_id: user_id) + if owner.is_a?(User) + members.exists?(user_id: user_id) + elsif owner.is_a?(Organization) + members.exists?(user_id: user_id) || team_projects.joins(team: :team_users).where(team_users: {user_id: user_id}).present? + else + false + end end # 除了项目创建者本身 @@ -35,26 +42,50 @@ module ProjectOperable end def owner?(user) - self.owner == user + if owner.is_a?(User) + self.owner == user + elsif owner.is_a?(Organization) + owner.is_owner?(user.id) + else + false + end end # 项目管理员(包含项目拥有者),权限:仓库设置、仓库可读可写 def manager?(user) - managers.exists?(user_id: user.id) + if owner.is_a?(User) + managers.exists?(user_id: user.id) + elsif owner.is_a?(Organization) + managers.exists?(user_id: user.id) || owner.is_admin?(user.id) + else + false + end end # 项目开发者,可读可写权限 def develper?(user) - developers.exists?(user_id: user.id) + if owner.is_a?(User) + developers.exists?(user_id: user.id) + elsif owner.is_a?(Organization) + developers.exists?(user_id: user.id) || owner.is_write?(user.id) + else + false + end end # 报告者,只有可读权限 def reporter?(user) - reporters.exists?(user_id: user.id) + if owner.is_a?(User) + reporters.exists?(user_id: user.id) + elsif owner.is_a?(Organization) + reporters.exists?(user_id: user.id) || owner.is_read?(user.id) + else + false + end end - def set_developer_role(member) - role = Role.find_by_name 'Developer' + def set_developer_role(member, role_name) + role = Role.find_by(name: role_name) member.member_roles.create!(role: role) end diff --git a/app/models/laboratory.rb b/app/models/laboratory.rb index 73002a841..699800c92 100644 --- a/app/models/laboratory.rb +++ b/app/models/laboratory.rb @@ -11,6 +11,11 @@ # sync_subject :boolean default("0") # sync_shixun :boolean default("0") # +# Indexes +# +# index_laboratories_on_identifier (identifier) UNIQUE +# index_laboratories_on_school_id (school_id) +# class Laboratory < ApplicationRecord belongs_to :school, optional: true diff --git a/app/models/laboratory_setting.rb b/app/models/laboratory_setting.rb index 5013dd547..61c677def 100644 --- a/app/models/laboratory_setting.rb +++ b/app/models/laboratory_setting.rb @@ -6,6 +6,10 @@ # laboratory_id :integer # config :text(65535) # +# Indexes +# +# index_laboratory_settings_on_laboratory_id (laboratory_id) +# class LaboratorySetting < ApplicationRecord belongs_to :laboratory diff --git a/app/models/organization.rb b/app/models/organization.rb new file mode 100644 index 000000000..a541b203f --- /dev/null +++ b/app/models/organization.rb @@ -0,0 +1,127 @@ +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# login :string(255) default(""), not null +# hashed_password :string(40) default(""), not null +# firstname :string(30) default(""), not null +# lastname :string(255) default(""), not null +# mail :string(60) +# admin :boolean default("0"), not null +# status :integer default("1"), not null +# last_login_on :datetime +# language :string(5) default("") +# auth_source_id :integer +# created_on :datetime +# updated_on :datetime +# type :string(255) +# identity_url :string(255) +# mail_notification :string(255) default(""), not null +# salt :string(64) +# gid :integer +# visits :integer default("0") +# excellent_teacher :integer default("0") +# excellent_student :integer default("0") +# phone :string(255) +# authentication :boolean default("0") +# grade :integer default("0") +# experience :integer default("0") +# nickname :string(255) +# show_realname :boolean default("1") +# professional_certification :boolean default("0") +# ID_number :string(255) +# certification :integer default("0") +# homepage_teacher :boolean default("0") +# homepage_engineer :boolean default("0") +# is_test :integer default("0") +# ecoder_user_id :integer default("0") +# business :boolean default("0") +# profile_completed :boolean default("0") +# laboratory_id :integer +# platform :string(255) default("0") +# gitea_token :string(255) +# gitea_uid :integer +# is_shixun_marker :boolean default("0") +# is_sync_pwd :boolean default("1") +# watchers_count :integer default("0") +# devops_step :integer default("0") +# +# Indexes +# +# index_users_on_ecoder_user_id (ecoder_user_id) +# index_users_on_homepage_engineer (homepage_engineer) +# index_users_on_homepage_teacher (homepage_teacher) +# index_users_on_laboratory_id (laboratory_id) +# index_users_on_login (login) +# index_users_on_mail (mail) +# index_users_on_type (type) +# + +class Organization < Owner + alias_attribute :name, :login + NAME_REGEX = /^(?!_)(?!.*?_$)[a-zA-Z0-9_-]+$/ #只含有数字、字母、下划线不能以下划线开头和结尾 + + default_scope { where(type: "Organization") } + + has_one :organization_extension, dependent: :destroy + has_many :teams, dependent: :destroy + has_many :organization_users, dependent: :destroy + has_many :team_users, dependent: :destroy + + 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: "只能含有数字、字母、下划线且不能以下划线开头和结尾" } + + delegate :description, :website, :location, :repo_admin_change_team_access, + :visibility, :max_repo_creation, :num_projects, :num_users, :num_teams, to: :organization_extension, allow_nil: true + + scope :with_visibility, ->(visibility) { joins(:organization_extension).where(organization_extensions: {visibility: visibility}) if visibility.present? } + + def self.build(name, gitea_token=nil) + self.create!(login: name, gitea_token: gitea_token) + end + + def can_create_project?(user_id) + team_users.joins(:team).where(user_id: user_id, teams: {can_create_org_project: true}).present? + end + + def is_member?(user_id) + organization_users.where(user_id: user_id).present? + end + + def is_owner?(user_id) + team_users.joins(:team).where(user_id: user_id, teams: {authorize: %w(owner)}).present? + end + + def is_admin?(user_id) + team_users.joins(:team).where(user_id: user_id, teams: {authorize: %w(admin owner)}).present? + end + + def is_write?(user_id) + team_users.joins(:team).where(user_id: user_id, teams: {authorize: %w(write admin owner)}).present? + end + + def is_read?(user_id) + team_users.joins(:team).where(user_id: user_id, teams: {authorize: %w(read write admin owner)}).present? + end + + # 是不是所有者团队的最后一个成员 + def is_owner_team_last_one?(user_id) + owner_team_users = team_users.joins(:team).where(teams: {authorize: %w(owner)}) + owner_team_users.pluck(:user_id).include?(user_id) && owner_team_users.size == 1 + end + + def real_name + login + end + + def show_real_name + name = lastname + firstname + if name.blank? + nickname.blank? ? login : nickname + else + name + end + end +end diff --git a/app/models/organization_extension.rb b/app/models/organization_extension.rb new file mode 100644 index 000000000..8b9946cd7 --- /dev/null +++ b/app/models/organization_extension.rb @@ -0,0 +1,42 @@ +# == Schema Information +# +# Table name: organization_extensions +# +# id :integer not null, primary key +# organization_id :integer +# description :string(255) +# website :string(255) +# location :string(255) +# repo_admin_change_team_access :boolean default("0") +# visibility :integer default("0") +# max_repo_creation :integer default("-1") +# created_at :datetime not null +# updated_at :datetime not null +# num_projects :integer default("0") +# num_users :integer default("0") +# num_teams :integer default("0") +# +# Indexes +# +# index_organization_extensions_on_organization_id (organization_id) +# + +class OrganizationExtension < ApplicationRecord + + belongs_to :organization + has_many :organization_users, foreign_key: :organization_id, primary_key: :organization_id + has_many :projects, foreign_key: :user_id, primary_key: :organization_id + has_many :teams, foreign_key: :organization_id, primary_key: :organization_id + + enum visibility: {common: 0, limited: 1, privacy: 2} + + def self.build(organization_id, description, website, location, repo_admin_change_team_access, visibility, max_repo_creation) + self.create!(organization_id: organization_id, + description: description, + website: website, + location: location, + repo_admin_change_team_access: repo_admin_change_team_access, + visibility: visibility, + max_repo_creation: max_repo_creation) + end +end diff --git a/app/models/organization_user.rb b/app/models/organization_user.rb new file mode 100644 index 000000000..cf9e22371 --- /dev/null +++ b/app/models/organization_user.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: organization_users +# +# id :integer not null, primary key +# user_id :integer +# organization_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_organization_users_on_organization_id (organization_id) +# index_organization_users_on_user_id (user_id) +# + +class OrganizationUser < ApplicationRecord + + belongs_to :organization + belongs_to :organization_extension, foreign_key: :organization_id, primary_key: :organization_id, counter_cache: :num_users + belongs_to :user + + validates :user_id, uniqueness: {scope: :organization_id} + + def self.build(organization_id, user_id) + org_user = self.find_by(organization_id: organization_id, user_id: user_id) + return org_user unless org_user.nil? + self.create!(organization_id: organization_id, user_id: user_id) + end + + def teams + organization.teams.joins(:team_users).where(team_users: {user_id: user_id}) + end +end diff --git a/app/models/owner.rb b/app/models/owner.rb new file mode 100644 index 000000000..d4194c32c --- /dev/null +++ b/app/models/owner.rb @@ -0,0 +1,68 @@ +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# login :string(255) default(""), not null +# hashed_password :string(40) default(""), not null +# firstname :string(30) default(""), not null +# lastname :string(255) default(""), not null +# mail :string(60) +# admin :boolean default("0"), not null +# status :integer default("1"), not null +# last_login_on :datetime +# language :string(5) default("") +# auth_source_id :integer +# created_on :datetime +# updated_on :datetime +# type :string(255) +# identity_url :string(255) +# mail_notification :string(255) default(""), not null +# salt :string(64) +# gid :integer +# visits :integer default("0") +# excellent_teacher :integer default("0") +# excellent_student :integer default("0") +# phone :string(255) +# authentication :boolean default("0") +# grade :integer default("0") +# experience :integer default("0") +# nickname :string(255) +# show_realname :boolean default("1") +# professional_certification :boolean default("0") +# ID_number :string(255) +# certification :integer default("0") +# homepage_teacher :boolean default("0") +# homepage_engineer :boolean default("0") +# is_test :integer default("0") +# ecoder_user_id :integer default("0") +# business :boolean default("0") +# profile_completed :boolean default("0") +# laboratory_id :integer +# platform :string(255) default("0") +# gitea_token :string(255) +# gitea_uid :integer +# is_shixun_marker :boolean default("0") +# is_sync_pwd :boolean default("1") +# watchers_count :integer default("0") +# devops_step :integer default("0") +# +# Indexes +# +# index_users_on_ecoder_user_id (ecoder_user_id) +# index_users_on_homepage_engineer (homepage_engineer) +# index_users_on_homepage_teacher (homepage_teacher) +# index_users_on_laboratory_id (laboratory_id) +# index_users_on_login (login) +# index_users_on_mail (mail) +# index_users_on_type (type) +# + +class Owner < ApplicationRecord + self.table_name = "users" + + include ProjectAbility + + has_many :projects, foreign_key: :user_id, dependent: :destroy + has_many :repositories, foreign_key: :user_id, dependent: :destroy +end diff --git a/app/models/project.rb b/app/models/project.rb index 4b9e72e19..f82674dbc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1,73 +1,74 @@ -# == Schema Information -# -# Table name: projects -# -# id :integer not null, primary key -# name :string(255) default(""), not null -# description :text(4294967295) -# homepage :string(255) default("") -# is_public :boolean default("1"), not null -# parent_id :integer -# created_on :datetime -# updated_on :datetime -# identifier :string(255) -# status :integer default("1"), not null -# lft :integer -# rgt :integer -# inherit_members :boolean default("0"), not null -# project_type :integer default("0") -# hidden_repo :boolean default("0"), not null -# attachmenttype :integer default("1") -# user_id :integer -# dts_test :integer default("0") -# enterprise_name :string(255) -# organization_id :integer -# project_new_type :integer -# gpid :integer -# forked_from_project_id :integer -# forked_count :integer default("0") -# publish_resource :integer default("0") -# visits :integer default("0") -# hot :integer default("0") -# invite_code :string(255) -# qrcode :string(255) -# qrcode_expiretime :integer default("0") -# script :text(65535) -# training_status :integer default("0") -# rep_identifier :string(255) -# project_category_id :integer -# project_language_id :integer -# license_id :integer -# ignore_id :integer -# praises_count :integer default("0") -# watchers_count :integer default("0") -# issues_count :integer default("0") -# pull_requests_count :integer default("0") -# language :string(255) -# versions_count :integer default("0") -# issue_tags_count :integer default("0") -# closed_issues_count :integer default("0") -# open_devops :boolean default("0") -# gitea_webhook_id :integer -# open_devops_count :integer default("0") -# recommend :boolean default("0") -# platform :integer default("0") -# -# Indexes -# -# index_projects_on_forked_from_project_id (forked_from_project_id) -# index_projects_on_identifier (identifier) -# index_projects_on_is_public (is_public) -# index_projects_on_lft (lft) -# index_projects_on_name (name) -# index_projects_on_platform (platform) -# index_projects_on_project_type (project_type) -# index_projects_on_recommend (recommend) -# index_projects_on_rgt (rgt) -# index_projects_on_status (status) -# index_projects_on_updated_on (updated_on) -# - +# == Schema Information +# +# Table name: projects +# +# id :integer not null, primary key +# name :string(255) default(""), not null +# description :text(4294967295) +# homepage :string(255) default("") +# is_public :boolean default("1"), not null +# parent_id :integer +# created_on :datetime +# updated_on :datetime +# identifier :string(255) +# status :integer default("1"), not null +# lft :integer +# rgt :integer +# inherit_members :boolean default("0"), not null +# project_type :integer default("0") +# hidden_repo :boolean default("0"), not null +# attachmenttype :integer default("1") +# user_id :integer +# dts_test :integer default("0") +# enterprise_name :string(255) +# organization_id :integer +# project_new_type :integer +# gpid :integer +# forked_from_project_id :integer +# forked_count :integer default("0") +# publish_resource :integer default("0") +# visits :integer default("0") +# hot :integer default("0") +# invite_code :string(255) +# qrcode :string(255) +# qrcode_expiretime :integer default("0") +# script :text(65535) +# training_status :integer default("0") +# rep_identifier :string(255) +# project_category_id :integer +# project_language_id :integer +# license_id :integer +# ignore_id :integer +# praises_count :integer default("0") +# watchers_count :integer default("0") +# issues_count :integer default("0") +# pull_requests_count :integer default("0") +# language :string(255) +# versions_count :integer default("0") +# issue_tags_count :integer default("0") +# closed_issues_count :integer default("0") +# open_devops :boolean default("0") +# gitea_webhook_id :integer +# open_devops_count :integer default("0") +# recommend :boolean default("0") +# platform :integer default("0") +# +# Indexes +# +# index_projects_on_forked_from_project_id (forked_from_project_id) +# index_projects_on_identifier (identifier) +# index_projects_on_is_public (is_public) +# index_projects_on_lft (lft) +# index_projects_on_name (name) +# index_projects_on_platform (platform) +# index_projects_on_project_type (project_type) +# index_projects_on_recommend (recommend) +# index_projects_on_rgt (rgt) +# index_projects_on_status (status) +# index_projects_on_updated_on (updated_on) +# + + class Project < ApplicationRecord include Matchable include Publicable @@ -85,7 +86,8 @@ class Project < ApplicationRecord belongs_to :ignore, optional: true belongs_to :license, optional: true - belongs_to :owner, class_name: 'User', foreign_key: :user_id, optional: true + belongs_to :owner, class_name: 'Owner', foreign_key: :user_id, optional: true + belongs_to :organization_extension, foreign_key: :user_id, primary_key: :organization_id, optional: true, counter_cache: :num_projects belongs_to :project_category, optional: true , :counter_cache => true belongs_to :project_language, optional: true , :counter_cache => true has_many :project_trends, dependent: :destroy @@ -106,6 +108,7 @@ class Project < ApplicationRecord has_many :praise_treads, as: :praise_tread_object, dependent: :destroy has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" has_one :project_detail, dependent: :destroy + has_many :team_projects, dependent: :destroy after_save :check_project_members scope :project_statics_select, -> {select(:id,:name, :is_public, :identifier, :status, :project_type, :user_id, :forked_count, :visits, :project_category_id, :project_language_id, :license_id, :ignore_id, :watchers_count, :created_on)} @@ -248,7 +251,7 @@ class Project < ApplicationRecord def self.find_with_namespace(namespace_path, identifier) logger.info "########namespace_path: #{namespace_path} ########identifier: #{identifier} " - user = User.find_by_login namespace_path + user = Owner.find_by_login namespace_path project = user&.projects&.find_by(identifier: identifier) || Project.find_by(identifier: "#{namespace_path}/#{identifier}") return nil if project.blank? diff --git a/app/models/repository.rb b/app/models/repository.rb index be6afcb10..934eaa0e3 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -33,7 +33,7 @@ class Repository < ApplicationRecord self.inheritance_column = nil # FIX The single-table inheritance mechanism failed belongs_to :project, :touch => true - belongs_to :user, optional: true + belongs_to :owner, class_name: 'Owner', foreign_key: :user_id, optional: true has_one :mirror, foreign_key: :repo_id has_one :ci_cloud_account, class_name: 'Ci::CloudAccount', foreign_key: :repo_id has_many :version_releases, dependent: :destroy diff --git a/app/models/team.rb b/app/models/team.rb new file mode 100644 index 000000000..82a3e88c1 --- /dev/null +++ b/app/models/team.rb @@ -0,0 +1,55 @@ +# == Schema Information +# +# Table name: teams +# +# id :integer not null, primary key +# organization_id :integer +# name :string(255) +# description :string(255) +# authorize :integer default("0") +# num_projects :integer default("0") +# num_users :integer default("0") +# includes_all_project :boolean default("0") +# can_create_org_project :boolean default("0") +# gtid :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_teams_on_organization_id (organization_id) +# + +class Team < ApplicationRecord + + belongs_to :organization + belongs_to :organization_extension, foreign_key: :organization_id, primary_key: :organization_id, counter_cache: :num_teams + has_many :team_projects, dependent: :destroy + has_many :team_units, dependent: :destroy + has_many :team_users, dependent: :destroy + + validates :name, uniqueness: {scope: :organization_id} + + enum authorize: {common: 0, read: 1, write: 2, admin: 3, owner: 4} + + def self.build(organization_id, name, description, authorize, includes_all_project, can_create_org_project) + self.create!(organization_id: organization_id, + name: name, + description: description, + authorize: authorize, + includes_all_project: includes_all_project, + can_create_org_project: can_create_org_project) + end + + def setup_team_project! + return unless includes_all_project + organization.projects.each do |project| + TeamProject.build(organization.id, id, project.id) + end + end + + def is_member?(user_id) + team_users.where(user_id: user_id).present? + end + +end diff --git a/app/models/team_project.rb b/app/models/team_project.rb new file mode 100644 index 000000000..ae2e43152 --- /dev/null +++ b/app/models/team_project.rb @@ -0,0 +1,30 @@ +# == Schema Information +# +# Table name: team_projects +# +# id :integer not null, primary key +# organization_id :integer +# project_id :integer +# team_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_team_projects_on_organization_id (organization_id) +# index_team_projects_on_project_id (project_id) +# index_team_projects_on_team_id (team_id) +# + +class TeamProject < ApplicationRecord + + belongs_to :organization + belongs_to :project + belongs_to :team, counter_cache: :num_projects + + validates :project_id, uniqueness: {scope: [:organization_id, :team_id]} + + def self.build(organization_id, team_id, project_id) + self.find_or_create_by!(organization_id: organization_id, team_id: team_id, project_id: project_id) + end +end diff --git a/app/models/team_unit.rb b/app/models/team_unit.rb new file mode 100644 index 000000000..c757cc684 --- /dev/null +++ b/app/models/team_unit.rb @@ -0,0 +1,31 @@ +# == Schema Information +# +# Table name: team_units +# +# id :integer not null, primary key +# organization_id :integer +# team_id :integer +# unit_type :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_team_units_on_organization_id (organization_id) +# index_team_units_on_team_id (team_id) +# + +class TeamUnit < ApplicationRecord + + belongs_to :organization + belongs_to :team + + enum unit_type: {code: 1, issues: 2, pulls: 3, releases: 4} + + validates :unit_type, uniqueness: { scope: [:organization_id, :team_id]} + + def self.build(organization_id, team_id, unit_type) + self.create!(organization_id: organization_id, team_id: team_id, unit_type: unit_type) + end + +end diff --git a/app/models/team_user.rb b/app/models/team_user.rb new file mode 100644 index 000000000..9670013a7 --- /dev/null +++ b/app/models/team_user.rb @@ -0,0 +1,30 @@ +# == Schema Information +# +# Table name: team_users +# +# id :integer not null, primary key +# organization_id :integer +# team_id :integer +# user_id :integer +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_team_users_on_organization_id (organization_id) +# index_team_users_on_team_id (team_id) +# index_team_users_on_user_id (user_id) +# + +class TeamUser < ApplicationRecord + + belongs_to :organization + belongs_to :team, counter_cache: :num_users + belongs_to :user + + validates :user_id, uniqueness: {scope: [:organization_id, :team_id]} + + def self.build(organization_id, user_id, team_id) + self.create!(organization_id: organization_id, user_id: user_id, team_id: team_id) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 5ea57880d..1cb6670c6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -58,14 +58,13 @@ # index_users_on_type (type) # -class User < ApplicationRecord +class User < Owner + default_scope {where(type: %w(User AnonymousUser))} extend Enumerize include Watchable include Likeable include BaseModel - include ProjectOperable - include ProjectAbility include Droneable # include Searchable::Dependents::User @@ -136,8 +135,8 @@ class User < ApplicationRecord has_many :attachments,foreign_key: :author_id, :dependent => :destroy # 关注 - has_many :be_watchers, foreign_key: :user_id, dependent: :destroy # 我的关注 - has_many :be_watcher_users, through: :be_watchers, dependent: :destroy # 我关注的用户 + # has_many :be_watchers, foreign_key: :user_id, dependent: :destroy # 我的关注 + # has_many :be_watcher_users, through: :be_watchers, dependent: :destroy # 我关注的用户 has_many :watchers, as: :watchable, dependent: :destroy has_one :ci_cloud_account, class_name: 'Ci::CloudAccount', dependent: :destroy @@ -154,21 +153,21 @@ class User < ApplicationRecord # 项目 has_many :applied_projects, dependent: :destroy - has_many :projects, dependent: :destroy - has_many :repositories, dependent: :destroy - # 教学案例 # has_many :libraries, dependent: :destroy has_many :project_trends, dependent: :destroy has_many :oauths , dependent: :destroy + has_many :organization_users, dependent: :destroy + has_many :organizations, through: :organization_users + # Groups and active users scope :active, lambda { where(status: STATUS_ACTIVE) } scope :like, lambda { |keywords| where("LOWER(concat(lastname, firstname, login, mail)) LIKE ?", "%#{keywords.split(" ").join('|')}%") unless keywords.blank? } - scope :simple_select, -> {select(:id, :login, :lastname,:firstname, :nickname, :gitea_uid)} + scope :simple_select, -> {select(:id, :login, :lastname,:firstname, :nickname, :gitea_uid, :type)} attr_accessor :password, :password_confirmation diff --git a/app/services/gitea/client_service.rb b/app/services/gitea/client_service.rb index 1e9de823c..1c13bc424 100644 --- a/app/services/gitea/client_service.rb +++ b/app/services/gitea/client_service.rb @@ -132,7 +132,8 @@ class Gitea::ClientService < ApplicationService when 204 puts "[gitea] " - raise Error, "[gitea] delete ok" + # raise Error, "[gitea] delete ok" + {status: 204} when 409 message = "创建失败,请检查该分支合并是否已存在" raise Error, mark + message diff --git a/app/services/gitea/organization/create_service.rb b/app/services/gitea/organization/create_service.rb new file mode 100644 index 000000000..4da1720cc --- /dev/null +++ b/app/services/gitea/organization/create_service.rb @@ -0,0 +1,44 @@ +class Gitea::Organization::CreateService < Gitea::ClientService + attr_reader :token, :org + + def initialize(token, org) + @token = token + @org = org + end + + def call + response = post(url, request_params) + render_status(response) + end + + private + def request_params + create_params = { + username: org.login, + description: org.description, + location: org.location, + repo_admin_change_team_access: org.repo_admin_change_team_access, + visibility: visibility(org.visibility), + website: org.website, + max_repo_creation: org.max_repo_creation + } + Hash.new.merge(token: token, data: create_params) + end + + def visibility(visibility) + case visibility + when "common" + "public" + when "limited" + "limited" + when "privacy" + "private" + else + "public" + end + end + + def url + "/orgs".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/delete_service.rb b/app/services/gitea/organization/delete_service.rb new file mode 100644 index 000000000..e72820133 --- /dev/null +++ b/app/services/gitea/organization/delete_service.rb @@ -0,0 +1,23 @@ +class Gitea::Organization::DeleteService < Gitea::ClientService + attr_reader :token, :name + + def initialize(token, name) + @token = token + @name = name + end + + def call + response = delete(url, params) + render_status(response) + end + + private + + def params + Hash.new.merge(token: token) + end + + def url + "/orgs/#{name}".freeze + end +end diff --git a/app/services/gitea/organization/organization_user/delete_service.rb b/app/services/gitea/organization/organization_user/delete_service.rb new file mode 100644 index 000000000..ef6d48947 --- /dev/null +++ b/app/services/gitea/organization/organization_user/delete_service.rb @@ -0,0 +1,23 @@ +class Gitea::Organization::OrganizationUser::DeleteService < Gitea::ClientService + attr_reader :token, :org_name, :username + + def initialize(token, org_name, username) + @token = token + @org_name = org_name + @username = username + end + + def call + response = delete(url, params) + render_status(response) + end + + private + def params + Hash.new.merge(token: token) + end + + def url + "/orgs/#{org_name}/members/#{username}" + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/repository/create_service.rb b/app/services/gitea/organization/repository/create_service.rb new file mode 100644 index 000000000..060a8ab05 --- /dev/null +++ b/app/services/gitea/organization/repository/create_service.rb @@ -0,0 +1,25 @@ +class Gitea::Organization::Repository::CreateService < Gitea::ClientService + attr_reader :token, :org_name, :params + + def initialize(token, org_name, params) + @token = token + @org_name = org_name + @params = params + end + + def call + response = post(url, request_params) + render_201_response(response) + end + + private + + def request_params + create_params = params.merge(readme: "readme") + Hash.new.merge(token: token, data: create_params) + end + + def url + "/orgs/#{org_name}/repos".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/team/create_service.rb b/app/services/gitea/organization/team/create_service.rb new file mode 100644 index 000000000..3e9340af4 --- /dev/null +++ b/app/services/gitea/organization/team/create_service.rb @@ -0,0 +1,50 @@ +class Gitea::Organization::Team::CreateService < Gitea::ClientService + attr_reader :token, :org, :team + + def initialize(token, org, team) + @token = token + @org = org + @team = team + end + + def call + response = post(url, request_params) + render_status(response) + end + + private + def request_params + create_params = { + name: team.name, + description: team.description, + permission: permission(team.authorize), + includes_all_repositories: team.includes_all_project, + can_create_org_repo: team.can_create_org_project, + units: unit_arrays + } + Hash.new.merge(token: token, data: create_params) + end + + def permission(authorize) + case authorize + when "read" + "read" + when "write" + "write" + when "admin" + "admin" + when "owner" + "owner" + else + "none" + end + end + + def unit_arrays + team.team_units.pluck(:unit_type).collect{|t|"repo.#{t}"} + end + + def url + "/orgs/#{org.login}/teams".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/team/delete_service.rb b/app/services/gitea/organization/team/delete_service.rb new file mode 100644 index 000000000..4f94352bf --- /dev/null +++ b/app/services/gitea/organization/team/delete_service.rb @@ -0,0 +1,22 @@ +class Gitea::Organization::Team::DeleteService < Gitea::ClientService + attr_reader :token, :gtid + + def initialize(token, gtid) + @token = token + @gtid = gtid + end + + def call + response = delete(url, params) + render_status(response) + end + + private + def params + Hash.new.merge(token: token) + end + + def url + "/teams/#{gtid}".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/team/update_service.rb b/app/services/gitea/organization/team/update_service.rb new file mode 100644 index 000000000..68be59453 --- /dev/null +++ b/app/services/gitea/organization/team/update_service.rb @@ -0,0 +1,49 @@ +class Gitea::Organization::Team::UpdateService < Gitea::ClientService + attr_reader :token, :team + + def initialize(token, team) + @token = token + @team = team + end + + def call + response = patch(url, request_params) + render_status(response) + end + + private + def request_params + update_params = { + name: team.name, + description: team.description, + permission: permission(team.authorize), + includes_all_repositories: team.includes_all_project, + can_create_org_repo: team.can_create_org_project, + units: unit_arrays + } + Hash.new.merge(token: token, data: update_params) + end + + def permission(authorize) + case authorize + when "read" + "read" + when "write" + "write" + when "admin" + "admin" + when "owner" + "owner" + else + "none" + end + end + + def unit_arrays + team.team_units.pluck(:unit_type).collect{|t|"repo.#{t}"} + end + + def url + "/teams/#{team.gtid}".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/team_project/create_service.rb b/app/services/gitea/organization/team_project/create_service.rb new file mode 100644 index 000000000..562480ac5 --- /dev/null +++ b/app/services/gitea/organization/team_project/create_service.rb @@ -0,0 +1,24 @@ +class Gitea::Organization::TeamProject::CreateService < Gitea::ClientService + attr_reader :token, :gtid, :org_name, :repo_name + + def initialize(token, gtid, org_name, repo_name) + @token = token + @gtid = gtid + @org_name = org_name + @repo_name = repo_name + end + + def call + response = put(url, request_params) + render_status(response) + end + + private + def request_params + Hash.new.merge(token: token) + end + + def url + "/teams/#{gtid}/repos/#{org_name}/#{repo_name}".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/team_project/delete_service.rb b/app/services/gitea/organization/team_project/delete_service.rb new file mode 100644 index 000000000..9d59f5e2f --- /dev/null +++ b/app/services/gitea/organization/team_project/delete_service.rb @@ -0,0 +1,24 @@ +class Gitea::Organization::TeamProject::DeleteService < Gitea::ClientService + attr_reader :token, :gtid, :org_name, :repo_name + + def initialize(token, gtid, org_name, repo_name) + @token = token + @gtid = gtid + @org_name = org_name + @repo_name = repo_name + end + + def call + response = delete(url, params) + render_status(response) + end + + private + def params + Hash.new.merge(token: token) + end + + def url + "/teams/#{gtid}/repos/#{org_name}/#{repo_name}".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/team_user/create_service.rb b/app/services/gitea/organization/team_user/create_service.rb new file mode 100644 index 000000000..11aa4da23 --- /dev/null +++ b/app/services/gitea/organization/team_user/create_service.rb @@ -0,0 +1,23 @@ +class Gitea::Organization::TeamUser::CreateService < Gitea::ClientService + attr_reader :token, :gtid, :username + + def initialize(token, gtid, username) + @token = token + @gtid = gtid + @username = username + end + + def call + response = put(url, request_params) + render_status(response) + end + + private + def request_params + Hash.new.merge(token: token) + end + + def url + "/teams/#{gtid}/members/#{username}".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/team_user/delete_service.rb b/app/services/gitea/organization/team_user/delete_service.rb new file mode 100644 index 000000000..199f86956 --- /dev/null +++ b/app/services/gitea/organization/team_user/delete_service.rb @@ -0,0 +1,23 @@ +class Gitea::Organization::TeamUser::DeleteService < Gitea::ClientService + attr_reader :token, :gtid, :username + + def initialize(token, gtid, username) + @token = token + @gtid = gtid + @username = username + end + + def call + response = delete(url, params) + render_status(response) + end + + private + def params + Hash.new.merge(token: token) + end + + def url + "/teams/#{gtid}/members/#{username}" + end +end \ No newline at end of file diff --git a/app/services/gitea/organization/update_service.rb b/app/services/gitea/organization/update_service.rb new file mode 100644 index 000000000..c04dcbc86 --- /dev/null +++ b/app/services/gitea/organization/update_service.rb @@ -0,0 +1,45 @@ +class Gitea::Organization::UpdateService < Gitea::ClientService + attr_reader :token, :login, :org + + def initialize(token, login, org) + @token = token + @login = login + @org = org + end + + def call + response = patch(url, request_params) + render_status(response) + end + + private + def request_params + update_params = { + name: org.login, + description: org.description, + location: org.location, + repo_admin_change_team_access: org.repo_admin_change_team_access, + visibility: visibility(org.visibility), + website: org.website, + max_repo_creation: org.max_repo_creation + } + Hash.new.merge(token: token, data: update_params) + end + + def visibility(visibility) + case visibility + when "common" + "public" + when "limited" + "limited" + when "privacy" + "private" + else + "public" + end + end + + def url + "/orgs/#{login}".freeze + end +end \ No newline at end of file diff --git a/app/services/gitea/repository/transfer_service.rb b/app/services/gitea/repository/transfer_service.rb new file mode 100644 index 000000000..358ac9421 --- /dev/null +++ b/app/services/gitea/repository/transfer_service.rb @@ -0,0 +1,27 @@ +class Gitea::Repository::TransferService < Gitea::ClientService + attr_reader :token, :owner, :repo, :new_owner + + def initialize(token, owner, repo, new_owner) + @token = token + @owner = owner + @repo = repo + @new_owner = new_owner + end + + def call + response = post(url, request_params) + render_status(response) + end + + private + def request_params + transfer_params = { + new_owner: new_owner + } + Hash.new.merge(token: token, data: transfer_params) + end + + def url + "/repos/#{owner}/#{repo}/transfer".freeze + end +end \ No newline at end of file diff --git a/app/services/organizations/create_service.rb b/app/services/organizations/create_service.rb new file mode 100644 index 000000000..2d2b29e0f --- /dev/null +++ b/app/services/organizations/create_service.rb @@ -0,0 +1,73 @@ +class Organizations::CreateService < ApplicationService + attr_reader :user, :params + attr_accessor :organization, :gitea_organization, :owner_team + + def initialize(user, params) + @user = user + @params = params + end + + def call + Rails.logger.info("######Organization create_service begin######") + Rails.logger.info("######params #{params}######") + ActiveRecord::Base.transaction do + create_org_and_extension + create_owner_info + create_gitea_org + sync_gitea_info + + Rails.logger.info("######Organization create_service end######") + end + @organization + end + + private + def description + params[:description] + end + + def website + params[:website] + end + + def location + params[:location].present? ? params[:location] : nil + end + + def repo_admin_change_team_access + params[:repo_admin_change_team_access].present? ? params[:repo_admin_change_team_access] : false + end + + def visibility + params[:visibility].present? ? params[:visibility] : "common" + end + + def max_repo_creation + params[:max_repo_creation].present? ? params[:max_repo_creation] : -1 + end + + def create_org_and_extension + @organization = Organization.build(params[:name], user.gitea_token) + org_extension = OrganizationExtension.build(organization.id, description, website, + location, repo_admin_change_team_access, + visibility, max_repo_creation) + end + + def create_owner_info + @owner_team = Team.build(organization.id, "Owners", "", 4, true, true) + TeamUnit.unit_types.keys.each do |u_type| + TeamUnit.build(organization.id, owner_team.id, u_type) + end + OrganizationUser.build(organization.id, user.id) + TeamUser.build(organization.id, user.id, owner_team.id) + end + + def create_gitea_org + @gitea_organization = Gitea::Organization::CreateService.call(@organization.gitea_token, organization) + end + + def sync_gitea_info + organization.update!(gitea_uid: gitea_organization["id"]) + owner_team.update!(gtid: gitea_organization["owner_team"]["id"]) + end +end \ No newline at end of file diff --git a/app/services/organizations/teams/create_service.rb b/app/services/organizations/teams/create_service.rb new file mode 100644 index 000000000..81ad78772 --- /dev/null +++ b/app/services/organizations/teams/create_service.rb @@ -0,0 +1,75 @@ +class Organizations::Teams::CreateService < ApplicationService + attr_reader :user, :org, :params + attr_accessor :team, :gitea_team + + def initialize(user, org, params) + @user = user + @org = org + @params = params + end + + def call + Rails.logger.info("######Team create_service begin######") + Rails.logger.info("######params #{params}######") + ActiveRecord::Base.transaction do + create_team + create_units + create_gitea_team + sync_team_gtid + team.setup_team_project! + end + Rails.logger.info("######Team create_service end######") + + team + end + + private + def name + params[:name] + end + + def description + params[:description] + end + + def authorize + params[:authorize].present? ? params[:authorize] : "common" + end + + def includes_all_project + params[:includes_all_project].present? ? params[:includes_all_project] : false + end + + def can_create_org_project + params[:can_create_org_project].present? ? params[:can_create_org_project] : false + end + + def create_team + @team = Team.build(org.id, name, description, authorize, + includes_all_project, can_create_org_project) + end + + def units_params + %w(admin owner).include?(authorize) ? %w(code issues pulls releases) : params[:unit_types] + end + + def create_units + return if units_params.blank? + begin + units_params.each do |unit| + TeamUnit.build(org.id, team.id, unit) + end + rescue + raise ActiveRecord::Rollback, "TeamUnit create error" + end + end + + def create_gitea_team + @gitea_team = Gitea::Organization::Team::CreateService.call(org.gitea_token, org, team) + end + + def sync_team_gtid + team.update!(gtid: gitea_team["id"]) + end + +end \ No newline at end of file diff --git a/app/services/organizations/teams/update_service.rb b/app/services/organizations/teams/update_service.rb new file mode 100644 index 000000000..71c82c40b --- /dev/null +++ b/app/services/organizations/teams/update_service.rb @@ -0,0 +1,59 @@ +class Organizations::Teams::UpdateService < ApplicationService + attr_reader :user, :team, :params + + def initialize(user, team, params) + @user = user + @team = team + @params = params + end + + def call + Rails.logger.info("######Team update_service begin######") + Rails.logger.info("######params #{params}######") + ActiveRecord::Base.transaction do + update_team(update_params) + update_units + team.reload + team.setup_team_project! + update_gitea_team + end + Rails.logger.info("######Team update_service end######") + + team + end + + private + def update_params + if team.authorize == "owner" + update_params = params.slice(:description) + else + update_params = params.slice(:name, :description, :authorize, :includes_all_project, :can_create_org_project) + end + update_params + end + + def units_params + %w(admin owner).include?(team.authorize) ? %w(code issues pulls releases) : params[:unit_types] + end + + def update_team(update_params) + team.update_attributes!(update_params) + end + + def update_units + return if units_params.blank? + begin + team.team_units.map{|u|u.destroy!} + Rails.logger.info units_params + units_params.each do |unit| + TeamUnit.build(team&.organization&.id, team.id, unit) + end + rescue + raise ActiveRecord::Rollback, "TeamUnit update error" + end + end + + def update_gitea_team + Gitea::Organization::Team::UpdateService.call(team&.organization&.gitea_token, team) + end +end \ No newline at end of file diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 3efd3e43c..3204e5fc7 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -20,7 +20,7 @@ class Projects::ForkService < ApplicationService clone_project.save! new_repository = clone_project.repository - new_repository.user = @target_owner + new_repository.owner = @target_owner new_repository.identifier = @project.identifier new_repository.save! diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb new file mode 100644 index 000000000..f9b5c5700 --- /dev/null +++ b/app/services/projects/transfer_service.rb @@ -0,0 +1,45 @@ +class Projects::TransferService < ApplicationService + attr_accessor :project, :owner, :new_owner + + def initialize(project, new_owner) + @project = project + @owner = project.owner + @new_owner = new_owner + end + + def call + Rails.logger.info("###### Project transfer_service begin ######") + ActiveRecord::Base.transaction do + gitea_update_owner + update_owner + update_visit_teams + end + + Rails.logger.info("##### Project transfer_service end ######") + + @project.reload + end + + private + def update_owner + project.update!(user_id: new_owner.id) + end + + def update_visit_teams + if new_owner.is_a?(Organization) + new_owner.teams.where(includes_all_project: true).each do |team| + TeamProject.build(new_owner.id, team.id, project.id) + end + else + project.team_projects.each(&:destroy!) + end + end + + def gitea_update_owner + begin + Gitea::Repository::TransferService.call(owner&.gitea_token, owner&.login, project.identifier, new_owner&.login) + rescue Exception => e + Rails.logger.info("##### Project transfer_service, gitea transfer error #{e}") + end + end +end \ No newline at end of file diff --git a/app/services/repositories/create_service.rb b/app/services/repositories/create_service.rb index 9458e513f..4c5519f2e 100644 --- a/app/services/repositories/create_service.rb +++ b/app/services/repositories/create_service.rb @@ -1,5 +1,6 @@ class Repositories::CreateService < ApplicationService attr_reader :user, :project, :params + attr_accessor :repository, :gitea_repository def initialize(user, project, params) @project = project @@ -10,10 +11,10 @@ class Repositories::CreateService < ApplicationService def call @repository = Repository.new(repository_params) ActiveRecord::Base.transaction do - if @repository.save! - gitea_repository = Gitea::Repository::CreateService.new(user.gitea_token, gitea_repository_params).call - sync_project(@repository, gitea_repository) - sync_repository(@repository, gitea_repository) + if repository.save! + create_gitea_repository + sync_project + sync_repository # if project.project_type == "common" # chain_params = { # type: "create", @@ -29,7 +30,7 @@ class Repositories::CreateService < ApplicationService else Rails.logger.info("#############___________create_repository_erros______###########{@repository.errors.messages}") end - @repository + repository end rescue => e puts "create repository service error: #{e.message}" @@ -38,7 +39,25 @@ class Repositories::CreateService < ApplicationService private - def sync_project(repository, gitea_repository) + def create_gitea_repository + if project.owner.is_a?(User) + @gitea_repository = Gitea::Repository::CreateService.new(user.gitea_token, gitea_repository_params).call + elsif project.owner.is_a?(Organization) + @gitea_repository = Gitea::Organization::Repository::CreateService.call(user.gitea_token, project.owner.login, gitea_repository_params) + project.owner.teams.each do |team| + next unless team.includes_all_project + TeamProject.build(project.user_id, team.id, project.id) + end + create_manager_member + end + end + + def create_manager_member + return if project.owner.is_owner?(user.id) + project.add_member!(user.id, "Manager") + end + + def sync_project if gitea_repository project.update_columns( gpid: gitea_repository["id"], @@ -47,7 +66,7 @@ class Repositories::CreateService < ApplicationService end end - def sync_repository(repository, gitea_repository) + def sync_repository repository.update_columns(url: remote_repository_url,) if gitea_repository end diff --git a/app/views/organizations/_user_detail.json.jbuilder b/app/views/organizations/_user_detail.json.jbuilder new file mode 100644 index 000000000..0fa35215a --- /dev/null +++ b/app/views/organizations/_user_detail.json.jbuilder @@ -0,0 +1,8 @@ +json.user_id user.id +json.login user.login +json.name user.full_name +json.mail user.mail +json.identity user.identity +json.watched current_user.watched?(user) +# json.email user.mail # 邮箱原则上不暴露的,如果实在需要的话只能对某些具体的接口公开 +json.image_url url_to_avatar(user) diff --git a/app/views/organizations/organization_users/_detail.json.jbuilder b/app/views/organizations/organization_users/_detail.json.jbuilder new file mode 100644 index 000000000..d4c21d5f5 --- /dev/null +++ b/app/views/organizations/organization_users/_detail.json.jbuilder @@ -0,0 +1,7 @@ +json.id org_user.id +json.user do + json.partial! "organizations/user_detail", user: org_user.user +end + +json.team_names org_user.teams.pluck(:name) +json.created_at org_user.created_at.strftime("%Y-%m-%d") diff --git a/app/views/organizations/organization_users/index.json.jbuilder b/app/views/organizations/organization_users/index.json.jbuilder new file mode 100644 index 000000000..361c3f242 --- /dev/null +++ b/app/views/organizations/organization_users/index.json.jbuilder @@ -0,0 +1,4 @@ +json.total_count @organization_users.total_count +json.organization_users @organization_users do |org_user| + json.partial! "detail", org_user: org_user, organization: @organization +end diff --git a/app/views/organizations/organizations/_detail.json.jbuilder b/app/views/organizations/organizations/_detail.json.jbuilder new file mode 100644 index 000000000..e7aafd4e9 --- /dev/null +++ b/app/views/organizations/organizations/_detail.json.jbuilder @@ -0,0 +1,13 @@ +json.id organization.id +json.name organization.login +json.description organization.description +json.website organization.website +json.location organization.location +json.repo_admin_change_team_access organization.repo_admin_change_team_access +json.visibility organization.visibility +json.max_repo_creation organization.max_repo_creation +json.num_projects organization.num_projects +json.num_users organization.num_users +json.num_teams organization.num_teams +json.avatar_url url_to_avatar(organization) +json.created_at organization.created_on.strftime("%Y-%m-%d") \ No newline at end of file diff --git a/app/views/organizations/organizations/create.json.jbuilder b/app/views/organizations/organizations/create.json.jbuilder new file mode 100644 index 000000000..55de73daf --- /dev/null +++ b/app/views/organizations/organizations/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! "detail", organization: @organization \ No newline at end of file diff --git a/app/views/organizations/organizations/index.json.jbuilder b/app/views/organizations/organizations/index.json.jbuilder new file mode 100644 index 000000000..33846d662 --- /dev/null +++ b/app/views/organizations/organizations/index.json.jbuilder @@ -0,0 +1,4 @@ +json.total_count @organizations.total_count +json.organizations @organizations do |organization| + json.partial! "detail", organization: organization +end diff --git a/app/views/organizations/organizations/show.json.jbuilder b/app/views/organizations/organizations/show.json.jbuilder new file mode 100644 index 000000000..ab803751c --- /dev/null +++ b/app/views/organizations/organizations/show.json.jbuilder @@ -0,0 +1,4 @@ +json.partial! "detail", organization: @organization +json.can_create_project @can_create_project +json.is_admin @is_admin +json.is_member @is_member \ No newline at end of file diff --git a/app/views/organizations/organizations/update.json.jbuilder b/app/views/organizations/organizations/update.json.jbuilder new file mode 100644 index 000000000..55de73daf --- /dev/null +++ b/app/views/organizations/organizations/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! "detail", organization: @organization \ No newline at end of file diff --git a/app/views/organizations/projects/index.json.jbuilder b/app/views/organizations/projects/index.json.jbuilder new file mode 100644 index 000000000..a663b0c18 --- /dev/null +++ b/app/views/organizations/projects/index.json.jbuilder @@ -0,0 +1,9 @@ +json.total_count @projects.total_count +json.projects @projects.each do |project| + json.(project, :id, :name, :identifier, :description, :forked_count, :praises_count, :forked_from_project_id) + 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) +end \ No newline at end of file diff --git a/app/views/organizations/projects/search.json.jbuilder b/app/views/organizations/projects/search.json.jbuilder new file mode 100644 index 000000000..cf2b86019 --- /dev/null +++ b/app/views/organizations/projects/search.json.jbuilder @@ -0,0 +1,9 @@ +json.total_count @projects.size +json.projects @projects.each do |project| + json.(project, :id, :name, :identifier, :description, :forked_count, :praises_count, :forked_from_project_id) + 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) +end \ No newline at end of file diff --git a/app/views/organizations/team_projects/_detail.json.jbuilder b/app/views/organizations/team_projects/_detail.json.jbuilder new file mode 100644 index 000000000..9af32d1df --- /dev/null +++ b/app/views/organizations/team_projects/_detail.json.jbuilder @@ -0,0 +1,7 @@ +json.id team_project.id +json.project do + json.owner_name team_project&.project&.owner&.login + json.owner_image_url url_to_avatar(team_project&.project&.owner) + json.name team_project&.project&.name + json.identifier team_project&.project&.identifier +end \ No newline at end of file diff --git a/app/views/organizations/team_projects/create.json.jbuilder b/app/views/organizations/team_projects/create.json.jbuilder new file mode 100644 index 000000000..e9f32468c --- /dev/null +++ b/app/views/organizations/team_projects/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! "detail", team_project: @team_project, team: @team, organization: @organization \ No newline at end of file diff --git a/app/views/organizations/team_projects/index.json.jbuilder b/app/views/organizations/team_projects/index.json.jbuilder new file mode 100644 index 000000000..102c042cb --- /dev/null +++ b/app/views/organizations/team_projects/index.json.jbuilder @@ -0,0 +1,4 @@ +json.total_count @team_projects.total_count +json.team_projects @team_projects do |team_project| + json.partial! "detail", team_project: team_project, team: @team, organization: @organization +end diff --git a/app/views/organizations/team_users/_detail.json.jbuilder b/app/views/organizations/team_users/_detail.json.jbuilder new file mode 100644 index 000000000..040a3c172 --- /dev/null +++ b/app/views/organizations/team_users/_detail.json.jbuilder @@ -0,0 +1,5 @@ +json.id team_user.id +json.user do + json.partial! "organizations/user_detail", user: team_user.user +end +json.created_at team_user.created_at.strftime("%Y-%m-%d") diff --git a/app/views/organizations/team_users/create.json.jbuilder b/app/views/organizations/team_users/create.json.jbuilder new file mode 100644 index 000000000..7f6a5280d --- /dev/null +++ b/app/views/organizations/team_users/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! "detail", team_user: @team_user, team: @team, organization: @organization \ No newline at end of file diff --git a/app/views/organizations/team_users/index.json.jbuilder b/app/views/organizations/team_users/index.json.jbuilder new file mode 100644 index 000000000..ee569aacf --- /dev/null +++ b/app/views/organizations/team_users/index.json.jbuilder @@ -0,0 +1,4 @@ +json.total_count @team_users.total_count +json.team_users @team_users do |team_user| + json.partial! "detail", team_user: team_user, team: @team, organization: @organization +end diff --git a/app/views/organizations/teams/_detail.json.jbuilder b/app/views/organizations/teams/_detail.json.jbuilder new file mode 100644 index 000000000..4f3588cb9 --- /dev/null +++ b/app/views/organizations/teams/_detail.json.jbuilder @@ -0,0 +1,14 @@ +json.id team.id +json.name team.name +json.description team.description +json.authorize team.authorize +json.includes_all_project team.includes_all_project +json.can_create_org_project team.can_create_org_project +json.num_projects team.num_projects +json.num_users team.num_users +json.units team.team_units.pluck(:unit_type) +json.users team.team_users.each do |user| + json.partial! "organizations/user_detail", user: user&.user +end +json.is_admin @is_admin +json.is_member team.is_member?(current_user.id) diff --git a/app/views/organizations/teams/create.json.jbuilder b/app/views/organizations/teams/create.json.jbuilder new file mode 100644 index 000000000..25f229ce8 --- /dev/null +++ b/app/views/organizations/teams/create.json.jbuilder @@ -0,0 +1 @@ +json.partial! "detail", team: @team, organization: @organization \ No newline at end of file diff --git a/app/views/organizations/teams/index.json.jbuilder b/app/views/organizations/teams/index.json.jbuilder new file mode 100644 index 000000000..ad3479ec4 --- /dev/null +++ b/app/views/organizations/teams/index.json.jbuilder @@ -0,0 +1,4 @@ +json.total_count @teams.total_count +json.teams @teams do |team| + json.partial! "detail", team: team, organization: @organization +end diff --git a/app/views/organizations/teams/search.json.jbuilder b/app/views/organizations/teams/search.json.jbuilder new file mode 100644 index 000000000..c07b8b870 --- /dev/null +++ b/app/views/organizations/teams/search.json.jbuilder @@ -0,0 +1,4 @@ +json.total_count @teams.size +json.teams @teams do |team| + json.partial! "detail", team: team, organization: @organization +end diff --git a/app/views/organizations/teams/show.json.jbuilder b/app/views/organizations/teams/show.json.jbuilder new file mode 100644 index 000000000..d0290897a --- /dev/null +++ b/app/views/organizations/teams/show.json.jbuilder @@ -0,0 +1,3 @@ +json.partial! "detail", team: @team, organization: @organization +json.is_admin @is_admin +json.is_member @is_member \ No newline at end of file diff --git a/app/views/organizations/teams/update.json.jbuilder b/app/views/organizations/teams/update.json.jbuilder new file mode 100644 index 000000000..25f229ce8 --- /dev/null +++ b/app/views/organizations/teams/update.json.jbuilder @@ -0,0 +1 @@ +json.partial! "detail", team: @team, organization: @organization \ No newline at end of file diff --git a/app/views/owners/index.json.jbuilder b/app/views/owners/index.json.jbuilder new file mode 100644 index 000000000..677aff71a --- /dev/null +++ b/app/views/owners/index.json.jbuilder @@ -0,0 +1,7 @@ +json.total_count @owners.size +json.owners @owners.each do |owner| + json.id owner.id + json.type owner.type + json.name owner.login + json.avatar_url url_to_avatar(owner) +end \ No newline at end of file diff --git a/app/views/projects/_project_detail.json.jbuilder b/app/views/projects/_project_detail.json.jbuilder index fdbd05dfb..94aee427e 100644 --- a/app/views/projects/_project_detail.json.jbuilder +++ b/app/views/projects/_project_detail.json.jbuilder @@ -18,11 +18,13 @@ json.author do if project.educoder? project_educoder = project.project_educoder json.name project_educoder&.owner + json.type 'Educoder' json.login project_educoder&.repo_name.split('/')[0] json.image_url render_educoder_avatar_url(project.project_educoder) else user = project.owner json.name user.try(:show_real_name) + json.type user&.type json.login user.login json.image_url render_avatar_url(user) end diff --git a/app/views/projects/teams/index.json.jbuilder b/app/views/projects/teams/index.json.jbuilder new file mode 100644 index 000000000..0a48e884d --- /dev/null +++ b/app/views/projects/teams/index.json.jbuilder @@ -0,0 +1,5 @@ +json.total_count @teams.total_count +json.teams @teams.each do |team| + json.(team, :id, :name, :authorize) + json.can_remove !team.includes_all_project && team&.organization&.repo_admin_change_team_access +end \ No newline at end of file diff --git a/app/views/repositories/_author.json.jbuilder b/app/views/repositories/_author.json.jbuilder index 326920223..1dbcc4ebc 100644 --- a/app/views/repositories/_author.json.jbuilder +++ b/app/views/repositories/_author.json.jbuilder @@ -1,11 +1,13 @@ json.author do if @project.forge? json.login user.login + json.type user&.type json.name user.real_name json.image_url url_to_avatar(user) else json.login @project.project_educoder&.repo_name&.split('/')[0] json.name @project.project_educoder&.owner + json.type 'Educoder' json.image_url @project.project_educoder&.image_url end end diff --git a/app/views/users/organizations/index.json.jbuilder b/app/views/users/organizations/index.json.jbuilder new file mode 100644 index 000000000..0a1950367 --- /dev/null +++ b/app/views/users/organizations/index.json.jbuilder @@ -0,0 +1,4 @@ +json.total_count @organizations.total_count +json.organizations @organizations do |organization| + json.partial! "/organizations/organizations/detail", organization: organization +end diff --git a/app/views/users/show.json.jbuilder b/app/views/users/show.json.jbuilder index 9b4956197..54f85c5c2 100644 --- a/app/views/users/show.json.jbuilder +++ b/app/views/users/show.json.jbuilder @@ -12,6 +12,7 @@ json.watched_count @user.fan_count #粉丝 json.watching_count @user.follow_count #关注数 json.undo_events @undo_events json.user_composes_count @user_composes_count +json.user_org_count @user_org_count json.common_projects_count @projects_common_count json.mirror_projects_count @projects_mirrior_count json.sync_mirror_projects_count @projects_sync_mirrior_count \ No newline at end of file diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index ab6601ed4..c040f9bd1 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -61,6 +61,8 @@ zh-CN: close_issue: 工单 activerecord: attributes: + organization: + login: '组织名称' user: login: '登录名' lastname: '姓名' diff --git a/config/routes.rb b/config/routes.rb index 041b84454..2d8861416 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,6 +103,34 @@ Rails.application.routes.draw do put 'commons/unhidden', to: 'commons#unhidden' delete 'commons/delete', to: 'commons#delete' + resources :owners, only: [:index] + + scope module: :organizations do + resources :organizations, except: [:edit, :new] do + resources :organization_users, only: [:index, :destroy] do + collection do + delete :quit + end + end + resources :teams, except: [:edit, :new] do + collection do + get :search + end + resources :team_users, only: [:index, :create, :destroy] do + collection do + delete :quit + end + end + resources :team_projects, only: [:index, :create, :destroy] do ;end + end + resources :projects, only: [:index] do + collection do + get :search + end + end + end + end + resources :issues, except: [:index, :new,:create, :update, :edit, :destroy] do resources :journals, only: [:index, :create, :destroy, :edit, :update] do member do @@ -225,6 +253,7 @@ Rails.application.routes.draw do end scope module: :users do + resources :organizations, only: [:index] # resources :projects, only: [:index] # resources :subjects, only: [:index] resources :project_packages, only: [:index] @@ -493,6 +522,7 @@ Rails.application.routes.draw do end scope module: :projects do + resources :teams, only: [:index, :create, :destroy] scope do get( '/blob/*id/diff', diff --git a/db/migrate/20210111065934_create_organization_users.rb b/db/migrate/20210111065934_create_organization_users.rb new file mode 100644 index 000000000..3300d9330 --- /dev/null +++ b/db/migrate/20210111065934_create_organization_users.rb @@ -0,0 +1,10 @@ +class CreateOrganizationUsers < ActiveRecord::Migration[5.2] + def change + create_table :organization_users do |t| + t.references :user + t.references :organization + + t.timestamps + end + end +end diff --git a/db/migrate/20210111065954_create_organization_extensions.rb b/db/migrate/20210111065954_create_organization_extensions.rb new file mode 100644 index 000000000..b99a43579 --- /dev/null +++ b/db/migrate/20210111065954_create_organization_extensions.rb @@ -0,0 +1,15 @@ +class CreateOrganizationExtensions < ActiveRecord::Migration[5.2] + def change + create_table :organization_extensions do |t| + t.references :organization + t.string :description, comment: "组织描述" + t.string :website, comment: "组织官方网站" + t.string :location, comment: "组织地区" + t.boolean :repo_admin_change_team_access, comment: "项目管理员是否可以添加或移除团队的访问权限", default: false + t.integer :visibility, comment: "组织可见性", default: 0 + t.integer :max_repo_creation, comment: "组织最大仓库数", default: -1 + + t.timestamps + end + end +end diff --git a/db/migrate/20210111093008_create_teams.rb b/db/migrate/20210111093008_create_teams.rb new file mode 100644 index 000000000..486835e9d --- /dev/null +++ b/db/migrate/20210111093008_create_teams.rb @@ -0,0 +1,17 @@ +class CreateTeams < ActiveRecord::Migration[5.2] + def change + create_table :teams do |t| + t.references :organization + t.string :name, comment: "团队名称" + t.string :description, comment: "团队描述" + t.integer :authorize, comment: "团队权限", default: 0 + t.integer :num_projects, comment: "团队项目数量", default: 0 + t.integer :num_users, comment: "团队成员数量", default: 0 + t.boolean :includes_all_project, comment: "团队是否拥有所有项目", default: false + t.boolean :can_create_org_project, comment: "团队是否能创建项目", default: false + t.integer :gtid, comment: "团队在gitea里的id" + + t.timestamps + end + end +end diff --git a/db/migrate/20210111093016_create_team_projects.rb b/db/migrate/20210111093016_create_team_projects.rb new file mode 100644 index 000000000..1bb35cf16 --- /dev/null +++ b/db/migrate/20210111093016_create_team_projects.rb @@ -0,0 +1,11 @@ +class CreateTeamProjects < ActiveRecord::Migration[5.2] + def change + create_table :team_projects do |t| + t.references :organization + t.references :project + t.references :team + + t.timestamps + end + end +end diff --git a/db/migrate/20210111093024_create_team_users.rb b/db/migrate/20210111093024_create_team_users.rb new file mode 100644 index 000000000..4e3579e5d --- /dev/null +++ b/db/migrate/20210111093024_create_team_users.rb @@ -0,0 +1,10 @@ +class CreateTeamUsers < ActiveRecord::Migration[5.2] + def change + create_table :team_users do |t| + t.references :organization + t.references :team + t.references :user + t.timestamps + end + end +end diff --git a/db/migrate/20210111093200_create_team_units.rb b/db/migrate/20210111093200_create_team_units.rb new file mode 100644 index 000000000..62948732c --- /dev/null +++ b/db/migrate/20210111093200_create_team_units.rb @@ -0,0 +1,11 @@ +class CreateTeamUnits < ActiveRecord::Migration[5.2] + def change + create_table :team_units do |t| + t.references :organization + t.references :team + t.integer :unit_type, comment: "访问单元类型" + + t.timestamps + end + end +end diff --git a/db/migrate/20210120081821_add_columns_to_organization_extension.rb b/db/migrate/20210120081821_add_columns_to_organization_extension.rb new file mode 100644 index 000000000..4bcb4d9f3 --- /dev/null +++ b/db/migrate/20210120081821_add_columns_to_organization_extension.rb @@ -0,0 +1,13 @@ +class AddColumnsToOrganizationExtension < ActiveRecord::Migration[5.2] + def change + add_column :organization_extensions, :num_projects, :integer, default: 0 + add_column :organization_extensions, :num_users, :integer, default: 0 + add_column :organization_extensions, :num_teams, :integer, default: 0 + + OrganizationExtension.find_each do |e| + OrganizationExtension.reset_counters(e.id, :organization_users) + OrganizationExtension.reset_counters(e.id, :projects) + OrganizationExtension.reset_counters(e.id, :teams) + end + end +end diff --git a/spec/models/organization_extension_spec.rb b/spec/models/organization_extension_spec.rb new file mode 100644 index 000000000..fe155b50b --- /dev/null +++ b/spec/models/organization_extension_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe OrganizationExtension, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/organization_user_spec.rb b/spec/models/organization_user_spec.rb new file mode 100644 index 000000000..bdf82dc5c --- /dev/null +++ b/spec/models/organization_user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe OrganizationUser, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/team_project_spec.rb b/spec/models/team_project_spec.rb new file mode 100644 index 000000000..217e27d1f --- /dev/null +++ b/spec/models/team_project_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe TeamProject, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/team_spec.rb b/spec/models/team_spec.rb new file mode 100644 index 000000000..8fddabf72 --- /dev/null +++ b/spec/models/team_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Team, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/team_unit_spec.rb b/spec/models/team_unit_spec.rb new file mode 100644 index 000000000..7c903b8a0 --- /dev/null +++ b/spec/models/team_unit_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe TeamUnit, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/team_user_spec.rb b/spec/models/team_user_spec.rb new file mode 100644 index 000000000..86d4cece7 --- /dev/null +++ b/spec/models/team_user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe TeamUser, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end