Description:
- login report - fix bugs on pagination on user manage
Commit status:
[Not Reviewed]
References:
Comments:
0 Commit comments 0 Inline Comments
Unresolved TODOs:
There are no unresolved TODOs
Add another comment

r792:6ae18fbc4b91 - - 14 files changed: 255 inserted, 76 deleted

@@ -0,0 +1,130
1 + - content_for :header do
2 + = javascript_include_tag 'local_jquery'
3 +
4 + %h1 Logins detail
5 +
6 + .row
7 + .col-md-4
8 + .alert.alert-info
9 + %ul
10 + %li You have to click refresh when changing the filter above
11 + %li Detail tab shows each logins separately
12 + %li Summary tab shows logins summary of each user
13 + .col-md-4
14 + = render partial: 'shared/date_filter'
15 + .col-md-4
16 + = render partial: 'shared/user_select'
17 +
18 + .row.form-group
19 + .col-sm-12
20 + %ul.nav.nav-tabs
21 + %li.active
22 + %a{href: '#detail', data: {toggle: :tab}} Detail
23 + %li
24 + %a{href: '#summary', data: {toggle: :tab}} Summary
25 + .row
26 + .col-sm-12
27 + .tab-content
28 + .tab-pane.active#detail
29 + %table#detail-table.table.table-hover.table-condense.datatable{style: 'width: 100%'}
30 + .tab-pane#summary
31 + %table#summary-table.table.table-hover.table-condense.datatable{style: 'width: 100%'}
32 +
33 +
34 +
35 + :javascript
36 + $(function() {
37 + detail_table = $('#detail-table').DataTable({
38 + dom: "<'row'<'col-sm-3'B><'col-sm-3'l><'col-sm-6'f>>" + "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-5'i><'col-sm-7'p>>",
39 + autoWidth: true,
40 + buttons: [
41 + {
42 + text: 'Refresh',
43 + action: (e,dt,node,config) => {
44 + detail_table.clear().draw()
45 + detail_table.ajax.reload( () => { detail_table.columns.adjust().draw() } )
46 + summary_table.clear().draw()
47 + summary_table.ajax.reload( () => { summary_table.columns.adjust().draw() } )
48 + }
49 + },
50 + 'copy',
51 + {
52 + extend: 'excel',
53 + title: 'Login detail',
54 + }
55 + ],
56 + columns: [
57 + {title: 'User', data: 'login_text'},
58 + {title: 'Time', data: 'created_at'},
59 + {title: 'IP', data: 'ip_address'},
60 + ],
61 + ajax: {
62 + url: '#{login_detail_query_report_path}',
63 + type: 'POST',
64 + data: (d) => {
65 + d.since_datetime = $('#since_datetime').val()
66 + d.until_datetime = $('#until_datetime').val()
67 + d.users = $("input[name='users']:checked").val()
68 + d.groups = $("#group_id").select2('val')
69 + },
70 + dataType: 'json',
71 + beforeSend: (request) => {
72 + request.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
73 + },
74 + }, //end ajax
75 + pageLength: 25,
76 + processing: true,
77 + });
78 +
79 + summary_table = $('#summary-table').DataTable({
80 + dom: "<'row'<'col-sm-3'B><'col-sm-3'l><'col-sm-6'f>>" + "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-5'i><'col-sm-7'p>>",
81 + autoWidth: true,
82 + buttons: [
83 + {
84 + text: 'Refresh',
85 + action: (e,dt,node,config) => {
86 + summary_table.clear().draw()
87 + summary_table.ajax.reload( () => { summary_table.columns.adjust().draw() } )
88 + detail_table.clear().draw()
89 + detail_table.ajax.reload( () => { detail_table.columns.adjust().draw() } )
90 + }
91 + },
92 + 'copy',
93 + {
94 + extend: 'excel',
95 + title: 'Login summary',
96 + }
97 + ],
98 + columns: [
99 + {title: 'User', data: 'login_text'},
100 + {title: 'Login Count', data: 'count'},
101 + {title: 'Earliest', data: 'earliest'},
102 + {title: 'Latest', data: 'latest'},
103 + {title: 'IP', data: 'ip_address'},
104 + ],
105 + ajax: {
106 + url: '#{login_summary_query_report_path}',
107 + type: 'POST',
108 + data: (d) => {
109 + d.since_datetime = $('#since_datetime').val()
110 + d.until_datetime = $('#until_datetime').val()
111 + d.users = $("input[name='users']:checked").val()
112 + d.groups = $("#group_id").select2('val')
113 + },
114 + dataType: 'json',
115 + beforeSend: (request) => {
116 + request.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
117 + },
118 + }, //end ajax
119 + pageLength: 25,
120 + processing: true,
121 + });
122 +
123 + $('.input-group.date').datetimepicker({
124 + format: 'YYYY-MM-DD HH:mm',
125 + showTodayButton: true,
126 + locale: 'en',
127 + widgetPositioning: {horizontal: 'auto', vertical: 'bottom'},
128 + defaultDate: moment()
129 + });
130 + });
@@ -0,0 +1,10
1 + json.draw params['draw']&.to_i
2 + json.recordsTotal @recordsTotal
3 + json.recordsFiltered @recordsFiltered
4 + json.data do
5 + json.array! @logins do |login|
6 + json.login_text login.user ? "<a href='#{stat_user_path(login.user_id)}'>(#{login.user.login})</a> #{login.user.full_name}" : '-- deletec user --'
7 + json.created_at login.created_at.strftime('%Y-%m-%d %H:%M')
8 + json.ip_address login.ip_address
9 + end
10 + end
@@ -0,0 +1,12
1 + json.draw params['draw']&.to_i
2 + json.recordsTotal @recordsTotal
3 + json.recordsFiltered @recordsFiltered
4 + json.data do
5 + json.array! @users do |user|
6 + json.login_text "<a href='#{stat_user_path(user[:id])}'>(#{user[:login]})</a> #{user[:full_name]}"
7 + json.count user[:count]
8 + json.earliest user[:min].strftime('%Y-%m-%d %H:%M')
9 + json.latest user[:max].strftime('%Y-%m-%d %H:%M')
10 + json.ip_address user[:ip].join('<br/>')
11 + end
12 + end
@@ -0,0 +1,5
1 + class AddIndexToLogin < ActiveRecord::Migration[5.2]
2 + def change
3 + add_index :logins, :user_id
4 + end
5 + end
@@ -1,522 +1,564
1 1 require 'csv'
2 2
3 3 class ReportController < ApplicationController
4 4
5 5 before_action :check_valid_login
6 6
7 - before_action :admin_authorization, only: [:login_stat,:submission, :submission_query, :stuck, :cheat_report, :cheat_scruntinize, :show_max_score, :current_score]
7 + before_action :admin_authorization, only: [:login_stat,:submission, :submission_query,
8 + :login, :login_detail_query, :login_summary_query,
9 + :stuck, :cheat_report, :cheat_scruntinize, :show_max_score, :current_score]
8 10
9 11 before_action(only: [:problem_hof]) { |c|
10 12 return false unless check_valid_login
11 13
12 14 admin_authorization unless GraderConfiguration["right.user_view_submission"]
13 15 }
14 16
15 17 def max_score
16 18 end
17 19
18 20 def current_score
19 21 @problems = Problem.available_problems
20 22 if params[:group_id]
21 23 @group = Group.find(params[:group_id])
22 24 @users = @group.users.where(enabled: true)
23 25 else
24 26 @users = User.includes(:contests).includes(:contest_stat).where(enabled: true)
25 27 end
26 28 @scorearray = calculate_max_score(@problems, @users,0,0,true)
27 29
28 30 #rencer accordingly
29 31 if params[:button] == 'download' then
30 32 csv = gen_csv_from_scorearray(@scorearray,@problems)
31 33 send_data csv, filename: 'max_score.csv'
32 34 else
33 35 #render template: 'user_admin/user_stat'
34 36 render 'current_score'
35 37 end
36 38 end
37 39
38 40 def show_max_score
39 41 #process parameters
40 42 #problems
41 43 @problems = []
42 44 if params[:problem_id]
43 45 params[:problem_id].each do |id|
44 46 next unless id.strip != ""
45 47 pid = Problem.find_by_id(id.to_i)
46 48 @problems << pid if pid
47 49 end
48 50 end
49 51
50 52 #users
51 53 @users = if params[:users] == "all" then
52 54 User.includes(:contests).includes(:contest_stat)
53 55 else
54 56 User.includes(:contests).includes(:contest_stat).where(enabled: true)
55 57 end
56 58
57 59 #set up range from param
58 60 @since_id = params.fetch(:from_id, 0).to_i
59 61 @until_id = params.fetch(:to_id, 0).to_i
60 62 @since_id = nil if @since_id == 0
61 63 @until_id = nil if @until_id == 0
62 64
63 65 #calculate the routine
64 66 @scorearray = calculate_max_score(@problems, @users, @since_id, @until_id)
65 67
66 68 #rencer accordingly
67 69 if params[:button] == 'download' then
68 70 csv = gen_csv_from_scorearray(@scorearray,@problems)
69 71 send_data csv, filename: 'max_score.csv'
70 72 else
71 73 #render template: 'user_admin/user_stat'
72 74 render 'max_score'
73 75 end
74 76
75 77 end
76 78
77 79 def score
78 80 if params[:commit] == 'download csv'
79 81 @problems = Problem.all
80 82 else
81 83 @problems = Problem.available_problems
82 84 end
83 85 @users = User.includes(:contests, :contest_stat).where(enabled: true)
84 86 @scorearray = Array.new
85 87 @users.each do |u|
86 88 ustat = Array.new
87 89 ustat[0] = u
88 90 @problems.each do |p|
89 91 sub = Submission.find_last_by_user_and_problem(u.id,p.id)
90 92 if (sub!=nil) and (sub.points!=nil) and p and p.full_score
91 93 ustat << [(sub.points.to_f*100/p.full_score).round, (sub.points>=p.full_score)]
92 94 else
93 95 ustat << [0,false]
94 96 end
95 97 end
96 98 @scorearray << ustat
97 99 end
98 100 if params[:commit] == 'download csv' then
99 101 csv = gen_csv_from_scorearray(@scorearray,@problems)
100 102 send_data csv, filename: 'last_score.csv'
101 103 else
102 104 render template: 'user_admin/user_stat'
103 105 end
104 106
105 107 end
106 108
107 - def login_stat
109 + def login
110 + end
111 +
112 + def login_summary_query
113 + @users = Array.new
114 +
115 + date_and_time = '%Y-%m-%d %H:%M'
116 + begin
117 + md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
118 + @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)
119 + rescue
120 + @since_time = DateTime.new(1000,1,1)
121 + end
122 + begin
123 + md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
124 + @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)
125 + rescue
126 + @until_time = DateTime.new(3000,1,1)
127 + end
128 +
129 + record = User
130 + .left_outer_joins(:logins).group('users.id')
131 + .where("logins.created_at >= ? AND logins.created_at <= ?",@since_time, @until_time)
132 + case params[:users]
133 + when 'enabled'
134 + record = record.where(enabled: true)
135 + when 'group'
136 + record = record.joins(:groups).where(groups: {id: params[:groups]}) if params[:groups]
137 + end
138 +
139 + record = record.pluck("users.id,users.login,users.full_name,count(logins.created_at),min(logins.created_at),max(logins.created_at)")
140 + record.each do |user|
141 + x = Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
142 + user[0],@since_time,@until_time)
143 + .pluck(:ip_address).uniq
144 + @users << { id: user[0],
145 + login: user[1],
146 + full_name: user[2],
147 + count: user[3],
148 + min: user[4],
149 + max: user[5],
150 + ip: x
151 + }
152 + end
153 + end
154 +
155 + def login_detail_query
108 156 @logins = Array.new
109 157
110 158 date_and_time = '%Y-%m-%d %H:%M'
111 159 begin
112 160 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
113 161 @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)
114 162 rescue
115 163 @since_time = DateTime.new(1000,1,1)
116 164 end
117 165 begin
118 166 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
119 167 @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)
120 168 rescue
121 169 @until_time = DateTime.new(3000,1,1)
122 170 end
123 -
124 - User.all.each do |user|
125 - @logins << { id: user.id,
126 - login: user.login,
127 - full_name: user.full_name,
128 - count: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
129 - user.id,@since_time,@until_time)
130 - .count(:id),
131 - min: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
132 - user.id,@since_time,@until_time)
133 - .minimum(:created_at),
134 - max: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
135 - user.id,@since_time,@until_time)
136 - .maximum(:created_at),
137 - ip: Login.where("user_id = ? AND created_at >= ? AND created_at <= ?",
138 - user.id,@since_time,@until_time)
139 - .select(:ip_address).uniq
140 171
141 - }
172 + @logins = Login.includes(:user).where("logins.created_at >= ? AND logins.created_at <= ?",@since_time, @until_time)
173 + case params[:users]
174 + when 'enabled'
175 + @logins = @logins.where(users: {enabled: true})
176 + when 'group'
177 + @logins = @logins.joins(user: :groups).where(user: {groups: {id: params[:groups]}}) if params[:groups]
142 178 end
143 179 end
144 180
145 181 def submission
146 182 end
147 183
148 184 def submission_query
149 185 @submissions = Submission
150 186 .includes(:problem).includes(:user).includes(:language)
151 187
152 - if params[:problem]
153 - @submission = @submission.where(problem_id: params[:problem])
188 + case params[:users]
189 + when 'enabled'
190 + @submissions = @submissions.where(users: {enabled: true})
191 + when 'group'
192 + @submissions = @submissions.joins(user: :groups).where(user: {groups: {id: params[:groups]}}) if params[:groups]
154 193 end
155 194
156 - case params[:users]
195 + case params[:problems]
157 196 when 'enabled'
158 - @submissions = @submissions.where('user.enabled': true)
159 - when 'group'
160 - @submissions = @submissions.joins(user: :groups).where(user: {groups: {id: params[:groups]}}) if params[:groups]
197 + @submissions = @submissions.where(problems: {available: true})
198 + when 'selected'
199 + @submissions = @submissions.where(problem_id: params[:problem_id])
161 200 end
162 201
163 202 #set default
164 203 params[:since_datetime] = Date.today.to_s if params[:since_datetime].blank?
165 204
166 205 @submissions, @recordsTotal, @recordsFiltered = process_query_record( @submissions,
167 206 global_search: ['user.login','user.full_name','problem.name','problem.full_name','points'],
168 207 date_filter: 'submitted_at',
169 208 date_param_since: 'since_datetime',
170 209 date_param_until: 'until_datetime',
171 210 hard_limit: 100_000
172 211 )
173 212 end
174 213
214 + def login
215 + end
216 +
175 217 def problem_hof
176 218 # gen problem list
177 219 @user = User.find(session[:user_id])
178 220 @problems = @user.available_problems
179 221
180 222 # get selected problems or the default
181 223 if params[:id]
182 224 begin
183 225 @problem = Problem.available.find(params[:id])
184 226 rescue
185 227 redirect_to action: :problem_hof
186 228 flash[:notice] = 'Error: submissions for that problem are not viewable.'
187 229 return
188 230 end
189 231 end
190 232
191 233 return unless @problem
192 234
193 235 @by_lang = {} #aggregrate by language
194 236
195 237 range =65
196 238 @histogram = { data: Array.new(range,0), summary: {} }
197 239 @summary = {count: 0, solve: 0, attempt: 0}
198 240 user = Hash.new(0)
199 241 Submission.where(problem_id: @problem.id).find_each do |sub|
200 242 #histogram
201 243 d = (DateTime.now.in_time_zone - sub.submitted_at) / 24 / 60 / 60
202 244 @histogram[:data][d.to_i] += 1 if d < range
203 245
204 246 next unless sub.points
205 247 @summary[:count] += 1
206 248 user[sub.user_id] = [user[sub.user_id], (sub.points >= @problem.full_score) ? 1 : 0].max
207 249
208 250 lang = Language.find_by_id(sub.language_id)
209 251 next unless lang
210 252 next unless sub.points >= @problem.full_score
211 253
212 254 #initialize
213 255 unless @by_lang.has_key?(lang.pretty_name)
214 256 @by_lang[lang.pretty_name] = {
215 257 runtime: { avail: false, value: 2**30-1 },
216 258 memory: { avail: false, value: 2**30-1 },
217 259 length: { avail: false, value: 2**30-1 },
218 260 first: { avail: false, value: DateTime.new(3000,1,1) }
219 261 }
220 262 end
221 263
222 264 if sub.max_runtime and sub.max_runtime < @by_lang[lang.pretty_name][:runtime][:value]
223 265 @by_lang[lang.pretty_name][:runtime] = { avail: true, user_id: sub.user_id, value: sub.max_runtime, sub_id: sub.id }
224 266 end
225 267
226 268 if sub.peak_memory and sub.peak_memory < @by_lang[lang.pretty_name][:memory][:value]
227 269 @by_lang[lang.pretty_name][:memory] = { avail: true, user_id: sub.user_id, value: sub.peak_memory, sub_id: sub.id }
228 270 end
229 271
230 272 if sub.submitted_at and sub.submitted_at < @by_lang[lang.pretty_name][:first][:value] and sub.user and
231 273 !sub.user.admin?
232 274 @by_lang[lang.pretty_name][:first] = { avail: true, user_id: sub.user_id, value: sub.submitted_at, sub_id: sub.id }
233 275 end
234 276
235 277 if @by_lang[lang.pretty_name][:length][:value] > sub.effective_code_length
236 278 @by_lang[lang.pretty_name][:length] = { avail: true, user_id: sub.user_id, value: sub.effective_code_length, sub_id: sub.id }
237 279 end
238 280 end
239 281
240 282 #process user_id
241 283 @by_lang.each do |lang,prop|
242 284 prop.each do |k,v|
243 285 v[:user] = User.exists?(v[:user_id]) ? User.find(v[:user_id]).full_name : "(NULL)"
244 286 end
245 287 end
246 288
247 289 #sum into best
248 290 if @by_lang and @by_lang.first
249 291 @best = @by_lang.first[1].clone
250 292 @by_lang.each do |lang,prop|
251 293 if @best[:runtime][:value] >= prop[:runtime][:value]
252 294 @best[:runtime] = prop[:runtime]
253 295 @best[:runtime][:lang] = lang
254 296 end
255 297 if @best[:memory][:value] >= prop[:memory][:value]
256 298 @best[:memory] = prop[:memory]
257 299 @best[:memory][:lang] = lang
258 300 end
259 301 if @best[:length][:value] >= prop[:length][:value]
260 302 @best[:length] = prop[:length]
261 303 @best[:length][:lang] = lang
262 304 end
263 305 if @best[:first][:value] >= prop[:first][:value]
264 306 @best[:first] = prop[:first]
265 307 @best[:first][:lang] = lang
266 308 end
267 309 end
268 310 end
269 311
270 312 @histogram[:summary][:max] = [@histogram[:data].max,1].max
271 313 @summary[:attempt] = user.count
272 314 user.each_value { |v| @summary[:solve] += 1 if v == 1 }
273 315 end
274 316
275 317 def stuck #report struggling user,problem
276 318 # init
277 319 user,problem = nil
278 320 solve = true
279 321 tries = 0
280 322 @struggle = Array.new
281 323 record = {}
282 324 Submission.includes(:problem,:user).order(:problem_id,:user_id).find_each do |sub|
283 325 next unless sub.problem and sub.user
284 326 if user != sub.user_id or problem != sub.problem_id
285 327 @struggle << { user: record[:user], problem: record[:problem], tries: tries } unless solve
286 328 record = {user: sub.user, problem: sub.problem}
287 329 user,problem = sub.user_id, sub.problem_id
288 330 solve = false
289 331 tries = 0
290 332 end
291 333 if sub.points >= sub.problem.full_score
292 334 solve = true
293 335 else
294 336 tries += 1
295 337 end
296 338 end
297 339 @struggle.sort!{|a,b| b[:tries] <=> a[:tries] }
298 340 @struggle = @struggle[0..50]
299 341 end
300 342
301 343
302 344 def multiple_login
303 345 #user with multiple IP
304 346 raw = Submission.joins(:user).joins(:problem).where("problems.available != 0").group("login,ip_address").order(:login)
305 347 last,count = 0,0
306 348 first = 0
307 349 @users = []
308 350 raw.each do |r|
309 351 if last != r.user.login
310 352 count = 1
311 353 last = r.user.login
312 354 first = r
313 355 else
314 356 @users << first if count == 1
315 357 @users << r
316 358 count += 1
317 359 end
318 360 end
319 361
320 362 #IP with multiple user
321 363 raw = Submission.joins(:user).joins(:problem).where("problems.available != 0").group("login,ip_address").order(:ip_address)
322 364 last,count = 0,0
323 365 first = 0
324 366 @ip = []
325 367 raw.each do |r|
326 368 if last != r.ip_address
327 369 count = 1
328 370 last = r.ip_address
329 371 first = r
330 372 else
331 373 @ip << first if count == 1
332 374 @ip << r
333 375 count += 1
334 376 end
335 377 end
336 378 end
337 379
338 380 def cheat_report
339 381 date_and_time = '%Y-%m-%d %H:%M'
340 382 begin
341 383 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
342 384 @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)
343 385 rescue
344 386 @since_time = Time.zone.now.ago( 90.minutes)
345 387 end
346 388 begin
347 389 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
348 390 @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)
349 391 rescue
350 392 @until_time = Time.zone.now
351 393 end
352 394
353 395 #multi login
354 396 @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")
355 397
356 398 st = <<-SQL
357 399 SELECT l2.*
358 400 FROM logins l2 INNER JOIN
359 401 (SELECT u.id,COUNT(DISTINCT ip_address) as count,u.login,u.full_name
360 402 FROM logins l
361 403 INNER JOIN users u ON l.user_id = u.id
362 404 WHERE l.created_at >= '#{@since_time.in_time_zone("UTC")}' and l.created_at <= '#{@until_time.in_time_zone("UTC")}'
363 405 GROUP BY u.id
364 406 HAVING count > 1
365 407 ) ml ON l2.user_id = ml.id
366 408 WHERE l2.created_at >= '#{@since_time.in_time_zone("UTC")}' and l2.created_at <= '#{@until_time.in_time_zone("UTC")}'
367 409 UNION
368 410 SELECT l2.*
369 411 FROM logins l2 INNER JOIN
370 412 (SELECT l.ip_address,COUNT(DISTINCT u.id) as count
371 413 FROM logins l
372 414 INNER JOIN users u ON l.user_id = u.id
373 415 WHERE l.created_at >= '#{@since_time.in_time_zone("UTC")}' and l.created_at <= '#{@until_time.in_time_zone("UTC")}'
374 416 GROUP BY l.ip_address
375 417 HAVING count > 1
376 418 ) ml on ml.ip_address = l2.ip_address
377 419 INNER JOIN users u ON l2.user_id = u.id
378 420 WHERE l2.created_at >= '#{@since_time.in_time_zone("UTC")}' and l2.created_at <= '#{@until_time.in_time_zone("UTC")}'
379 421 ORDER BY ip_address,created_at
380 422 SQL
381 423 @mld = Login.find_by_sql(st)
382 424
383 425 st = <<-SQL
384 426 SELECT s.id,s.user_id,s.ip_address,s.submitted_at,s.problem_id
385 427 FROM submissions s INNER JOIN
386 428 (SELECT u.id,COUNT(DISTINCT ip_address) as count,u.login,u.full_name
387 429 FROM logins l
388 430 INNER JOIN users u ON l.user_id = u.id
389 431 WHERE l.created_at >= ? and l.created_at <= ?
390 432 GROUP BY u.id
391 433 HAVING count > 1
392 434 ) ml ON s.user_id = ml.id
393 435 WHERE s.submitted_at >= ? and s.submitted_at <= ?
394 436 UNION
395 437 SELECT s.id,s.user_id,s.ip_address,s.submitted_at,s.problem_id
396 438 FROM submissions s INNER JOIN
397 439 (SELECT l.ip_address,COUNT(DISTINCT u.id) as count
398 440 FROM logins l
399 441 INNER JOIN users u ON l.user_id = u.id
400 442 WHERE l.created_at >= ? and l.created_at <= ?
401 443 GROUP BY l.ip_address
402 444 HAVING count > 1
403 445 ) ml on ml.ip_address = s.ip_address
404 446 WHERE s.submitted_at >= ? and s.submitted_at <= ?
405 447 ORDER BY ip_address,submitted_at
406 448 SQL
407 449 @subs = Submission.joins(:problem).find_by_sql([st,@since_time,@until_time,
408 450 @since_time,@until_time,
409 451 @since_time,@until_time,
410 452 @since_time,@until_time])
411 453
412 454 end
413 455
414 456 def cheat_scruntinize
415 457 #convert date & time
416 458 date_and_time = '%Y-%m-%d %H:%M'
417 459 begin
418 460 md = params[:since_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
419 461 @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)
420 462 rescue
421 463 @since_time = Time.zone.now.ago( 90.minutes)
422 464 end
423 465 begin
424 466 md = params[:until_datetime].match(/(\d+)-(\d+)-(\d+) (\d+):(\d+)/)
425 467 @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)
426 468 rescue
427 469 @until_time = Time.zone.now
428 470 end
429 471
430 472 #convert sid
431 473 @sid = params[:SID].split(/[,\s]/) if params[:SID]
432 474 unless @sid and @sid.size > 0
433 475 return
434 476 redirect_to actoin: :cheat_scruntinize
435 477 flash[:notice] = 'Please enter at least 1 student id'
436 478 end
437 479 mark = Array.new(@sid.size,'?')
438 480 condition = "(u.login = " + mark.join(' OR u.login = ') + ')'
439 481
440 482 @st = <<-SQL
441 483 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
442 484 FROM logins l INNER JOIN users u on l.user_id = u.id
443 485 WHERE l.created_at >= ? AND l.created_at <= ? AND #{condition}
444 486 UNION
445 487 SELECT s.submitted_at,s.id,u.login,u.full_name,s.ip_address,s.problem_id,s.points,s.user_id
446 488 FROM submissions s INNER JOIN users u ON s.user_id = u.id
447 489 WHERE s.submitted_at >= ? AND s.submitted_at <= ? AND #{condition}
448 490 ORDER BY submitted_at
449 491 SQL
450 492
451 493 p = [@st,@since_time,@until_time] + @sid + [@since_time,@until_time] + @sid
452 494 @logs = Submission.joins(:problem).find_by_sql(p)
453 495
454 496
455 497
456 498
457 499
458 500 end
459 501
460 502 protected
461 503
462 504 def calculate_max_score(problems, users,since_id,until_id, get_last_score = false)
463 505 #scorearray[i] = user #i's user stat where i is the index (not id)
464 506 scorearray = Array.new
465 507 users.each do |u|
466 508 ustat = Array.new
467 509 ustat[0] = u
468 510 problems.each do |p|
469 511 unless get_last_score
470 512 #get max score
471 513 max_points = 0
472 514 Submission.find_in_range_by_user_and_problem(u.id,p.id,since_id,until_id).each do |sub|
473 515 max_points = sub.points if sub and sub.points and (sub.points > max_points)
474 516 end
475 517 ustat << [(max_points.to_f*100/p.full_score).round, (max_points>=p.full_score)]
476 518 else
477 519 #get latest score
478 520 sub = Submission.find_last_by_user_and_problem(u.id,p.id)
479 521 if (sub!=nil) and (sub.points!=nil) and p and p.full_score
480 522 ustat << [(sub.points.to_f*100/p.full_score).round, (sub.points>=p.full_score)]
481 523 else
482 524 ustat << [0,false]
483 525 end
484 526 end
485 527 end
486 528 scorearray << ustat
487 529 end
488 530 return scorearray
489 531 end
490 532
491 533 def gen_csv_from_scorearray(scorearray,problem)
492 534 CSV.generate do |csv|
493 535 #add header
494 536 header = ['User','Name', 'Activated?', 'Logged in', 'Contest']
495 537 problem.each { |p| header << p.name }
496 538 header += ['Total','Passed']
497 539 csv << header
498 540 #add data
499 541 scorearray.each do |sc|
500 542 total = num_passed = 0
501 543 row = Array.new
502 544 sc.each_index do |i|
503 545 if i == 0
504 546 row << sc[i].login
505 547 row << sc[i].full_name
506 548 row << sc[i].activated
507 549 row << (sc[i].try(:contest_stat).try(:started_at)!=nil ? 'yes' : 'no')
508 550 row << sc[i].contests.collect {|c| c.name}.join(', ')
509 551 else
510 552 row << sc[i][0]
511 553 total += sc[i][0]
512 554 num_passed += 1 if sc[i][1]
513 555 end
514 556 end
515 557 row << total
516 558 row << num_passed
517 559 csv << row
518 560 end
519 561 end
520 562 end
521 563
522 564 end
@@ -1,621 +1,614
1 1 require 'csv'
2 2
3 3 class UserAdminController < ApplicationController
4 4
5 5 include MailHelperMethods
6 6
7 7 before_action :admin_authorization
8 8
9 9 def index
10 10 @user_count = User.count
11 - if params[:page] == 'all'
12 - @users = User.all
13 - @paginated = false
14 - else
15 - @users = User.paginate :page => params[:page]
16 - @paginated = true
17 - end
18 11 @users = User.all
19 12 @hidden_columns = ['hashed_password', 'salt', 'created_at', 'updated_at']
20 13 @contests = Contest.enabled
21 14 end
22 15
23 16 def active
24 17 sessions = ActiveRecord::SessionStore::Session.where("updated_at >= ?", 60.minutes.ago)
25 18 @users = []
26 19 sessions.each do |session|
27 20 if session.data[:user_id]
28 21 @users << User.find(session.data[:user_id])
29 22 end
30 23 end
31 24 end
32 25
33 26 def show
34 27 @user = User.find(params[:id])
35 28 end
36 29
37 30 def new
38 31 @user = User.new
39 32 end
40 33
41 34 def create
42 35 @user = User.new(user_params)
43 36 @user.activated = true
44 37 if @user.save
45 38 flash[:notice] = 'User was successfully created.'
46 39 redirect_to :action => 'index'
47 40 else
48 41 render :action => 'new'
49 42 end
50 43 end
51 44
52 45 def clear_last_ip
53 46 @user = User.find(params[:id])
54 47 @user.last_ip = nil
55 48 @user.save
56 49 redirect_to action: 'index', page: params[:page]
57 50 end
58 51
59 52 def create_from_list
60 53 lines = params[:user_list]
61 54
62 55 note = []
63 56 error_note = []
64 57 error_msg = nil
65 58 ok_user = []
66 59
67 60 lines.split("\n").each do |line|
68 61 items = line.chomp.split(',')
69 62 if items.length>=2
70 63 login = items[0]
71 64 full_name = items[1]
72 65 remark =''
73 66 user_alias = ''
74 67
75 68 added_random_password = false
76 69 if items.length >= 3 and items[2].chomp(" ").length > 0;
77 70 password = items[2].chomp(" ")
78 71 else
79 72 password = random_password
80 73 added_random_password=true;
81 74 end
82 75
83 76 if items.length>= 4 and items[3].chomp(" ").length > 0;
84 77 user_alias = items[3].chomp(" ")
85 78 else
86 79 user_alias = login
87 80 end
88 81
89 82 if items.length>=5
90 83 remark = items[4].strip;
91 84 end
92 85
93 86 user = User.find_by_login(login)
94 87 if (user)
95 88 user.full_name = full_name
96 89 user.password = password
97 90 user.remark = remark
98 91 else
99 92 user = User.new({:login => login,
100 93 :full_name => full_name,
101 94 :password => password,
102 95 :password_confirmation => password,
103 96 :alias => user_alias,
104 97 :remark => remark})
105 98 end
106 99 user.activated = true
107 100
108 101 if user.save
109 102 if added_random_password
110 103 note << "'#{login}' (+)"
111 104 else
112 105 note << login
113 106 end
114 107 ok_user << user
115 108 else
116 109 error_note << "'#{login}'"
117 110 error_msg = user.errors.full_messages.to_sentence unless error_msg
118 111 end
119 112
120 113 end
121 114 end
122 115
123 116 #add to group
124 117 if params[:add_to_group]
125 118 group = Group.where(id: params[:group_id]).first
126 119 if group
127 120 group.users << ok_user
128 121 end
129 122 end
130 123
131 124 # show flash
132 125 if note.size > 0
133 126 flash[:success] = 'User(s) ' + note.join(', ') +
134 127 ' were successfully created. ' +
135 128 '( (+) - created with random passwords.)'
136 129 end
137 130 if error_note.size > 0
138 131 flash[:error] = "Following user(s) failed to be created: " + error_note.join(', ') + ". The error of the first failed one are: " + error_msg;
139 132 end
140 133 redirect_to :action => 'index'
141 134 end
142 135
143 136 def edit
144 137 @user = User.find(params[:id])
145 138 end
146 139
147 140 def update
148 141 @user = User.find(params[:id])
149 142 if @user.update_attributes(user_params)
150 143 flash[:notice] = 'User was successfully updated.'
151 144 redirect_to :action => 'show', :id => @user
152 145 else
153 146 render :action => 'edit'
154 147 end
155 148 end
156 149
157 150 def destroy
158 151 User.find(params[:id]).destroy
159 152 redirect_to :action => 'index'
160 153 end
161 154
162 155 def user_stat
163 156 if params[:commit] == 'download csv'
164 157 @problems = Problem.all
165 158 else
166 159 @problems = Problem.available_problems
167 160 end
168 161 @users = User.includes(:contests, :contest_stat).where(enabled: true)
169 162 @scorearray = Array.new
170 163 @users.each do |u|
171 164 ustat = Array.new
172 165 ustat[0] = u
173 166 @problems.each do |p|
174 167 sub = Submission.find_last_by_user_and_problem(u.id,p.id)
175 168 if (sub!=nil) and (sub.points!=nil) and p and p.full_score
176 169 ustat << [(sub.points.to_f*100/p.full_score).round, (sub.points>=p.full_score)]
177 170 else
178 171 ustat << [0,false]
179 172 end
180 173 end
181 174 @scorearray << ustat
182 175 end
183 176 if params[:commit] == 'download csv' then
184 177 csv = gen_csv_from_scorearray(@scorearray,@problems)
185 178 send_data csv, filename: 'last_score.csv'
186 179 else
187 180 render template: 'user_admin/user_stat'
188 181 end
189 182 end
190 183
191 184 def user_stat_max
192 185 if params[:commit] == 'download csv'
193 186 @problems = Problem.all
194 187 else
195 188 @problems = Problem.available_problems
196 189 end
197 190 @users = User.includes(:contests).includes(:contest_stat).all
198 191 @scorearray = Array.new
199 192 #set up range from param
200 193 since_id = params.fetch(:since_id, 0).to_i
201 194 until_id = params.fetch(:until_id, 0).to_i
202 195 @users.each do |u|
203 196 ustat = Array.new
204 197 ustat[0] = u
205 198 @problems.each do |p|
206 199 max_points = 0
207 200 Submission.find_in_range_by_user_and_problem(u.id,p.id,since_id,until_id).each do |sub|
208 201 max_points = sub.points if sub and sub.points and (sub.points > max_points)
209 202 end
210 203 ustat << [(max_points.to_f*100/p.full_score).round, (max_points>=p.full_score)]
211 204 end
212 205 @scorearray << ustat
213 206 end
214 207
215 208 if params[:commit] == 'download csv' then
216 209 csv = gen_csv_from_scorearray(@scorearray,@problems)
217 210 send_data csv, filename: 'max_score.csv'
218 211 else
219 212 render template: 'user_admin/user_stat'
220 213 end
221 214 end
222 215
223 216 def import
224 217 if params[:file]==''
225 218 flash[:notice] = 'Error importing no file'
226 219 redirect_to :action => 'index' and return
227 220 end
228 221 import_from_file(params[:file])
229 222 end
230 223
231 224 def random_all_passwords
232 225 users = User.all
233 226 @prefix = params[:prefix] || ''
234 227 @non_admin_users = User.find_non_admin_with_prefix(@prefix)
235 228 @changed = false
236 229 if params[:commit] == 'Go ahead'
237 230 @non_admin_users.each do |user|
238 231 password = random_password
239 232 user.password = password
240 233 user.password_confirmation = password
241 234 user.save
242 235 end
243 236 @changed = true
244 237 end
245 238 end
246 239
247 240 # contest management
248 241
249 242 def contests
250 243 @contest, @users = find_contest_and_user_from_contest_id(params[:id])
251 244 @contests = Contest.enabled
252 245 end
253 246
254 247 def assign_from_list
255 248 contest_id = params[:users_contest_id]
256 249 org_contest, users = find_contest_and_user_from_contest_id(contest_id)
257 250 contest = Contest.find(params[:new_contest][:id])
258 251 if !contest
259 252 flash[:notice] = 'Error: no contest'
260 253 redirect_to :action => 'contests', :id =>contest_id
261 254 end
262 255
263 256 note = []
264 257 users.each do |u|
265 258 u.contests = [contest]
266 259 note << u.login
267 260 end
268 261 flash[:notice] = 'User(s) ' + note.join(', ') +
269 262 " were successfully reassigned to #{contest.title}."
270 263 redirect_to :action => 'contests', :id =>contest.id
271 264 end
272 265
273 266 def add_to_contest
274 267 user = User.find(params[:id])
275 268 contest = Contest.find(params[:contest_id])
276 269 if user and contest
277 270 user.contests << contest
278 271 end
279 272 redirect_to :action => 'index'
280 273 end
281 274
282 275 def remove_from_contest
283 276 user = User.find(params[:id])
284 277 contest = Contest.find(params[:contest_id])
285 278 if user and contest
286 279 user.contests.delete(contest)
287 280 end
288 281 redirect_to :action => 'index'
289 282 end
290 283
291 284 def contest_management
292 285 end
293 286
294 287 def manage_contest
295 288 contest = Contest.find(params[:contest][:id])
296 289 if !contest
297 290 flash[:notice] = 'You did not choose the contest.'
298 291 redirect_to :action => 'contest_management' and return
299 292 end
300 293
301 294 operation = params[:operation]
302 295
303 296 if not ['add','remove','assign'].include? operation
304 297 flash[:notice] = 'You did not choose the operation to perform.'
305 298 redirect_to :action => 'contest_management' and return
306 299 end
307 300
308 301 lines = params[:login_list]
309 302 if !lines or lines.blank?
310 303 flash[:notice] = 'You entered an empty list.'
311 304 redirect_to :action => 'contest_management' and return
312 305 end
313 306
314 307 note = []
315 308 users = []
316 309 lines.split("\n").each do |line|
317 310 user = User.find_by_login(line.chomp)
318 311 if user
319 312 if operation=='add'
320 313 if ! user.contests.include? contest
321 314 user.contests << contest
322 315 end
323 316 elsif operation=='remove'
324 317 user.contests.delete(contest)
325 318 else
326 319 user.contests = [contest]
327 320 end
328 321
329 322 if params[:reset_timer]
330 323 user.contest_stat.forced_logout = true
331 324 user.contest_stat.reset_timer_and_save
332 325 end
333 326
334 327 if params[:notification_emails]
335 328 send_contest_update_notification_email(user, contest)
336 329 end
337 330
338 331 note << user.login
339 332 users << user
340 333 end
341 334 end
342 335
343 336 if params[:reset_timer]
344 337 logout_users(users)
345 338 end
346 339
347 340 flash[:notice] = 'User(s) ' + note.join(', ') +
348 341 ' were successfully modified. '
349 342 redirect_to :action => 'contest_management'
350 343 end
351 344
352 345 # admin management
353 346
354 347 def admin
355 348 @admins = User.all.find_all {|user| user.admin? }
356 349 end
357 350
358 351 def grant_admin
359 352 login = params[:login]
360 353 user = User.find_by_login(login)
361 354 if user!=nil
362 355 admin_role = Role.find_by_name('admin')
363 356 user.roles << admin_role
364 357 else
365 358 flash[:notice] = 'Unknown user'
366 359 end
367 360 flash[:notice] = 'User added as admins'
368 361 redirect_to :action => 'admin'
369 362 end
370 363
371 364 def revoke_admin
372 365 user = User.find(params[:id])
373 366 if user==nil
374 367 flash[:notice] = 'Unknown user'
375 368 redirect_to :action => 'admin' and return
376 369 elsif user.login == 'root'
377 370 flash[:notice] = 'You cannot revoke admisnistrator permission from root.'
378 371 redirect_to :action => 'admin' and return
379 372 end
380 373
381 374 admin_role = Role.find_by_name('admin')
382 375 user.roles.delete(admin_role)
383 376 flash[:notice] = 'User permission revoked'
384 377 redirect_to :action => 'admin'
385 378 end
386 379
387 380 # mass mailing
388 381
389 382 def mass_mailing
390 383 end
391 384
392 385 def bulk_mail
393 386 lines = params[:login_list]
394 387 if !lines or lines.blank?
395 388 flash[:notice] = 'You entered an empty list.'
396 389 redirect_to :action => 'mass_mailing' and return
397 390 end
398 391
399 392 mail_subject = params[:subject]
400 393 if !mail_subject or mail_subject.blank?
401 394 flash[:notice] = 'You entered an empty mail subject.'
402 395 redirect_to :action => 'mass_mailing' and return
403 396 end
404 397
405 398 mail_body = params[:email_body]
406 399 if !mail_body or mail_body.blank?
407 400 flash[:notice] = 'You entered an empty mail body.'
408 401 redirect_to :action => 'mass_mailing' and return
409 402 end
410 403
411 404 note = []
412 405 users = []
413 406 lines.split("\n").each do |line|
414 407 user = User.find_by_login(line.chomp)
415 408 if user
416 409 send_mail(user.email, mail_subject, mail_body)
417 410 note << user.login
418 411 end
419 412 end
420 413
421 414 flash[:notice] = 'User(s) ' + note.join(', ') +
422 415 ' were successfully modified. '
423 416 redirect_to :action => 'mass_mailing'
424 417 end
425 418
426 419 #bulk manage
427 420 def bulk_manage
428 421
429 422 begin
430 423 @users = User.where('(login REGEXP ?) OR (remark REGEXP ?)',params[:regex],params[:regex]) if params[:regex]
431 424 @users.count if @users #i don't know why I have to call count, but if I won't exception is not raised
432 425 rescue Exception
433 426 flash[:error] = 'Regular Expression is malformed'
434 427 @users = nil
435 428 end
436 429
437 430 if params[:commit]
438 431 @action = {}
439 432 @action[:set_enable] = params[:enabled]
440 433 @action[:enabled] = params[:enable] == "1"
441 434 @action[:gen_password] = params[:gen_password]
442 435 @action[:add_group] = params[:add_group]
443 436 @action[:group_name] = params[:group_name]
444 437 end
445 438
446 439 if params[:commit] == "Perform"
447 440 if @action[:set_enable]
448 441 @users.update_all(enabled: @action[:enabled])
449 442 end
450 443 if @action[:gen_password]
451 444 @users.each do |u|
452 445 password = random_password
453 446 u.password = password
454 447 u.password_confirmation = password
455 448 u.save
456 449 end
457 450 end
458 451 if @action[:add_group] and @action[:group_name]
459 452 @group = Group.find(@action[:group_name])
460 453 ok = []
461 454 failed = []
462 455 @users.each do |user|
463 456 begin
464 457 @group.users << user
465 458 ok << user.login
466 459 rescue => e
467 460 failed << user.login
468 461 end
469 462 end
470 463 flash[:success] = "The following users are added to the 'group #{@group.name}': " + ok.join(', ') if ok.count > 0
471 464 flash[:alert] = "The following users are already in the 'group #{@group.name}': " + failed.join(', ') if failed.count > 0
472 465 end
473 466 end
474 467 end
475 468
476 469 protected
477 470
478 471 def random_password(length=5)
479 472 chars = 'abcdefghijkmnopqrstuvwxyz23456789'
480 473 newpass = ""
481 474 length.times { newpass << chars[rand(chars.size-1)] }
482 475 return newpass
483 476 end
484 477
485 478 def import_from_file(f)
486 479 data_hash = YAML.load(f)
487 480 @import_log = ""
488 481
489 482 country_data = data_hash[:countries]
490 483 site_data = data_hash[:sites]
491 484 user_data = data_hash[:users]
492 485
493 486 # import country
494 487 countries = {}
495 488 country_data.each_pair do |id,country|
496 489 c = Country.find_by_name(country[:name])
497 490 if c!=nil
498 491 countries[id] = c
499 492 @import_log << "Found #{country[:name]}\n"
500 493 else
501 494 countries[id] = Country.new(:name => country[:name])
502 495 countries[id].save
503 496 @import_log << "Created #{country[:name]}\n"
504 497 end
505 498 end
506 499
507 500 # import sites
508 501 sites = {}
509 502 site_data.each_pair do |id,site|
510 503 s = Site.find_by_name(site[:name])
511 504 if s!=nil
512 505 @import_log << "Found #{site[:name]}\n"
513 506 else
514 507 s = Site.new(:name => site[:name])
515 508 @import_log << "Created #{site[:name]}\n"
516 509 end
517 510 s.password = site[:password]
518 511 s.country = countries[site[:country_id]]
519 512 s.save
520 513 sites[id] = s
521 514 end
522 515
523 516 # import users
524 517 user_data.each_pair do |id,user|
525 518 u = User.find_by_login(user[:login])
526 519 if u!=nil
527 520 @import_log << "Found #{user[:login]}\n"
528 521 else
529 522 u = User.new(:login => user[:login])
530 523 @import_log << "Created #{user[:login]}\n"
531 524 end
532 525 u.full_name = user[:name]
533 526 u.password = user[:password]
534 527 u.country = countries[user[:country_id]]
535 528 u.site = sites[user[:site_id]]
536 529 u.activated = true
537 530 u.email = "empty-#{u.login}@none.com"
538 531 if not u.save
539 532 @import_log << "Errors\n"
540 533 u.errors.each { |attr,msg| @import_log << "#{attr} - #{msg}\n" }
541 534 end
542 535 end
543 536
544 537 end
545 538
546 539 def logout_users(users)
547 540 users.each do |user|
548 541 contest_stat = user.contest_stat(true)
549 542 if contest_stat and !contest_stat.forced_logout
550 543 contest_stat.forced_logout = true
551 544 contest_stat.save
552 545 end
553 546 end
554 547 end
555 548
556 549 def send_contest_update_notification_email(user, contest)
557 550 contest_title_name = GraderConfiguration['contest.name']
558 551 contest_name = contest.name
559 552 mail_subject = t('contest.notification.email_subject', {
560 553 :contest_title_name => contest_title_name,
561 554 :contest_name => contest_name })
562 555 mail_body = t('contest.notification.email_body', {
563 556 :full_name => user.full_name,
564 557 :contest_title_name => contest_title_name,
565 558 :contest_name => contest.name,
566 559 })
567 560
568 561 logger.info mail_body
569 562 send_mail(user.email, mail_subject, mail_body)
570 563 end
571 564
572 565 def find_contest_and_user_from_contest_id(id)
573 566 if id!='none'
574 567 @contest = Contest.find(id)
575 568 else
576 569 @contest = nil
577 570 end
578 571 if @contest
579 572 @users = @contest.users
580 573 else
581 574 @users = User.find_users_with_no_contest
582 575 end
583 576 return [@contest, @users]
584 577 end
585 578
586 579 def gen_csv_from_scorearray(scorearray,problem)
587 580 CSV.generate do |csv|
588 581 #add header
589 582 header = ['User','Name', 'Activated?', 'Logged in', 'Contest']
590 583 problem.each { |p| header << p.name }
591 584 header += ['Total','Passed']
592 585 csv << header
593 586 #add data
594 587 scorearray.each do |sc|
595 588 total = num_passed = 0
596 589 row = Array.new
597 590 sc.each_index do |i|
598 591 if i == 0
599 592 row << sc[i].login
600 593 row << sc[i].full_name
601 594 row << sc[i].activated
602 595 row << (sc[i].try(:contest_stat).try(:started_at).nil? ? 'no' : 'yes')
603 596 row << sc[i].contests.collect {|c| c.name}.join(', ')
604 597 else
605 598 row << sc[i][0]
606 599 total += sc[i][0]
607 600 num_passed += 1 if sc[i][1]
608 601 end
609 602 end
610 603 row << total
611 604 row << num_passed
612 605 csv << row
613 606 end
614 607 end
615 608 end
616 609
617 610 private
618 611 def user_params
619 612 params.require(:user).permit(:login,:password,:password_confirmation,:email, :alias, :full_name,:remark)
620 613 end
621 614 end
@@ -1,374 +1,376
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 + has_many :logins
26 +
25 27 has_one :contest_stat, :class_name => "UserContestStat", :dependent => :destroy
26 28
27 29 belongs_to :site
28 30 belongs_to :country
29 31
30 32 has_and_belongs_to_many :contests, -> { order(:name)}
31 33
32 34 scope :activated_users, -> {where activated: true}
33 35
34 36 validates_presence_of :login
35 37 validates_uniqueness_of :login
36 38 validates_format_of :login, :with => /\A[\_A-Za-z0-9]+\z/
37 39 validates_length_of :login, :within => 3..30
38 40
39 41 validates_presence_of :full_name
40 42 validates_length_of :full_name, :minimum => 1
41 43
42 44 validates_presence_of :password, :if => :password_required?
43 45 validates_length_of :password, :within => 4..50, :if => :password_required?
44 46 validates_confirmation_of :password, :if => :password_required?
45 47
46 48 validates_format_of :email,
47 49 :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
48 50 :if => :email_validation?
49 51 validate :uniqueness_of_email_from_activated_users,
50 52 :if => :email_validation?
51 53 validate :enough_time_interval_between_same_email_registrations,
52 54 :if => :email_validation?
53 55
54 56 # these are for ytopc
55 57 # disable for now
56 58 #validates_presence_of :province
57 59
58 60 attr_accessor :password
59 61
60 62 before_save :encrypt_new_password
61 63 before_save :assign_default_site
62 64 before_save :assign_default_contest
63 65
64 66 # this is for will_paginate
65 67 cattr_reader :per_page
66 68 @@per_page = 50
67 69
68 70 def self.authenticate(login, password)
69 71 user = find_by_login(login)
70 72 if user
71 73 return user if user.authenticated?(password)
72 74 end
73 75 end
74 76
75 77 def authenticated?(password)
76 78 if self.activated
77 79 hashed_password == User.encrypt(password,self.salt)
78 80 else
79 81 false
80 82 end
81 83 end
82 84
83 85 def admin?
84 86 self.roles.where(name: 'admin').count > 0
85 87 end
86 88
87 89 def email_for_editing
88 90 if self.email==nil
89 91 "(unknown)"
90 92 elsif self.email==''
91 93 "(blank)"
92 94 else
93 95 self.email
94 96 end
95 97 end
96 98
97 99 def email_for_editing=(e)
98 100 self.email=e
99 101 end
100 102
101 103 def alias_for_editing
102 104 if self.alias==nil
103 105 "(unknown)"
104 106 elsif self.alias==''
105 107 "(blank)"
106 108 else
107 109 self.alias
108 110 end
109 111 end
110 112
111 113 def alias_for_editing=(e)
112 114 self.alias=e
113 115 end
114 116
115 117 def activation_key
116 118 if self.hashed_password==nil
117 119 encrypt_new_password
118 120 end
119 121 Digest::SHA1.hexdigest(self.hashed_password)[0..7]
120 122 end
121 123
122 124 def verify_activation_key(key)
123 125 key == activation_key
124 126 end
125 127
126 128 def self.random_password(length=5)
127 129 chars = 'abcdefghjkmnopqrstuvwxyz'
128 130 password = ''
129 131 length.times { password << chars[rand(chars.length - 1)] }
130 132 password
131 133 end
132 134
133 135 def self.find_non_admin_with_prefix(prefix='')
134 136 users = User.all
135 137 return users.find_all { |u| !(u.admin?) and u.login.index(prefix)==0 }
136 138 end
137 139
138 140 # Contest information
139 141
140 142 def self.find_users_with_no_contest()
141 143 users = User.all
142 144 return users.find_all { |u| u.contests.length == 0 }
143 145 end
144 146
145 147
146 148 def contest_time_left
147 149 if GraderConfiguration.contest_mode?
148 150 return nil if site==nil
149 151 return site.time_left
150 152 elsif GraderConfiguration.indv_contest_mode?
151 153 time_limit = GraderConfiguration.contest_time_limit
152 154 if time_limit == nil
153 155 return nil
154 156 end
155 157 if contest_stat==nil or contest_stat.started_at==nil
156 158 return (Time.now.gmtime + time_limit) - Time.now.gmtime
157 159 else
158 160 finish_time = contest_stat.started_at + time_limit
159 161 current_time = Time.now.gmtime
160 162 if current_time > finish_time
161 163 return 0
162 164 else
163 165 return finish_time - current_time
164 166 end
165 167 end
166 168 else
167 169 return nil
168 170 end
169 171 end
170 172
171 173 def contest_finished?
172 174 if GraderConfiguration.contest_mode?
173 175 return false if site==nil
174 176 return site.finished?
175 177 elsif GraderConfiguration.indv_contest_mode?
176 178 return false if self.contest_stat==nil
177 179 return contest_time_left == 0
178 180 else
179 181 return false
180 182 end
181 183 end
182 184
183 185 def contest_started?
184 186 if GraderConfiguration.indv_contest_mode?
185 187 stat = self.contest_stat
186 188 return ((stat != nil) and (stat.started_at != nil))
187 189 elsif GraderConfiguration.contest_mode?
188 190 return true if site==nil
189 191 return site.started
190 192 else
191 193 return true
192 194 end
193 195 end
194 196
195 197 def update_start_time
196 198 stat = self.contest_stat
197 199 if stat.nil? or stat.started_at.nil?
198 200 stat ||= UserContestStat.new(:user => self)
199 201 stat.started_at = Time.now.gmtime
200 202 stat.save
201 203 end
202 204 end
203 205
204 206 def problem_in_user_contests?(problem)
205 207 problem_contests = problem.contests.all
206 208
207 209 if problem_contests.length == 0 # this is public contest
208 210 return true
209 211 end
210 212
211 213 contests.each do |contest|
212 214 if problem_contests.find {|c| c.id == contest.id }
213 215 return true
214 216 end
215 217 end
216 218 return false
217 219 end
218 220
219 221 def available_problems_group_by_contests
220 222 contest_problems = []
221 223 pin = {}
222 224 contests.enabled.each do |contest|
223 225 available_problems = contest.problems.available
224 226 contest_problems << {
225 227 :contest => contest,
226 228 :problems => available_problems
227 229 }
228 230 available_problems.each {|p| pin[p.id] = true}
229 231 end
230 232 other_avaiable_problems = Problem.available.find_all {|p| pin[p.id]==nil and p.contests.length==0}
231 233 contest_problems << {
232 234 :contest => nil,
233 235 :problems => other_avaiable_problems
234 236 }
235 237 return contest_problems
236 238 end
237 239
238 240 def solve_all_available_problems?
239 241 available_problems.each do |p|
240 242 u = self
241 243 sub = Submission.find_last_by_user_and_problem(u.id,p.id)
242 244 return false if !p or !sub or sub.points < p.full_score
243 245 end
244 246 return true
245 247 end
246 248
247 249 #get a list of available problem
248 250 def available_problems
249 251 # first, we check if this is normal mode
250 252 if not GraderConfiguration.multicontests?
251 253
252 254 #if this is a normal mode
253 255 #we show problem based on problem_group, if the config said so
254 256 if GraderConfiguration.use_problem_group?
255 257 return available_problems_in_group
256 258 else
257 259 return Problem.available_problems
258 260 end
259 261 else
260 262 #this is multi contest mode
261 263 contest_problems = []
262 264 pin = {}
263 265 contests.enabled.each do |contest|
264 266 contest.problems.available.each do |problem|
265 267 if not pin.has_key? problem.id
266 268 contest_problems << problem
267 269 end
268 270 pin[problem.id] = true
269 271 end
270 272 end
271 273 other_avaiable_problems = Problem.available.find_all {|p| pin[p.id]==nil and p.contests.length==0}
272 274 return contest_problems + other_avaiable_problems
273 275 end
274 276 end
275 277
276 278 def available_problems_in_group
277 279 problem = []
278 280 self.groups.each do |group|
279 281 group.problems.where(available: true).each { |p| problem << p }
280 282 end
281 283 problem.uniq!
282 284 if problem
283 285 problem.sort! do |a,b|
284 286 case
285 287 when a.date_added < b.date_added
286 288 1
287 289 when a.date_added > b.date_added
288 290 -1
289 291 else
290 292 a.name <=> b.name
291 293 end
292 294 end
293 295 return problem
294 296 else
295 297 return []
296 298 end
297 299 end
298 300
299 301 def can_view_problem?(problem)
300 302 return true if admin?
301 303 return available_problems.include? problem
302 304 end
303 305
304 306 def self.clear_last_login
305 307 User.update_all(:last_ip => nil)
306 308 end
307 309
308 310 protected
309 311 def encrypt_new_password
310 312 return if password.blank?
311 313 self.salt = (10+rand(90)).to_s
312 314 self.hashed_password = User.encrypt(self.password,self.salt)
313 315 end
314 316
315 317 def assign_default_site
316 318 # have to catch error when migrating (because self.site is not available).
317 319 begin
318 320 if self.site==nil
319 321 self.site = Site.find_by_name('default')
320 322 if self.site==nil
321 323 self.site = Site.find(1) # when 'default has be renamed'
322 324 end
323 325 end
324 326 rescue
325 327 end
326 328 end
327 329
328 330 def assign_default_contest
329 331 # have to catch error when migrating (because self.site is not available).
330 332 begin
331 333 if self.contests.length == 0
332 334 default_contest = Contest.find_by_name(GraderConfiguration['contest.default_contest_name'])
333 335 if default_contest
334 336 self.contests = [default_contest]
335 337 end
336 338 end
337 339 rescue
338 340 end
339 341 end
340 342
341 343 def password_required?
342 344 self.hashed_password.blank? || !self.password.blank?
343 345 end
344 346
345 347 def self.encrypt(string,salt)
346 348 Digest::SHA1.hexdigest(salt + string)
347 349 end
348 350
349 351 def uniqueness_of_email_from_activated_users
350 352 user = User.activated_users.find_by_email(self.email)
351 353 if user and (user.login != self.login)
352 354 self.errors.add(:base,"Email has already been taken")
353 355 end
354 356 end
355 357
356 358 def enough_time_interval_between_same_email_registrations
357 359 return if !self.new_record?
358 360 return if self.activated
359 361 open_user = User.find_by_email(self.email,
360 362 :order => 'created_at DESC')
361 363 if open_user and open_user.created_at and
362 364 (open_user.created_at > Time.now.gmtime - 5.minutes)
363 365 self.errors.add(:base,"There are already unactivated registrations with this e-mail address (please wait for 5 minutes)")
364 366 end
365 367 end
366 368
367 369 def email_validation?
368 370 begin
369 371 return VALIDATE_USER_EMAILS
370 372 rescue
371 373 return false
372 374 end
373 375 end
374 376 end
@@ -1,95 +1,96
1 1 %header.navbar.navbar-default.navbar-fixed-top
2 2 %nav
3 3 .container-fluid
4 4 .navbar-header
5 5 %button.navbar-toggle.collapsed{ data: {toggle: 'collapse', target: '#navbar-collapse'} }
6 6 %span.sr-only Togggle Navigation
7 7 %span.icon-bar
8 8 %span.icon-bar
9 9 %span.icon-bar
10 10 %a.navbar-brand{href: list_main_path}
11 11 %span.glyphicon.glyphicon-home
12 12 MAIN
13 13 .collapse.navbar-collapse#navbar-collapse
14 14 %ul.nav.navbar-nav
15 15 / submission
16 16 - if (@current_user!=nil) and (GraderConfiguration.show_tasks_to?(@current_user))
17 17 %li.dropdown
18 18 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
19 19 = "#{I18n.t 'menu.submissions'}"
20 20 %span.caret
21 21 %ul.dropdown-menu
22 22 = add_menu("View", 'submissions', 'index')
23 23 = add_menu("Self Test", 'test', 'index')
24 24 / hall of fame
25 25 - if GraderConfiguration['right.user_hall_of_fame']
26 26 = add_menu("#{I18n.t 'menu.hall_of_fame'}", 'report', 'problem_hof')
27 27 / display MODE button (with countdown in contest mode)
28 28 - if GraderConfiguration.analysis_mode?
29 29 %div.navbar-btn.btn.btn-success#countdown= "ANALYSIS MODE"
30 30 - elsif GraderConfiguration.time_limit_mode?
31 31 - if @current_user.contest_finished?
32 32 %div.navbar-btn.btn.btn-danger#countdown= "Contest is over"
33 33 - elsif !@current_user.contest_started?
34 34 %div.navbar-btn.btn.btn-primary#countdown= (t 'title_bar.contest_not_started')
35 35 - else
36 36 %div.navbar-btn.btn.btn-primary#countdown asdf
37 37 :javascript
38 38 $("#countdown").countdown({until: "+#{@current_user.contest_time_left.to_i}s", layout: 'Time left: {hnn}:{mnn}:{snn}'});
39 39 / admin section
40 40 - if (@current_user!=nil) and (session[:admin])
41 41 / management
42 42 %li.dropdown
43 43 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
44 44 Manage
45 45 %span.caret
46 46 %ul.dropdown-menu
47 47 = add_menu( 'Announcements', 'announcements', 'index')
48 48 = add_menu( 'Problems', 'problems', 'index')
49 49 = add_menu( 'Tags', 'tags', 'index')
50 50 = add_menu( 'Users', 'user_admin', 'index')
51 51 = add_menu( 'User Groups', 'groups', 'index')
52 52 = add_menu( 'Graders', 'graders', 'list')
53 53 = add_menu( 'Message ', 'messages', 'console')
54 54 %li.divider{role: 'separator'}
55 55 = add_menu( 'System config', 'configurations', 'index')
56 56 %li.divider{role: 'separator'}
57 57 = add_menu( 'Sites', 'sites', 'index')
58 58 = add_menu( 'Contests', 'contest_management', 'index')
59 59 / report
60 60 %li.dropdown
61 61 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
62 62 Report
63 63 %span.caret
64 64 %ul.dropdown-menu
65 65 = add_menu( 'Current Score', 'report', 'current_score')
66 66 = add_menu( 'Score Report', 'report', 'max_score')
67 67 = add_menu( 'Submission Report', 'report', 'submission')
68 + = add_menu( 'Login Report', 'report', 'login')
68 69 - if (ungraded = Submission.where('graded_at is null').where('submitted_at < ?', 1.minutes.ago).count) > 0
69 70 =link_to "#{ungraded} backlogs!",
70 71 grader_list_path,
71 72 class: 'navbar-btn btn btn-default btn-warning', data: {toggle: 'tooltip'},title: 'Number of ungraded submission'
72 73
73 74 %ul.nav.navbar-nav.navbar-right
74 75 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-question-sign')}".html_safe, 'main', 'help')
75 76 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-comment')}".html_safe, 'messages', 'index', {title: I18n.t('menu.messages'), data: {toggle: 'tooltip'}})
76 77 - if GraderConfiguration['system.user_setting_enabled']
77 78 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-cog', id: 'user_profile')}".html_safe, 'users', 'profile', {title: I18n.t('menu.settings'), data: {toggle: 'tooltip'}})
78 79 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-log-out')} #{@current_user.full_name}".html_safe, 'main', 'login', {title: I18n.t('menu.log_out'), data: {toggle: 'tooltip'}})
79 80
80 81 /
81 82 - if (@current_user!=nil) and (session[:admin])
82 83 %nav.navbar.navbar-fixed-top.navbar-inverse.secondnavbar
83 84 .container-fluid
84 85 .collapse.navbar-collapse
85 86 %ul.nav.navbar-nav
86 87 = add_menu( '[Announcements]', 'announcements', 'index')
87 88 = add_menu( '[Msg console]', 'messages', 'console')
88 89 = add_menu( '[Problems]', 'problems', 'index')
89 90 = add_menu( '[Users]', 'user_admin', 'index')
90 91 = add_menu( '[Results]', 'user_admin', 'user_stat')
91 92 = add_menu( '[Report]', 'report', 'multiple_login')
92 93 = add_menu( '[Graders]', 'graders', 'list')
93 94 = add_menu( '[Contests]', 'contest_management', 'index')
94 95 = add_menu( '[Sites]', 'sites', 'index')
95 96 = add_menu( '[System config]', 'configurations', 'index')
@@ -1,77 +1,82
1 1 - content_for :header do
2 2 = javascript_include_tag 'local_jquery'
3 3
4 4 %h1 Submissions detail
5 5
6 6 .row
7 7 .col-md-4
8 8 = render partial: 'shared/problem_select'
9 9 .col-md-4
10 10 = render partial: 'shared/date_filter'
11 11 .col-md-4
12 12 = render partial: 'shared/user_select'
13 13
14 14 .row
15 15 .col-md-6
16 16 .alert.alert-info
17 17 %ul
18 18 %li Display a maximum of 100,000 entries to save computation power
19 19 %li You have to click refresh when changing the filter above
20 20
21 21 .row
22 22 .col-sm-12
23 23 %table.table.table-hover.table-condense.datatable{style: 'width: 100%'}
24 24
25 25
26 26 :javascript
27 27 $(function() {
28 28 submission_table = $('.datatable').DataTable({
29 29 dom: "<'row'<'col-sm-3'B><'col-sm-3'l><'col-sm-6'f>>" + "<'row'<'col-sm-12'tr>>" + "<'row'<'col-sm-5'i><'col-sm-7'p>>",
30 30 autoWidth: true,
31 31 buttons: [
32 32 {
33 33 text: 'Refresh',
34 34 action: (e,dt,node,config) => {
35 35 submission_table.clear().draw()
36 36 submission_table.ajax.reload( () => { submission_table.columns.adjust().draw() } )
37 37 }
38 38 },
39 - 'copy', 'excel'
39 + 'copy',
40 + {
41 + extend: 'excel',
42 + title: 'Submission detail'
43 + }
40 44 ],
41 45 columns: [
42 46 {title: 'Sub ID', data: 'id'},
43 47 {title: 'User', data: 'user.login'},
44 48 {title: 'Problem', data: 'problem.long_name'},
45 49 {title: 'Language', data: 'language.pretty_name'},
46 50 {title: 'Submit at', data: 'submitted_at'},
47 51 {title: 'Result', data: 'grader_comment'},
48 52 {title: 'Score', data: 'points'},
49 53 {title: 'IP', data: 'ip_address'},
50 54 ],
51 55 ajax: {
52 56 url: '#{submission_query_report_path}',
53 57 type: 'POST',
54 58 data: (d) => {
55 59 d.since_datetime = $('#since_datetime').val()
56 60 d.until_datetime = $('#until_datetime').val()
57 61 d.users = $("input[name='users']:checked").val()
58 - d.problems = $("#problem_id").select2('val')
59 62 d.groups = $("#group_id").select2('val')
63 + d.problems = $("input[name='problems']:checked").val()
64 + d.problem_id = $("#problem_id").select2('val')
60 65 },
61 66 dataType: 'json',
62 67 beforeSend: (request) => {
63 68 request.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
64 69 },
65 70 }, //end ajax
66 71 'pageLength': 50,
67 72 processing: true,
68 73 });
69 74
70 75 $('.input-group.date').datetimepicker({
71 76 format: 'YYYY-MM-DD',
72 77 showTodayButton: true,
73 78 locale: 'en',
74 79 widgetPositioning: {horizontal: 'auto', vertical: 'bottom'},
75 80 defaultDate: moment()
76 81 });
77 82 });
@@ -1,10 +1,19
1 1 .panel.panel-primary
2 2 .panel-heading
3 3 Problems
4 4 .panel-body
5 - %p
6 - Select problem(s) to be included in the report
7 - = label_tag :problem_id, "Problems"
5 + .radio
6 + %label
7 + = radio_button_tag 'problems', 'all', (params[:users] == "all")
8 + All problems
9 + .radio
10 + %label
11 + = radio_button_tag 'problems', 'enabled', (params[:users] == "enabled"), checked: true
12 + Only problems with available = "yes"
13 + .radio
14 + %label
15 + = radio_button_tag 'problems', 'selected', (params[:users] == "group")
16 + Only these selected problems
8 17 = select_tag 'problem_id[]',
9 18 options_for_select(Problem.all.collect {|p| ["[#{p.name}] #{p.full_name}", p.id]},params[:problem_id]),
10 19 { id: :problem_id, class: 'select2 form-control', multiple: "true" }
@@ -1,19 +1,19
1 1 .panel.panel-primary
2 2 .panel-heading
3 3 Users
4 4 .panel-body
5 5 .radio
6 6 %label
7 7 = radio_button_tag 'users', 'all', (params[:users] == "all")
8 8 All users
9 9 .radio
10 10 %label
11 - = radio_button_tag 'users', 'enabled', (params[:users] == "enabled")
11 + = radio_button_tag 'users', 'enabled', (params[:users] == "enabled"), checked: true
12 12 Only enabled users
13 13 .radio
14 14 %label
15 15 = radio_button_tag 'users', 'group', (params[:users] == "group")
16 16 Only these groups
17 17 = select_tag 'group_id[]',
18 18 options_for_select(Group.all.collect {|g| ["[#{g.name}] #{g.description}", g.id]}),
19 19 { id: 'group_id', class: 'select2 form-control', multiple: "true"}
@@ -1,206 +1,211
1 1 Rails.application.routes.draw do
2 2 resources :tags
3 3 get "sources/direct_edit"
4 4
5 5 root :to => 'main#login'
6 6
7 7 #logins
8 8 match 'login/login', to: 'login#login', via: [:get,:post]
9 9
10 10 resources :contests
11 11 resources :sites
12 12 resources :test
13 13
14 14 resources :messages do
15 15 member do
16 16 get 'hide'
17 17 post 'reply'
18 18 end
19 19 collection do
20 20 get 'console'
21 21 get 'list_all'
22 22 end
23 23 end
24 24
25 25 resources :announcements do
26 26 member do
27 27 get 'toggle','toggle_front'
28 28 end
29 29 end
30 30
31 31 resources :problems do
32 32 member do
33 33 get 'toggle'
34 34 get 'toggle_test'
35 35 get 'toggle_view_testcase'
36 36 get 'stat'
37 37 end
38 38 collection do
39 39 get 'turn_all_off'
40 40 get 'turn_all_on'
41 41 get 'import'
42 42 get 'manage'
43 43 get 'quick_create'
44 44 post 'do_manage'
45 45 post 'do_import'
46 46 end
47 47 end
48 48
49 49 resources :groups do
50 50 member do
51 51 post 'add_user', to: 'groups#add_user', as: 'add_user'
52 52 delete 'remove_user/:user_id', to: 'groups#remove_user', as: 'remove_user'
53 53 delete 'remove_all_user', to: 'groups#remove_all_user', as: 'remove_all_user'
54 54 post 'add_problem', to: 'groups#add_problem', as: 'add_problem'
55 55 delete 'remove_problem/:problem_id', to: 'groups#remove_problem', as: 'remove_problem'
56 56 delete 'remove_all_problem', to: 'groups#remove_all_problem', as: 'remove_all_problem'
57 57 end
58 58 collection do
59 59
60 60 end
61 61 end
62 62
63 63 resources :testcases, only: [] do
64 64 member do
65 65 get 'download_input'
66 66 get 'download_sol'
67 67 end
68 68 collection do
69 69 get 'show_problem/:problem_id(/:test_num)' => 'testcases#show_problem', as: 'show_problem'
70 70 end
71 71 end
72 72
73 73 resources :grader_configuration, controller: 'configurations' do
74 74 collection do
75 75 get 'set_exam_right(/:value)', action: 'set_exam_right', as: 'set_exam_right'
76 76 end
77 77 end
78 78
79 79 resources :users do
80 80 member do
81 81 get 'toggle_activate', 'toggle_enable'
82 82 get 'stat'
83 83 end
84 84 collection do
85 85 get 'profile'
86 86 post 'chg_passwd'
87 87 end
88 88 end
89 89
90 90 resources :submissions do
91 91 member do
92 92 get 'download'
93 93 get 'compiler_msg'
94 94 get 'rejudge'
95 95 get 'source'
96 96 end
97 97 collection do
98 98 get 'prob/:problem_id', to: 'submissions#index', as: 'problem'
99 99 get 'direct_edit_problem/:problem_id(/:user_id)', to: 'submissions#direct_edit_problem', as: 'direct_edit_problem'
100 100 get 'get_latest_submission_status/:uid/:pid', to: 'submissions#get_latest_submission_status', as: 'get_latest_submission_status'
101 101 end
102 102 end
103 103
104 104
105 105 #user admin
106 106 resources :user_admin do
107 107 collection do
108 108 match 'bulk_manage', via: [:get, :post]
109 109 get 'bulk_mail'
110 110 get 'user_stat'
111 111 get 'import'
112 112 get 'new_list'
113 113 get 'admin'
114 114 get 'active'
115 115 get 'mass_mailing'
116 116 get 'revoke_admin'
117 117 post 'grant_admin'
118 118 match 'create_from_list', via: [:get, :post]
119 119 match 'random_all_passwords', via: [:get, :post]
120 120 end
121 121 member do
122 122 get 'clear_last_ip'
123 123 end
124 124 end
125 125
126 126 resources :contest_management, only: [:index] do
127 127 collection do
128 128 get 'user_stat'
129 129 get 'clear_stat'
130 130 get 'clear_all_stat'
131 131 get 'change_contest_mode'
132 132 end
133 133 end
134 134
135 135 #get 'user_admin', to: 'user_admin#index'
136 136 #get 'user_admin/bulk_manage', to: 'user_admin#bulk_manage', as: 'bulk_manage_user_admin'
137 137 #post 'user_admin', to: 'user_admin#create'
138 138 #delete 'user_admin/:id', to: 'user_admin#destroy', as: 'user_admin_destroy'
139 139
140 140 #singular resource
141 141 #---- BEWARE ---- singular resource maps to plural controller by default, we can override by provide controller name directly
142 142 #report
143 143 resource :report, only: [], controller: 'report' do
144 144 get 'login'
145 145 get 'multiple_login'
146 146 get 'problem_hof(/:id)', action: 'problem_hof', as: 'problem_hof'
147 147 get 'current_score(/:group_id)', action: 'current_score', as: 'current_score'
148 148 get 'max_score'
149 149 post 'show_max_score'
150 150 get 'stuck'
151 151 get 'cheat_report'
152 152 post 'cheat_report'
153 153 get 'cheat_scruntinize'
154 154 post 'cheat_scruntinize'
155 155 get 'submission'
156 156 post 'submission_query'
157 + get 'login_stat'
158 + post 'login_stat'
159 + get 'login'
160 + post 'login_summary_query'
161 + post 'login_detail_query'
157 162 end
158 163 #get 'report/current_score', to: 'report#current_score', as: 'report_current_score'
159 164 #get 'report/problem_hof(/:id)', to: 'report#problem_hof', as: 'report_problem_hof'
160 165 #get "report/login"
161 166 #get 'report/max_score', to: 'report#max_score', as: 'report_max_score'
162 167 #post 'report/show_max_score', to: 'report#show_max_score', as: 'report_show_max_score'
163 168
164 169 resource :main, only: [], controller: 'main' do
165 170 get 'login'
166 171 get 'logout'
167 172 get 'list'
168 173 get 'submission(/:id)', action: 'submission', as: 'main_submission'
169 174 get 'announcements'
170 175 get 'help'
171 176 post 'submit'
172 177 end
173 178 #main
174 179 #get "main/list"
175 180 #get 'main/submission(/:id)', to: 'main#submission', as: 'main_submission'
176 181 #post 'main/submit', to: 'main#submit'
177 182 #get 'main/announcements', to: 'main#announcements'
178 183
179 184
180 185 #
181 186 get 'tasks/view/:file.:ext' => 'tasks#view'
182 187 get 'tasks/download/:id/:file.:ext' => 'tasks#download'
183 188 get 'heartbeat/:id/edit' => 'heartbeat#edit'
184 189
185 190 #grader
186 191 get 'graders/list', to: 'graders#list', as: 'grader_list'
187 192 namespace :graders do
188 193 get 'task/:id/:type', action: 'task', as: 'task'
189 194 get 'view/:id/:type', action: 'view', as: 'view'
190 195 get 'clear/:id', action: 'clear', as: 'clear'
191 196 get 'stop'
192 197 get 'stop_all'
193 198 get 'clear_all'
194 199 get 'clear_terminated'
195 200 get 'start_grading'
196 201 get 'start_exam'
197 202
198 203 end
199 204
200 205
201 206 # See how all your routes lay out with "rake routes"
202 207
203 208 # This is a legacy wild controller route that's not recommended for RESTful applications.
204 209 # Note: This route will make all actions in every controller accessible via GET requests.
205 210 # match ':controller(/:action(/:id))(.:format)', via: [:get, :post]
206 211 end
@@ -1,308 +1,309
1 1 # This file is auto-generated from the current state of the database. Instead
2 2 # of editing this file, please use the migrations feature of Active Record to
3 3 # incrementally modify your database, and then regenerate this schema definition.
4 4 #
5 5 # Note that this schema.rb definition is the authoritative source for your
6 6 # database schema. If you need to create the application database on another
7 7 # system, you should be using db:schema:load, not running all the migrations
8 8 # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 9 # you'll amass, the slower it'll run and the greater likelihood for issues).
10 10 #
11 11 # It's strongly recommended that you check this file into your version control system.
12 12
13 - ActiveRecord::Schema.define(version: 2020_04_04_142959) do
13 + ActiveRecord::Schema.define(version: 2020_04_05_112919) do
14 14
15 15 create_table "announcements", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
16 16 t.string "author"
17 17 t.text "body", limit: 16777215
18 18 t.boolean "published"
19 19 t.datetime "created_at", null: false
20 20 t.datetime "updated_at", null: false
21 21 t.boolean "frontpage", default: false
22 22 t.boolean "contest_only", default: false
23 23 t.string "title"
24 24 t.string "notes"
25 25 end
26 26
27 27 create_table "contests", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
28 28 t.string "title"
29 29 t.boolean "enabled"
30 30 t.datetime "created_at", null: false
31 31 t.datetime "updated_at", null: false
32 32 t.string "name"
33 33 end
34 34
35 35 create_table "contests_problems", id: false, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
36 36 t.integer "contest_id"
37 37 t.integer "problem_id"
38 38 end
39 39
40 40 create_table "contests_users", id: false, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
41 41 t.integer "contest_id"
42 42 t.integer "user_id"
43 43 end
44 44
45 45 create_table "countries", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
46 46 t.string "name"
47 47 t.datetime "created_at", null: false
48 48 t.datetime "updated_at", null: false
49 49 end
50 50
51 51 create_table "descriptions", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
52 52 t.text "body", limit: 16777215
53 53 t.boolean "markdowned"
54 54 t.datetime "created_at", null: false
55 55 t.datetime "updated_at", null: false
56 56 end
57 57
58 58 create_table "grader_configurations", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
59 59 t.string "key"
60 60 t.string "value_type"
61 61 t.string "value"
62 62 t.datetime "created_at", null: false
63 63 t.datetime "updated_at", null: false
64 64 t.text "description", limit: 16777215
65 65 end
66 66
67 67 create_table "grader_processes", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
68 68 t.string "host"
69 69 t.integer "pid"
70 70 t.string "mode"
71 71 t.boolean "active"
72 72 t.datetime "created_at", null: false
73 73 t.datetime "updated_at", null: false
74 74 t.integer "task_id"
75 75 t.string "task_type"
76 76 t.boolean "terminated"
77 77 t.index ["host", "pid"], name: "index_grader_processes_on_ip_and_pid"
78 78 end
79 79
80 80 create_table "groups", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
81 81 t.string "name"
82 82 t.string "description"
83 83 end
84 84
85 85 create_table "groups_problems", id: false, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
86 86 t.integer "problem_id", null: false
87 87 t.integer "group_id", null: false
88 88 t.index ["group_id", "problem_id"], name: "index_groups_problems_on_group_id_and_problem_id"
89 89 end
90 90
91 91 create_table "groups_users", options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
92 92 t.integer "group_id", null: false
93 93 t.integer "user_id", null: false
94 94 t.index ["user_id", "group_id"], name: "index_groups_users_on_user_id_and_group_id"
95 95 end
96 96
97 97 create_table "heart_beats", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
98 98 t.integer "user_id"
99 99 t.string "ip_address"
100 100 t.datetime "created_at", null: false
101 101 t.datetime "updated_at", null: false
102 102 t.string "status"
103 103 t.index ["updated_at"], name: "index_heart_beats_on_updated_at"
104 104 end
105 105
106 106 create_table "languages", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
107 107 t.string "name", limit: 10
108 108 t.string "pretty_name"
109 109 t.string "ext", limit: 10
110 110 t.string "common_ext"
111 111 end
112 112
113 113 create_table "logins", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
114 114 t.integer "user_id"
115 115 t.string "ip_address"
116 116 t.datetime "created_at", null: false
117 117 t.datetime "updated_at", null: false
118 + t.index ["user_id"], name: "index_logins_on_user_id"
118 119 end
119 120
120 121 create_table "messages", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
121 122 t.integer "sender_id"
122 123 t.integer "receiver_id"
123 124 t.integer "replying_message_id"
124 125 t.text "body", limit: 16777215
125 126 t.boolean "replied"
126 127 t.datetime "created_at", null: false
127 128 t.datetime "updated_at", null: false
128 129 end
129 130
130 131 create_table "problems", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
131 132 t.string "name", limit: 100
132 133 t.string "full_name"
133 134 t.integer "full_score"
134 135 t.date "date_added"
135 136 t.boolean "available"
136 137 t.string "url"
137 138 t.integer "description_id"
138 139 t.boolean "test_allowed"
139 140 t.boolean "output_only"
140 141 t.string "description_filename"
141 142 t.boolean "view_testcase"
142 143 end
143 144
144 145 create_table "problems_tags", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
145 146 t.integer "problem_id"
146 147 t.integer "tag_id"
147 148 t.index ["problem_id", "tag_id"], name: "index_problems_tags_on_problem_id_and_tag_id", unique: true
148 149 t.index ["problem_id"], name: "index_problems_tags_on_problem_id"
149 150 t.index ["tag_id"], name: "index_problems_tags_on_tag_id"
150 151 end
151 152
152 153 create_table "rights", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
153 154 t.string "name"
154 155 t.string "controller"
155 156 t.string "action"
156 157 end
157 158
158 159 create_table "rights_roles", id: false, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
159 160 t.integer "right_id"
160 161 t.integer "role_id"
161 162 t.index ["role_id"], name: "index_rights_roles_on_role_id"
162 163 end
163 164
164 165 create_table "roles", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
165 166 t.string "name"
166 167 end
167 168
168 169 create_table "roles_users", id: false, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
169 170 t.integer "role_id"
170 171 t.integer "user_id"
171 172 t.index ["user_id"], name: "index_roles_users_on_user_id"
172 173 end
173 174
174 175 create_table "sessions", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
175 176 t.string "session_id"
176 177 t.text "data", limit: 16777215
177 178 t.datetime "updated_at"
178 179 t.index ["session_id"], name: "index_sessions_on_session_id"
179 180 t.index ["updated_at"], name: "index_sessions_on_updated_at"
180 181 end
181 182
182 183 create_table "sites", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
183 184 t.string "name"
184 185 t.boolean "started"
185 186 t.datetime "start_time"
186 187 t.datetime "created_at", null: false
187 188 t.datetime "updated_at", null: false
188 189 t.integer "country_id"
189 190 t.string "password"
190 191 end
191 192
192 193 create_table "submission_view_logs", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
193 194 t.integer "user_id"
194 195 t.integer "submission_id"
195 196 t.datetime "created_at", null: false
196 197 t.datetime "updated_at", null: false
197 198 end
198 199
199 200 create_table "submissions", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
200 201 t.integer "user_id"
201 202 t.integer "problem_id"
202 203 t.integer "language_id"
203 204 t.text "source", limit: 16777215
204 205 t.binary "binary"
205 206 t.datetime "submitted_at"
206 207 t.datetime "compiled_at"
207 208 t.text "compiler_message", limit: 16777215
208 209 t.datetime "graded_at"
209 210 t.integer "points"
210 211 t.text "grader_comment", limit: 16777215
211 212 t.integer "number"
212 213 t.string "source_filename"
213 214 t.float "max_runtime"
214 215 t.integer "peak_memory"
215 216 t.integer "effective_code_length"
216 217 t.string "ip_address"
217 218 t.index ["submitted_at"], name: "index_submissions_on_submitted_at"
218 219 t.index ["user_id", "problem_id", "number"], name: "index_submissions_on_user_id_and_problem_id_and_number", unique: true
219 220 t.index ["user_id", "problem_id"], name: "index_submissions_on_user_id_and_problem_id"
220 221 end
221 222
222 223 create_table "tags", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
223 224 t.string "name", null: false
224 225 t.text "description"
225 226 t.boolean "public"
226 227 t.datetime "created_at", null: false
227 228 t.datetime "updated_at", null: false
228 229 end
229 230
230 231 create_table "tasks", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
231 232 t.integer "submission_id"
232 233 t.datetime "created_at"
233 234 t.integer "status"
234 235 t.datetime "updated_at"
235 236 t.index ["submission_id"], name: "index_tasks_on_submission_id"
236 237 end
237 238
238 239 create_table "test_pairs", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
239 240 t.integer "problem_id"
240 241 t.text "input", limit: 4294967295
241 242 t.text "solution", limit: 4294967295
242 243 t.datetime "created_at", null: false
243 244 t.datetime "updated_at", null: false
244 245 end
245 246
246 247 create_table "test_requests", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
247 248 t.integer "user_id"
248 249 t.integer "problem_id"
249 250 t.integer "submission_id"
250 251 t.string "input_file_name"
251 252 t.string "output_file_name"
252 253 t.string "running_stat"
253 254 t.integer "status"
254 255 t.datetime "updated_at", null: false
255 256 t.datetime "submitted_at"
256 257 t.datetime "compiled_at"
257 258 t.text "compiler_message", limit: 16777215
258 259 t.datetime "graded_at"
259 260 t.string "grader_comment"
260 261 t.datetime "created_at", null: false
261 262 t.float "running_time"
262 263 t.string "exit_status"
263 264 t.integer "memory_usage"
264 265 t.index ["user_id", "problem_id"], name: "index_test_requests_on_user_id_and_problem_id"
265 266 end
266 267
267 268 create_table "testcases", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t|
268 269 t.integer "problem_id"
269 270 t.integer "num"
270 271 t.integer "group"
271 272 t.integer "score"
272 273 t.text "input", limit: 4294967295
273 274 t.text "sol", limit: 4294967295
274 275 t.datetime "created_at", null: false
275 276 t.datetime "updated_at", null: false
276 277 t.index ["problem_id"], name: "index_testcases_on_problem_id"
277 278 end
278 279
279 280 create_table "user_contest_stats", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
280 281 t.integer "user_id"
281 282 t.datetime "started_at"
282 283 t.datetime "created_at", null: false
283 284 t.datetime "updated_at", null: false
284 285 t.boolean "forced_logout"
285 286 end
286 287
287 288 create_table "users", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
288 289 t.string "login", limit: 50
289 290 t.string "full_name"
290 291 t.string "hashed_password"
291 292 t.string "salt", limit: 5
292 293 t.string "alias"
293 294 t.string "email"
294 295 t.integer "site_id"
295 296 t.integer "country_id"
296 297 t.boolean "activated", default: false
297 298 t.datetime "created_at"
298 299 t.datetime "updated_at"
299 300 t.string "section"
300 301 t.boolean "enabled", default: true
301 302 t.string "remark"
302 303 t.string "last_ip"
303 304 t.index ["login"], name: "index_users_on_login", unique: true
304 305 end
305 306
306 307 add_foreign_key "problems_tags", "problems"
307 308 add_foreign_key "problems_tags", "tags"
308 309 end
deleted file
You need to be logged in to leave comments. Login now