Description:
- streamline current score report and fix some bug
Commit status:
[Not Reviewed]
References:
Comments:
0 Commit comments 0 Inline Comments
Unresolved TODOs:
There are no unresolved TODOs
Add another comment

r610:0c5b815f0163 - - 3 files changed: 12 inserted, 9 deleted

@@ -332,207 +332,207
332 user = User.find_by_login(login)
332 user = User.find_by_login(login)
333 if user!=nil
333 if user!=nil
334 admin_role = Role.find_by_name('admin')
334 admin_role = Role.find_by_name('admin')
335 user.roles << admin_role
335 user.roles << admin_role
336 else
336 else
337 flash[:notice] = 'Unknown user'
337 flash[:notice] = 'Unknown user'
338 end
338 end
339 flash[:notice] = 'User added as admins'
339 flash[:notice] = 'User added as admins'
340 redirect_to :action => 'admin'
340 redirect_to :action => 'admin'
341 end
341 end
342
342
343 def revoke_admin
343 def revoke_admin
344 user = User.find(params[:id])
344 user = User.find(params[:id])
345 if user==nil
345 if user==nil
346 flash[:notice] = 'Unknown user'
346 flash[:notice] = 'Unknown user'
347 redirect_to :action => 'admin' and return
347 redirect_to :action => 'admin' and return
348 elsif user.login == 'root'
348 elsif user.login == 'root'
349 flash[:notice] = 'You cannot revoke admisnistrator permission from root.'
349 flash[:notice] = 'You cannot revoke admisnistrator permission from root.'
350 redirect_to :action => 'admin' and return
350 redirect_to :action => 'admin' and return
351 end
351 end
352
352
353 admin_role = Role.find_by_name('admin')
353 admin_role = Role.find_by_name('admin')
354 user.roles.delete(admin_role)
354 user.roles.delete(admin_role)
355 flash[:notice] = 'User permission revoked'
355 flash[:notice] = 'User permission revoked'
356 redirect_to :action => 'admin'
356 redirect_to :action => 'admin'
357 end
357 end
358
358
359 # mass mailing
359 # mass mailing
360
360
361 def mass_mailing
361 def mass_mailing
362 end
362 end
363
363
364 def bulk_mail
364 def bulk_mail
365 lines = params[:login_list]
365 lines = params[:login_list]
366 if !lines or lines.blank?
366 if !lines or lines.blank?
367 flash[:notice] = 'You entered an empty list.'
367 flash[:notice] = 'You entered an empty list.'
368 redirect_to :action => 'mass_mailing' and return
368 redirect_to :action => 'mass_mailing' and return
369 end
369 end
370
370
371 mail_subject = params[:subject]
371 mail_subject = params[:subject]
372 if !mail_subject or mail_subject.blank?
372 if !mail_subject or mail_subject.blank?
373 flash[:notice] = 'You entered an empty mail subject.'
373 flash[:notice] = 'You entered an empty mail subject.'
374 redirect_to :action => 'mass_mailing' and return
374 redirect_to :action => 'mass_mailing' and return
375 end
375 end
376
376
377 mail_body = params[:email_body]
377 mail_body = params[:email_body]
378 if !mail_body or mail_body.blank?
378 if !mail_body or mail_body.blank?
379 flash[:notice] = 'You entered an empty mail body.'
379 flash[:notice] = 'You entered an empty mail body.'
380 redirect_to :action => 'mass_mailing' and return
380 redirect_to :action => 'mass_mailing' and return
381 end
381 end
382
382
383 note = []
383 note = []
384 users = []
384 users = []
385 lines.split("\n").each do |line|
385 lines.split("\n").each do |line|
386 user = User.find_by_login(line.chomp)
386 user = User.find_by_login(line.chomp)
387 if user
387 if user
388 send_mail(user.email, mail_subject, mail_body)
388 send_mail(user.email, mail_subject, mail_body)
389 note << user.login
389 note << user.login
390 end
390 end
391 end
391 end
392
392
393 flash[:notice] = 'User(s) ' + note.join(', ') +
393 flash[:notice] = 'User(s) ' + note.join(', ') +
394 ' were successfully modified. '
394 ' were successfully modified. '
395 redirect_to :action => 'mass_mailing'
395 redirect_to :action => 'mass_mailing'
396 end
396 end
397
397
398 protected
398 protected
399
399
400 def random_password(length=5)
400 def random_password(length=5)
401 chars = 'abcdefghijkmnopqrstuvwxyz23456789'
401 chars = 'abcdefghijkmnopqrstuvwxyz23456789'
402 newpass = ""
402 newpass = ""
403 length.times { newpass << chars[rand(chars.size-1)] }
403 length.times { newpass << chars[rand(chars.size-1)] }
404 return newpass
404 return newpass
405 end
405 end
406
406
407 def import_from_file(f)
407 def import_from_file(f)
408 data_hash = YAML.load(f)
408 data_hash = YAML.load(f)
409 @import_log = ""
409 @import_log = ""
410
410
411 country_data = data_hash[:countries]
411 country_data = data_hash[:countries]
412 site_data = data_hash[:sites]
412 site_data = data_hash[:sites]
413 user_data = data_hash[:users]
413 user_data = data_hash[:users]
414
414
415 # import country
415 # import country
416 countries = {}
416 countries = {}
417 country_data.each_pair do |id,country|
417 country_data.each_pair do |id,country|
418 c = Country.find_by_name(country[:name])
418 c = Country.find_by_name(country[:name])
419 if c!=nil
419 if c!=nil
420 countries[id] = c
420 countries[id] = c
421 @import_log << "Found #{country[:name]}\n"
421 @import_log << "Found #{country[:name]}\n"
422 else
422 else
423 countries[id] = Country.new(:name => country[:name])
423 countries[id] = Country.new(:name => country[:name])
424 countries[id].save
424 countries[id].save
425 @import_log << "Created #{country[:name]}\n"
425 @import_log << "Created #{country[:name]}\n"
426 end
426 end
427 end
427 end
428
428
429 # import sites
429 # import sites
430 sites = {}
430 sites = {}
431 site_data.each_pair do |id,site|
431 site_data.each_pair do |id,site|
432 s = Site.find_by_name(site[:name])
432 s = Site.find_by_name(site[:name])
433 if s!=nil
433 if s!=nil
434 @import_log << "Found #{site[:name]}\n"
434 @import_log << "Found #{site[:name]}\n"
435 else
435 else
436 s = Site.new(:name => site[:name])
436 s = Site.new(:name => site[:name])
437 @import_log << "Created #{site[:name]}\n"
437 @import_log << "Created #{site[:name]}\n"
438 end
438 end
439 s.password = site[:password]
439 s.password = site[:password]
440 s.country = countries[site[:country_id]]
440 s.country = countries[site[:country_id]]
441 s.save
441 s.save
442 sites[id] = s
442 sites[id] = s
443 end
443 end
444
444
445 # import users
445 # import users
446 user_data.each_pair do |id,user|
446 user_data.each_pair do |id,user|
447 u = User.find_by_login(user[:login])
447 u = User.find_by_login(user[:login])
448 if u!=nil
448 if u!=nil
449 @import_log << "Found #{user[:login]}\n"
449 @import_log << "Found #{user[:login]}\n"
450 else
450 else
451 u = User.new(:login => user[:login])
451 u = User.new(:login => user[:login])
452 @import_log << "Created #{user[:login]}\n"
452 @import_log << "Created #{user[:login]}\n"
453 end
453 end
454 u.full_name = user[:name]
454 u.full_name = user[:name]
455 u.password = user[:password]
455 u.password = user[:password]
456 u.country = countries[user[:country_id]]
456 u.country = countries[user[:country_id]]
457 u.site = sites[user[:site_id]]
457 u.site = sites[user[:site_id]]
458 u.activated = true
458 u.activated = true
459 u.email = "empty-#{u.login}@none.com"
459 u.email = "empty-#{u.login}@none.com"
460 if not u.save
460 if not u.save
461 @import_log << "Errors\n"
461 @import_log << "Errors\n"
462 u.errors.each { |attr,msg| @import_log << "#{attr} - #{msg}\n" }
462 u.errors.each { |attr,msg| @import_log << "#{attr} - #{msg}\n" }
463 end
463 end
464 end
464 end
465
465
466 end
466 end
467
467
468 def logout_users(users)
468 def logout_users(users)
469 users.each do |user|
469 users.each do |user|
470 contest_stat = user.contest_stat(true)
470 contest_stat = user.contest_stat(true)
471 if contest_stat and !contest_stat.forced_logout
471 if contest_stat and !contest_stat.forced_logout
472 contest_stat.forced_logout = true
472 contest_stat.forced_logout = true
473 contest_stat.save
473 contest_stat.save
474 end
474 end
475 end
475 end
476 end
476 end
477
477
478 def send_contest_update_notification_email(user, contest)
478 def send_contest_update_notification_email(user, contest)
479 contest_title_name = GraderConfiguration['contest.name']
479 contest_title_name = GraderConfiguration['contest.name']
480 contest_name = contest.name
480 contest_name = contest.name
481 mail_subject = t('contest.notification.email_subject', {
481 mail_subject = t('contest.notification.email_subject', {
482 :contest_title_name => contest_title_name,
482 :contest_title_name => contest_title_name,
483 :contest_name => contest_name })
483 :contest_name => contest_name })
484 mail_body = t('contest.notification.email_body', {
484 mail_body = t('contest.notification.email_body', {
485 :full_name => user.full_name,
485 :full_name => user.full_name,
486 :contest_title_name => contest_title_name,
486 :contest_title_name => contest_title_name,
487 :contest_name => contest.name,
487 :contest_name => contest.name,
488 })
488 })
489
489
490 logger.info mail_body
490 logger.info mail_body
491 send_mail(user.email, mail_subject, mail_body)
491 send_mail(user.email, mail_subject, mail_body)
492 end
492 end
493
493
494 def find_contest_and_user_from_contest_id(id)
494 def find_contest_and_user_from_contest_id(id)
495 if id!='none'
495 if id!='none'
496 @contest = Contest.find(id)
496 @contest = Contest.find(id)
497 else
497 else
498 @contest = nil
498 @contest = nil
499 end
499 end
500 if @contest
500 if @contest
501 @users = @contest.users
501 @users = @contest.users
502 else
502 else
503 @users = User.find_users_with_no_contest
503 @users = User.find_users_with_no_contest
504 end
504 end
505 return [@contest, @users]
505 return [@contest, @users]
506 end
506 end
507
507
508 def gen_csv_from_scorearray(scorearray,problem)
508 def gen_csv_from_scorearray(scorearray,problem)
509 CSV.generate do |csv|
509 CSV.generate do |csv|
510 #add header
510 #add header
511 header = ['User','Name', 'Activated?', 'Logged in', 'Contest']
511 header = ['User','Name', 'Activated?', 'Logged in', 'Contest']
512 problem.each { |p| header << p.name }
512 problem.each { |p| header << p.name }
513 header += ['Total','Passed']
513 header += ['Total','Passed']
514 csv << header
514 csv << header
515 #add data
515 #add data
516 scorearray.each do |sc|
516 scorearray.each do |sc|
517 total = num_passed = 0
517 total = num_passed = 0
518 row = Array.new
518 row = Array.new
519 sc.each_index do |i|
519 sc.each_index do |i|
520 if i == 0
520 if i == 0
521 row << sc[i].login
521 row << sc[i].login
522 row << sc[i].full_name
522 row << sc[i].full_name
523 row << sc[i].activated
523 row << sc[i].activated
524 - row << (sc[i].try(:contest_stat).try(:started_at).nil? 'no' : 'yes')
524 + row << (sc[i].try(:contest_stat).try(:started_at).nil? ? 'no' : 'yes')
525 row << sc[i].contests.collect {|c| c.name}.join(', ')
525 row << sc[i].contests.collect {|c| c.name}.join(', ')
526 else
526 else
527 row << sc[i][0]
527 row << sc[i][0]
528 total += sc[i][0]
528 total += sc[i][0]
529 num_passed += 1 if sc[i][1]
529 num_passed += 1 if sc[i][1]
530 end
530 end
531 end
531 end
532 row << total
532 row << total
533 row << num_passed
533 row << num_passed
534 csv << row
534 csv << row
535 end
535 end
536 end
536 end
537 end
537 end
538 end
538 end
@@ -1,84 +1,87
1 %header.navbar.navbar-default.navbar-fixed-top
1 %header.navbar.navbar-default.navbar-fixed-top
2 %nav
2 %nav
3 .container-fluid
3 .container-fluid
4 .navbar-header
4 .navbar-header
5 %a.navbar-brand{href: main_list_path}
5 %a.navbar-brand{href: main_list_path}
6 %span.glyphicon.glyphicon-home
6 %span.glyphicon.glyphicon-home
7 MAIN
7 MAIN
8 .collapse.navbar-collapse
8 .collapse.navbar-collapse
9 %ul.nav.navbar-nav
9 %ul.nav.navbar-nav
10 - if (@current_user!=nil) and (GraderConfiguration.show_tasks_to?(@current_user))
10 - if (@current_user!=nil) and (GraderConfiguration.show_tasks_to?(@current_user))
11 //= add_menu("#{I18n.t 'menu.tasks'}", 'tasks', 'list')
11 //= add_menu("#{I18n.t 'menu.tasks'}", 'tasks', 'list')
12 %li.dropdown
12 %li.dropdown
13 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
13 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
14 = "#{I18n.t 'menu.submissions'}"
14 = "#{I18n.t 'menu.submissions'}"
15 %span.caret
15 %span.caret
16 %ul.dropdown-menu
16 %ul.dropdown-menu
17 = add_menu("View", 'main', 'submission')
17 = add_menu("View", 'main', 'submission')
18 = add_menu("Self Test", 'test', 'index')
18 = add_menu("Self Test", 'test', 'index')
19 - if GraderConfiguration['right.user_hall_of_fame']
19 - if GraderConfiguration['right.user_hall_of_fame']
20 = add_menu("#{I18n.t 'menu.hall_of_fame'}", 'report', 'problem_hof')
20 = add_menu("#{I18n.t 'menu.hall_of_fame'}", 'report', 'problem_hof')
21 / display MODE button (with countdown in contest mode)
21 / display MODE button (with countdown in contest mode)
22 - if GraderConfiguration.analysis_mode?
22 - if GraderConfiguration.analysis_mode?
23 %div.navbar-btn.btn.btn-success#countdown= "ANALYSIS MODE"
23 %div.navbar-btn.btn.btn-success#countdown= "ANALYSIS MODE"
24 - elsif GraderConfiguration.time_limit_mode?
24 - elsif GraderConfiguration.time_limit_mode?
25 - if @current_user.contest_finished?
25 - if @current_user.contest_finished?
26 %div.navbar-btn.btn.btn-danger#countdown= "Contest is over"
26 %div.navbar-btn.btn.btn-danger#countdown= "Contest is over"
27 - elsif !@current_user.contest_started?
27 - elsif !@current_user.contest_started?
28 %div.navbar-btn.btn.btn-primary#countdown= (t 'title_bar.contest_not_started')
28 %div.navbar-btn.btn.btn-primary#countdown= (t 'title_bar.contest_not_started')
29 - else
29 - else
30 %div.navbar-btn.btn.btn-primary#countdown asdf
30 %div.navbar-btn.btn.btn-primary#countdown asdf
31 :javascript
31 :javascript
32 $("#countdown").countdown({until: "+#{@current_user.contest_time_left.to_i}s", layout: 'Time left: {hnn}:{mnn}:{snn}'});
32 $("#countdown").countdown({until: "+#{@current_user.contest_time_left.to_i}s", layout: 'Time left: {hnn}:{mnn}:{snn}'});
33 / admin section
33 / admin section
34 - if (@current_user!=nil) and (session[:admin])
34 - if (@current_user!=nil) and (session[:admin])
35 + / management
35 %li.dropdown
36 %li.dropdown
36 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
37 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
37 Manage
38 Manage
38 %span.caret
39 %span.caret
39 %ul.dropdown-menu
40 %ul.dropdown-menu
40 = add_menu( 'Announcements', 'announcements', 'index')
41 = add_menu( 'Announcements', 'announcements', 'index')
41 = add_menu( 'Problems', 'problems', 'index')
42 = add_menu( 'Problems', 'problems', 'index')
42 = add_menu( 'Users', 'user_admin', 'index')
43 = add_menu( 'Users', 'user_admin', 'index')
43 = add_menu( 'Graders', 'graders', 'list')
44 = add_menu( 'Graders', 'graders', 'list')
44 = add_menu( 'Message ', 'messages', 'console')
45 = add_menu( 'Message ', 'messages', 'console')
45 %li.divider{role: 'separator'}
46 %li.divider{role: 'separator'}
46 = add_menu( 'System config', 'configurations', 'index')
47 = add_menu( 'System config', 'configurations', 'index')
47 %li.divider{role: 'separator'}
48 %li.divider{role: 'separator'}
48 = add_menu( 'Sites', 'sites', 'index')
49 = add_menu( 'Sites', 'sites', 'index')
49 = add_menu( 'Contests', 'contest_management', 'index')
50 = add_menu( 'Contests', 'contest_management', 'index')
51 + / report
50 %li.dropdown
52 %li.dropdown
51 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
53 %a.dropdown-toggle{href: '#', data: {toggle:'dropdown'}, aria: {haspopup:"true", expanded:"false"}, role: "button"}
52 Report
54 Report
53 %span.caret
55 %span.caret
54 %ul.dropdown-menu
56 %ul.dropdown-menu
55 - = add_menu( 'Results', 'report', 'current_score')
57 + = add_menu( 'Current Score', 'report', 'current_score')
58 + = add_menu( 'Score Report', 'report', 'max_score')
56 = add_menu( 'Report', 'report', 'multiple_login')
59 = add_menu( 'Report', 'report', 'multiple_login')
57 - if (ungraded = Submission.where('graded_at is null').where('submitted_at < ?', 1.minutes.ago).count) > 0
60 - if (ungraded = Submission.where('graded_at is null').where('submitted_at < ?', 1.minutes.ago).count) > 0
58 =link_to "#{ungraded} backlogs!",
61 =link_to "#{ungraded} backlogs!",
59 grader_list_path,
62 grader_list_path,
60 class: 'navbar-btn btn btn-default btn-warning', data: {toggle: 'tooltip'},title: 'Number of ungraded submission'
63 class: 'navbar-btn btn btn-default btn-warning', data: {toggle: 'tooltip'},title: 'Number of ungraded submission'
61
64
62 %ul.nav.navbar-nav.navbar-right
65 %ul.nav.navbar-nav.navbar-right
63 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-question-sign')}".html_safe, 'main', 'help')
66 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-question-sign')}".html_safe, 'main', 'help')
64 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-comment')}".html_safe, 'messages', 'list', {title: I18n.t('menu.messages'), data: {toggle: 'tooltip'}})
67 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-comment')}".html_safe, 'messages', 'list', {title: I18n.t('menu.messages'), data: {toggle: 'tooltip'}})
65 - if GraderConfiguration['system.user_setting_enabled']
68 - if GraderConfiguration['system.user_setting_enabled']
66 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-cog')}".html_safe, 'users', 'index', {title: I18n.t('menu.settings'), data: {toggle: 'tooltip'}})
69 = add_menu("#{content_tag(:span,'',class: 'glyphicon glyphicon-cog')}".html_safe, 'users', 'index', {title: I18n.t('menu.settings'), data: {toggle: 'tooltip'}})
67 = 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'}})
70 = 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'}})
68
71
69 /
72 /
70 - if (@current_user!=nil) and (session[:admin])
73 - if (@current_user!=nil) and (session[:admin])
71 %nav.navbar.navbar-fixed-top.navbar-inverse.secondnavbar
74 %nav.navbar.navbar-fixed-top.navbar-inverse.secondnavbar
72 .container-fluid
75 .container-fluid
73 .collapse.navbar-collapse
76 .collapse.navbar-collapse
74 %ul.nav.navbar-nav
77 %ul.nav.navbar-nav
75 = add_menu( '[Announcements]', 'announcements', 'index')
78 = add_menu( '[Announcements]', 'announcements', 'index')
76 = add_menu( '[Msg console]', 'messages', 'console')
79 = add_menu( '[Msg console]', 'messages', 'console')
77 = add_menu( '[Problems]', 'problems', 'index')
80 = add_menu( '[Problems]', 'problems', 'index')
78 = add_menu( '[Users]', 'user_admin', 'index')
81 = add_menu( '[Users]', 'user_admin', 'index')
79 = add_menu( '[Results]', 'user_admin', 'user_stat')
82 = add_menu( '[Results]', 'user_admin', 'user_stat')
80 = add_menu( '[Report]', 'report', 'multiple_login')
83 = add_menu( '[Report]', 'report', 'multiple_login')
81 = add_menu( '[Graders]', 'graders', 'list')
84 = add_menu( '[Graders]', 'graders', 'list')
82 = add_menu( '[Contests]', 'contest_management', 'index')
85 = add_menu( '[Contests]', 'contest_management', 'index')
83 = add_menu( '[Sites]', 'sites', 'index')
86 = add_menu( '[Sites]', 'sites', 'index')
84 = add_menu( '[System config]', 'configurations', 'index')
87 = add_menu( '[System config]', 'configurations', 'index')
@@ -1,34 +1,34
1 %table.table.sortable.table-striped.table-bordered.table-condensed
1 %table.table.sortable.table-striped.table-bordered.table-condensed
2 %thead
2 %thead
3 %tr
3 %tr
4 %th Login
4 %th Login
5 %th Name
5 %th Name
6 - %th Activated?
6 + / %th Activated?
7 - %th Logged_in
7 + / %th Logged_in
8 - %th Contest(s)
8 + / %th Contest(s)
9 %th Remark
9 %th Remark
10 - @problems.each do |p|
10 - @problems.each do |p|
11 - %th.text-right= p.name
11 + %th.text-right= p.name.gsub('_',' ')
12 %th.text-right Total
12 %th.text-right Total
13 %th.text-right Passed
13 %th.text-right Passed
14 %tbody
14 %tbody
15 - @scorearray.each do |sc|
15 - @scorearray.each do |sc|
16 %tr
16 %tr
17 - total,num_passed = 0,0
17 - total,num_passed = 0,0
18 - sc.each_index do |i|
18 - sc.each_index do |i|
19 - if i == 0
19 - if i == 0
20 %td= link_to sc[i].login, controller: 'users', action: 'profile', id: sc[i]
20 %td= link_to sc[i].login, controller: 'users', action: 'profile', id: sc[i]
21 %td= sc[i].full_name
21 %td= sc[i].full_name
22 - %td= sc[i].activated
22 + / %td= sc[i].activated
23 - %td= sc[i].try(:contest_stat).try(:started_at) ? 'yes' : 'no'
23 + / %td= sc[i].try(:contest_stat).try(:started_at) ? 'yes' : 'no'
24 - %td= sc[i].contests.collect {|c| c.name}.join(', ')
24 + / %td= sc[i].contests.collect {|c| c.name}.join(', ')
25 %td= sc[i].remark
25 %td= sc[i].remark
26 - else
26 - else
27 %td.text-right= sc[i][0]
27 %td.text-right= sc[i][0]
28 - total += sc[i][0]
28 - total += sc[i][0]
29 - num_passed += 1 if sc[i][1]
29 - num_passed += 1 if sc[i][1]
30 %td.text-right= total
30 %td.text-right= total
31 %td.text-right= num_passed
31 %td.text-right= num_passed
32
32
33 :javascript
33 :javascript
34 $.bootstrapSortable(true,'reversed')
34 $.bootstrapSortable(true,'reversed')
You need to be logged in to leave comments. Login now