From 896ca84c722dde96ae8b1bb33778216bdf6bb69e Mon Sep 17 00:00:00 2001 From: p31729568 <winse.wang@foxmail.com> Date: Thu, 18 Jul 2019 11:24:50 +0800 Subject: [PATCH] competition api --- app/controllers/application_controller.rb | 4 + .../competitions/base_controller.rb | 2 +- .../competition_modules_controller.rb | 28 ++++++ .../competition_teams_controller.rb | 49 ++++++++++ .../competitions/competitions_controller.rb | 6 ++ .../competitions/students_controller.rb | 22 +++++ .../competitions/teachers_controller.rb | 22 +++++ app/forms/competitions/save_team_form.rb | 98 +++++++++++++++++++ app/models/competition.rb | 25 +++++ app/models/competition_module_md_content.rb | 3 + app/models/competition_team.rb | 25 +++++ app/models/team_member.rb | 8 ++ .../competitions/join_team_service.rb | 35 +++++++ .../competitions/save_team_service.rb | 70 +++++++++++++ .../competition_modules/index.json.jbuilder | 7 ++ .../competition_modules/show.json.jbuilder | 8 ++ .../competition_teams/index.json.jbuilder | 23 +++++ .../competitions/students/index.json.jbuilder | 9 ++ .../competitions/teachers/index.json.jbuilder | 9 ++ config/locales/competitions/zh-CN.yml | 8 ++ config/locales/forms/save_team_form.zh-CN.yml | 25 +++++ config/routes.rb | 8 +- 22 files changed, 491 insertions(+), 3 deletions(-) create mode 100644 app/controllers/competitions/competition_modules_controller.rb create mode 100644 app/controllers/competitions/competition_teams_controller.rb create mode 100644 app/controllers/competitions/students_controller.rb create mode 100644 app/controllers/competitions/teachers_controller.rb create mode 100644 app/forms/competitions/save_team_form.rb create mode 100644 app/services/competitions/join_team_service.rb create mode 100644 app/services/competitions/save_team_service.rb create mode 100644 app/views/competitions/competition_modules/index.json.jbuilder create mode 100644 app/views/competitions/competition_modules/show.json.jbuilder create mode 100644 app/views/competitions/competition_teams/index.json.jbuilder create mode 100644 app/views/competitions/students/index.json.jbuilder create mode 100644 app/views/competitions/teachers/index.json.jbuilder create mode 100644 config/locales/competitions/zh-CN.yml create mode 100644 config/locales/forms/save_team_form.zh-CN.yml diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4bc3d5133..875050642 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -148,6 +148,10 @@ class ApplicationController < ActionController::Base normal_status(403, "") unless User.current.admin? end + def require_business + normal_status(403, "") unless admin_or_business? + end + # 前端会捕捉401,弹登录弹框 # 未授权的捕捉407,弹试用申请弹框 def require_login diff --git a/app/controllers/competitions/base_controller.rb b/app/controllers/competitions/base_controller.rb index 127474ee7..b42a4ce8f 100644 --- a/app/controllers/competitions/base_controller.rb +++ b/app/controllers/competitions/base_controller.rb @@ -6,6 +6,6 @@ class Competitions::BaseController < ApplicationController helper_method :current_competition def current_competition - @_current_competition ||= Competition.find_by!(identifier: params[:competition_id].presence || params[:id]) + @_current_competition ||= Competition.find_by!(identifier: params[:competition_id]) end end \ No newline at end of file diff --git a/app/controllers/competitions/competition_modules_controller.rb b/app/controllers/competitions/competition_modules_controller.rb new file mode 100644 index 000000000..da9873c43 --- /dev/null +++ b/app/controllers/competitions/competition_modules_controller.rb @@ -0,0 +1,28 @@ +class Competitions::CompetitionModulesController < Competitions::BaseController + skip_before_action :require_login, only: [:index, :show] + + before_action :require_business, only: [:update] + + def index + @modules = current_competition.unhidden_competition_modules.order(position: :asc) + end + + def show + @module = current_module + end + + def update + md = current_module.competition_module_md_content || current_module.build_competition_module_md_content + md.name = params[:md_name] + md.content = params[:md_content] + md.save! + + render_ok + end + + private + + def current_module + @_current_module ||= current_competition.unhidden_competition_modules.find(params[:id]) + end +end diff --git a/app/controllers/competitions/competition_teams_controller.rb b/app/controllers/competitions/competition_teams_controller.rb new file mode 100644 index 000000000..34c80d24c --- /dev/null +++ b/app/controllers/competitions/competition_teams_controller.rb @@ -0,0 +1,49 @@ +class Competitions::CompetitionTeamsController < Competitions::BaseController + def index + admin_or_business? ? all_competition_teams : user_competition_teams + end + + def create + team = current_competition.competition_teams.new(user: current_user) + Competitions::SaveTeamService.call(team, save_params) + render_ok + end + + def update + team = current_competition.competition_teams.where(user: current_user).find(params[:id]) + Competitions::SaveTeamService.call(team, save_params) + render_ok + end + + def join + Competitions::JoinTeamService.call(current_competition, current_user, params) + render_ok + rescue Competitions::JoinTeamService::Error => ex + render_error(ex.message) + end + + private + + def all_competition_teams + teams = current_competition.competition_teams + + keyword = params[:keyword].to_s.strip + if keyword.present? + teams = teams.joins(users: { user_extension: :school }).where('schools.name LIKE ?', "%#{keyword}%") + end + + @count = teams.count + @teams = paginate(teams.includes(:user, users: :user_extension)) + end + + def user_competition_teams + teams = current_competition.competition_teams + teams = teams.joins(:team_members).where(team_members: { user_id: current_user.id }) + @teams = teams.includes(:user, users: :user_extension).to_a + @count = @teams.size + end + + def save_params + params.permit(:name, teacher_ids: [], member_ids: []) + end +end diff --git a/app/controllers/competitions/competitions_controller.rb b/app/controllers/competitions/competitions_controller.rb index f962682a1..34dac7350 100644 --- a/app/controllers/competitions/competitions_controller.rb +++ b/app/controllers/competitions/competitions_controller.rb @@ -29,4 +29,10 @@ class Competitions::CompetitionsController < Competitions::BaseController return end end + + private + + def current_competition + @_current_competition ||= Competition.find_by!(identifier: params[:id]) + end end \ No newline at end of file diff --git a/app/controllers/competitions/students_controller.rb b/app/controllers/competitions/students_controller.rb new file mode 100644 index 000000000..8fd235fd1 --- /dev/null +++ b/app/controllers/competitions/students_controller.rb @@ -0,0 +1,22 @@ +class Competitions::StudentsController < Competitions::BaseController + def index + keyword = params[:keyword].to_s.strip + if keyword.blank? + @students = [] + return + end + + students = User.joins(:user_extension).where(status: 1, user_extensions: { identity: 1 }) + students = students.where.not(id: params[:student_ids]) if params[:student_ids].present? + students = students.where('LOWER(CONCAT(lastname, firstname, login, nickname)) LIKE ?', "%#{keyword}%") + @students = students.includes(user_extension: :school).limit(20) + + # 队员多次报名限制 + if current_competition.member_multiple_limited? + ids = @students.map(&:id) + members = current_competition.team_members.where(user_id: ids) + members = members.where.not(competition_team_id: params[:team_id]) if params[:team_id].present? + @enrolled_map = members.group(:user_id).count + end + end +end diff --git a/app/controllers/competitions/teachers_controller.rb b/app/controllers/competitions/teachers_controller.rb new file mode 100644 index 000000000..76c7cfe6a --- /dev/null +++ b/app/controllers/competitions/teachers_controller.rb @@ -0,0 +1,22 @@ +class Competitions::TeachersController < Competitions::BaseController + def index + keyword = params[:keyword].to_s.strip + if keyword.blank? + @teachers = [] + return + end + + teachers = User.joins(:user_extension).where(status: 1, user_extensions: { identity: 0 }) + teachers = teachers.where.not(id: params[:teacher_ids]) if params[:teacher_ids].present? + teachers = teachers.where('LOWER(CONCAT(lastname, firstname, login, nickname)) LIKE ?', "%#{keyword}%") + @teachers = teachers.includes(user_extension: :school).limit(10) + + # 老师多次报名限制 + if current_competition.teacher_multiple_limited? + ids = @teachers.map(&:id) + members = current_competition.team_members.where(user_id: ids) + members = members.where.not(competition_team_id: params[:team_id]) if params[:team_id].present? + @enrolled_map = members.group(:user_id).count + end + end +end diff --git a/app/forms/competitions/save_team_form.rb b/app/forms/competitions/save_team_form.rb new file mode 100644 index 000000000..10685d260 --- /dev/null +++ b/app/forms/competitions/save_team_form.rb @@ -0,0 +1,98 @@ +class Competitions::SaveTeamForm + include ActiveModel::Model + + attr_accessor :competition, :team, :creator + attr_accessor :name, :teacher_ids, :member_ids + + validates :name, presence: true + + validate :check_creator_enrollable + def check_creator_enrollable + return unless check_creator_identity_enrollable + + check_creator_multiple_enrollable + end + + validate :check_teachers_enrollable + def check_teachers_enrollable + if competition.teacher_enroll_forbidden? && teacher_ids.present? + errors.add(:teacher_ids, :enroll_forbidden) + return + end + + self.teacher_ids = teacher_ids.map(&:to_i) + all_teachers = creator.is_teacher? ? teacher_ids + [creator.id] : teacher_ids + all_teachers.uniq! + + if all_teachers.size < competition.teacher_staff.minimum || all_teachers.size > competition.teacher_staff.maximum + errors.add(:teacher_ids, :invalid_count, minimum: competition.teacher_staff.minimum, maximum: competition.teacher_staff.maximum) + return + end + + # 老师可多次报名,不检查 + return unless competition.teacher_multiple_limited? + + # 存在已报名老师 + enrolled_teacher_members = competition.team_members.where(user_id: all_teachers) + .where.not(competition_team_id: team.id).includes(:user) + if enrolled_teacher_members.present? + errors.add(:teacher_ids, :enrolled, names: enrolled_teacher_members.map { |m| m.user.real_name }.join(',')) + return + end + end + + validate :check_members_enrollable + def check_members_enrollable + if competition.member_enroll_forbidden? && member_ids.present? + errors.add(:member_ids, :enroll_forbidden) + return + end + + self.member_ids = member_ids.map(&:to_i) + all_members = creator.is_teacher? ? member_ids : member_ids + [creator.id] + all_members.uniq! + + if all_members.size < competition.member_staff.minimum || all_members.size > competition.member_staff.maximum + errors.add(:member_ids, :invalid_count, minimum: competition.member_staff.minimum, maximum: competition.member_staff.maximum) + return + end + + # 成员可多次报名,不检查 + return unless competition.member_multiple_limited? + + # 存在已报名成员 + enrolled_members = competition.team_members.where(user_id: all_members) + .where.not(competition_team_id: team.id).includes(:user) + if enrolled_members.present? + errors.add(:member_ids, :enrolled, names: enrolled_members.map { |m| m.user.real_name }.join(',')) + return + end + end + + private + + # 竞赛是否限制了职业 + def check_creator_identity_enrollable + if user.is_teacher? && competition.teacher_enroll_forbidden? + errors.add(:creator, :teacher_enroll_forbidden) + return false + elsif !user.is_teacher? && competition.member_enroll_forbidden? + errors.add(:creator, :member_enroll_forbidden) + return false + end + + true + end + + # 创建者是否能多次报名 + def check_creator_multiple_enrollable + return unless competition.enrolled?(user) + + if (user.is_teacher? && competition.teacher_multiple_limited?) || (!user.is_teacher? && competition.member_multiple_limited?) + errors.add(:creator, :enrolled) + return false + end + + true + end +end \ No newline at end of file diff --git a/app/models/competition.rb b/app/models/competition.rb index 9a85bda58..f5d146ab4 100644 --- a/app/models/competition.rb +++ b/app/models/competition.rb @@ -1,10 +1,15 @@ class Competition < ApplicationRecord has_many :competition_modules, dependent: :destroy + has_many :unhidden_competition_modules, -> { where(hidden: false) }, class_name: 'CompetitionModule' + has_many :competition_stages, dependent: :destroy has_many :competition_stage_sections, dependent: :destroy has_one :current_stage_section, -> { where('end_time > NOW()') }, class_name: 'CompetitionStageSection' + + has_many :competition_teams, dependent: :destroy has_many :team_members, dependent: :destroy + has_many :competition_staffs, dependent: :destroy has_one :teacher_staff, -> { where(category: :teacher) }, class_name: 'CompetitionStaff' has_one :member_staff, -> { where.not(category: :teacher) }, class_name: 'CompetitionStaff' @@ -28,6 +33,26 @@ class Competition < ApplicationRecord team_members.exists?(user_id: user.id) end + # 是否禁止教师报名 + def teacher_enroll_forbidden? + teacher_staff.blank? || teacher_staff.maximum.zero? + end + + # 是否禁止学生报名 + def member_enroll_forbidden? + member_staff.blank? || member_staff.maximum.zero? + end + + # 老师是否能多次报名 + def teacher_multiple_limited? + teacher_staff.mutiple_limited? + end + + # 队员是否能多次报名 + def member_multiple_limited? + member_staff.mutiple_limited? + end + private def create_competition_modules diff --git a/app/models/competition_module_md_content.rb b/app/models/competition_module_md_content.rb index cbf5a829a..9dfcfca84 100644 --- a/app/models/competition_module_md_content.rb +++ b/app/models/competition_module_md_content.rb @@ -2,4 +2,7 @@ class CompetitionModuleMdContent < ApplicationRecord belongs_to :competition_module has_many :attachments, as: :container, dependent: :destroy + + validates :name, presence: true + validates :content, presence: true end \ No newline at end of file diff --git a/app/models/competition_team.rb b/app/models/competition_team.rb index aa19db3b0..625b29421 100644 --- a/app/models/competition_team.rb +++ b/app/models/competition_team.rb @@ -1,8 +1,33 @@ class CompetitionTeam < ApplicationRecord + + CODE_CHARS = %W(2 3 4 5 6 7 8 9 a b c f e f g h i j k l m n o p q r s t u v w x y z).freeze + belongs_to :user belongs_to :competition has_many :team_members, dependent: :destroy + has_many :users, through: :team_members, source: :user has_many :members, -> { without_teachers }, class_name: 'TeamMember' has_many :teachers, -> { only_teachers }, class_name: 'TeamMember' + + def group_team_type? + team_type.zero? + end + + def personal_team_type? + team_type == 1 + end + + def en_team_type + group_team_type? ? 'group' : 'personal' + end + + def generate_invite_code + code = CODE_CHARS.sample(6).join + while self.class.exists?(invite_code: code) + code = CODE_CHARS.sample(6).join + end + self.code = code + code + end end \ No newline at end of file diff --git a/app/models/team_member.rb b/app/models/team_member.rb index 3909325e8..31890ea2e 100644 --- a/app/models/team_member.rb +++ b/app/models/team_member.rb @@ -5,4 +5,12 @@ class TeamMember < ApplicationRecord scope :only_teachers, -> { where(is_teacher: true) } scope :without_teachers, -> { where(is_teacher: false) } + + def creator? + role == 1 + end + + def en_role + is_teacher? ? 'teacher' : 'member' + end end \ No newline at end of file diff --git a/app/services/competitions/join_team_service.rb b/app/services/competitions/join_team_service.rb new file mode 100644 index 000000000..df889abe2 --- /dev/null +++ b/app/services/competitions/join_team_service.rb @@ -0,0 +1,35 @@ +class Competitions::JoinTeamService < ApplicationService + Error = Class.new(StandardError) + + attr_reader :competition, :user, :params + + def initialize(competition, user, params) + @competition = competition + @user = user + @params = params + end + + def call + invite_code = params[:invite_code].to_s.strip + raise Error, '战队邀请码不能为空' if invite_code.blank? + + is_teacher = user.is_teacher? + raise Error, '本竞赛的参赛者限定为:学生' if is_teacher && competition.teacher_enroll_forbidden? + raise Error, '本竞赛的参赛者限定为:教师' if !is_teacher && competition.member_enroll_forbidden? + + team = competition.competition_teams.find_by(invite_code: invite_code) + raise Error, '战队邀请码无效' if team.blank? + raise Error, '您已加入该战队' if team.team_members.exists?(user_id: user.id) + + enrolled = competition.team_members.exists?(user_id: user.id) + if enrolled && (is_teacher && competition.teacher_multiple_limited?) || (!is_teacher && competition.member_multiple_limited?) + raise Error, '您已加入其它战队' + end + + raise Error, '该战队教师人数已满' if is_teacher && team.teachers.count == competition.teacher_staff.maximum + raise Error, '该战队队员人数已满' if !is_teacher && team.members.count == competition.member_staff.maximum + + role = is_teacher ? 3 : 2 + team.team_members.create!(competition_id: competition.id, user_id: user, role: role, is_teacher: is_teacher) + end +end \ No newline at end of file diff --git a/app/services/competitions/save_team_service.rb b/app/services/competitions/save_team_service.rb new file mode 100644 index 000000000..c134e70d7 --- /dev/null +++ b/app/services/competitions/save_team_service.rb @@ -0,0 +1,70 @@ +class Competitions::SaveTeamService < ApplicationService + attr_reader :competition, :team, :creator, :params + + TEAM_MEMBER_ATTRIBUTES = %i[competition_id competition_team_id user_id role is_teacher created_at updated_at] + + def initialize(team, params) + @team = team + @competition = team.competition + @creator = team.user + @params = params + end + + def call + Competitions::SaveTeamForm.new(form_params).validate! + + new_record = team.new_record? + is_teacher = team.user.is_teacher? + ActiveRecord::Base.transaction do + team.generate_invite_code if new_record + team.name = params[:name].to_s.strip + team.save! + + # 创建者 + team.team_members.create!(user_id: creator.id, competition_id: competition.id, role: 1, is_teacher: is_teacher) if new_record + + update_teacher_team_members! + update_member_team_members! + end + end + + private + + def update_teacher_team_members! + teacher_ids = Array.wrap(params[:teacher_ids]).map(:to_i) + old_teacher_ids = team.team_members.where(role: 3).pluck(:user_id) + + destroy_teacher_ids = old_teacher_ids - teacher_ids + team.team_members.where(role: 3).where(user_id: destroy_teacher_ids).delete_all + + new_teacher_ids = teacher_ids - old_teacher_ids + TeamMember.bulk_insert(*TEAM_MEMBER_ATTRIBUTES) do |worker| + base_attr = { competition_id: competition.id, competition_team_id: team.id, role: 3, is_teacher: true } + new_teacher_ids.each do |teacher_id| + next if teacher_id == creator.id + worker.add(base_attr.merge(user_id: teacher_id)) + end + end + end + + def update_member_team_members! + member_ids = Array.wrap(params[:member_ids]).map(:to_i) + old_member_ids = team.team_members.where(role: 2).pluck(:user_id) + + destroy_member_ids = old_member_ids - member_ids + team.team_members.where(role: 2).where(user_id: destroy_member_ids).delete_all + + new_member_ids = member_ids - old_member_ids + TeamMember.bulk_insert(*TEAM_MEMBER_ATTRIBUTES) do |worker| + base_attr = { competition_id: competition.id, competition_team_id: team.id, role: 2, is_teacher: false } + new_member_ids.each do |member_id| + next if member_id == creator.id + worker.add(base_attr.merge(user_id: member_id)) + end + end + end + + def form_params + params.merge(competition: competition, team: team, creator: creator) + end +end \ No newline at end of file diff --git a/app/views/competitions/competition_modules/index.json.jbuilder b/app/views/competitions/competition_modules/index.json.jbuilder new file mode 100644 index 000000000..decfcb415 --- /dev/null +++ b/app/views/competitions/competition_modules/index.json.jbuilder @@ -0,0 +1,7 @@ + +json.modules do + json.array! @modules.each do |m| + json.extract! m, :id, :name, :position, :url + end +end +json.count @modules.size diff --git a/app/views/competitions/competition_modules/show.json.jbuilder b/app/views/competitions/competition_modules/show.json.jbuilder new file mode 100644 index 000000000..d47742cf0 --- /dev/null +++ b/app/views/competitions/competition_modules/show.json.jbuilder @@ -0,0 +1,8 @@ +json.extract! @module, :id, :name, :position, :url, :md_edit + +md = @module.competition_module_md_content +if md.present? + json.md_name md.name + json.md_content md.content + json.created_at md.created_at.strftime('%Y-%m-%d %H:%M:%S') +end \ No newline at end of file diff --git a/app/views/competitions/competition_teams/index.json.jbuilder b/app/views/competitions/competition_teams/index.json.jbuilder new file mode 100644 index 000000000..86bb86a1c --- /dev/null +++ b/app/views/competitions/competition_teams/index.json.jbuilder @@ -0,0 +1,23 @@ +json.count @count +json.competition_teams do + json.array! @teams.each do |team| + json.extract! team, :id, :name, :invite_code + json.team_type team.en_team_type + json.school_name team.user.school_name + + json.manage_permission current_user.id == team.user_id + + json.creator do + json.partial! 'users/user_simple', user: team.user + json.role team.team_members.find(&:creator?).en_role + end + + json.team_members do + json.array! team.team_members.each do |member| + json.partial! 'users/user_simple', user: member.user + json.user_id member.user_id + json.role member.en_role + end + end + end +end diff --git a/app/views/competitions/students/index.json.jbuilder b/app/views/competitions/students/index.json.jbuilder new file mode 100644 index 000000000..73ccd7d2d --- /dev/null +++ b/app/views/competitions/students/index.json.jbuilder @@ -0,0 +1,9 @@ +json.teachers do + json.array! @students.each do |student| + json.id student.id + json.name student.full_name + json.student_id student.student_id + json.school_name student.school_name + json.enrollable !current_competition.member_multiple_limited? || !@enrolled_map.key?(student.id) + end +end \ No newline at end of file diff --git a/app/views/competitions/teachers/index.json.jbuilder b/app/views/competitions/teachers/index.json.jbuilder new file mode 100644 index 000000000..4a8d2961d --- /dev/null +++ b/app/views/competitions/teachers/index.json.jbuilder @@ -0,0 +1,9 @@ +json.teachers do + json.array! @teachers.each do |teacher| + json.id teacher.id + json.name teacher.full_name + json.identity teacher.identity + json.school_name teacher.school_name + json.enrollable !current_competition.teacher_multiple_limited? || !@enrolled_map.key?(teacher.id) + end +end \ No newline at end of file diff --git a/config/locales/competitions/zh-CN.yml b/config/locales/competitions/zh-CN.yml new file mode 100644 index 000000000..9c3ff2f1e --- /dev/null +++ b/config/locales/competitions/zh-CN.yml @@ -0,0 +1,8 @@ +'zh-CN': + activerecord: + models: + competition_module_md_content: '' + attributes: + competition_module_md_content: + name: '标题' + content: '内容' diff --git a/config/locales/forms/save_team_form.zh-CN.yml b/config/locales/forms/save_team_form.zh-CN.yml new file mode 100644 index 000000000..106527687 --- /dev/null +++ b/config/locales/forms/save_team_form.zh-CN.yml @@ -0,0 +1,25 @@ +'zh-CN': + activemodel: + attributes: + competitions/save_team_form: + competition: '' + name: '战队名称' + creator: '' + teacher_ids: '' + member_ids: '' + errors: + models: + competitions/save_team_form: + attributes: + creator: + teacher_enroll_forbidden: "本竞赛的参赛者限定为:学生" + member_enroll_forbidden: "本竞赛的参赛者限定为:教师" + teacher_ids: + enroll_forbidden: "本竞赛的参赛者限定为:学生" + invalid_count: "教师数量应为%{minimum}~%{maximum}人" + enrolled: "教师 ${names} 已加入其它战队了" + member_ids: + enroll_forbidden: "本竞赛的参赛者限定为:教师" + invalid_count: "队员数量应为%{minimum}~%{maximum}人" + enrolled: "队员 ${names} 已加入其它战队了" + diff --git a/config/routes.rb b/config/routes.rb index 71cfb8c55..dc521266c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -672,9 +672,13 @@ Rails.application.routes.draw do scope module: :competitions do resources :competitions, only: [:index, :show] do - resources :competition_modules, only: [:index, :show] + resources :competition_modules, only: [:index, :show, :update] resource :competition_staff - resources :competition_teams, only: [:index, :show] + resources :competition_teams, only: [:index, :show] do + post :join, on: :collection + end + resources :teachers, only: [:index] + resources :students, only: [:index] end end end