mirror of
https://github.com/ENSL/ensl.org.git
synced 2024-12-25 12:01:03 +00:00
Merge: First Iteration of Match-Scheduling feature
This commit is contained in:
commit
7c4a7b9c69
16 changed files with 352 additions and 10 deletions
|
@ -1,5 +1,5 @@
|
|||
class ContestsController < ApplicationController
|
||||
before_filter :get_contest, only: [:show, :edit, :update, :destroy, :del_map, :scores, :recalc]
|
||||
before_filter :get_contest, only: [:show, :edit, :update, :destroy, :del_map, :scores, :recalc, :confirmed_matches]
|
||||
|
||||
def index
|
||||
# @contests = Contest.all
|
||||
|
@ -102,6 +102,10 @@ class ContestsController < ApplicationController
|
|||
redirect_to contests_url
|
||||
end
|
||||
|
||||
def confirmed_matches
|
||||
@match_props = MatchProposal.confirmed_for_contest(@contest)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_contest
|
||||
|
|
115
app/controllers/match_proposals_controller.rb
Normal file
115
app/controllers/match_proposals_controller.rb
Normal file
|
@ -0,0 +1,115 @@
|
|||
class MatchProposalsController < ApplicationController
|
||||
before_filter :get_match
|
||||
def index
|
||||
raise AccessError unless @match.user_in_match?(cuser)
|
||||
end
|
||||
|
||||
def new
|
||||
# Don't allow creation of new proposals if there is a confirmed one already
|
||||
if MatchProposal.exists?(
|
||||
match_id: @match.id,
|
||||
status: MatchProposal::STATUS_CONFIRMED
|
||||
)
|
||||
flash[:error] = 'Cannot create a new proposal if there is already a confirmed one'
|
||||
redirect_to(match_proposals_path(@match)) && return
|
||||
end
|
||||
@proposal = MatchProposal.new
|
||||
@proposal.match = @match
|
||||
raise AccessError unless @proposal.can_create? cuser
|
||||
end
|
||||
|
||||
def create
|
||||
@proposal = MatchProposal.new(params[:match_proposal])
|
||||
@proposal.match = @match
|
||||
raise AccessError unless @proposal.can_create? cuser
|
||||
@proposal.team = cuser.team
|
||||
@proposal.status = MatchProposal::STATUS_PENDING
|
||||
if @proposal.save
|
||||
# TODO: send message to teamleaders of opposite team
|
||||
msg = Message.new
|
||||
msg.sender_type = 'System'
|
||||
msg.recipient_type = 'Team'
|
||||
msg.title = 'New Scheduling Proposal'
|
||||
recipient = @match.get_opposing_team(cuser.team)
|
||||
msg.recipient = recipient
|
||||
msg.text = "There is a new scheduling proposal for your match against #{recipient.name}.\n" \
|
||||
"Find it [url=#{match_proposals_path(@match)}]here[/url]"
|
||||
msg.save
|
||||
flash[:notice] = 'Created new proposal'
|
||||
redirect_to(match_proposals_path(@match))
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
raise AccessError unless request.xhr? # Only respond to ajax requests
|
||||
rjson = {}
|
||||
proposal = MatchProposal.find(params[:id])
|
||||
unless proposal
|
||||
rjson[:error] = {
|
||||
code: 404,
|
||||
message: "No proposal with id #{params[:id]}"
|
||||
}
|
||||
render(json: rjson, status: :not_found) && return
|
||||
end
|
||||
unless proposal.can_update?(cuser, params[:match_proposal])
|
||||
rjson[:error] = {
|
||||
code: 403,
|
||||
message: "You are not allowed to update the state to #{MatchProposal.status_strings[params[:match_proposal][:status].to_i]}"
|
||||
}
|
||||
render(json: rjson, status: :forbidden) && return
|
||||
end
|
||||
new_status = params[:match_proposal][:status]
|
||||
curr_status = proposal.status
|
||||
status_updated = curr_status != new_status
|
||||
proposal.status = new_status
|
||||
if proposal.save
|
||||
|
||||
if status_updated
|
||||
msg = Message.new
|
||||
msg.sender_type = 'System'
|
||||
msg.recipient_type = 'Team'
|
||||
msg.title = 'New Scheduling Proposal'
|
||||
recipient = @match.get_opposing_team(cuser.team)
|
||||
msg.recipient = recipient
|
||||
msg.text = message_text(new_status)
|
||||
msg.save if msg.text
|
||||
end
|
||||
|
||||
rjson[:status] = MatchProposal.status_strings[proposal.status]
|
||||
rjson[:message] = "Successfully updated status to #{MatchProposal.status_strings[proposal.status]}"
|
||||
render(json: rjson, status: :accepted)
|
||||
else
|
||||
rjson[:error] = {
|
||||
code: 500,
|
||||
message: 'Something went wrong! Please try again.'
|
||||
}
|
||||
render(json: rjson, status: 500)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def get_match
|
||||
@match = Match.find params[:match_id]
|
||||
end
|
||||
|
||||
def message_text(new_status)
|
||||
case new_status
|
||||
when MatchProposal::STATUS_CONFIRMED
|
||||
"A scheduling proposal for your match against #{recipient.name} was confirmed!.\n" \
|
||||
"Find it [url=#{match_proposals_path(@match)}]here[/url]"
|
||||
when MatchProposal::STATUS_REJECTED
|
||||
"A scheduling proposal for your match against #{recipient.name} was rejected!.\n" \
|
||||
"Find it [url=#{match_proposals_path(@match)}]here[/url]"
|
||||
when MatchProposal::STATUS_REVOKED
|
||||
"A scheduling proposal for your match against #{recipient.name} was revoked!.\n" \
|
||||
"Find it [url=#{match_proposals_path(@match)}]here[/url]"
|
||||
when MatchProposal::STATUS_DELAYED
|
||||
"Delaying for your match against #{recipient.name} was permitted!.\n" \
|
||||
"Schedule a new time as soon as possible [url=#{match_proposals_path(@match)}]here[/url]"
|
||||
else
|
||||
false # Should not happen as transition to any other state is not allowed
|
||||
end
|
||||
end
|
||||
end
|
|
@ -42,6 +42,7 @@ class Match < ActiveRecord::Base
|
|||
has_many :users, through: :matchers
|
||||
has_many :predictions, dependent: :destroy
|
||||
has_many :comments, as: :commentable, order: "created_at", dependent: :destroy
|
||||
has_many :match_proposals, inverse_of: :match, dependent: :destroy
|
||||
belongs_to :challenge
|
||||
belongs_to :contest
|
||||
belongs_to :contester1, class_name: "Contester", include: "team"
|
||||
|
@ -188,6 +189,11 @@ class Match < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def get_opposing_team(team)
|
||||
team == contester1.team ? contester2.team : contester1.team
|
||||
end
|
||||
|
||||
|
||||
def set_hltv
|
||||
get_hltv if match_time.future?
|
||||
end
|
||||
|
@ -357,4 +363,12 @@ class Match < ActiveRecord::Base
|
|||
def can_destroy?(cuser)
|
||||
cuser && cuser.admin?
|
||||
end
|
||||
|
||||
def can_make_proposal?(cuser)
|
||||
cuser && (contester1.team.is_leader?(cuser) || contester2.team.is_leader?(cuser))
|
||||
end
|
||||
|
||||
def user_in_match?(user)
|
||||
user && (user.team == contester1.team || user.team == contester2.team)
|
||||
end
|
||||
end
|
||||
|
|
87
app/models/match_proposal.rb
Normal file
87
app/models/match_proposal.rb
Normal file
|
@ -0,0 +1,87 @@
|
|||
class MatchProposal < ActiveRecord::Base
|
||||
|
||||
STATUS_PENDING = 0
|
||||
STATUS_REVOKED = 1
|
||||
STATUS_REJECTED = 2
|
||||
STATUS_CONFIRMED = 3
|
||||
STATUS_DELAYED = 4
|
||||
|
||||
# latest time before a match to be confirmed/rejected (in minutes)
|
||||
CONFIRMATION_LIMIT = 30
|
||||
|
||||
belongs_to :match
|
||||
belongs_to :team
|
||||
#has_many :confirmed_by, class_name: 'Team', uniq: true
|
||||
attr_accessible :proposed_time, :status
|
||||
|
||||
validates_presence_of :match, :team, :proposed_time
|
||||
|
||||
scope :of_match, ->(match) { where('match_id = ?', match.id) }
|
||||
scope :confirmed_for_match, ->(match) { where('match_id = ? AND status = ?', match.id, STATUS_CONFIRMED) }
|
||||
scope :confirmed_upcoming, ->{ where('status = ? AND proposed_time > UTC_TIMESTAMP()', STATUS_CONFIRMED) }
|
||||
scope :confirmed_for_contest,
|
||||
->(contest){ includes(:match).where(matches:{contest_id: contest.id}, status: STATUS_CONFIRMED).all}
|
||||
|
||||
def self.status_strings
|
||||
{STATUS_PENDING => 'Pending',
|
||||
STATUS_REVOKED => 'Revoked',
|
||||
STATUS_REJECTED => 'Rejected',
|
||||
STATUS_CONFIRMED => 'Confirmed',
|
||||
STATUS_DELAYED => 'Delayed'}
|
||||
end
|
||||
|
||||
def can_create? cuser
|
||||
return false unless cuser && match
|
||||
return true if cuser.admin?
|
||||
match.can_make_proposal?(cuser)
|
||||
end
|
||||
|
||||
def can_update? cuser, params = {}
|
||||
return false unless cuser && match && (cuser.admin? || match.can_make_proposal?(cuser))
|
||||
|
||||
if params.key?(:status) && (self.status !=(new_status = params[:status].to_i))
|
||||
return status_change_allowed?(cuser,new_status)
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def can_destroy?
|
||||
cuser && cuser.admin?
|
||||
end
|
||||
|
||||
def state_immutable?
|
||||
status == STATUS_REJECTED ||
|
||||
status == STATUS_DELAYED ||
|
||||
status == STATUS_REVOKED
|
||||
end
|
||||
|
||||
def status_change_allowed?(cuser, new_status)
|
||||
case new_status
|
||||
when STATUS_PENDING
|
||||
# never go back to pending
|
||||
return false
|
||||
when STATUS_DELAYED
|
||||
# only confirmed matches can be set to delayed
|
||||
# only admins can set matches to delayed and only if they are not playing in that match
|
||||
# matches can only be delayed if they are not to far in the future
|
||||
return self.status == STATUS_CONFIRMED && cuser.admin? &&
|
||||
!self.match.user_in_match?(cuser) && self.proposed_time <= CONFIRMATION_LIMIT.minutes.from_now
|
||||
when STATUS_REVOKED
|
||||
# unconfirmed can only be revoked by team making the proposal
|
||||
# confirmed can only be revoked if soon enough before match time
|
||||
return self.status == STATUS_PENDING && self.team == cuser.team ||
|
||||
self.status == STATUS_CONFIRMED && self.proposed_time > CONFIRMATION_LIMIT.minutes.from_now
|
||||
when STATUS_CONFIRMED, STATUS_REJECTED
|
||||
# only team proposed to can reject or confirm and only if soon enough before match time
|
||||
status_ok = self.status == STATUS_PENDING
|
||||
team_ok = self.team != cuser.team
|
||||
time_ok = CONFIRMATION_LIMIT.minutes.from_now < self.proposed_time
|
||||
|
||||
return status_ok && team_ok && time_ok
|
||||
else
|
||||
# invalid status
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -44,11 +44,15 @@ class Message < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def thread
|
||||
Message.find_by_sql ["
|
||||
if sender_type == 'System'
|
||||
Message.where(recipient_id: recipient.id, sender_type: 'System')
|
||||
else
|
||||
Message.find_by_sql ["
|
||||
(SELECT `messages`.* FROM `messages` WHERE `messages`.`sender_id` = ? AND `messages`.`sender_type` = 'User' AND `messages`.`recipient_id` = ?)
|
||||
UNION
|
||||
(SELECT `messages`.* FROM `messages` WHERE `messages`.`sender_id` = ? AND `messages`.`sender_type` = 'User' AND `messages`.`recipient_id` = ?)
|
||||
ORDER BY id", sender.id, recipient.id, recipient.id, sender.id]
|
||||
end
|
||||
end
|
||||
|
||||
def parse_text
|
||||
|
|
|
@ -199,6 +199,10 @@ class User < ActiveRecord::Base
|
|||
groups.exists? id: Group::STAFF
|
||||
end
|
||||
|
||||
def staff?
|
||||
groups.exists? :id => Group::STAFF
|
||||
end
|
||||
|
||||
def caster?
|
||||
groups.exists? id: Group::CASTERS
|
||||
end
|
||||
|
@ -216,6 +220,14 @@ class User < ActiveRecord::Base
|
|||
admin? or moderator?
|
||||
end
|
||||
|
||||
def gather_moderator?
|
||||
groups.exists? id: Group::GATHER_MODERATORS
|
||||
end
|
||||
|
||||
def allowed_to_ban?
|
||||
admin? or gather_moderator?
|
||||
end
|
||||
|
||||
def verified?
|
||||
# created_at < DateTime.now.ago(VERIFICATION_TIME)
|
||||
true
|
||||
|
|
13
app/views/contests/confirmed_matches.html.erb
Normal file
13
app/views/contests/confirmed_matches.html.erb
Normal file
|
@ -0,0 +1,13 @@
|
|||
<h1 class="title">Confirmed Matches for <%= @contest.name %></h1>
|
||||
<table class="striped">
|
||||
<tr>
|
||||
<th>Match</th>
|
||||
<th>Scheduled for</th>
|
||||
</tr>
|
||||
<% @match_props && @match_props.each do |mp| %>
|
||||
<tr class="<%= cycle('even', 'odd') %>">
|
||||
<td><%= link_to mp.match.contester1, contester_path(mp.match.contester1) %> VS <%= link_to mp.match.contester2, contester_path(mp.match.contester2) %></td>
|
||||
<td><%= link_to longertime(mp.proposed_time), match_path(mp.match) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
|
@ -20,6 +20,8 @@
|
|||
<dd>Sunday: <%= Time.use_zone(timezone_offset) { @contest.default_time.strftime("%H:%M %Z") } %></dd>
|
||||
</dl>
|
||||
|
||||
<%= link_to 'Scheduled Matches', confirmed_matches_path(@contest), class: 'button' %>
|
||||
|
||||
<% if cuser and cuser.admin? %>
|
||||
<%= link_to 'Edit Contest', edit_contest_path(@contest), class: 'button' %>
|
||||
<% end %>
|
||||
|
|
61
app/views/match_proposals/index.html.erb
Normal file
61
app/views/match_proposals/index.html.erb
Normal file
|
@ -0,0 +1,61 @@
|
|||
<h1>Proposals</h1>
|
||||
<h2><%= @match.contester1 %> VS <%= @match.contester2 %></h2>
|
||||
<%= link_to 'Back', match_path(@match), class: 'button' %>
|
||||
<% if @match.match_proposals.empty? %>
|
||||
<h4 style="clear: both;">There are no proposals yet</h4>
|
||||
<% else %>
|
||||
<table id="proposals" class="striped">
|
||||
<tr>
|
||||
<th>Team</th>
|
||||
<th>Time</th>
|
||||
<th>Status</th>
|
||||
<% if @match.can_make_proposal?(cuser) %>
|
||||
<th class="actions">Actions</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% @match.match_proposals.each do |proposal| %>
|
||||
<tr class="<%=cycle('even', 'odd') %>">
|
||||
<td><%= proposal.team.name %></td>
|
||||
<td><%= longtime proposal.proposed_time %> </td>
|
||||
<td><%= MatchProposal.status_strings[proposal.status] %></td>
|
||||
<% if @match.can_make_proposal?(cuser) %>
|
||||
<td class="actions">
|
||||
<% unless proposal.state_immutable? %>
|
||||
<%= form_for proposal, url: match_proposal_path(@match, proposal) do |f| %>
|
||||
<%= f.hidden_field :status, value: 0 %>
|
||||
<% if proposal.status_change_allowed?(cuser, MatchProposal::STATUS_CONFIRMED) %>
|
||||
<%= link_to_function icon('check'), "proposalStateSubmit(#{MatchProposal::STATUS_CONFIRMED},#{proposal.id})", title: 'Confirm' %>
|
||||
<% end %>
|
||||
<% if proposal.status_change_allowed?(cuser, MatchProposal::STATUS_REJECTED) %>
|
||||
<%= link_to_function icon('times'), "proposalStateSubmit(#{MatchProposal::STATUS_REJECTED},#{proposal.id})", title: 'Reject' %>
|
||||
<% end %>
|
||||
<% if proposal.status_change_allowed?(cuser, MatchProposal::STATUS_REVOKED) %>
|
||||
<%= link_to_function icon('undo'), "proposalStateSubmit(#{MatchProposal::STATUS_REVOKED},#{proposal.id})", title: 'Revoke' %>
|
||||
<% end %>
|
||||
<% if proposal.status_change_allowed?(cuser, MatchProposal::STATUS_DELAYED) %>
|
||||
<%= link_to_function icon('hourglass'), "proposalStateSubmit(#{MatchProposal::STATUS_DELAYED},#{proposal.id})", title: 'Delay' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
<script type="text/javascript">
|
||||
function proposalStateSubmit(newState, formID) {
|
||||
var form = $('#edit_match_proposal_' + formID);
|
||||
form.children("input[type='hidden']").val(newState);
|
||||
$.post(form.attr('action'),form.serialize(), function(data) {
|
||||
tr = form.closest('tr');
|
||||
tr.children('td').eq(2).text(data.status);
|
||||
if(data.status === 'Revoked' || data.status === 'Rejected') tr.children('td').eq(3).empty();
|
||||
}, 'json')
|
||||
.error(function (err) {
|
||||
errjson = JSON.parse(err.responseText);
|
||||
alert(errjson.error.message);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<% end %>
|
||||
<%= link_to 'Back', match_path(@match), class: 'button' %><%= link_to 'Propose match time', new_match_proposal_path(@match), class: 'button' %>
|
11
app/views/match_proposals/new.html.erb
Normal file
11
app/views/match_proposals/new.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
|||
<h1>New Proposal</h1>
|
||||
<%= form_for @proposal, html: { class: 'square' } do |f| %>
|
||||
<%= render 'shared/errors', messages: @proposal.errors.full_messages %>
|
||||
<div class="fields horizontal">
|
||||
<%= f.label :proposed_time %>
|
||||
<%= f.datetime_select :proposed_time, datetime_separator: '', time_separator: '', minute_step: 15 %>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<%= f.submit 'Propose' %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -116,6 +116,11 @@
|
|||
<p><%= @match.report.html_safe %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if cuser and (cuser.admin? or @match.user_in_match? cuser) %>
|
||||
<div class="referee">
|
||||
<%= link_to 'Proposals', match_proposals_path(@match), class: 'button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if cuser and @match.can_update? cuser, [:report] %>
|
||||
<div class="referee">
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
</p>
|
||||
|
||||
<p>
|
||||
To: <%= namelink message.recipient %> From: <%= namelink message.sender %> on <em><%= longdate message.created_at %></em></em>
|
||||
To: <%= namelink message.recipient %> From: <% if message.sender_type == 'System' %>System<% else %><%= namelink message.sender %><% end %> on <em><%= longdate message.created_at %></em></em>
|
||||
</p>
|
|
@ -10,7 +10,7 @@
|
|||
<div class="content">
|
||||
<%= raw message.text_parsed %>
|
||||
</div>
|
||||
<p>Sent by: <%= namelink message.sender %> on <%= shortdate message.created_at %></p>
|
||||
<p>Sent by: <% if message.sender_type == 'System' %>System<% else %><%= namelink message.sender %><% end %> on <%= shortdate message.created_at %></p>
|
||||
<p>
|
||||
<%= link_to "Reply",
|
||||
{ controller: "messages", action: "new", id: message.sender_type, id2: message.sender_id, title: message.title },
|
||||
|
|
|
@ -51,6 +51,8 @@ Ensl::Application.routes.draw do
|
|||
resources :users
|
||||
resources :locks
|
||||
resources :contesters
|
||||
|
||||
get "contests/:id/confirmedmatches" => "contests#confirmed_matches", as: :confirmed_matches
|
||||
resources :contests
|
||||
resources :challenges
|
||||
resources :servers
|
||||
|
@ -60,6 +62,7 @@ Ensl::Application.routes.draw do
|
|||
get "matches/ref/:id" => "matches#ref", as: :match_ref
|
||||
resources :matches do
|
||||
get :admin, to: "matches#admin", on: :collection
|
||||
resources :match_proposals, path: "proposals", as: :proposals, only: [:index, :new, :create, :update]
|
||||
end
|
||||
|
||||
resources :maps
|
||||
|
|
14
db/migrate/20171013154050_create_match_proposals.rb
Normal file
14
db/migrate/20171013154050_create_match_proposals.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class CreateMatchProposals < ActiveRecord::Migration
|
||||
def up
|
||||
create_table :match_proposals do |t|
|
||||
t.references :match, index: true, forign_key: true
|
||||
t.references :team, forign_key: true
|
||||
t.datetime :proposed_time
|
||||
t.integer :status
|
||||
end
|
||||
|
||||
add_index :match_proposals, :status
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20170702150454) do
|
||||
ActiveRecord::Schema.define(:version => 20171013154050) do
|
||||
|
||||
create_table "admin_requests", :force => true do |t|
|
||||
t.string "addr"
|
||||
|
@ -435,14 +435,11 @@ ActiveRecord::Schema.define(:version => 20170702150454) do
|
|||
create_table "match_proposals", :force => true do |t|
|
||||
t.integer "match_id"
|
||||
t.integer "team_id"
|
||||
t.integer "status"
|
||||
t.datetime "proposed_time"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.integer "status"
|
||||
end
|
||||
|
||||
add_index "match_proposals", ["match_id"], :name => "index_match_proposals_on_match_id"
|
||||
add_index "match_proposals", ["team_id"], :name => "index_match_proposals_on_team_id"
|
||||
add_index "match_proposals", ["status"], :name => "index_match_proposals_on_status"
|
||||
|
||||
create_table "matchers", :force => true do |t|
|
||||
t.integer "match_id", :null => false
|
||||
|
|
Loading…
Reference in a new issue