diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index 1b6731b..d2157ef 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -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 diff --git a/app/controllers/match_proposals_controller.rb b/app/controllers/match_proposals_controller.rb new file mode 100644 index 0000000..0032ad7 --- /dev/null +++ b/app/controllers/match_proposals_controller.rb @@ -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 diff --git a/app/models/match.rb b/app/models/match.rb index 4626848..0772406 100755 --- a/app/models/match.rb +++ b/app/models/match.rb @@ -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 diff --git a/app/models/match_proposal.rb b/app/models/match_proposal.rb new file mode 100644 index 0000000..4d74e54 --- /dev/null +++ b/app/models/match_proposal.rb @@ -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 diff --git a/app/models/message.rb b/app/models/message.rb index b1db6e9..395f990 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 32a1f46..f7a08f9 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/contests/confirmed_matches.html.erb b/app/views/contests/confirmed_matches.html.erb new file mode 100644 index 0000000..59a4427 --- /dev/null +++ b/app/views/contests/confirmed_matches.html.erb @@ -0,0 +1,13 @@ +
Match | +Scheduled for | +
---|---|
<%= link_to mp.match.contester1, contester_path(mp.match.contester1) %> VS <%= link_to mp.match.contester2, contester_path(mp.match.contester2) %> | +<%= link_to longertime(mp.proposed_time), match_path(mp.match) %> | +
Team | +Time | +Status | + <% if @match.can_make_proposal?(cuser) %> +Actions | + <% end %> +
---|---|---|---|
<%= proposal.team.name %> | +<%= longtime proposal.proposed_time %> | +<%= MatchProposal.status_strings[proposal.status] %> | + <% if @match.can_make_proposal?(cuser) %> ++ <% 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 %> + | + <% end %> +
<%= @match.report.html_safe %>
<% end %> + <% if cuser and (cuser.admin? or @match.user_in_match? cuser) %> +- To: <%= namelink message.recipient %> From: <%= namelink message.sender %> on <%= longdate message.created_at %> + To: <%= namelink message.recipient %> From: <% if message.sender_type == 'System' %>System<% else %><%= namelink message.sender %><% end %> on <%= longdate message.created_at %>
\ No newline at end of file diff --git a/app/views/messages/show.html.erb b/app/views/messages/show.html.erb index 5a4d399..f0e033b 100644 --- a/app/views/messages/show.html.erb +++ b/app/views/messages/show.html.erb @@ -10,7 +10,7 @@Sent by: <%= namelink message.sender %> on <%= shortdate message.created_at %>
+Sent by: <% if message.sender_type == 'System' %>System<% else %><%= namelink message.sender %><% end %> on <%= shortdate message.created_at %>
<%= link_to "Reply", { controller: "messages", action: "new", id: message.sender_type, id2: message.sender_id, title: message.title }, diff --git a/config/routes.rb b/config/routes.rb index c0c9665..399be2c 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20171013154050_create_match_proposals.rb b/db/migrate/20171013154050_create_match_proposals.rb new file mode 100644 index 0000000..7460482 --- /dev/null +++ b/db/migrate/20171013154050_create_match_proposals.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index a260857..a960534 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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