Description:
wip
Commit status:
[Not Reviewed]
References:
Comments:
0 Commit comments 0 Inline Comments
Unresolved TODOs:
There are no unresolved TODOs
Add another comment

r880:31e27f513ce9 - - 9 files changed: 173 inserted, 142 deleted

@@ -1,287 +1,290
1 1 class ProblemsController < ApplicationController
2 2
3 3 include ActiveStorage::SetCurrent
4 4
5 5 before_action :admin_authorization, except: [:stat]
6 6 before_action :set_problem, only: [:show, :edit, :update, :destroy, :get_statement, :toggle, :toggle_test, :toggle_view_testcase, :stat]
7 7 before_action only: [:stat] do
8 8 authorization_by_roles(['admin','ta'])
9 9 end
10 10
11 11
12 12 def index
13 13 @problems = Problem.order(date_added: :desc)
14 14 end
15 15
16 16
17 17 def show
18 18 end
19 19
20 20 #get statement download link
21 21 def get_statement
22 22 unless @current_user.can_view_problem? @problem
23 23 redirect_to list_main_path, error: 'You are not authorized to access this file'
24 24 return
25 25 end
26 26
27 27 if params[:ext]=='pdf'
28 28 content_type = 'application/pdf'
29 29 else
30 30 content_type = 'application/octet-stream'
31 31 end
32 32
33 33 filename = @problem.statement.filename.to_s
34 34 data =@problem.statement.download
35 35
36 36 send_data data, stream: false, disposition: 'inline', filename: filename, type: content_type
37 37 end
38 38
39 39 def new
40 40 @problem = Problem.new
41 41 end
42 42
43 43 def create
44 44 @problem = Problem.new(problem_params)
45 45 if @problem.save
46 46 redirect_to action: :index, notice: 'Problem was successfully created.'
47 47 else
48 48 render :action => 'new'
49 49 end
50 50 end
51 51
52 52 def quick_create
53 53 @problem = Problem.new(problem_params)
54 54 @problem.full_name = @problem.name if @problem.full_name == ''
55 55 @problem.full_score = 100
56 56 @problem.available = false
57 57 @problem.test_allowed = true
58 58 @problem.output_only = false
59 59 @problem.date_added = Time.new
60 60 if @problem.save
61 61 flash[:notice] = 'Problem was successfully created.'
62 62 redirect_to action: :index
63 63 else
64 64 flash[:notice] = 'Error saving problem'
65 65 redirect_to action: :index
66 66 end
67 67 end
68 68
69 69 def edit
70 70 @description = @problem.description
71 71 end
72 72
73 73 def update
74 74 if problem_params[:statement] && problem_params[:statement].content_type != 'application/pdf'
75 75 flash[:error] = 'Error: Uploaded file is not PDF'
76 76 render :action => 'edit'
77 77 return
78 78 end
79 79 if @problem.update(problem_params)
80 80 flash[:notice] = 'Problem was successfully updated. '
81 81 flash[:notice] += 'A new statement PDF is uploaded' if problem_params[:statement]
82 82 @problem.save
83 83 redirect_to edit_problem_path(@problem)
84 84 else
85 85 render :action => 'edit'
86 86 end
87 87 end
88 88
89 89 def destroy
90 90 @problem.destroy
91 91 redirect_to action: :index
92 92 end
93 93
94 94 def toggle
95 95 @problem.update(available: !(@problem.available) )
96 96 respond_to do |format|
97 97 format.js { }
98 98 end
99 99 end
100 100
101 101 def toggle_test
102 102 @problem.update(test_allowed: !(@problem.test_allowed?) )
103 103 respond_to do |format|
104 104 format.js { }
105 105 end
106 106 end
107 107
108 108 def toggle_view_testcase
109 109 @problem.update(view_testcase: !(@problem.view_testcase?) )
110 110 respond_to do |format|
111 111 format.js { }
112 112 end
113 113 end
114 114
115 115 def turn_all_off
116 116 Problem.available.all.each do |problem|
117 117 problem.available = false
118 118 problem.save
119 119 end
120 120 redirect_to action: :index
121 121 end
122 122
123 123 def turn_all_on
124 124 Problem.where.not(available: true).each do |problem|
125 125 problem.available = true
126 126 problem.save
127 127 end
128 128 redirect_to action: :index
129 129 end
130 130
131 131 def stat
132 132 unless @problem.available or session[:admin]
133 133 redirect_to :controller => 'main', :action => 'list'
134 134 return
135 135 end
136 136 @submissions = Submission.includes(:user).includes(:language).where(problem_id: params[:id]).order(:user_id,:id)
137 137
138 138 #stat summary
139 139 range =65
140 140 @histogram = { data: Array.new(range,0), summary: {} }
141 141 user = Hash.new(0)
142 142 @submissions.find_each do |sub|
143 143 d = (DateTime.now.in_time_zone - sub.submitted_at) / 24 / 60 / 60
144 144 @histogram[:data][d.to_i] += 1 if d < range
145 145 user[sub.user_id] = [user[sub.user_id], ((sub.try(:points) || 0) >= @problem.full_score) ? 1 : 0].max
146 146 end
147 147 @histogram[:summary][:max] = [@histogram[:data].max,1].max
148 148
149 149 @summary = { attempt: user.count, solve: 0 }
150 150 user.each_value { |v| @summary[:solve] += 1 if v == 1 }
151 +
152 + #for new graph
153 + @chart_dataset = @problem.get_jschart_history.to_json.html_safe
151 154 end
152 155
153 156 def manage
154 157 @problems = Problem.order(date_added: :desc)
155 158 end
156 159
157 160 def do_manage
158 161 if params.has_key? 'change_date_added' and params[:date_added].strip.empty? == false
159 162 change_date_added
160 163 elsif params.has_key? 'add_to_contest'
161 164 add_to_contest
162 165 elsif params.has_key? 'enable_problem'
163 166 set_available(true)
164 167 elsif params.has_key? 'disable_problem'
165 168 set_available(false)
166 169 elsif params.has_key? 'add_group'
167 170 group = Group.find(params[:group_id])
168 171 ok = []
169 172 failed = []
170 173 get_problems_from_params.each do |p|
171 174 begin
172 175 group.problems << p
173 176 ok << p.full_name
174 177 rescue => e
175 178 failed << p.full_name
176 179 end
177 180 end
178 181 flash[:success] = "The following problems are added to the group #{group.name}: " + ok.join(', ') if ok.count > 0
179 182 flash[:alert] = "The following problems are already in the group #{group.name}: " + failed.join(', ') if failed.count > 0
180 183 elsif params.has_key? 'add_tags'
181 184 get_problems_from_params.each do |p|
182 185 p.tag_ids += params[:tag_ids]
183 186 end
184 187 end
185 188
186 189 redirect_to :action => 'manage'
187 190 end
188 191
189 192 def import
190 193 @allow_test_pair_import = allow_test_pair_import?
191 194 end
192 195
193 196 def do_import
194 197 old_problem = Problem.find_by_name(params[:name])
195 198 if !allow_test_pair_import? and params.has_key? :import_to_db
196 199 params.delete :import_to_db
197 200 end
198 201 @problem, import_log = Problem.create_from_import_form_params(params,
199 202 old_problem)
200 203
201 204 if !@problem.errors.empty?
202 205 render :action => 'import' and return
203 206 end
204 207
205 208 if old_problem!=nil
206 209 flash[:notice] = "The test data has been replaced for problem #{@problem.name}"
207 210 end
208 211 @log = import_log
209 212 end
210 213
211 214 def remove_contest
212 215 problem = Problem.find(params[:id])
213 216 contest = Contest.find(params[:contest_id])
214 217 if problem!=nil and contest!=nil
215 218 problem.contests.delete(contest)
216 219 end
217 220 redirect_to :action => 'manage'
218 221 end
219 222
220 223 ##################################
221 224 protected
222 225
223 226 def allow_test_pair_import?
224 227 if defined? ALLOW_TEST_PAIR_IMPORT
225 228 return ALLOW_TEST_PAIR_IMPORT
226 229 else
227 230 return false
228 231 end
229 232 end
230 233
231 234 def change_date_added
232 235 problems = get_problems_from_params
233 236 date = Date.parse(params[:date_added])
234 237 problems.each do |p|
235 238 p.date_added = date
236 239 p.save
237 240 end
238 241 end
239 242
240 243 def add_to_contest
241 244 problems = get_problems_from_params
242 245 contest = Contest.find(params[:contest][:id])
243 246 if contest!=nil and contest.enabled
244 247 problems.each do |p|
245 248 p.contests << contest
246 249 end
247 250 end
248 251 end
249 252
250 253 def set_available(avail)
251 254 problems = get_problems_from_params
252 255 problems.each do |p|
253 256 p.available = avail
254 257 p.save
255 258 end
256 259 end
257 260
258 261 def get_problems_from_params
259 262 problems = []
260 263 params.keys.each do |k|
261 264 if k.index('prob-')==0
262 265 name, id, order = k.split('-')
263 266 problems << Problem.find(id)
264 267 end
265 268 end
266 269 problems
267 270 end
268 271
269 272 def get_problems_stat
270 273 end
271 274
272 275 private
273 276
274 277 def set_problem
275 278 @problem = Problem.find(params[:id])
276 279 end
277 280
278 281 def problem_params
279 282 params.require(:problem).permit(:name, :full_name, :full_score, :change_date_added, :date_added, :available,
280 283 :test_allowed, :output_only, :url, :description, :statement, :description, tag_ids:[])
281 284 end
282 285
283 286 def description_params
284 287 params.require(:description).permit(:body, :markdowned)
285 288 end
286 289
287 290 end
@@ -59,394 +59,395
59 59 end
60 60
61 61 #set up range from param
62 62 @since_id = params.fetch(:from_id, 0).to_i
63 63 @until_id = params.fetch(:to_id, 0).to_i
64 64 @since_id = nil if @since_id == 0
65 65 @until_id = nil if @until_id == 0
66 66
67 67 #calculate the routine
68 68 @scorearray = calculate_max_score(@problems, @users, @since_id, @until_id)
69 69
70 70 #rencer accordingly
71 71 if params[:button] == 'download' then
72 72 csv = gen_csv_from_scorearray(@scorearray,@problems)
73 73 send_data csv, filename: 'max_score.csv'
74 74 else
75 75 #render template: 'user_admin/user_stat'
76 76 render 'max_score'
77 77 end
78 78
79 79 end
80 80
81 81 def score
82 82 if params[:commit] == 'download csv'
83 83 @problems = Problem.all
84 84 else
85 85 @problems = Problem.available_problems
86 86 end
87 87 @users = User.includes(:contests, :contest_stat).where(enabled: true)
88 88 @scorearray = Array.new
89 89 @users.each do |u|
90 90 ustat = Array.new
91 91 ustat[0] = u
92 92 @problems.each do |p|
93 93 sub = Submission.find_last_by_user_and_problem(u.id,p.id)
94 94 if (sub!=nil) and (sub.points!=nil) and p and p.full_score
95 95 ustat << [(sub.points.to_f*100/p.full_score).round, (sub.points>=p.full_score)]
96 96 else
97 97 ustat << [0,false]
98 98 end
99 99 end
100 100 @scorearray << ustat
101 101 end
102 102 if params[:commit] == 'download csv' then
103 103 csv = gen_csv_from_scorearray(@scorearray,@problems)
104 104 send_data csv, filename: 'last_score.csv'
105 105 else
106 106 render template: 'user_admin/user_stat'
107 107 end
108 108
109 109 end
110 110
111 111 def login
112 112 end
113 113
114 114 def login_summary_query
115 115 @users = Array.new
116 116
117 117 date_and_time = '%Y-%m-%d %H:%M'
118 118 begin
119 119 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
120 120 @since_time = Time.zone.local(md[1].to_i,md[2].to_i,md[3].to_i,md[4].to_i,md[5].to_i)
121 121 rescue
122 122 @since_time = Time.zone.now
123 123 end
124 124 puts @since_time
125 125 begin
126 126 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
127 127 @until_time = Time.zone.local(md[1].to_i,md[2].to_i,md[3].to_i,md[4].to_i,md[5].to_i)
128 128 rescue
129 129 @until_time = DateTime.new(3000,1,1)
130 130 end
131 131
132 132 record = User
133 133 .left_outer_joins(:logins).group('users.id')
134 134 .where("logins.created_at >= ? AND logins.created_at <= ?",@since_time, @until_time)
135 135 case params[:users]
136 136 when 'enabled'
137 137 record = record.where(enabled: true)
138 138 when 'group'
139 139 record = record.joins(:groups).where(groups: {id: params[:groups]}) if params[:groups]
140 140 end
141 141
142 142 record = record.pluck("users.id,users.login,users.full_name,count(logins.created_at),min(logins.created_at),max(logins.created_at)")
143 143 record.each do |user|
144 144 x = Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
145 145 user[0],@since_time,@until_time)
146 146 .pluck(:ip_address).uniq
147 147 puts user[4]
148 148 puts user[5]
149 149 @users << { id: user[0],
150 150 login: user[1],
151 151 full_name: user[2],
152 152 count: user[3],
153 153 min: user[4].in_time_zone,
154 154 max: user[5].in_time_zone,
155 155 ip: x
156 156 }
157 157 end
158 158 end
159 159
160 160 def login_detail_query
161 161 @logins = Array.new
162 162
163 163 date_and_time = '%Y-%m-%d %H:%M'
164 164 begin
165 165 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
166 166 @since_time = Time.zone.local(md[1].to_i,md[2].to_i,md[3].to_i,md[4].to_i,md[5].to_i)
167 167 rescue
168 168 @since_time = Time.zone.now
169 169 end
170 170 begin
171 171 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
172 172 @until_time = Time.zone.local(md[1].to_i,md[2].to_i,md[3].to_i,md[4].to_i,md[5].to_i)
173 173 rescue
174 174 @until_time = DateTime.new(3000,1,1)
175 175 end
176 176
177 177 @logins = Login.includes(:user).where("logins.created_at >= ? AND logins.created_at <= ?",@since_time, @until_time)
178 178 case params[:users]
179 179 when 'enabled'
180 180 @logins = @logins.where(users: {enabled: true})
181 181 when 'group'
182 182 @logins = @logins.joins(user: :groups).where(user: {groups: {id: params[:groups]}}) if params[:groups]
183 183 end
184 184 end
185 185
186 186 def submission
187 187 end
188 188
189 189 def submission_query
190 190 @submissions = Submission
191 191 .includes(:problem).includes(:user).includes(:language)
192 192
193 193 case params[:users]
194 194 when 'enabled'
195 195 @submissions = @submissions.where(users: {enabled: true})
196 196 when 'group'
197 197 @submissions = @submissions.joins(user: :groups).where(user: {groups: {id: params[:groups]}}) if params[:groups]
198 198 end
199 199
200 200 case params[:problems]
201 201 when 'enabled'
202 202 @submissions = @submissions.where(problems: {available: true})
203 203 when 'selected'
204 204 @submissions = @submissions.where(problem_id: params[:problem_id])
205 205 end
206 206
207 207 #set default
208 208 params[:since_datetime] = Date.today.to_s if params[:since_datetime].blank?
209 209
210 210 @submissions, @recordsTotal, @recordsFiltered = process_query_record( @submissions,
211 211 global_search: ['user.login','user.full_name','problem.name','problem.full_name','points'],
212 212 date_filter: 'submitted_at',
213 213 date_param_since: 'since_datetime',
214 214 date_param_until: 'until_datetime',
215 215 hard_limit: 100_000
216 216 )
217 217 end
218 218
219 219 def login
220 220 end
221 221
222 222 def problem_hof
223 223 # gen problem list
224 224 @user = User.find(session[:user_id])
225 225 @problems = @user.available_problems
226 226
227 227 # get selected problems or the default
228 228 if params[:id]
229 229 begin
230 230 @problem = Problem.available.find(params[:id])
231 231 rescue
232 232 redirect_to action: :problem_hof
233 233 flash[:notice] = 'Error: submissions for that problem are not viewable.'
234 234 return
235 235 end
236 236 end
237 237
238 238 return unless @problem
239 239
240 240 #model submisssion
241 241 @model_subs = Submission.where(problem: @problem,tag: Submission.tags[:model])
242 242
243 243
244 244 #calculate best submission
245 245 @by_lang = {} #aggregrate by language
246 246
247 247 range =65
248 248 #@histogram = { data: Array.new(range,0), summary: {} }
249 249 @summary = {count: 0, solve: 0, attempt: 0}
250 250 user = Hash.new(0)
251 - Submission.where(problem_id: @problem.id).find_each do |sub|
251 + Submission.where(problem_id: @problem.id).includes(:language).each do |sub|
252 252 #histogram
253 253 d = (DateTime.now.in_time_zone - sub.submitted_at) / 24 / 60 / 60
254 254 #@histogram[:data][d.to_i] += 1 if d < range
255 255
256 256 next unless sub.points
257 257 @summary[:count] += 1
258 258 user[sub.user_id] = [user[sub.user_id], (sub.points >= @problem.full_score) ? 1 : 0].max
259 259
260 - lang = Language.find_by_id(sub.language_id)
260 + #lang = Language.find_by_id(sub.language_id)
261 + lang = sub.language
261 262 next unless lang
262 263 next unless sub.points >= @problem.full_score
263 264
264 265 #initialize
265 266 unless @by_lang.has_key?(lang.pretty_name)
266 267 @by_lang[lang.pretty_name] = {
267 268 runtime: { avail: false, value: 2**30-1 },
268 269 memory: { avail: false, value: 2**30-1 },
269 270 length: { avail: false, value: 2**30-1 },
270 271 first: { avail: false, value: DateTime.new(3000,1,1) }
271 272 }
272 273 end
273 274
274 275 if sub.max_runtime and sub.max_runtime < @by_lang[lang.pretty_name][:runtime][:value]
275 276 @by_lang[lang.pretty_name][:runtime] = { avail: true, user_id: sub.user_id, value: sub.max_runtime, sub_id: sub.id }
276 277 end
277 278
278 279 if sub.peak_memory and sub.peak_memory < @by_lang[lang.pretty_name][:memory][:value]
279 280 @by_lang[lang.pretty_name][:memory] = { avail: true, user_id: sub.user_id, value: sub.peak_memory, sub_id: sub.id }
280 281 end
281 282
282 283 if sub.submitted_at and sub.submitted_at < @by_lang[lang.pretty_name][:first][:value] and sub.user and
283 284 !sub.user.admin?
284 285 @by_lang[lang.pretty_name][:first] = { avail: true, user_id: sub.user_id, value: sub.submitted_at, sub_id: sub.id }
285 286 end
286 287
287 288 if @by_lang[lang.pretty_name][:length][:value] > sub.effective_code_length
288 289 @by_lang[lang.pretty_name][:length] = { avail: true, user_id: sub.user_id, value: sub.effective_code_length, sub_id: sub.id }
289 290 end
290 291 end
291 292
292 293 #process user_id
293 294 @by_lang.each do |lang,prop|
294 295 prop.each do |k,v|
295 296 v[:user] = User.exists?(v[:user_id]) ? User.find(v[:user_id]).full_name : "(NULL)"
296 297 end
297 298 end
298 299
299 300 #sum into best
300 301 if @by_lang and @by_lang.first
301 302 @best = @by_lang.first[1].clone
302 303 @by_lang.each do |lang,prop|
303 304 if @best[:runtime][:value] >= prop[:runtime][:value]
304 305 @best[:runtime] = prop[:runtime]
305 306 @best[:runtime][:lang] = lang
306 307 end
307 308 if @best[:memory][:value] >= prop[:memory][:value]
308 309 @best[:memory] = prop[:memory]
309 310 @best[:memory][:lang] = lang
310 311 end
311 312 if @best[:length][:value] >= prop[:length][:value]
312 313 @best[:length] = prop[:length]
313 314 @best[:length][:lang] = lang
314 315 end
315 316 if @best[:first][:value] >= prop[:first][:value]
316 317 @best[:first] = prop[:first]
317 318 @best[:first][:lang] = lang
318 319 end
319 320 end
320 321 end
321 322
322 323 #@histogram[:summary][:max] = [@histogram[:data].max,1].max
323 324 @summary[:attempt] = user.count
324 325 user.each_value { |v| @summary[:solve] += 1 if v == 1 }
325 326
326 327
327 328 #for new graph
328 329 @chart_dataset = @problem.get_jschart_history.to_json.html_safe
329 330 end
330 331
331 332 def stuck #report struggling user,problem
332 333 # init
333 334 user,problem = nil
334 335 solve = true
335 336 tries = 0
336 337 @struggle = Array.new
337 338 record = {}
338 339 Submission.includes(:problem,:user).order(:problem_id,:user_id).find_each do |sub|
339 340 next unless sub.problem and sub.user
340 341 if user != sub.user_id or problem != sub.problem_id
341 342 @struggle << { user: record[:user], problem: record[:problem], tries: tries } unless solve
342 343 record = {user: sub.user, problem: sub.problem}
343 344 user,problem = sub.user_id, sub.problem_id
344 345 solve = false
345 346 tries = 0
346 347 end
347 348 if sub.points >= sub.problem.full_score
348 349 solve = true
349 350 else
350 351 tries += 1
351 352 end
352 353 end
353 354 @struggle.sort!{|a,b| b[:tries] <=> a[:tries] }
354 355 @struggle = @struggle[0..50]
355 356 end
356 357
357 358
358 359 def multiple_login
359 360 #user with multiple IP
360 361 raw = Submission.joins(:user).joins(:problem).where("problems.available != 0").group("login,ip_address").order(:login)
361 362 last,count = 0,0
362 363 first = 0
363 364 @users = []
364 365 raw.each do |r|
365 366 if last != r.user.login
366 367 count = 1
367 368 last = r.user.login
368 369 first = r
369 370 else
370 371 @users << first if count == 1
371 372 @users << r
372 373 count += 1
373 374 end
374 375 end
375 376
376 377 #IP with multiple user
377 378 raw = Submission.joins(:user).joins(:problem).where("problems.available != 0").group("login,ip_address").order(:ip_address)
378 379 last,count = 0,0
379 380 first = 0
380 381 @ip = []
381 382 raw.each do |r|
382 383 if last != r.ip_address
383 384 count = 1
384 385 last = r.ip_address
385 386 first = r
386 387 else
387 388 @ip << first if count == 1
388 389 @ip << r
389 390 count += 1
390 391 end
391 392 end
392 393 end
393 394
394 395 def cheat_report
395 396 date_and_time = '%Y-%m-%d %H:%M'
396 397 begin
397 398 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
398 399 @since_time = Time.zone.local(md[1].to_i,md[2].to_i,md[3].to_i,md[4].to_i,md[5].to_i)
399 400 rescue
400 401 @since_time = Time.zone.now.ago( 90.minutes)
401 402 end
402 403 begin
403 404 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
404 405 @until_time = Time.zone.local(md[1].to_i,md[2].to_i,md[3].to_i,md[4].to_i,md[5].to_i)
405 406 rescue
406 407 @until_time = Time.zone.now
407 408 end
408 409
409 410 #multi login
410 411 @ml = Login.joins(:user).where("logins.created_at >= ? and logins.created_at <= ?",@since_time,@until_time).select('users.login,count(distinct ip_address) as count,users.full_name').group("users.id").having("count > 1")
411 412
412 413 st = <<-SQL
413 414 SELECT l2.*
414 415 FROM logins l2 INNER JOIN
415 416 (SELECT u.id,COUNT(DISTINCT ip_address) as count,u.login,u.full_name
416 417 FROM logins l
417 418 INNER JOIN users u ON l.user_id = u.id
418 419 WHERE l.created_at >= '#{@since_time.in_time_zone("UTC")}' and l.created_at <= '#{@until_time.in_time_zone("UTC")}'
419 420 GROUP BY u.id
420 421 HAVING count > 1
421 422 ) ml ON l2.user_id = ml.id
422 423 WHERE l2.created_at >= '#{@since_time.in_time_zone("UTC")}' and l2.created_at <= '#{@until_time.in_time_zone("UTC")}'
423 424 UNION
424 425 SELECT l2.*
425 426 FROM logins l2 INNER JOIN
426 427 (SELECT l.ip_address,COUNT(DISTINCT u.id) as count
427 428 FROM logins l
428 429 INNER JOIN users u ON l.user_id = u.id
429 430 WHERE l.created_at >= '#{@since_time.in_time_zone("UTC")}' and l.created_at <= '#{@until_time.in_time_zone("UTC")}'
430 431 GROUP BY l.ip_address
431 432 HAVING count > 1
432 433 ) ml on ml.ip_address = l2.ip_address
433 434 INNER JOIN users u ON l2.user_id = u.id
434 435 WHERE l2.created_at >= '#{@since_time.in_time_zone("UTC")}' and l2.created_at <= '#{@until_time.in_time_zone("UTC")}'
435 436 ORDER BY ip_address,created_at
436 437 SQL
437 438 @mld = Login.find_by_sql(st)
438 439
439 440 st = <<-SQL
440 441 SELECT s.id,s.user_id,s.ip_address,s.submitted_at,s.problem_id
441 442 FROM submissions s INNER JOIN
442 443 (SELECT u.id,COUNT(DISTINCT ip_address) as count,u.login,u.full_name
443 444 FROM logins l
444 445 INNER JOIN users u ON l.user_id = u.id
445 446 WHERE l.created_at >= ? and l.created_at <= ?
446 447 GROUP BY u.id
447 448 HAVING count > 1
448 449 ) ml ON s.user_id = ml.id
449 450 WHERE s.submitted_at >= ? and s.submitted_at <= ?
450 451 UNION
451 452 SELECT s.id,s.user_id,s.ip_address,s.submitted_at,s.problem_id
452 453 FROM submissions s INNER JOIN
@@ -1,237 +1,231
1 1 require 'net/smtp'
2 2
3 3 class UsersController < ApplicationController
4 4
5 5 include MailHelperMethods
6 6
7 7 before_action :check_valid_login, :except => [:new,
8 8 :register,
9 9 :confirm,
10 10 :forget,
11 11 :retrieve_password]
12 12
13 13 before_action :verify_online_registration, :only => [:new,
14 14 :register,
15 15 :forget,
16 16 :retrieve_password]
17 17
18 18 before_action :admin_authorization, only: [:stat, :toggle_activate, :toggle_enable]
19 19
20 20
21 21 #in_place_edit_for :user, :alias_for_editing
22 22 #in_place_edit_for :user, :email_for_editing
23 23
24 24 def index
25 25 if !GraderConfiguration['system.user_setting_enabled']
26 26 redirect_to :controller => 'main', :action => 'list'
27 27 else
28 28 @user = User.find(session[:user_id])
29 29 end
30 30 end
31 31
32 32 # edit logged in user profile
33 33 def profile
34 34 if !GraderConfiguration['system.user_setting_enabled']
35 35 redirect_to :controller => 'main', :action => 'list'
36 36 else
37 37 @user = current_user;
38 38 end
39 39 end
40 40
41 41 def chg_passwd
42 42 user = User.find(session[:user_id])
43 43 user.password = params[:password]
44 44 user.password_confirmation = params[:password_confirmation]
45 45 if user.save
46 46 flash[:notice] = 'password changed'
47 47 else
48 48 flash[:notice] = 'Error: password changing failed'
49 49 end
50 50 redirect_to :action => 'profile'
51 51 end
52 52
53 53 def chg_default_language
54 54 user = User.find(session[:user_id])
55 55 user.default_language = params[:default_language]
56 56 if user.save
57 57 flash[:notice] = 'default language changed'
58 58 else
59 59 flash[:notice] = 'Error: default language changing failed'
60 60 end
61 61 redirect_to :action => 'profile'
62 62 end
63 63
64 64 def new
65 65 @user = User.new
66 66 render :action => 'new', :layout => 'empty'
67 67 end
68 68
69 69 def register
70 70 if(params[:cancel])
71 71 redirect_to :controller => 'main', :action => 'login'
72 72 return
73 73 end
74 74 @user = User.new(user_params)
75 75 @user.password_confirmation = @user.password = User.random_password
76 76 @user.activated = false
77 77 if (@user.valid?) and (@user.save)
78 78 if send_confirmation_email(@user)
79 79 render :action => 'new_splash', :layout => 'empty'
80 80 else
81 81 @admin_email = GraderConfiguration['system.admin_email']
82 82 render :action => 'email_error', :layout => 'empty'
83 83 end
84 84 else
85 85 @user.errors.add(:base,"Email cannot be blank") if @user.email==''
86 86 render :action => 'new', :layout => 'empty'
87 87 end
88 88 end
89 89
90 90 def confirm
91 91 login = params[:login]
92 92 key = params[:activation]
93 93 @user = User.find_by_login(login)
94 94 if (@user) and (@user.verify_activation_key(key))
95 95 if @user.valid? # check uniquenss of email
96 96 @user.activated = true
97 97 @user.save
98 98 @result = :successful
99 99 else
100 100 @result = :email_used
101 101 end
102 102 else
103 103 @result = :failed
104 104 end
105 105 render :action => 'confirm', :layout => 'empty'
106 106 end
107 107
108 108 def forget
109 109 render :action => 'forget', :layout => 'empty'
110 110 end
111 111
112 112 def retrieve_password
113 113 email = params[:email]
114 114 user = User.find_by_email(email)
115 115 if user
116 116 last_updated_time = user.updated_at || user.created_at || (Time.now.gmtime - 1.hour)
117 117 if last_updated_time > Time.now.gmtime - 5.minutes
118 118 flash[:notice] = 'The account has recently created or new password has recently been requested. Please wait for 5 minutes'
119 119 else
120 120 user.password = user.password_confirmation = User.random_password
121 121 user.save
122 122 send_new_password_email(user)
123 123 flash[:notice] = 'New password has been mailed to you.'
124 124 end
125 125 else
126 126 flash[:notice] = I18n.t 'registration.password_retrieval.no_email'
127 127 end
128 128 redirect_to :action => 'forget'
129 129 end
130 130
131 131 def stat
132 132 @user = User.find(params[:id])
133 - @submission = Submission.joins(:problem).where(user_id: params[:id])
133 + @submission = Submission.joins(:problem).includes(:problem).includes(:language).where(user_id: params[:id])
134 134 @submission = @submission.where('problems.available = true') unless current_user.admin?
135 135
136 - range = 120
137 - @histogram = { data: Array.new(range,0), summary: {} }
138 136 @summary = {count: 0, solve: 0, attempt: 0}
139 137 problem = Hash.new(0)
140 138
141 139 @submission.find_each do |sub|
142 - #histogram
143 - d = (DateTime.now.in_time_zone - sub.submitted_at) / 24 / 60 / 60
144 - @histogram[:data][d.to_i] += 1 if d < range
145 -
146 140 @summary[:count] += 1
147 141 next unless sub.problem
148 142 problem[sub.problem] = [problem[sub.problem], ( (sub.try(:points) || 0) >= sub.problem.full_score) ? 1 : 0].max
149 143 end
150 144
151 - @histogram[:summary][:max] = [@histogram[:data].max,1].max
152 145 @summary[:attempt] = problem.count
153 146 problem.each_value { |v| @summary[:solve] += 1 if v == 1 }
147 + @chart_dataset = @user.get_jschart_user_sub_history.to_json.html_safe
154 148 end
155 149
156 150 def toggle_activate
157 151 @user = User.find(params[:id])
158 152 @user.update_attributes( activated: !@user.activated? )
159 153 respond_to do |format|
160 154 format.js { render partial: 'toggle_button',
161 155 locals: {button_id: "#toggle_activate_user_#{@user.id}",button_on: @user.activated? } }
162 156 end
163 157 end
164 158
165 159 def toggle_enable
166 160 @user = User.find(params[:id])
167 161 @user.update_attributes( enabled: !@user.enabled? )
168 162 respond_to do |format|
169 163 format.js { render partial: 'toggle_button',
170 164 locals: {button_id: "#toggle_enable_user_#{@user.id}",button_on: @user.enabled? } }
171 165 end
172 166 end
173 167
174 168 protected
175 169
176 170 def verify_online_registration
177 171 if !GraderConfiguration['system.online_registration']
178 172 redirect_to :controller => 'main', :action => 'login'
179 173 end
180 174 end
181 175
182 176 def send_confirmation_email(user)
183 177 contest_name = GraderConfiguration['contest.name']
184 178 activation_url = url_for(:action => 'confirm',
185 179 :login => user.login,
186 180 :activation => user.activation_key)
187 181 home_url = url_for(:controller => 'main', :action => 'index')
188 182 mail_subject = "[#{contest_name}] Confirmation"
189 183 mail_body = t('registration.email_body', {
190 184 :full_name => user.full_name,
191 185 :contest_name => contest_name,
192 186 :login => user.login,
193 187 :password => user.password,
194 188 :activation_url => activation_url,
195 189 :admin_email => GraderConfiguration['system.admin_email']
196 190 })
197 191
198 192 logger.info mail_body
199 193
200 194 send_mail(user.email, mail_subject, mail_body)
201 195 end
202 196
203 197 def send_new_password_email(user)
204 198 contest_name = GraderConfiguration['contest.name']
205 199 mail_subject = "[#{contest_name}] Password recovery"
206 200 mail_body = t('registration.password_retrieval.email_body', {
207 201 :full_name => user.full_name,
208 202 :contest_name => contest_name,
209 203 :login => user.login,
210 204 :password => user.password,
211 205 :admin_email => GraderConfiguration['system.admin_email']
212 206 })
213 207
214 208 logger.info mail_body
215 209
216 210 send_mail(user.email, mail_subject, mail_body)
217 211 end
218 212
219 213 # allow viewing of regular user profile only when options allow so
220 214 # only admins can view admins profile
221 215 def profile_authorization
222 216 #if view admins' profile, allow only admin
223 217 return false unless(params[:id])
224 218 user = User.find(params[:id])
225 219 return false unless user
226 220 return admin_authorization if user.admin?
227 221 return true if GraderConfiguration["right.user_view_submission"]
228 222
229 223 #finally, we allow only admin
230 224 admin_authorization
231 225 end
232 226
233 227 private
234 228 def user_params
235 229 params.require(:user).permit(:login, :full_name, :email)
236 230 end
237 231 end
@@ -125,337 +125,352
125 125 def activation_key
126 126 if self.hashed_password==nil
127 127 encrypt_new_password
128 128 end
129 129 Digest::SHA1.hexdigest(self.hashed_password)[0..7]
130 130 end
131 131
132 132 def verify_activation_key(key)
133 133 key == activation_key
134 134 end
135 135
136 136 def self.random_password(length=5)
137 137 chars = 'abcdefghjkmnopqrstuvwxyz'
138 138 password = ''
139 139 length.times { password << chars[rand(chars.length - 1)] }
140 140 password
141 141 end
142 142
143 143
144 144 # Contest information
145 145
146 146 def self.find_users_with_no_contest()
147 147 users = User.all
148 148 return users.find_all { |u| u.contests.length == 0 }
149 149 end
150 150
151 151
152 152 def contest_time_left
153 153 if GraderConfiguration.contest_mode?
154 154 return nil if site==nil
155 155 return site.time_left
156 156 elsif GraderConfiguration.indv_contest_mode?
157 157 time_limit = GraderConfiguration.contest_time_limit
158 158 if time_limit == nil
159 159 return nil
160 160 end
161 161 if contest_stat==nil or contest_stat.started_at==nil
162 162 return (Time.now.gmtime + time_limit) - Time.now.gmtime
163 163 else
164 164 finish_time = contest_stat.started_at + time_limit
165 165 current_time = Time.now.gmtime
166 166 if current_time > finish_time
167 167 return 0
168 168 else
169 169 return finish_time - current_time
170 170 end
171 171 end
172 172 else
173 173 return nil
174 174 end
175 175 end
176 176
177 177 def contest_finished?
178 178 if GraderConfiguration.contest_mode?
179 179 return false if site==nil
180 180 return site.finished?
181 181 elsif GraderConfiguration.indv_contest_mode?
182 182 return false if self.contest_stat==nil
183 183 return contest_time_left == 0
184 184 else
185 185 return false
186 186 end
187 187 end
188 188
189 189 def contest_started?
190 190 if GraderConfiguration.indv_contest_mode?
191 191 stat = self.contest_stat
192 192 return ((stat != nil) and (stat.started_at != nil))
193 193 elsif GraderConfiguration.contest_mode?
194 194 return true if site==nil
195 195 return site.started
196 196 else
197 197 return true
198 198 end
199 199 end
200 200
201 201 def update_start_time
202 202 stat = self.contest_stat
203 203 if stat.nil? or stat.started_at.nil?
204 204 stat ||= UserContestStat.new(:user => self)
205 205 stat.started_at = Time.now.gmtime
206 206 stat.save
207 207 end
208 208 end
209 209
210 210 def problem_in_user_contests?(problem)
211 211 problem_contests = problem.contests.all
212 212
213 213 if problem_contests.length == 0 # this is public contest
214 214 return true
215 215 end
216 216
217 217 contests.each do |contest|
218 218 if problem_contests.find {|c| c.id == contest.id }
219 219 return true
220 220 end
221 221 end
222 222 return false
223 223 end
224 224
225 225 def available_problems_group_by_contests
226 226 contest_problems = []
227 227 pin = {}
228 228 contests.enabled.each do |contest|
229 229 available_problems = contest.problems.available
230 230 contest_problems << {
231 231 :contest => contest,
232 232 :problems => available_problems
233 233 }
234 234 available_problems.each {|p| pin[p.id] = true}
235 235 end
236 236 other_avaiable_problems = Problem.available.find_all {|p| pin[p.id]==nil and p.contests.length==0}
237 237 contest_problems << {
238 238 :contest => nil,
239 239 :problems => other_avaiable_problems
240 240 }
241 241 return contest_problems
242 242 end
243 243
244 244 def solve_all_available_problems?
245 245 available_problems.each do |p|
246 246 u = self
247 247 sub = Submission.find_last_by_user_and_problem(u.id,p.id)
248 248 return false if !p or !sub or sub.points < p.full_score
249 249 end
250 250 return true
251 251 end
252 252
253 253 #get a list of available problem
254 254 def available_problems
255 255 # first, we check if this is normal mode
256 256 if not GraderConfiguration.multicontests?
257 257
258 258 #if this is a normal mode
259 259 #we show problem based on problem_group, if the config said so
260 260 if GraderConfiguration.use_problem_group?
261 261 return available_problems_in_group
262 262 else
263 263 return Problem.available_problems
264 264 end
265 265 else
266 266 #this is multi contest mode
267 267 contest_problems = []
268 268 pin = {}
269 269 contests.enabled.each do |contest|
270 270 contest.problems.available.each do |problem|
271 271 if not pin.has_key? problem.id
272 272 contest_problems << problem
273 273 end
274 274 pin[problem.id] = true
275 275 end
276 276 end
277 277 other_avaiable_problems = Problem.available.find_all {|p| pin[p.id]==nil and p.contests.length==0}
278 278 return contest_problems + other_avaiable_problems
279 279 end
280 280 end
281 281
282 282 # new feature, get list of available problem in all enabled group that the user belongs to
283 283 def available_problems_in_group
284 284 problem = []
285 285 self.groups.where(enabled: true).each do |group|
286 286 group.problems.where(available: true).each { |p| problem << p }
287 287 end
288 288 problem.uniq!
289 289 if problem
290 290 problem.sort! do |a,b|
291 291 case
292 292 when a.date_added < b.date_added
293 293 1
294 294 when a.date_added > b.date_added
295 295 -1
296 296 else
297 297 a.name <=> b.name
298 298 end
299 299 end
300 300 return problem
301 301 else
302 302 return []
303 303 end
304 304 end
305 305
306 306 #check if the user has the right to view that problem
307 307 #this also consider group based problem policy
308 308 def can_view_problem?(problem)
309 309 return true if admin?
310 310 return available_problems.include? problem
311 311 end
312 312
313 313 def self.clear_last_login
314 314 User.update_all(:last_ip => nil)
315 315 end
316 316
317 + def get_jschart_user_sub_history
318 + start = 4.month.ago.beginning_of_day
319 + start_date = start.to_date
320 + count = Submission.where(user: self).where('submitted_at >= ?', start).group('DATE(submitted_at)').count
321 + i = 0
322 + label = []
323 + value = []
324 + while (start_date + i < Time.zone.now.to_date)
325 + label << (start_date+i).strftime("%d-%b")
326 + value << (count[start_date+i] || 0)
327 + i+=1
328 + end
329 + return {labels: label,datasets: [label:'sub',data: value, backgroundColor: 'rgba(54, 162, 235, 0.2)', borderColor: 'rgb(75, 192, 192)']}
330 + end
331 +
317 332 #create multiple user, one per lines of input
318 333 def self.create_from_list(lines)
319 334 error_logins = []
320 335 first_error = nil
321 336 created_users = []
322 337
323 338 lines.split("\n").each do |line|
324 339 #split with large limit, this will cause consecutive ',' to be result in a blank
325 340 items = line.chomp.split(',',1000)
326 341 if items.length>=2
327 342 login = items[0]
328 343 full_name = items[1]
329 344 remark =''
330 345 user_alias = ''
331 346
332 347 added_random_password = false
333 348 added_password = false
334 349
335 350 #given password?
336 351 if items.length >= 3
337 352 if items[2].chomp(" ").length > 0
338 353 password = items[2].chomp(" ")
339 354 added_password = true
340 355 end
341 356 else
342 357 password = random_password
343 358 added_random_password=true;
344 359 end
345 360
346 361 #given alias?
347 362 if items.length>= 4 and items[3].chomp(" ").length > 0;
348 363 user_alias = items[3].chomp(" ")
349 364 else
350 365 user_alias = login
351 366 end
352 367
353 368 #given remark?
354 369 has_remark = false
355 370 if items.length>=5
356 371 remark = items[4].strip;
357 372 has_remark = true
358 373 end
359 374
360 375 user = User.find_by_login(login)
361 376 if (user)
362 377 user.full_name = full_name
363 378 user.remark = remark if has_remark
364 379 user.password = password if added_password || added_random_password
365 380 else
366 381 #create a random password if none are given
367 382 password = random_password unless password
368 383 user = User.new({:login => login,
369 384 :full_name => full_name,
370 385 :password => password,
371 386 :password_confirmation => password,
372 387 :alias => user_alias,
373 388 :remark => remark})
374 389 end
375 390 user.activated = true
376 391
377 392 if user.save
378 393 created_users << user
379 394 else
380 395 error_logins << "'#{login}'"
381 396 first_error = user.errors.full_messages.to_sentence unless first_error
382 397 end
383 398 end
384 399 end
385 400
386 401 return {error_logins: error_logins, first_error: first_error, created_users: created_users}
387 402
388 403 end
389 404
390 405 def self.find_non_admin_with_prefix(prefix='')
391 406 users = User.all
392 407 return users.find_all { |u| !(u.admin?) and u.login.index(prefix)==0 }
393 408 end
394 409
395 410 protected
396 411 def encrypt_new_password
397 412 return if password.blank?
398 413 self.salt = (10+rand(90)).to_s
399 414 self.hashed_password = User.encrypt(self.password,self.salt)
400 415 end
401 416
402 417 def assign_default_site
403 418 # have to catch error when migrating (because self.site is not available).
404 419 begin
405 420 if self.site==nil
406 421 self.site = Site.find_by_name('default')
407 422 if self.site==nil
408 423 self.site = Site.find(1) # when 'default has be renamed'
409 424 end
410 425 end
411 426 rescue
412 427 end
413 428 end
414 429
415 430 def assign_default_contest
416 431 # have to catch error when migrating (because self.site is not available).
417 432 begin
418 433 if self.contests.length == 0
419 434 default_contest = Contest.find_by_name(GraderConfiguration['contest.default_contest_name'])
420 435 if default_contest
421 436 self.contests = [default_contest]
422 437 end
423 438 end
424 439 rescue
425 440 end
426 441 end
427 442
428 443 def password_required?
429 444 self.hashed_password.blank? || !self.password.blank?
430 445 end
431 446
432 447 def self.encrypt(string,salt)
433 448 Digest::SHA1.hexdigest(salt + string)
434 449 end
435 450
436 451 def uniqueness_of_email_from_activated_users
437 452 user = User.activated_users.find_by_email(self.email)
438 453 if user and (user.login != self.login)
439 454 self.errors.add(:base,"Email has already been taken")
440 455 end
441 456 end
442 457
443 458 def enough_time_interval_between_same_email_registrations
444 459 return if !self.new_record?
445 460 return if self.activated
446 461 open_user = User.find_by_email(self.email,
447 462 :order => 'created_at DESC')
448 463 if open_user and open_user.created_at and
449 464 (open_user.created_at > Time.now.gmtime - 5.minutes)
450 465 self.errors.add(:base,"There are already unactivated registrations with this e-mail address (please wait for 5 minutes)")
451 466 end
452 467 end
453 468
454 469 def email_validation?
455 470 begin
456 471 return VALIDATE_USER_EMAILS
457 472 rescue
458 473 return false
459 474 end
460 475 end
461 476 end
@@ -1,33 +1,33
1 1 - if submission.nil?
2 2 = "-"
3 3 - else
4 4 - if local_assigns[:show_id]
5 5 .row
6 - .col-3.text-secondary
7 - Sub ID:
8 - %strong.col-9= submission.id
6 + .col-3.fw-bold
7 + Sub ID
8 + .col-9.text-secondary-x= submission.id
9 9 - unless submission.graded_at
10 10 .row
11 - .col-3.text-secondary= t 'main.submitted_at'
12 - %strong.col-9= format_full_time_ago(submission.submitted_at.localtime)
11 + .col-3.fw-bold= t 'main.submitted_at'
12 + .col-9.text-secondary-x= format_full_time_ago(submission.submitted_at.localtime)
13 13 - else
14 14 .row
15 - .col-3.text-secondary= t 'main.graded_at'
16 - %strong.col-9= format_full_time_ago(submission.graded_at.localtime)
15 + .col-3.fw-bold= t 'main.graded_at'
16 + .col-9.text-secondary-x= format_full_time_ago(submission.graded_at.localtime)
17 17 - if GraderConfiguration['ui.show_score']
18 18 .row
19 - .col-3.text-secondary=t 'main.score'
20 - %strong.col-9
19 + .col-3.fw-bold=t 'main.score'
20 + .col-9.text-secondary-x
21 21 = (submission.points*100/submission.problem.full_score).to_i
22 22 %tt.grader-comment
23 23 = " [#{submission.grader_comment}]"
24 24 - if local_assigns[:show_button]
25 25 - if submission.graded_at
26 26 - if GraderConfiguration.show_grading_result
27 27 = link_to '[detailed result]', :action => 'result', :id => submission.id
28 28 = link_to "#{t 'main.cmp_msg'}", compiler_msg_submission_path(submission), {popup: true,remote: true,class: 'btn btn-sm btn-info'}
29 29 = link_to "#{t 'main.src_link'}",download_submission_path(submission), class: 'btn btn-sm btn-info'
30 30 = link_to "#{t 'main.submissions_link'}", problem_submissions_path(submission.problem.id), class: 'btn btn-sm btn-info'
31 31 - if GraderConfiguration.show_testcase
32 32 = link_to "testcases", show_problem_testcases_path(submission.problem.id), class: 'btn btn-sm btn-info'
33 33
@@ -1,119 +1,129
1 1 - content_for :head do
2 2 = stylesheet_link_tag 'problems'
3 3 = javascript_include_tag 'local_jquery'
4 4
5 - :javascript
6 - $(document).ready( function() {
7 - function shiftclick(start,stop,value) {
8 - $('tr input').each( function(id,input) {
9 - var $input=$(input);
10 - var iid=parseInt($input.attr('id').split('-')[2]);
11 - if(iid>=start&&iid<=stop){
12 - $input.prop('checked',value)
13 - }
14 - });
15 - }
16 -
17 - $('tr input').click( function(e) {
18 - if (e.shiftKey) {
19 - stop = parseInt($(this).attr('id').split('-')[2]);
20 - var orig_stop = stop
21 - if (typeof start !== 'undefined') {
22 - if (start > stop) {
23 - var tmp = start;
24 - start = stop;
25 - stop = tmp;
26 - }
27 - shiftclick(start,stop,$(this).is(':checked') )
28 - }
29 - start = orig_stop
30 - } else {
31 - start = parseInt($(this).attr('id').split('-')[2]);
32 - }
33 - });
34 - });
35 5
36 6
37 7 %h1 Manage problems
38 8
39 9 %p= link_to '[Back to problem list]', problems_path
40 10
41 - = form_tag :action=>'do_manage' do
42 - .panel.panel-primary
43 - .panel-heading
11 + = form_with url: do_manage_problems_path do |f|
12 + .card.border-primary.mb-2
13 + .card-header.text-bg-primary.border-primary
44 14 Action
45 - .panel-body
15 + .card-body
46 16 .submit-box
47 17 What do you want to do to the selected problem?
48 18 %br/
49 19 (You can shift-click to select a range of problems)
20 + .row
21 + .col-md-auto
22 + = f.check_box :change_date_added, class: 'form-check-input'
23 + .col-md-auto
24 + .form-check-label
25 + Change "Date added" to
26 + .col-md-auto
27 +
50 28 %ul.form-inline
51 29 %li
52 30 Change "Date added" to
53 31 .input-group.date
54 32 = text_field_tag :date_added, class: 'form-control'
55 33 %span.input-group-addon
56 34 %span.glyphicon.glyphicon-calendar
57 35 -# = select_date Date.current, :prefix => 'date_added'
58 36 &nbsp;&nbsp;&nbsp;
59 37 = submit_tag 'Change', :name => 'change_date_added', class: 'btn btn-primary btn-sm'
60 38 %li
61 39 Set "Available" to
62 40 = submit_tag 'True', :name => 'enable_problem', class: 'btn btn-primary btn-sm'
63 41 = submit_tag 'False', :name => 'disable_problem', class: 'btn btn-primary btn-sm'
64 42
65 43 - if GraderConfiguration.multicontests?
66 44 %li
67 45 Add selected problems to contest
68 46 = select("contest","id",Contest.all.collect {|c| [c.title, c.id]})
69 47 = submit_tag 'Add', :name => 'add_to_contest', class: 'btn btn-primary btn-sm'
70 48 %li
71 49 Add selected problems to user group
72 50 = select_tag "group_id", options_from_collection_for_select( Group.all, 'id','name',params[:group_name]), id: 'group_name',class: 'select2'
73 51 = submit_tag 'Add', name: 'add_group', class: 'btn btn-primary'
74 52 %li
75 53 Add the following tags to the selected problems
76 54 = select_tag "tag_ids", options_from_collection_for_select( Tag.all, 'id','name'), id: 'tags_name',class: 'select2', multiple: true, data: {placeholder: 'Select tags by clicking', width: "200px"}
77 55 = submit_tag 'Add', name: 'add_tags', class: 'btn btn-primary'
78 56
79 57 %table.table.table-hover.datatable
80 58 %thead
81 59 %tr{style: "text-align: left;"}
82 60 %th= check_box_tag 'select_all'
83 61 %th Name
84 62 %th Full name
85 63 %th Tags
86 64 %th Available
87 65 %th Date added
88 66 - if GraderConfiguration.multicontests?
89 67 %th Contests
90 68
91 69 %tbody
92 70 - num = 0
93 71 - for problem in @problems
94 72 - num += 1
95 73 %tr{:id => "row-prob-#{problem.id}", :name=> "prob-#{problem.id}"}
96 74 %td= check_box_tag "prob-#{problem.id}-#{num}"
97 75 %td= problem.name
98 76 %td= problem.full_name
99 77 %td
100 78 - problem.tags.each do |t|
101 79 %span.label.label-default= t.name
102 80 %td= problem.available
103 81 %td= problem.date_added
104 82 - if GraderConfiguration.multicontests?
105 83 %td
106 84 - problem.contests.each do |contest|
107 85 = "(#{contest.name} [#{link_to 'x', :action => 'remove_contest', :id => problem.id, :contest_id => contest.id }])"
108 86
109 87 :javascript
88 +
89 + $(document).on('import-map-loaded', function() {
90 + function shiftclick(start,stop,value) {
91 + $('tr input').each( function(id,input) {
92 + var $input=$(input);
93 + var iid=parseInt($input.attr('id').split('-')[2]);
94 + if(iid>=start&&iid<=stop){
95 + $input.prop('checked',value)
96 + }
97 + });
98 + }
99 +
100 + $('tr input').click( function(e) {
101 + if (e.shiftKey) {
102 + stop = parseInt($(this).attr('id').split('-')[2]);
103 + var orig_stop = stop
104 + if (typeof start !== 'undefined') {
105 + if (start > stop) {
106 + var tmp = start;
107 + start = stop;
108 + stop = tmp;
109 + }
110 + shiftclick(start,stop,$(this).is(':checked') )
111 + }
112 + start = orig_stop
113 + } else {
114 + start = parseInt($(this).attr('id').split('-')[2]);
115 + }
116 + });
117 +
110 118 $('.input-group.date').datetimepicker({
111 119 format: 'DD/MMM/YYYY',
112 120 showTodayButton: true,
113 121 locale: 'en',
114 122 widgetPositioning: {horizontal: 'auto', vertical: 'bottom'},
115 123
116 124 });
125 +
117 126 $('.datatable').DataTable({
118 127 paging: false
119 128 });
129 + });
@@ -1,63 +1,90
1 1 :css
2 2 .fix-width {
3 3 font-family: "Consolas, Monaco, Droid Sans Mono,Mono, Monospace,Courier"
4 4 }
5 5
6 6 %h1 Problem stat: #{@problem.name}
7 - %h2 Overview
8 7
8 + .row.mb-3
9 + .col-md-8
10 + .card
11 + .card-body
12 + %h2.card-title Submission History
13 + %canvas#chart{height: '50px'}
14 +
15 + .col-md-4
16 + .card
17 + .card-body
18 + %h2.card-title General Info
9 19 .row
10 - .col-md-2
11 - %strong Name:
12 - .col-md-10
20 + .col-sm-6
21 + Name
22 + .col-sm-6
13 23 = @problem.full_name #in_place_editor_field :problem, :full_name, {}, :rows=>1
14 24 = link_to_description_if_any "[#{t 'main.problem_desc'}] <span class='glyphicon glyphicon-file'></span>".html_safe, @problem
15 25 .row
16 - .col-md-2.strong
17 - %strong Submissions:
18 - .col-md-10
26 + .col-sm-6
27 + Subs
28 + .col-sm-6
19 29 = @submissions.count
20 30 .row
21 - .col-md-2.strong
22 - %strong Solved/Attemped User
23 - .col-md-10
31 + .col-sm-6
32 + Solved/Attempted User
33 + .col-sm-6
24 34 #{@summary[:solve]}/#{@summary[:attempt]} (#{(@summary[:solve]*100.0/@summary[:attempt]).round(1)}%)
25 35
26 -
27 - %h2 Submissions Count
28 - = render partial: 'application/bar_graph', locals: { histogram: @histogram }
29 -
30 36 %h2 Submissions
31 37 - if @submissions and @submissions.count > 0
32 38 %table#main_table.table.table-condensed.table-striped
33 39 %thead
34 40 %tr
35 41 %th ID
36 42 %th Login
37 43 %th Name
38 44 %th Submitted_at
39 45 %th language
40 46 %th Points
41 47 %th comment
42 48 %th IP
43 49 %tbody
44 50 - row_odd,curr = true,''
45 51 - @submissions.each do |sub|
46 52 - next unless sub.user
47 53 - row_odd,curr = !row_odd, sub.user if curr != sub.user
48 54 %tr
49 55 %td= link_to sub.id, submission_path(sub)
50 56 %td= link_to sub.user.login, stat_user_path(sub.user)
51 57 %td= sub.user.full_name
52 58 %td{data: {order: sub.submitted_at}}= time_ago_in_words(sub.submitted_at) + " ago"
53 59 %td= sub.language.name
54 60 %td= sub.points
55 61 %td.fix-width= sub.grader_comment
56 62 %td= sub.ip_address
57 63 - else
58 64 No submission
59 65
60 66 :javascript
67 + $(document).on('import-map-loaded',(e) => {
68 + //init datatable
61 69 $("#main_table").DataTable({
62 70 paging: false
63 71 });
72 +
73 + //history graph
74 + data = #{@chart_dataset}
75 + config = {
76 + type: 'bar',
77 + data: data,
78 + options: {
79 + plugins: {
80 + legend: {
81 + display: false
82 + },
83 + },
84 + }
85 + }
86 + Chart.defaults.font.size = 15
87 + //Chart.defaults.font.family = 'Sarabun Light'
88 + chart = new Chart($('#chart'),config)
89 + });
90 +
@@ -1,70 +1,95
1 - - content_for :header do
2 - = javascript_include_tag 'local_jquery'
3 -
4 - :javascript
5 - $(function () {
6 - $('#submission_table').tablesorter({widgets: ['zebra']});
7 - });
8 1
9 2 :css
10 3 .fix-width {
11 4 font-family: Droid Sans Mono,Consolas, monospace, mono, Courier New, Courier;
12 5 }
13 6
14 - %h1= @user.full_name
7 + %h1 User stats
8 + %h5.text-secondary= @user.login
9 +
10 + .row.my-3
11 + .col-md-8
12 + .card
13 + .card-body
14 + %h2.card-title Sub Info
15 + %canvas#chart{height: '50px'}
15 16
16 - <b>Login:</b> #{@user.login} <br/>
17 - <b>Full name:</b> #{@user.full_name} <br />
17 + .col-md-4
18 + .card
19 + .card-body
20 + %h2.card-title General Info
21 + .row
22 + .col-sm-6.fw-bold
23 + Login
24 + .col-sm-6
25 + = @user.login
26 + .row
27 + .col-sm-6.fw-bold
28 + Full name
29 + .col-sm-6
30 + = @user.full_name
31 + .row
32 + .col-sm-6.fw-bold
33 + Subs
34 + .col-sm-6
35 + = @summary[:count]
36 + .row
37 + .col-sm-6.fw-bold
38 + Solved/Attempted Problem
39 + .col-sm-6
40 + #{@summary[:solve]}/#{@summary[:attempt]} (#{(@summary[:solve]*100.0/@summary[:attempt]).round(1)}%)
18 41
19 42
20 - %h2 Problem Stat
21 - %table.info
22 - %thead
23 - %tr.info-head
24 - %th Stat
25 - %th Value
26 - %tbody
27 - %tr{class: cycle('info-even','info-odd')}
28 - %td.info_param Submissions
29 - %td= @summary[:count]
30 - %tr{class: cycle('info-even','info-odd')}
31 - %td.info_param Solved/Attempted Problem
32 - %td #{@summary[:solve]}/#{@summary[:attempt]} (#{(@summary[:solve]*100.0/@summary[:attempt]).round(1)}%)
33 -
34 - %h2 Submission History
35 -
36 - =render partial: 'application/bar_graph', locals: {histogram: @histogram, param: {bar_width: 7}}
37 -
38 -
39 - %table#submission_table.table.table-striped
43 + %table#main_table.table.table-striped
40 44 %thead
41 45 %tr
42 46 %th ID
43 47 %th Problem code
44 48 %th Problem full name
45 49 %th Language
46 50 %th Submitted at
47 51 %th Result
48 52 %th Score
49 53 - if session[:admin]
50 54 %th IP
51 55 %tbody
52 56 - @submission.each do |s|
53 57 - next unless s.problem
54 58 %tr
55 59 %td= link_to s.id, submission_path(s)
56 60 %td= link_to s.problem.name, stat_problem_path(s.problem)
57 61 %td= s.problem.full_name
58 62 %td= s.language.pretty_name
59 63 %td #{s.submitted_at.strftime('%Y-%m-%d %H:%M')} (#{time_ago_in_words(s.submitted_at)} ago)
60 64 %td.fix-width= s.grader_comment
61 65 %td= ( s.try(:points) ? (s.points*100/s.problem.full_score) : '' )
62 66 - if session[:admin]
63 67 %td= s.ip_address
64 68
65 69
66 70
67 71 :javascript
68 - $("#submission_table").DataTable({
72 + $(document).on('import-map-loaded',(e) => {
73 + //init datatable
74 + $("#main_table").DataTable({
69 75 paging: false
70 76 });
77 +
78 + //history graph
79 + data = #{@chart_dataset}
80 + config = {
81 + type: 'bar',
82 + data: data,
83 + options: {
84 + plugins: {
85 + legend: {
86 + display: false
87 + },
88 + },
89 + }
90 + }
91 + Chart.defaults.font.size = 15
92 + //Chart.defaults.font.family = 'Sarabun Light'
93 + chart = new Chart($('#chart'),config)
94 + });
95 +
deleted file
You need to be logged in to leave comments. Login now