Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fdf957c
Add new resource Team inheriting from UserBase
OmeletWithoutEgg Oct 30, 2024
1ddb238
Allow users to register contest as team
OmeletWithoutEgg Oct 30, 2024
0466548
Allow users to view teammate's submission and submit to contests regi…
OmeletWithoutEgg Oct 30, 2024
aed8627
Give resource Team its own table
OmeletWithoutEgg Oct 31, 2024
9928e88
Fix registration and dashboard after moving Team to an independent table
OmeletWithoutEgg Oct 31, 2024
61eb78a
Fix submission visibility about team
OmeletWithoutEgg Oct 31, 2024
c956350
Fix minor mistakes
OmeletWithoutEgg Oct 31, 2024
caf44a4
Complete management of team, add invite token mechanism
OmeletWithoutEgg Oct 31, 2024
47b38a3
Fix routing error
OmeletWithoutEgg Oct 31, 2024
160a4b2
Fix layout and syntax error
OmeletWithoutEgg Nov 1, 2024
0100919
Add index of team to contest_registration
OmeletWithoutEgg Nov 1, 2024
fff21f1
Update UI, add display of team member registration
OmeletWithoutEgg Nov 1, 2024
99c00c1
Update teams/show.html.erb avatar size
OmeletWithoutEgg Nov 1, 2024
5cfe050
Fix summary columns in dashboard
OmeletWithoutEgg Nov 1, 2024
6ae87cd
Raise teamname length upper limit
OmeletWithoutEgg Nov 2, 2024
16b8f2c
Change description about team invite url
OmeletWithoutEgg Nov 9, 2024
b73726b
Increase teams per page
OmeletWithoutEgg Feb 15, 2025
c711d31
Allow duplicate teamnames
OmeletWithoutEgg Aug 16, 2025
2737d3e
Merge remote-tracking branch 'ntuicpc/tioj/features/team' into featur…
OmeletWithoutEgg Oct 19, 2025
5a77010
Merge branch 'main' into features/team
OmeletWithoutEgg Oct 19, 2025
59a7419
Try to fix tests
OmeletWithoutEgg Oct 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions app/controllers/contest_registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,12 @@ def destroy
def index
registrations = @contest.contest_registrations.includes(:user)
@duplicate_names = registrations.group_by{|x| x.user.username }.select{|k, v| v.size > 1 }.map(&:first).to_set
groups = registrations.group_by{|x| x.approved ? (x.user.type == 'User' ? 1 : 0) : -1}
@contest_users = groups[0]
@registered_users = groups[1]
@unapproved_users = groups[-1]

@registered_teams = registrations.select{|x| x.approved && x.team.present?}.sort_by{|x| x.team} # make same team consecutive
approved_users = registrations.select{|x| x.approved && x.team.nil?}.group_by{|x| x.user.type}
@contest_users = approved_users['ContestUser']
@registered_users = approved_users['User']
@unapproved_users = registrations.select{|x| not x.approved}.sort_by{|x| x.team} # make same team consecutive
end

private
Expand Down
56 changes: 48 additions & 8 deletions app/controllers/contests_controller.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
class ContestsController < ApplicationController
before_action :authenticate_user_and_running_if_single_contest!, only: [:dashboard, :dashboard_update]
before_action :authenticate_user!, only: [:register]
before_action :authenticate_user!, only: [:register, :register_update]
before_action :authenticate_admin!, only: [:set_contest_task, :new, :create, :edit, :update, :destroy]
before_action :check_started!, only: [:dashboard]
before_action :set_tasks, only: [:show, :dashboard, :dashboard_update, :set_contest_task]
before_action :calculate_ranking, only: [:dashboard, :dashboard_update]
layout :set_contest_layout, only: [:show, :edit, :dashboard, :sign_in]
layout :set_contest_layout, only: [:show, :edit, :register, :dashboard, :sign_in]

