Description:
Merge pull request #26 from cafe-grader-team/master merge edit from upstream
Commit status:
[Not Reviewed]
References:
merge default
Comments:
0 Commit comments 0 Inline Comments
Unresolved TODOs:
There are no unresolved TODOs
Add another comment

r777:e61e522c673f - - 8 files changed: 82 inserted, 8 deleted

@@ -0,0 +1,33
1 + # Authentication and user imports through programming.in.th web request
2 + require 'net/http'
3 + require 'uri'
4 + require 'json'
5 +
6 + class ProgrammingAuthenticator
7 + PROGRAMMING_AUTHEN_URL = "https://programming.in.th/authen.php"
8 +
9 + def find_or_create_user(result)
10 + user = User.find_by(login: result['username'])
11 + if not user
12 + user = User.new(login: result['username'],
13 + full_name: result['firstname'] + ' ' + result['surname'],
14 + alias: result['display'],
15 + email: result['email'])
16 + user.password = User.random_password
17 + user.save
18 + end
19 + return user
20 + end
21 +
22 + def authenticate(login, password)
23 + uri = URI(PROGRAMMING_AUTHEN_URL)
24 + result = Net::HTTP.post_form(uri, 'username' => login, 'password' => password)
25 + request_result = JSON.parse(result.body)
26 +
27 + if request_result.fetch('status', 'incorrect') == 'OK'
28 + return find_or_create_user(request_result)
29 + else
30 + return nil
31 + end
32 + end
33 + end
@@ -1,63 +1,85
1 1 class LoginController < ApplicationController
2 2
3 + @@authenticators = []
4 +
3 5 def index
4 6 # show login screen
5 7 reset_session
6 8 redirect_to :controller => 'main', :action => 'login'
7 9 end
8 10
9 11 def login
10 - user = User.authenticate(params[:login], params[:password])
12 + user = get_authenticated_user(params[:login], params[:password])
11 13 unless user
12 14 flash[:notice] = 'Wrong password'
13 15 redirect_to :controller => 'main', :action => 'login'
14 16 return
15 17 end
16 18
17 19 if (!GraderConfiguration['right.bypass_agreement']) and (!params[:accept_agree]) and !user.admin?
18 20 flash[:notice] = 'You must accept the agreement before logging in'
19 21 redirect_to :controller => 'main', :action => 'login'
20 22 return
21 23 end
22 24
23 25 #process logging in
24 26 session[:user_id] = user.id
25 27 session[:admin] = user.admin?
26 28
27 29 # clear forced logout flag for multicontests contest change
28 30 if GraderConfiguration.multicontests?
29 31 contest_stat = user.contest_stat
30 32 if contest_stat.respond_to? :forced_logout
31 33 if contest_stat.forced_logout
32 34 contest_stat.forced_logout = false
33 35 contest_stat.save
34 36 end
35 37 end
36 38 end
37 39
38 40 #save login information
39 41 Login.create(user_id: user.id, ip_address: request.remote_ip)
40 42
41 43 redirect_to :controller => 'main', :action => 'list'
42 44 end
43 45
44 46 def site_login
45 47 begin
46 48 site = Site.find(params[:login][:site_id])
47 49 rescue ActiveRecord::RecordNotFound
48 50 site = nil
49 51 end
50 52 if site==nil
51 53 flash[:notice] = 'Wrong site'
52 54 redirect_to :controller => 'main', :action => 'login' and return
53 55 end
54 56 if (site.password) and (site.password == params[:login][:password])
55 57 session[:site_id] = site.id
56 58 redirect_to :controller => 'site', :action => 'index'
57 59 else
58 60 flash[:notice] = 'Wrong site password'
59 61 redirect_to :controller => 'site', :action => 'login'
60 62 end
61 63 end
62 64
65 + def self.add_authenticator(authenticator)
66 + @@authenticators << authenticator
67 + end
68 +
69 + protected
70 +
71 + def get_authenticated_user(login, password)
72 + if @@authenticators.empty?
73 + return User.authenticate(login, password)
74 + else
75 + user = User.authenticate(login, password)
76 + @@authenticators.each do |authenticator|
77 + if not user
78 + user = authenticator.authenticate(login, password)
79 + end
80 + end
81 + return user
82 + end
83 + end
84 +
63 85 end
@@ -1,80 +1,80
1 1 class ProblemsController < ApplicationController
2 2
3 3 before_action :authenticate, :authorization
4 4 before_action :testcase_authorization, only: [:show_testcase]
5 5
6 6 in_place_edit_for :problem, :name
7 7 in_place_edit_for :problem, :full_name
8 8 in_place_edit_for :problem, :full_score
9 9
10 10 def index
11 11 @problems = Problem.order(date_added: :desc)
12 12 end
13 13
14 14 # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
15 15 verify :method => :post, :only => [ :create, :quick_create,
16 16 :do_manage,
17 17 :do_import,
18 18 ],
19 19 :redirect_to => { :action => :index }
20 20
21 21 def show
22 22 @problem = Problem.find(params[:id])
23 23 end
24 24
25 25 def new
26 26 @problem = Problem.new
27 27 @description = nil
28 28 end
29 29
30 30 def create
31 31 @problem = Problem.new(problem_params)
32 - @description = Description.new(params[:description])
32 + @description = Description.new(description_params)
33 33 if @description.body!=''
34 34 if !@description.save
35 35 render :action => new and return
36 36 end
37 37 else
38 38 @description = nil
39 39 end
40 40 @problem.description = @description
41 41 if @problem.save
42 42 flash[:notice] = 'Problem was successfully created.'
43 43 redirect_to action: :index
44 44 else
45 45 render :action => 'new'
46 46 end
47 47 end
48 48
49 49 def quick_create
50 50 @problem = Problem.new(problem_params)
51 51 @problem.full_name = @problem.name if @problem.full_name == ''
52 52 @problem.full_score = 100
53 53 @problem.available = false
54 54 @problem.test_allowed = true
55 55 @problem.output_only = false
56 56 @problem.date_added = Time.new
57 57 if @problem.save
58 58 flash[:notice] = 'Problem was successfully created.'
59 59 redirect_to action: :index
60 60 else
61 61 flash[:notice] = 'Error saving problem'
62 62 redirect_to action: :index
63 63 end
64 64 end
65 65
66 66 def edit
67 67 @problem = Problem.find(params[:id])
68 68 @description = @problem.description
69 69 end
70 70
71 71 def update
72 72 @problem = Problem.find(params[:id])
73 73 @description = @problem.description
74 74 if @description.nil? and params[:description][:body]!=''
75 75 @description = Description.new(params[:description])
76 76 if !@description.save
77 77 flash[:notice] = 'Error saving description'
78 78 render :action => 'edit' and return
79 79 end
80 80 @problem.description = @description
@@ -207,96 +207,100
207 207 failed << p.full_name
208 208 end
209 209 end
210 210 flash[:success] = "The following problems are added to the group #{group.name}: " + ok.join(', ') if ok.count > 0
211 211 flash[:alert] = "The following problems are already in the group #{group.name}: " + failed.join(', ') if failed.count > 0
212 212 elsif params.has_key? 'add_tags'
213 213 get_problems_from_params.each do |p|
214 214 p.tag_ids += params[:tag_ids]
215 215 end
216 216 end
217 217
218 218 redirect_to :action => 'manage'
219 219 end
220 220
221 221 def import
222 222 @allow_test_pair_import = allow_test_pair_import?
223 223 end
224 224
225 225 def do_import
226 226 old_problem = Problem.find_by_name(params[:name])
227 227 if !allow_test_pair_import? and params.has_key? :import_to_db
228 228 params.delete :import_to_db
229 229 end
230 230 @problem, import_log = Problem.create_from_import_form_params(params,
231 231 old_problem)
232 232
233 233 if !@problem.errors.empty?
234 234 render :action => 'import' and return
235 235 end
236 236
237 237 if old_problem!=nil
238 238 flash[:notice] = "The test data has been replaced for problem #{@problem.name}"
239 239 end
240 240 @log = import_log
241 241 end
242 242
243 243 def remove_contest
244 244 problem = Problem.find(params[:id])
245 245 contest = Contest.find(params[:contest_id])
246 246 if problem!=nil and contest!=nil
247 247 problem.contests.delete(contest)
248 248 end
249 249 redirect_to :action => 'manage'
250 250 end
251 251
252 252 ##################################
253 253 protected
254 254
255 + def description_params
256 + params.require(:description).permit(:body, :markdowned)
257 + end
258 +
255 259 def allow_test_pair_import?
256 260 if defined? ALLOW_TEST_PAIR_IMPORT
257 261 return ALLOW_TEST_PAIR_IMPORT
258 262 else
259 263 return false
260 264 end
261 265 end
262 266
263 267 def change_date_added
264 268 problems = get_problems_from_params
265 269 date = Date.parse(params[:date_added])
266 270 problems.each do |p|
267 271 p.date_added = date
268 272 p.save
269 273 end
270 274 end
271 275
272 276 def add_to_contest
273 277 problems = get_problems_from_params
274 278 contest = Contest.find(params[:contest][:id])
275 279 if contest!=nil and contest.enabled
276 280 problems.each do |p|
277 281 p.contests << contest
278 282 end
279 283 end
280 284 end
281 285
282 286 def set_available(avail)
283 287 problems = get_problems_from_params
284 288 problems.each do |p|
285 289 p.available = avail
286 290 p.save
287 291 end
288 292 end
289 293
290 294 def get_problems_from_params
291 295 problems = []
292 296 params.keys.each do |k|
293 297 if k.index('prob-')==0
294 298 name, id, order = k.split('-')
295 299 problems << Problem.find(id)
296 300 end
297 301 end
298 302 problems
299 303 end
300 304
301 305 def get_problems_stat
302 306 end
@@ -82,85 +82,87
82 82 if i==10
83 83 return nil
84 84 end
85 85 end
86 86 return nil
87 87 end
88 88
89 89 def self.find_language_in_source(source, source_filename="")
90 90 langopt = find_option_in_source(/^LANG:/,source)
91 91 if langopt
92 92 return (Language.find_by_name(langopt) ||
93 93 Language.find_by_pretty_name(langopt))
94 94 else
95 95 if source_filename
96 96 return Language.find_by_extension(source_filename.split('.').last)
97 97 else
98 98 return nil
99 99 end
100 100 end
101 101 end
102 102
103 103 def self.find_problem_in_source(source, source_filename="")
104 104 prob_opt = find_option_in_source(/^TASK:/,source)
105 105 if problem = Problem.find_by_name(prob_opt)
106 106 return problem
107 107 else
108 108 if source_filename
109 109 return Problem.find_by_name(source_filename.split('.').first)
110 110 else
111 111 return nil
112 112 end
113 113 end
114 114 end
115 115
116 116 def assign_problem
117 117 if self.problem_id!=-1
118 118 begin
119 119 self.problem = Problem.find(self.problem_id)
120 120 rescue ActiveRecord::RecordNotFound
121 121 self.problem = nil
122 122 end
123 123 else
124 124 self.problem = Submission.find_problem_in_source(self.source,
125 125 self.source_filename)
126 126 end
127 127 end
128 128
129 129 def assign_language
130 - self.language = Submission.find_language_in_source(self.source,
131 - self.source_filename)
130 + if self.language == nil
131 + self.language = Submission.find_language_in_source(self.source,
132 + self.source_filename)
133 + end
132 134 end
133 135
134 136 # validation codes
135 137 def must_specify_language
136 138 return if self.source==nil
137 139
138 140 # for output_only tasks
139 141 return if self.problem!=nil and self.problem.output_only
140 142
141 - if self.language==nil
142 - errors.add('source',"Cannot detect language. Did you submit a correct source file?") unless self.language!=nil
143 + if self.language == nil
144 + errors.add('source',"Cannot detect language. Did you submit a correct source file?")
143 145 end
144 146 end
145 147
146 148 def must_have_valid_problem
147 149 return if self.source==nil
148 150 if self.problem==nil
149 151 errors.add('problem',"must be specified.")
150 152 else
151 153 #admin always have right
152 154 return if self.user.admin?
153 155
154 156 #check if user has the right to submit the problem
155 157 errors.add('problem',"must be valid.") if (!self.user.available_problems.include?(self.problem)) and (self.new_record?)
156 158 end
157 159 end
158 160
159 161 # callbacks
160 162 def assign_latest_number_if_new_recond
161 163 return if !self.new_record?
162 164 latest = Submission.find_last_by_user_and_problem(self.user_id, self.problem_id)
163 165 self.number = (latest==nil) ? 1 : latest.number + 1;
164 166 end
165 167
166 168 end
@@ -1,91 +1,91
1 1 require 'digest/sha1'
2 2 require 'net/pop'
3 3 require 'net/https'
4 4 require 'net/http'
5 5 require 'json'
6 6
7 7 class User < ActiveRecord::Base
8 8
9 9 has_and_belongs_to_many :roles
10 10
11 11 #has_and_belongs_to_many :groups
12 12 has_many :groups_users, class_name: GroupUser
13 13 has_many :groups, :through => :groups_users
14 14
15 15 has_many :test_requests, -> {order(submitted_at: DESC)}
16 16
17 17 has_many :messages, -> { order(created_at: DESC) },
18 18 :class_name => "Message",
19 19 :foreign_key => "sender_id"
20 20
21 21 has_many :replied_messages, -> { order(created_at: DESC) },
22 22 :class_name => "Message",
23 23 :foreign_key => "receiver_id"
24 24
25 25 has_one :contest_stat, :class_name => "UserContestStat", :dependent => :destroy
26 26
27 27 belongs_to :site
28 28 belongs_to :country
29 29
30 30 has_and_belongs_to_many :contests, -> { order(:name); uniq}
31 31
32 32 scope :activated_users, -> {where activated: true}
33 33
34 34 validates_presence_of :login
35 35 validates_uniqueness_of :login
36 36 validates_format_of :login, :with => /\A[\_A-Za-z0-9]+\z/
37 37 validates_length_of :login, :within => 3..30
38 38
39 39 validates_presence_of :full_name
40 40 validates_length_of :full_name, :minimum => 1
41 41
42 42 validates_presence_of :password, :if => :password_required?
43 - validates_length_of :password, :within => 4..20, :if => :password_required?
43 + validates_length_of :password, :within => 4..50, :if => :password_required?
44 44 validates_confirmation_of :password, :if => :password_required?
45 45
46 46 validates_format_of :email,
47 47 :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
48 48 :if => :email_validation?
49 49 validate :uniqueness_of_email_from_activated_users,
50 50 :if => :email_validation?
51 51 validate :enough_time_interval_between_same_email_registrations,
52 52 :if => :email_validation?
53 53
54 54 # these are for ytopc
55 55 # disable for now
56 56 #validates_presence_of :province
57 57
58 58 attr_accessor :password
59 59
60 60 before_save :encrypt_new_password
61 61 before_save :assign_default_site
62 62 before_save :assign_default_contest
63 63
64 64 # this is for will_paginate
65 65 cattr_reader :per_page
66 66 @@per_page = 50
67 67
68 68 def self.authenticate(login, password)
69 69 user = find_by_login(login)
70 70 if user
71 71 return user if user.authenticated?(password)
72 72 end
73 73 end
74 74
75 75 def authenticated?(password)
76 76 if self.activated
77 77 hashed_password == User.encrypt(password,self.salt)
78 78 else
79 79 false
80 80 end
81 81 end
82 82
83 83 def admin?
84 84 self.roles.detect {|r| r.name == 'admin' }
85 85 end
86 86
87 87 def email_for_editing
88 88 if self.email==nil
89 89 "(unknown)"
90 90 elsif self.email==''
91 91 "(blank)"
@@ -14,97 +14,99
14 14 - # submission form
15 15 = form_tag({controller: :main, :action => 'submit'}, :multipart => true, class: 'form') do
16 16
17 17 = hidden_field_tag 'editor_text', @source
18 18 = hidden_field_tag 'submission[problem_id]', @problem.id
19 19 .form-group
20 20 = label_tag "Task:"
21 21 = text_field_tag 'asdf', "#{@problem.long_name}", class: 'form-control', disabled: true
22 22 .form-group
23 23 = label_tag "Description:"
24 24 = link_to_description_if_any "[download] <span class='glyphicon glyphicon-file'></span>".html_safe, @problem
25 25
26 26 .form-group
27 27 = label_tag 'Language:'
28 28 = select_tag 'language_id', options_from_collection_for_select(Language.all, 'id', 'pretty_name', @lang_id || Language.find_by_pretty_name("Python").id || Language.first.id), class: 'form-control select', style: "width: 100px"
29 29 .form-group
30 30 .input-group
31 31 %span.input-group-btn
32 32 %span.btn.btn-default.btn-file
33 33 Browse
34 34 = file_field_tag 'load_file'
35 35 = text_field_tag '' , nil, {readonly: true, class: 'form-control'}
36 36 .form-group
37 37 = submit_tag 'Submit', class: 'btn btn-success', id: 'live_submit',
38 38 data: {confirm: "Submitting this source code for task #{@problem.long_name}?"}
39 39 - # latest submission status
40 40 .panel{class: (@submission && @submission.graded_at) ? "panel-info" : "panel-warning"}
41 41 .panel-heading
42 42 Latest Submission Status
43 43 = link_to "Refresh",get_latest_submission_status_submissions_path(@submission.user,@problem), class: "btn btn-default btn-sm", remote: true if @submission
44 44 .panel-body
45 45 %div#latest_status
46 46 - if @submission
47 47 = render :partial => 'submission_short',
48 48 :locals => {submission: @submission, problem_name: @problem.name, problem_id: @problem.id }
49 49 .row
50 50 .col-md-12
51 51 %h2 Console
52 52 %textarea#console{style: 'height: 100%; width: 100%;background-color:#000;color:#fff;font-family: consolas, monaco, "Droid Sans Mono";',rows: 20}
53 53
54 54 .modal.fade#compiler{tabindex: -1,role: 'dialog'}
55 55 .modal-dialog.modal-lg{role:'document'}
56 56 .modal-content
57 57 .modal-header
58 58 %button.close{type: 'button', data: {dismissed: :modal}, aria: {label: 'close'}}
59 59 %span{aria: {hidden: 'true'}, data: {dismiss: 'modal'}} &times;
60 60 %h4 Compiler message
61 61 .modal-body
62 - %pre#compiler_msg= @submission.compiler_message
62 + %pre#compiler_msg
63 + - if @submission
64 + = @submission.compiler_message
63 65 .modal-footer
64 66 %button.btn.btn-default{type: 'button', data: {dismiss: 'modal'}} Close
65 67
66 68 :javascript
67 69 $(document).ready(function() {
68 70 e = ace.edit("editor")
69 71 e.setValue($("#text_sourcecode").val());
70 72 e.gotoLine(1);
71 73 $("#language_id").trigger('change');
72 74
73 75 $("#load_file").on('change',function(evt) {
74 76 var file = evt.target.files[0];
75 77 var reader = new FileReader();
76 78 reader.onload = function(theFile) {
77 79 var e = ace.edit("editor")
78 80 e.setValue(theFile.target.result);
79 81 e.gotoLine(1);
80 82 };
81 83 reader.readAsText(file)
82 84 });
83 85
84 86 //brython();
85 87 });
86 88
87 89
88 90
89 91
90 92
91 93 %script#__main__{type:'text/python3'}
92 94 :plain
93 95 import sys
94 96 import traceback
95 97
96 98 from browser import document as doc
97 99 from browser import window, alert, console
98 100
99 101 _credits = """ Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands
100 102 for supporting Python development. See www.python.org for more information."""
101 103
102 104 _copyright = """Copyright (c) 2012, Pierre Quentel pierre.quentel@gmail.com
103 105 All Rights Reserved.
104 106
105 107 Copyright (c) 2001-2013 Python Software Foundation.
106 108 All Rights Reserved.
107 109
108 110 Copyright (c) 2000 BeOpen.com.
109 111 All Rights Reserved.
110 112
@@ -1,30 +1,33
1 1 # If you want to manage graders through web interface, set the path to
2 2 # the grader directory below. This dir is where raw, ev, ev-exam,
3 3 # scripts reside. All grader scripts will be in
4 4 # #{GRADER_ROOT_DIR}/scripts.
5 5 GRADER_ROOT_DIR = ''
6 6
7 7 # These are where inputs and outputs of test requests are stored
8 8 TEST_REQUEST_INPUT_FILE_DIR = (Rails.root + 'data/test_request/input').to_s
9 9 TEST_REQUEST_OUTPUT_FILE_DIR = (Rails.root + 'data/test_request/output').to_s
10 10
11 11 # To use ANALYSIS MODE, provide the testcases/testruns breakdown,
12 12 # and the directory of the grading result (usually in judge's dir).
13 13 TASK_GRADING_INFO_FILENAME = Rails.root + 'config/tasks.yml'
14 14
15 15 # TODO: change this to where results are kept.
16 16 GRADING_RESULT_DIR = 'RESULT-DIR'
17 17
18 18 # Change this to allow importing testdata into database as test-pairs.
19 19 # This is mainly for Code Jom contest.
20 20 ALLOW_TEST_PAIR_IMPORT = false
21 21
22 22 # Uncomment so that the system validates user e-mails
23 23 # VALIDATE_USER_EMAILS = true
24 24
25 25 # Uncomment so that Apache X-Sendfile is used when delivering files
26 26 # (e.g., in /tasks/view).
27 27 # USE_APACHE_XSENDFILE = true
28 28
29 29 # Uncomment so that configuration is read only once when the server is loaded
30 30 # CONFIGURATION_CACHE_ENABLED = true
31 +
32 + # Uncomment to allow authentication and user import from programming.in.th
33 + # LoginController.add_authenticator(ProgrammingAuthenticator.new)
@@ -57,96 +57,104
57 57 {
58 58 :key => 'right.user_hall_of_fame',
59 59 :value_type => 'boolean',
60 60 :default_value => 'false',
61 61 :description => 'If true, any user can access hall of fame page.'
62 62 },
63 63
64 64 {
65 65 :key => 'right.multiple_ip_login',
66 66 :value_type => 'boolean',
67 67 :default_value => 'true',
68 68 :description => 'When change from true to false, a user can login from the first IP they logged into afterward.'
69 69 },
70 70
71 71 {
72 72 :key => 'right.user_view_submission',
73 73 :value_type => 'boolean',
74 74 :default_value => 'false',
75 75 :description => 'If true, any user can view submissions of every one.'
76 76 },
77 77
78 78 {
79 79 :key => 'right.bypass_agreement',
80 80 :value_type => 'boolean',
81 81 :default_value => 'true',
82 82 :description => 'When false, a user must accept usage agreement before login'
83 83 },
84 84
85 85 {
86 86 :key => 'right.heartbeat_response',
87 87 :value_type => 'string',
88 88 :default_value => 'OK',
89 89 :description => 'Heart beat response text'
90 90 },
91 91
92 92 {
93 93 :key => 'right.heartbeat_response_full',
94 94 :value_type => 'string',
95 95 :default_value => 'OK',
96 96 :description => 'Heart beat response text when user got full score (set this value to the empty string to disable this feature)'
97 97 },
98 98
99 99 {
100 100 :key => 'right.view_testcase',
101 101 :value_type => 'boolean',
102 102 :default_value => 'false',
103 103 :description => 'When true, any user can view/download test data'
104 104 },
105 +
106 + {
107 + :key => 'system.online_registration',
108 + :value_type => 'boolean',
109 + :default_value => 'false',
110 + :description => 'This option enables online registration.'
111 + },
112 +
105 113 # If Configuration['system.online_registration'] is true, the
106 114 # system allows online registration, and will use these
107 115 # information for sending confirmation emails.
108 116 {
109 117 :key => 'system.online_registration.smtp',
110 118 :value_type => 'string',
111 119 :default_value => 'smtp.somehost.com'
112 120 },
113 121
114 122 {
115 123 :key => 'system.online_registration.from',
116 124 :value_type => 'string',
117 125 :default_value => 'your.email@address'
118 126 },
119 127
120 128 {
121 129 :key => 'system.admin_email',
122 130 :value_type => 'string',
123 131 :default_value => 'admin@admin.email'
124 132 },
125 133
126 134 {
127 135 :key => 'system.user_setting_enabled',
128 136 :value_type => 'boolean',
129 137 :default_value => 'true',
130 138 :description => 'If this option is true, users can change their settings'
131 139 },
132 140
133 141 {
134 142 :key => 'system.user_setting_enabled',
135 143 :value_type => 'boolean',
136 144 :default_value => 'true',
137 145 :description => 'If this option is true, users can change their settings'
138 146 },
139 147
140 148 # If Configuration['contest.test_request.early_timeout'] is true
141 149 # the user will not be able to use test request at 30 minutes
142 150 # before the contest ends.
143 151 {
144 152 :key => 'contest.test_request.early_timeout',
145 153 :value_type => 'boolean',
146 154 :default_value => 'false'
147 155 },
148 156
149 157 {
150 158 :key => 'system.multicontests',
151 159 :value_type => 'boolean',
152 160 :default_value => 'false'
You need to be logged in to leave comments. Login now