Description:
make report max_score remember user options
Commit status:
[Not Reviewed]
References:
Comments:
0 Commit comments 0 Inline Comments
Unresolved TODOs:
There are no unresolved TODOs
Add another comment

r647:bb242b3ef68d - - 3 files changed: 8 inserted, 6 deleted

@@ -1,517 +1,519
1 1 require 'csv'
2 2
3 3 class ReportController < ApplicationController
4 4
5 5 before_filter :authenticate
6 6
7 7 before_filter :admin_authorization, only: [:login_stat,:submission_stat, :stuck, :cheat_report, :cheat_scruntinize, :show_max_score]
8 8
9 9 before_filter(only: [:problem_hof]) { |c|
10 10 return false unless authenticate
11 11
12 12 admin_authorization unless GraderConfiguration["right.user_view_submission"]
13 13 }
14 14
15 15 def max_score
16 16 end
17 17
18 18 def current_score
19 19 @problems = Problem.available_problems
20 20 @users = User.includes(:contests).includes(:contest_stat).where(enabled: true)
21 21 @scorearray = calculate_max_score(@problems, @users,0,0,true)
22 22
23 23 #rencer accordingly
24 24 if params[:button] == 'download' then
25 25 csv = gen_csv_from_scorearray(@scorearray,@problems)
26 26 send_data csv, filename: 'max_score.csv'
27 27 else
28 28 #render template: 'user_admin/user_stat'
29 29 render 'current_score'
30 30 end
31 31 end
32 32
33 33 def show_max_score
34 34 #process parameters
35 35 #problems
36 36 @problems = []
37 + if params[:problem_id]
37 38 params[:problem_id].each do |id|
38 39 next unless id.strip != ""
39 40 pid = Problem.find_by_id(id.to_i)
40 41 @problems << pid if pid
41 42 end
43 + end
42 44
43 45 #users
44 46 @users = if params[:user] == "all" then
45 47 User.includes(:contests).includes(:contest_stat)
46 48 else
47 49 User.includes(:contests).includes(:contest_stat).where(enabled: true)
48 50 end
49 51
50 52 #set up range from param
51 - since_id = params.fetch(:from_id, 0).to_i
52 - until_id = params.fetch(:to_id, 0).to_i
53 + @since_id = params.fetch(:from_id, 0).to_i
54 + @until_id = params.fetch(:to_id, 0).to_i
53 55
54 56 #calculate the routine
55 - @scorearray = calculate_max_score(@problems, @users,since_id,until_id)
57 + @scorearray = calculate_max_score(@problems, @users, @since_id, @until_id)
56 58
57 59 #rencer accordingly
58 60 if params[:button] == 'download' then
59 61 csv = gen_csv_from_scorearray(@scorearray,@problems)
60 62 send_data csv, filename: 'max_score.csv'
61 63 else
62 64 #render template: 'user_admin/user_stat'
63 65 render 'max_score'
64 66 end
65 67
66 68 end
67 69
68 70 def score
69 71 if params[:commit] == 'download csv'
70 72 @problems = Problem.all
71 73 else
72 74 @problems = Problem.available_problems
73 75 end
74 76 @users = User.includes(:contests, :contest_stat).where(enabled: true)
75 77 @scorearray = Array.new
76 78 @users.each do |u|
77 79 ustat = Array.new
78 80 ustat[0] = u
79 81 @problems.each do |p|
80 82 sub = Submission.find_last_by_user_and_problem(u.id,p.id)
81 83 if (sub!=nil) and (sub.points!=nil) and p and p.full_score
82 84 ustat << [(sub.points.to_f*100/p.full_score).round, (sub.points>=p.full_score)]
83 85 else
84 86 ustat << [0,false]
85 87 end
86 88 end
87 89 @scorearray << ustat
88 90 end
89 91 if params[:commit] == 'download csv' then
90 92 csv = gen_csv_from_scorearray(@scorearray,@problems)
91 93 send_data csv, filename: 'last_score.csv'
92 94 else
93 95 render template: 'user_admin/user_stat'
94 96 end
95 97
96 98 end
97 99
98 100 def login_stat
99 101 @logins = Array.new
100 102
101 103 date_and_time = '%Y-%m-%d %H:%M'
102 104 begin
103 105 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
104 106 @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)
105 107 rescue
106 108 @since_time = DateTime.new(1000,1,1)
107 109 end
108 110 begin
109 111 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
110 112 @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)
111 113 rescue
112 114 @until_time = DateTime.new(3000,1,1)
113 115 end
114 116
115 117 User.all.each do |user|
116 118 @logins << { id: user.id,
117 119 login: user.login,
118 120 full_name: user.full_name,
119 121 count: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
120 122 user.id,@since_time,@until_time)
121 123 .count(:id),
122 124 min: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
123 125 user.id,@since_time,@until_time)
124 126 .minimum(:created_at),
125 127 max: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
126 128 user.id,@since_time,@until_time)
127 129 .maximum(:created_at),
128 130 ip: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
129 131 user.id,@since_time,@until_time)
130 132 .select(:ip_address).uniq
131 133
132 134 }
133 135 end
134 136 end
135 137
136 138 def submission_stat
137 139
138 140 date_and_time = '%Y-%m-%d %H:%M'
139 141 begin
140 142 @since_time = DateTime.strptime(params[:since_datetime],date_and_time)
141 143 rescue
142 144 @since_time = DateTime.new(1000,1,1)
143 145 end
144 146 begin
145 147 @until_time = DateTime.strptime(params[:until_datetime],date_and_time)
146 148 rescue
147 149 @until_time = DateTime.new(3000,1,1)
148 150 end
149 151
150 152 @submissions = {}
151 153
152 154 User.find_each do |user|
153 155 @submissions[user.id] = { login: user.login, full_name: user.full_name, count: 0, sub: { } }
154 156 end
155 157
156 158 Submission.where("submitted_at >= ? AND submitted_at <= ?",@since_time,@until_time).find_each do |s|
157 159 if @submissions[s.user_id]
158 160 if not @submissions[s.user_id][:sub].has_key?(s.problem_id)
159 161 a = Problem.find_by_id(s.problem_id)
160 162 @submissions[s.user_id][:sub][s.problem_id] =
161 163 { prob_name: (a ? a.full_name : '(NULL)'),
162 164 sub_ids: [s.id] }
163 165 else
164 166 @submissions[s.user_id][:sub][s.problem_id][:sub_ids] << s.id
165 167 end
166 168 @submissions[s.user_id][:count] += 1
167 169 end
168 170 end
169 171 end
170 172
171 173 def problem_hof
172 174 # gen problem list
173 175 @user = User.find(session[:user_id])
174 176 @problems = @user.available_problems
175 177
176 178 # get selected problems or the default
177 179 if params[:id]
178 180 begin
179 181 @problem = Problem.available.find(params[:id])
180 182 rescue
181 183 redirect_to action: :problem_hof
182 184 flash[:notice] = 'Error: submissions for that problem are not viewable.'
183 185 return
184 186 end
185 187 end
186 188
187 189 return unless @problem
188 190
189 191 @by_lang = {} #aggregrate by language
190 192
191 193 range =65
192 194 @histogram = { data: Array.new(range,0), summary: {} }
193 195 @summary = {count: 0, solve: 0, attempt: 0}
194 196 user = Hash.new(0)
195 197 Submission.where(problem_id: @problem.id).find_each do |sub|
196 198 #histogram
197 199 d = (DateTime.now.in_time_zone - sub.submitted_at) / 24 / 60 / 60
198 200 @histogram[:data][d.to_i] += 1 if d < range
199 201
200 202 next unless sub.points
201 203 @summary[:count] += 1
202 204 user[sub.user_id] = [user[sub.user_id], (sub.points >= @problem.full_score) ? 1 : 0].max
203 205
204 206 lang = Language.find_by_id(sub.language_id)
205 207 next unless lang
206 208 next unless sub.points >= @problem.full_score
207 209
208 210 #initialize
209 211 unless @by_lang.has_key?(lang.pretty_name)
210 212 @by_lang[lang.pretty_name] = {
211 213 runtime: { avail: false, value: 2**30-1 },
212 214 memory: { avail: false, value: 2**30-1 },
213 215 length: { avail: false, value: 2**30-1 },
214 216 first: { avail: false, value: DateTime.new(3000,1,1) }
215 217 }
216 218 end
217 219
218 220 if sub.max_runtime and sub.max_runtime < @by_lang[lang.pretty_name][:runtime][:value]
219 221 @by_lang[lang.pretty_name][:runtime] = { avail: true, user_id: sub.user_id, value: sub.max_runtime, sub_id: sub.id }
220 222 end
221 223
222 224 if sub.peak_memory and sub.peak_memory < @by_lang[lang.pretty_name][:memory][:value]
223 225 @by_lang[lang.pretty_name][:memory] = { avail: true, user_id: sub.user_id, value: sub.peak_memory, sub_id: sub.id }
224 226 end
225 227
226 228 if sub.submitted_at and sub.submitted_at < @by_lang[lang.pretty_name][:first][:value] and
227 229 !sub.user.admin?
228 230 @by_lang[lang.pretty_name][:first] = { avail: true, user_id: sub.user_id, value: sub.submitted_at, sub_id: sub.id }
229 231 end
230 232
231 233 if @by_lang[lang.pretty_name][:length][:value] > sub.effective_code_length
232 234 @by_lang[lang.pretty_name][:length] = { avail: true, user_id: sub.user_id, value: sub.effective_code_length, sub_id: sub.id }
233 235 end
234 236 end
235 237
236 238 #process user_id
237 239 @by_lang.each do |lang,prop|
238 240 prop.each do |k,v|
239 241 v[:user] = User.exists?(v[:user_id]) ? User.find(v[:user_id]).full_name : "(NULL)"
240 242 end
241 243 end
242 244
243 245 #sum into best
244 246 if @by_lang and @by_lang.first
245 247 @best = @by_lang.first[1].clone
246 248 @by_lang.each do |lang,prop|
247 249 if @best[:runtime][:value] >= prop[:runtime][:value]
248 250 @best[:runtime] = prop[:runtime]
249 251 @best[:runtime][:lang] = lang
250 252 end
251 253 if @best[:memory][:value] >= prop[:memory][:value]
252 254 @best[:memory] = prop[:memory]
253 255 @best[:memory][:lang] = lang
254 256 end
255 257 if @best[:length][:value] >= prop[:length][:value]
256 258 @best[:length] = prop[:length]
257 259 @best[:length][:lang] = lang
258 260 end
259 261 if @best[:first][:value] >= prop[:first][:value]
260 262 @best[:first] = prop[:first]
261 263 @best[:first][:lang] = lang
262 264 end
263 265 end
264 266 end
265 267
266 268 @histogram[:summary][:max] = [@histogram[:data].max,1].max
267 269 @summary[:attempt] = user.count
268 270 user.each_value { |v| @summary[:solve] += 1 if v == 1 }
269 271 end
270 272
271 273 def stuck #report struggling user,problem
272 274 # init
273 275 user,problem = nil
274 276 solve = true
275 277 tries = 0
276 278 @struggle = Array.new
277 279 record = {}
278 280 Submission.includes(:problem,:user).order(:problem_id,:user_id).find_each do |sub|
279 281 next unless sub.problem and sub.user
280 282 if user != sub.user_id or problem != sub.problem_id
281 283 @struggle << { user: record[:user], problem: record[:problem], tries: tries } unless solve
282 284 record = {user: sub.user, problem: sub.problem}
283 285 user,problem = sub.user_id, sub.problem_id
284 286 solve = false
285 287 tries = 0
286 288 end
287 289 if sub.points >= sub.problem.full_score
288 290 solve = true
289 291 else
290 292 tries += 1
291 293 end
292 294 end
293 295 @struggle.sort!{|a,b| b[:tries] <=> a[:tries] }
294 296 @struggle = @struggle[0..50]
295 297 end
296 298
297 299
298 300 def multiple_login
299 301 #user with multiple IP
300 302 raw = Submission.joins(:user).joins(:problem).where("problems.available != 0").group("login,ip_address").order(:login)
301 303 last,count = 0,0
302 304 first = 0
303 305 @users = []
304 306 raw.each do |r|
305 307 if last != r.user.login
306 308 count = 1
307 309 last = r.user.login
308 310 first = r
309 311 else
310 312 @users << first if count == 1
311 313 @users << r
312 314 count += 1
313 315 end
314 316 end
315 317
316 318 #IP with multiple user
317 319 raw = Submission.joins(:user).joins(:problem).where("problems.available != 0").group("login,ip_address").order(:ip_address)
318 320 last,count = 0,0
319 321 first = 0
320 322 @ip = []
321 323 raw.each do |r|
322 324 if last != r.ip_address
323 325 count = 1
324 326 last = r.ip_address
325 327 first = r
326 328 else
327 329 @ip << first if count == 1
328 330 @ip << r
329 331 count += 1
330 332 end
331 333 end
332 334 end
333 335
334 336 def cheat_report
335 337 date_and_time = '%Y-%m-%d %H:%M'
336 338 begin
337 339 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
338 340 @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)
339 341 rescue
340 342 @since_time = Time.zone.now.ago( 90.minutes)
341 343 end
342 344 begin
343 345 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
344 346 @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)
345 347 rescue
346 348 @until_time = Time.zone.now
347 349 end
348 350
349 351 #multi login
350 352 @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")
351 353
352 354 st = <<-SQL
353 355 SELECT l2.*
354 356 FROM logins l2 INNER JOIN
355 357 (SELECT u.id,COUNT(DISTINCT ip_address) as count,u.login,u.full_name
356 358 FROM logins l
357 359 INNER JOIN users u ON l.user_id = u.id
358 360 WHERE l.created_at >= '#{@since_time.in_time_zone("UTC")}' and l.created_at <= '#{@until_time.in_time_zone("UTC")}'
359 361 GROUP BY u.id
360 362 HAVING count > 1
361 363 ) ml ON l2.user_id = ml.id
362 364 WHERE l2.created_at >= '#{@since_time.in_time_zone("UTC")}' and l2.created_at <= '#{@until_time.in_time_zone("UTC")}'
363 365 UNION
364 366 SELECT l2.*
365 367 FROM logins l2 INNER JOIN
366 368 (SELECT l.ip_address,COUNT(DISTINCT u.id) as count
367 369 FROM logins l
368 370 INNER JOIN users u ON l.user_id = u.id
369 371 WHERE l.created_at >= '#{@since_time.in_time_zone("UTC")}' and l.created_at <= '#{@until_time.in_time_zone("UTC")}'
370 372 GROUP BY l.ip_address
371 373 HAVING count > 1
372 374 ) ml on ml.ip_address = l2.ip_address
373 375 INNER JOIN users u ON l2.user_id = u.id
374 376 WHERE l2.created_at >= '#{@since_time.in_time_zone("UTC")}' and l2.created_at <= '#{@until_time.in_time_zone("UTC")}'
375 377 ORDER BY ip_address,created_at
376 378 SQL
377 379 @mld = Login.find_by_sql(st)
378 380
379 381 st = <<-SQL
380 382 SELECT s.id,s.user_id,s.ip_address,s.submitted_at,s.problem_id
381 383 FROM submissions s INNER JOIN
382 384 (SELECT u.id,COUNT(DISTINCT ip_address) as count,u.login,u.full_name
383 385 FROM logins l
384 386 INNER JOIN users u ON l.user_id = u.id
385 387 WHERE l.created_at >= ? and l.created_at <= ?
386 388 GROUP BY u.id
387 389 HAVING count > 1
388 390 ) ml ON s.user_id = ml.id
389 391 WHERE s.submitted_at >= ? and s.submitted_at <= ?
390 392 UNION
391 393 SELECT s.id,s.user_id,s.ip_address,s.submitted_at,s.problem_id
392 394 FROM submissions s INNER JOIN
393 395 (SELECT l.ip_address,COUNT(DISTINCT u.id) as count
394 396 FROM logins l
395 397 INNER JOIN users u ON l.user_id = u.id
396 398 WHERE l.created_at >= ? and l.created_at <= ?
397 399 GROUP BY l.ip_address
398 400 HAVING count > 1
399 401 ) ml on ml.ip_address = s.ip_address
400 402 WHERE s.submitted_at >= ? and s.submitted_at <= ?
401 403 ORDER BY ip_address,submitted_at
402 404 SQL
403 405 @subs = Submission.joins(:problem).find_by_sql([st,@since_time,@until_time,
404 406 @since_time,@until_time,
405 407 @since_time,@until_time,
406 408 @since_time,@until_time])
407 409
408 410 end
409 411
410 412 def cheat_scruntinize
411 413 #convert date & time
412 414 date_and_time = '%Y-%m-%d %H:%M'
413 415 begin
414 416 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
415 417 @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)
416 418 rescue
417 419 @since_time = Time.zone.now.ago( 90.minutes)
418 420 end
419 421 begin
420 422 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
421 423 @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)
422 424 rescue
423 425 @until_time = Time.zone.now
424 426 end
425 427
426 428 #convert sid
427 429 @sid = params[:SID].split(/[,\s]/) if params[:SID]
428 430 unless @sid and @sid.size > 0
429 431 return
430 432 redirect_to actoin: :cheat_scruntinize
431 433 flash[:notice] = 'Please enter at least 1 student id'
432 434 end
433 435 mark = Array.new(@sid.size,'?')
434 436 condition = "(u.login = " + mark.join(' OR u.login = ') + ')'
435 437
436 438 @st = <<-SQL
437 439 SELECT l.created_at as submitted_at ,-1 as id,u.login,u.full_name,l.ip_address,"" as problem_id,"" as points,l.user_id
438 440 FROM logins l INNER JOIN users u on l.user_id = u.id
439 441 WHERE l.created_at >= ? AND l.created_at <= ? AND #{condition}
440 442 UNION
441 443 SELECT s.submitted_at,s.id,u.login,u.full_name,s.ip_address,s.problem_id,s.points,s.user_id
442 444 FROM submissions s INNER JOIN users u ON s.user_id = u.id
443 445 WHERE s.submitted_at >= ? AND s.submitted_at <= ? AND #{condition}
444 446 ORDER BY submitted_at
445 447 SQL
446 448
447 449 p = [@st,@since_time,@until_time] + @sid + [@since_time,@until_time] + @sid
448 450 @logs = Submission.joins(:problem).find_by_sql(p)
449 451
450 452
451 453
452 454
453 455
454 456 end
455 457
456 458 protected
457 459
458 460 def calculate_max_score(problems, users,since_id,until_id, get_last_score = false)
459 461 scorearray = Array.new
460 462 users.each do |u|
461 463 ustat = Array.new
462 464 ustat[0] = u
463 465 problems.each do |p|
464 466 unless get_last_score
465 467 #get max score
466 468 max_points = 0
467 469 Submission.find_in_range_by_user_and_problem(u.id,p.id,since_id,until_id).each do |sub|
468 470 max_points = sub.points if sub and sub.points and (sub.points > max_points)
469 471 end
470 472 ustat << [(max_points.to_f*100/p.full_score).round, (max_points>=p.full_score)]
471 473 else
472 474 #get latest score
473 475 sub = Submission.find_last_by_user_and_problem(u.id,p.id)
474 476 if (sub!=nil) and (sub.points!=nil) and p and p.full_score
475 477 ustat << [(sub.points.to_f*100/p.full_score).round, (sub.points>=p.full_score)]
476 478 else
477 479 ustat << [0,false]
478 480 end
479 481 end
480 482 end
481 483 scorearray << ustat
482 484 end
483 485 return scorearray
484 486 end
485 487
486 488 def gen_csv_from_scorearray(scorearray,problem)
487 489 CSV.generate do |csv|
488 490 #add header
489 491 header = ['User','Name', 'Activated?', 'Logged in', 'Contest']
490 492 problem.each { |p| header << p.name }
491 493 header += ['Total','Passed']
492 494 csv << header
493 495 #add data
494 496 scorearray.each do |sc|
495 497 total = num_passed = 0
496 498 row = Array.new
497 499 sc.each_index do |i|
498 500 if i == 0
499 501 row << sc[i].login
500 502 row << sc[i].full_name
501 503 row << sc[i].activated
502 504 row << (sc[i].try(:contest_stat).try(:started_at)!=nil ? 'yes' : 'no')
503 505 row << sc[i].contests.collect {|c| c.name}.join(', ')
504 506 else
505 507 row << sc[i][0]
506 508 total += sc[i][0]
507 509 num_passed += 1 if sc[i][1]
508 510 end
509 511 end
510 512 row << total
511 513 row << num_passed
512 514 csv << row
513 515 end
514 516 end
515 517 end
516 518
517 519 end
@@ -1,49 +1,49
1 1 %h1 Maximum score
2 2
3 3 = form_tag report_show_max_score_path
4 4 .row
5 5 .col-md-4
6 6 .panel.panel-primary
7 7 .panel-heading
8 8 Problems
9 9 .panel-body
10 10 %p
11 11 Select problem(s) that we wish to know the score.
12 12 = label_tag :problem_id, "Problems"
13 13 = select_tag 'problem_id[]',
14 14 options_for_select(Problem.all.collect {|p| ["[#{p.name}] #{p.full_name}", p.id]},params[:problem_id]),
15 15 { class: 'select2 form-control', multiple: "true" }
16 16 .col-md-4
17 17 .panel.panel-primary
18 18 .panel-heading
19 19 Submission range
20 20 .panel-body
21 21 %p
22 22 Input minimum and maximum range of submission ID that should be included. A blank value for min and max means -1 and infinity, respectively.
23 23 .form-group
24 24 = label_tag :from, "Min"
25 - = text_field_tag 'from_id', nil, class: "form-control"
25 + = text_field_tag 'from_id', @since_id, class: "form-control"
26 26 .form-group
27 27 = label_tag :from, "Max"
28 - = text_field_tag 'to_id', nil, class: "form-control"
28 + = text_field_tag 'to_id', @until_id, class: "form-control"
29 29 .col-md-4
30 30 .panel.panel-primary
31 31 .panel-heading
32 32 Users
33 33 .panel-body
34 34 .radio
35 35 %label
36 36 = radio_button_tag 'users', 'all', true
37 37 All users
38 38 .radio
39 39 %label
40 40 = radio_button_tag 'users', 'enabled'
41 41 Only enabled users
42 42 .row
43 43 .col-md-12
44 44 = button_tag 'Show', class: "btn btn-primary btn-large", value: "show"
45 45 = button_tag 'Download CSV', class: "btn btn-primary btn-large", value: "download"
46 46
47 47 - if @scorearray
48 48 %h2 Result
49 49 =render "score_table"
@@ -1,2 +1,2
1 1 :plain
2 - $("body").prepend("<div class=\"alert alert-info\"> Submission #{@submission.id}'s task status has been chaned to \"#{@task.status_str}\" </div>")
2 + $("body").prepend("<div class=\"alert alert-info\"> Submission #{@submission.id}'s task status has been changed to \"#{@task.status_str}\". It will be re-judged soon. </div>")
You need to be logged in to leave comments. Login now