def set_contest_task
redirect_to contest_path(@contest)
Expand Down Expand Up @@ -43,12 +43,28 @@ def calculate_ranking
flash.now[:notice] = "Scoreboard is now frozen."
end

@data = helpers.ranklist_data(c_submissions.order(:created_at), @contest.start_time, freeze_start, @contest.contest_type)
@data[:participants] |= @contest.approved_registered_users.ids
user_team_mapping = @contest.contest_registrations.where.not(team_id: nil).map{|x| [x.user_id, x.team_id]}.to_h
@data = helpers.ranklist_data(
c_submissions.order(:created_at),
@contest.start_time, freeze_start, @contest.contest_type,
user_team_mapping
)
@data[:participants] |= @contest.approved_registered_users.ids - user_team_mapping.keys
@data[:teams] |= user_team_mapping.values
@participants = UserBase.where(id: @data[:participants])
@teams = Team.where(id: @data[:teams])
@data[:tasks] = @tasks.map(&:id)
@data[:contest_type] = @contest.contest_type
@data[:user_id] = current_user&.id

current_team = user_team_mapping[current_user&.id]
if current_user.nil?
@data[:user_id] = nil
elsif current_team.nil?
@data[:user_id] = "user_#{current_user.id}"
else
@data[:user_id] = "team_#{current_user.id}"
end

@data[:timestamps] = {
start: helpers.to_us(@contest.start_time),
end: helpers.to_us(@contest.end_time),
Expand All @@ -68,7 +84,7 @@ def dashboard_update

def index
@contests = Contest.order(id: :desc).page(params[:page])
@registrations = ContestRegistration.where(contest_id: @contests.map(&:id), user_id: current_user&.id).all
@registrations = @contests.map { |contest| contest.find_registration(current_user) }.compact
@registrations = @registrations.map{|x| [x.contest_id, x.approved]}.to_h
end

Expand Down Expand Up @@ -146,19 +162,43 @@ def destroy
end

def register
@teams = current_user.teams
@registration = @contest.find_registration(current_user)
team = @registration&.team
if team.present?
@teammate_registrations = team.users.map{|user|
registration = @contest.contest_registrations.where(user: user, team: team).first
[user, registration]
}
end
end

def register_update
unless @contest.can_register?
flash[:alert] = 'Registration is closed.'
redirect_to @contest
return
end
user_id = current_user.id
team_id = nil
if params[:team_id].present?
team = Team.find(params[:team_id])
if not current_user or not current_user.teams.include?(team) then
flash[:alert] = 'Cannot (un)register as this team.'
redirect_to @contest
return
end
team_id = team.id
end

if params[:cancel] == '1'
@contest.contest_registrations.where(user_id: current_user.id).destroy_all
@contest.contest_registrations.where(user_id: user_id, team_id: team_id).destroy_all
respond_to do |format|
format.html { redirect_back fallback_location: root_path, notice: 'Successfully unregistered.' }
format.json { head :no_content }
end
else
entry = @contest.contest_registrations.new(user_id: current_user.id, approved: [email protected]_approval?)
entry = @contest.contest_registrations.new(user_id: user_id, team_id: team_id, approved: [email protected]_approval?)
respond_to do |format|
begin
entry.save!
Expand Down
29 changes: 20 additions & 9 deletions app/controllers/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def index
end

def show
unless effective_admin? or current_user&.id == @submission.user_id or not @submission.contest
unless effective_admin? or user_can_view? or not @submission.contest
if not @submission.contest.is_ended?
redirect_to contest_path(@submission.contest), notice: 'Submission is censored during contest.'
return
Expand Down Expand Up @@ -148,16 +148,22 @@ def set_submissions
if @contest
@submissions = @submissions.where(contest_id: @contest.id)
unless effective_admin?
user_ids = []
if user_signed_in?
@submissions = @submissions.where('submissions.created_at < ? OR submissions.user_id = ?', @contest.freeze_after, current_user.id)
else
@submissions = @submissions.where('submissions.created_at < ?', @contest.freeze_after)
user_ids = [current_user.id]
registration = @contest.find_registration(current_user)
if registration&.team
registered_members = @contest.contest_registrations.where(team: registration.team).select(:user_id).map(&:user_id)
user_ids += registered_members
end
end
@submissions = @submissions.where('submissions.created_at < ?', @contest.freeze_after) \
.or(@submissions.where(user_id: user_ids))
# TODO: Add an option to still hide submission after contest
unless @contest.is_ended?
# only self submission
if user_signed_in?
@submissions = @submissions.where(user_id: current_user.id)
@submissions = @submissions.where(user_id: user_ids)
else
@submissions = Submission.none
return
Expand Down Expand Up @@ -188,8 +194,8 @@ def set_submission
if @contest
unless effective_admin?
# TODO: Add an option to still hide submission after contest
raise_not_found if @submission.created_at >= @contest.freeze_after && current_user&.id != @submission.user_id
raise_not_found unless @contest.is_ended? or current_user&.id == @submission.user_id
raise_not_found if @submission.created_at >= @contest.freeze_after and (not user_can_view?)
raise_not_found unless @contest.is_ended? or user_can_view?
end
end
end
Expand Down Expand Up @@ -221,7 +227,8 @@ def set_default_compiler
if @submission&.compiler_id
@default_compiler_id = @submission.compiler_id
else
last_compiler = current_user&.last_compiler_id
user = current_user
last_compiler = user&.last_compiler_id
if @compiler.map(&:id).include?(last_compiler)
@default_compiler_id = last_compiler
else
Expand Down Expand Up @@ -256,12 +263,16 @@ def check_problem_visibility
end

def check_code_visibility
unless effective_admin? || current_user&.id == @submission.user_id
unless effective_admin? || user_can_view?
redirect_to problem_path(@problem), alert: 'Insufficient User Permissions.'
return
end
end

def user_can_view?
helpers.user_can_view?(current_user, @submission, @contest)
end

def normalize_code
if params[:submission][:code_file]
code = params[:submission][:code_file].read
Expand Down
101 changes: 101 additions & 0 deletions app/controllers/teams_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
class TeamsController < ApplicationController
before_action :authenticate_user!
before_action :set_team, only: [:show, :edit, :update, :destroy, :invite, :invite_accept]
before_action :check_user_in_team!, only: [:edit, :update, :destroy]

def index
@teams = Team.order(id: :desc).page(params[:page]).per(100)
end

def show
end

def new
@team = Team.new
end

def create
@team = Team.new(team_params)
@team.generate_random_avatar
@team.users = [current_user]
if @team.save
redirect_to @team, notice: 'Team was successfully created.'
else
render action: "new"
end
end

def edit
end

def invite
@token = params[:token]
end

def invite_accept
if @team.token != params[:token]
redirect_to teams_path, alert: 'Invalid token'
return
end

begin
@team.users << current_user
if @team.save
flash[:notice] = 'Joined successfully.'
else
flash[:alert] = 'Failed to join.'
end
rescue ActiveRecord::RecordNotUnique
flash[:alert] = 'User has already been added to this team.'
end

redirect_to @team
end

def update
@team.attributes = team_params
if @team.save
redirect_to @team, notice: 'Team was successfully updated.'
else
render action: "edit"
end
end

def destroy
@team.destroy
redirect_to teams_url
end

private

def set_team
begin
@team = Team.find(params[:id])
if @team.blank?
redirect_to teams_path, alert: "Teamname '#{params[:id]}' not found."
return
end
rescue ActiveRecord::RecordNotFound => e
redirect_to teams_path, alert: "Teamname '#{params[:id]}' not found."
return
end
end

# Never trust parameters from the scary internet, only allow the white list through.
def team_params
params.require(:team).permit(
:teamname,
:avatar, :avatar_cache,
:motto,
:school,
users_attributes: [
:id,
:_destroy
]
)
end

def check_user_in_team!
raise_not_found unless effective_admin? or @team.users.include?(current_user)
end
end
27 changes: 17 additions & 10 deletions app/helpers/contests_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ def ioi_new_ranklist_state(submission, start_time, item_state, is_waiting)

public

def ranklist_data(submissions, start_time, freeze_start, rule)
def ranklist_data(submissions, start_time, freeze_start, rule, user_team_mapping)
res = Hash.new { |h, k| h[k] = [] }
participants = Set[]
teams = Set[]
func = {
'acm' => method(:acm_ranklist_state),
'ioi' => method(:ioi_ranklist_state),
Expand All @@ -97,18 +98,25 @@ def ranklist_data(submissions, start_time, freeze_start, rule)
first_ac = {}
submissions = submissions.to_a
submissions.each do |sub|
participants << sub.user_id
team_id = user_team_mapping[sub.user_id]
if team_id.nil?
effective_id = "user_#{sub.user_id}"
participants << sub.user_id
else
effective_id = "team_#{team_id}"
teams << team_id
end
next if ['CE', 'ER', 'CLE', 'JE'].include?(sub.result) && sub.created_at < freeze_start
key = "#{sub.user_id}_#{sub.problem_id}"
key = "#{effective_id}_#{sub.problem_id}"
is_waiting = ['queued', 'received', 'Validating'].include?(sub.result) || sub.created_at >= freeze_start
orig_state = res[key][-1]&.dig(:state)
new_state = func.call(sub, start_time, orig_state, is_waiting)
res[key] << {timestamp: submission_rel_timestamp(sub, start_time), state: new_state} unless new_state.nil?
first_ac[sub.problem_id] = first_ac.fetch(sub.problem_id, sub.user_id) if sub.result == 'AC' && sub.created_at < freeze_start
first_ac[sub.problem_id] = first_ac.fetch(sub.problem_id, effective_id) if sub.result == 'AC' && sub.created_at < freeze_start
end
res.delete_if { |key, value| value.empty? }
res.each_value {|x| x.each {|item| item[:state].pop}} if rule == 'ioi_new'
{result: res, participants: participants.to_a, first_ac: first_ac}
{result: res, participants: participants.to_a, teams: teams.to_a, first_ac: first_ac}
end

def problem_index_text(index)
Expand All @@ -122,21 +130,20 @@ def problem_index_text(index)

## --- HTML ---

def contest_register_button(contest, status, standalone)
def contest_registration_page_button(contest, status, standalone)
if contest.can_register?
btn_class = standalone ? 'btn-lg' : 'btn-xs'
btn_class += status.nil? ? ' btn-success' : ' btn-danger'
button_text = status.nil? ? "Register" : "Unregister"
btn_class += ' btn-primary'
button_text = standalone ? 'Change Registration' : 'Change'

content_tag(:div, class: 'pull-right') if standalone

html = button_to(
button_text,
register_contest_path(contest),
method: :post,
method: :get,
class: "btn #{btn_class}",
form: {style: 'display: inline'},
params: status.nil? ? {} : { cancel: 1 },
)
if standalone
content_tag(:div, class: 'pull-right') do
Expand Down
10 changes: 10 additions & 0 deletions app/helpers/submissions_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,14 @@ def time_str(x)
prefix = '0' * [0, 6 - ret.length].max
raw("<span style=\"visibility: hidden;\">#{prefix}</span>" + ret)
end

def user_can_view?(user, submission, contest)
return true if user&.id == submission.user_id
if contest && contest == submission.contest
sub_team = contest.find_registration(submission.user)&.team
team = contest.find_registration(user)&.team
return true if team && team == sub_team
end
false
end
end
2 changes: 2 additions & 0 deletions app/helpers/teams_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module TeamsHelper
end
Loading