diff --git a/Gemfile b/Gemfile --- a/Gemfile +++ b/Gemfile @@ -41,6 +41,8 @@ gem 'jquery-ui-sass-rails' gem 'jquery-timepicker-addon-rails' +#syntax highlighter +gem 'rouge' gem "haml" gem "mail" diff --git a/Gemfile.lock b/Gemfile.lock --- a/Gemfile.lock +++ b/Gemfile.lock @@ -104,6 +104,7 @@ rdiscount (2.1.7.1) rdoc (3.12.2) json (~> 1.4) + rouge (1.6.2) rspec-collection_matchers (1.0.0) rspec-expectations (>= 2.99.0.beta1) rspec-core (2.99.2) @@ -158,6 +159,7 @@ prototype-rails rails (= 3.2.19) rdiscount + rouge rspec-rails (~> 2.0) sass-rails (~> 3.2.3) test-unit diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,7 +6,12 @@ def admin_authorization return false unless authenticate user = User.find(session[:user_id], :include => ['roles']) - redirect_to :controller => 'main', :action => 'login' unless user.admin? + unless user.admin? + flash[:notice] = 'You are not authorized to view the page you requested' + redirect_to :controller => 'main', :action => 'login' unless user.admin? + return false + end + return true end def authorization_by_roles(allowed_roles) @@ -23,6 +28,10 @@ def authenticate unless session[:user_id] + flash[:notice] = 'You need to login' + if GraderConfiguration[SINGLE_USER_MODE_CONF_KEY] + flash[:notice] = 'You need to login but you cannot log in at this time' + end redirect_to :controller => 'main', :action => 'login' return false end diff --git a/app/controllers/graders_controller.rb b/app/controllers/graders_controller.rb --- a/app/controllers/graders_controller.rb +++ b/app/controllers/graders_controller.rb @@ -1,6 +1,15 @@ class GradersController < ApplicationController - before_filter :admin_authorization + before_filter :admin_authorization, except: [ :submission ] + before_filter(only: [:submission]) { + return false unless authenticate + + if GraderConfiguration["right.user_view_submission"] + return true; + end + + admin_authorization + } verify :method => :post, :only => ['clear_all', 'start_exam', @@ -63,6 +72,18 @@ def submission @submission = Submission.find(params[:id]) + formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', line_numbers: true ) + lexer = case @submission.language.name + when "c" then Rouge::Lexers::C.new + when "cpp" then Rouge::Lexers::Cpp.new + when "pas" then Rouge::Lexers::Pas.new + when "ruby" then Rouge::Lexers::Ruby.new + when "python" then Rouge::Lexers::Python.new + when "java" then Rouge::Lexers::Java.new + end + @formatted_code = formatter.format(lexer.lex(@submission.source)) + @css_style = Rouge::Themes::ThankfulEyes.render(scope: '.highlight') + end # various grader controls diff --git a/app/controllers/report_controller.rb b/app/controllers/report_controller.rb --- a/app/controllers/report_controller.rb +++ b/app/controllers/report_controller.rb @@ -1,4 +1,16 @@ class ReportController < ApplicationController + + before_filter :admin_authorization, only: [:login_stat,:submission_stat] + before_filter(only: [:problem_hof]) { |c| + return false unless authenticate + + if GraderConfiguration["right.user_view_submission"] + return true; + end + + admin_authorization + } + def login_stat @logins = Array.new @@ -69,4 +81,108 @@ end end end + + def problem_hof + # gen problem list + @user = User.find(session[:user_id]) + @problems = @user.available_problems + + # get selected problems or the default + if params[:id] + begin + @problem = Problem.available.find(params[:id]) + rescue + redirect_to action: :problem_hof + flash[:notice] = 'Error: submissions for that problem are not viewable.' + return + end + end + + if @problem + #aggregrate by language + @by_lang = {} + Submission.where(problem_id: @problem.id).find_each do |sub| + lang = Language.find_by_id(sub.language_id) + next unless lang + next unless sub.points >= @problem.full_score + + #initialize + unless @by_lang.has_key?(lang.pretty_name) + @by_lang[lang.pretty_name] = { + runtime: { avail: false, value: 2**30-1 }, + memory: { avail: false, value: 2**30-1 }, + length: { avail: false, value: 2**30-1 }, + first: { avail: false, value: DateTime.new(3000,1,1) } + } + end + + if sub.max_runtime and sub.max_runtime < @by_lang[lang.pretty_name][:runtime][:value] + @by_lang[lang.pretty_name][:runtime] = { + avail: true, + user_id: sub.user_id, + value: sub.max_runtime, + sub_id: sub.id + } + end + + if sub.peak_memory and sub.peak_memory < @by_lang[lang.pretty_name][:memory][:value] + @by_lang[lang.pretty_name][:memory] = { + avail: true, + user_id: sub.user_id, + value: sub.peak_memory, + sub_id: sub.id + } + end + + if sub.submitted_at and sub.submitted_at < @by_lang[lang.pretty_name][:first][:value] and + !sub.user.admin? + @by_lang[lang.pretty_name][:first] = { + avail: true, + user_id: sub.user_id, + value: sub.submitted_at, + sub_id: sub.id + } + end + + if @by_lang[lang.pretty_name][:length][:value] > sub.effective_code_length + @by_lang[lang.pretty_name][:length] = { + avail: true, + user_id: sub.user_id, + value: sub.effective_code_length, + sub_id: sub.id + } + end + end + + #process user_id + @by_lang.each do |lang,prop| + prop.each do |k,v| + v[:user] = User.exists?(v[:user_id]) ? User.find(v[:user_id]).full_name : "(NULL)" + end + end + + #sum into best + if @by_lang and @by_lang.first + @best = @by_lang.first[1] + @by_lang.each do |lang,prop| + if @best[:runtime][:value] >= prop[:runtime][:value] + @best[:runtime] = prop[:runtime] + @best[:runtime][:lang] = lang + end + if @best[:memory][:value] >= prop[:memory][:value] + @best[:memory] = prop[:memory] + @best[:memory][:lang] = lang + end + if @best[:length][:value] >= prop[:length][:value] + @best[:length] = prop[:length] + @best[:length][:lang] = lang + end + if @best[:first][:value] >= prop[:first][:value] + @best[:first] = prop[:first] + @best[:first][:lang] = lang + end + end + end + end + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -30,6 +30,10 @@ append_to menu_items, "[#{I18n.t 'menu.submissions'}]", 'main', 'submission' append_to menu_items, "[#{I18n.t 'menu.test'}]", 'test', 'index' end + + if GraderConfiguration['right.user_hall_of_fame'] + append_to menu_items, "[#{I18n.t 'menu.hall_of_fame'}]", 'report', 'problem_hof' + end append_to menu_items, "[#{I18n.t 'menu.help'}]", 'main', 'help' if GraderConfiguration['system.user_setting_enabled'] diff --git a/app/views/graders/submission.html.haml b/app/views/graders/submission.html.haml --- a/app/views/graders/submission.html.haml +++ b/app/views/graders/submission.html.haml @@ -1,8 +1,11 @@ +%style{type: "text/css"} + = @css_style + %h1= "Submission: #{@submission.id}" %p User: - = "#{@submission.user.login}" + = "(#{@submission.user.login}) #{@submission.user.full_name}" %br/ Problem: - if @submission.problem!=nil @@ -13,10 +16,12 @@ = "Number: #{@submission.number}" %br/ = "Submitted at: #{format_short_time(@submission.submitted_at)}" + %br/ + = "Points : #{@submission.points}/#{@submission.problem.full_score}" + %br/ + = "Comment : #{@submission.grader_comment}" %b Source code (first 10kb) -%div{:style => "border: 1px solid black; background: lightgrey"} - - if @submission.source - %pre - =h truncate @submission.source, :length => 10240 +//%div.highlight{:style => "border: 1px solid black;"} +=@formatted_code.html_safe diff --git a/app/views/report/_all_time_hof.html.haml b/app/views/report/_all_time_hof.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/_all_time_hof.html.haml @@ -0,0 +1,8 @@ +%h2 Paid in Full +User with highest number of problem solved + +%h2 Polymaths +User with highest number of problems each solved by more than 1 languages. + +%h2 Icebreakers +If you solve the problem before 95% of your friends, you are an icebreaker. diff --git a/app/views/report/_report_menu.html.haml b/app/views/report/_report_menu.html.haml --- a/app/views/report/_report_menu.html.haml +++ b/app/views/report/_report_menu.html.haml @@ -2,5 +2,6 @@ .task-menu Reports %br/ + = link_to '[Hall of Fame]', :action => 'problem_hof' = link_to '[Submission]', :action => 'submission_stat' = link_to '[Login]', :action => 'login_stat' diff --git a/app/views/report/_task_hof.html.haml b/app/views/report/_task_hof.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/_task_hof.html.haml @@ -0,0 +1,62 @@ +:css + .hof_user { color: orangered; font-style: italic; } + .hof_language { color: green; font-style: italic; } + .hof_value { color: deeppink;font-style: italic; } + +%h2 Overall + +- if @best + %b Best Runtime: + by #{@best[:runtime][:user]} using #{@best[:runtime][:lang]} with #{@best[:runtime][:value] * 1000} milliseconds at submission + = link_to("#" + @best[:runtime][:sub_id].to_s, controller: 'graders', action: 'submission', id:@best[:runtime][:sub_id]) + %br/ + + %b Best Memory Usage: + by #{@best[:memory][:user]} using #{@best[:memory][:lang]} with #{@best[:memory][:value]} kbytes at submission + = link_to("#" + @best[:memory][:sub_id].to_s, controller: 'graders' , action: 'submission', id:@best[:memory][:sub_id]) + %br/ + + %b Shortest Code: + by #{@best[:length][:user]} using #{@best[:length][:lang]} with #{@best[:length][:value]} bytes at submission + = link_to("#" + @best[:length][:sub_id].to_s, controller: 'graders' , action: 'submission', id: @best[:length][:sub_id]) + %br/ + + %b First solver: + #{@best[:first][:user]} is the first solver using #{@best[:first][:lang]} on #{@best[:first][:value]} at submission + = link_to("#" + @best[:first][:sub_id].to_s, controller: 'graders' , action: 'submission', id: @best[:first][:sub_id]) + %br/ + + + %p + This counts only for submission with 100% score
+ Right now, java is excluded from memory usage competition. (Because it always uses 2GB memory...) + + %h2 By language + + %table.info + %thead + %tr.info-head + %th Language + %th Best runtime (ms) + %th Best memory (kbytes) + %th Shortest Code (bytes) + %th First solver + %tbody + - @by_lang.each do |lang,value| + %tr{class: cycle('info-even','info-odd')} + %td= lang + %td + = "#{value[:runtime][:user]} (#{(value[:runtime][:value] * 1000).to_i} @" + = "#{link_to("#" + value[:runtime][:sub_id].to_s, controller: 'graders' , action: 'submission', id: value[:runtime][:sub_id])} )".html_safe + %td + = "#{value[:memory][:user]} (#{value[:memory][:value]} @" + = "#{link_to("#" + value[:memory][:sub_id].to_s, controller: 'graders' , action: 'submission', id: value[:memory][:sub_id])} )".html_safe + %td + = "#{value[:length][:user]} (#{value[:length][:value]} @" + = "#{link_to("#" + value[:length][:sub_id].to_s, controller: 'graders' , action: 'submission', id: value[:length][:sub_id])} )".html_safe + %td + = "#{value[:first][:user]} (#{value[:first][:value]} @" + = "#{link_to("#" + value[:first][:sub_id].to_s, controller: 'graders' , action: 'submission', id: value[:first][:sub_id])} )".html_safe + +- else + %h3 No submissions diff --git a/app/views/report/login_stat.html.haml b/app/views/report/login_stat.html.haml --- a/app/views/report/login_stat.html.haml +++ b/app/views/report/login_stat.html.haml @@ -7,10 +7,8 @@ $('#until_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); }); - %h1 Login status - =render partial: 'report_menu' =render partial: 'date_range', locals: {param_text: 'Login date range:', title: 'Query login stat in the range' } diff --git a/app/views/report/problem_hof.html.haml b/app/views/report/problem_hof.html.haml new file mode 100644 --- /dev/null +++ b/app/views/report/problem_hof.html.haml @@ -0,0 +1,22 @@ + +/- if params[:id] +/ %h1 Tasks Hall of Fame +/ = link_to('[back to All-Time Hall of Fame]', action: 'problem_hof', id: nil ) +/- else +/ %h1 All-Time Hall of Fame + + +%h1 Tasks Hall of Fame +.task-menu + Tasks + %br/ + - @problems.each do |prob| + = link_to( "[#{prob.name}]", {id: prob.id}) + +- unless params[:id] + /=render partial: 'all_time_hof' + Please select a problem. +- else + =render partial: 'task_hof' + + diff --git a/app/views/report/submission_stat.html.haml b/app/views/report/submission_stat.html.haml --- a/app/views/report/submission_stat.html.haml +++ b/app/views/report/submission_stat.html.haml @@ -7,12 +7,8 @@ $('#until_datetime').datetimepicker({ showButtonPanel: true, dateFormat: "yy-mm-dd", controlType: "slider"} ); }); - %h1 Login status - - - =render partial: 'report_menu' =render partial: 'date_range', locals: {param_text: 'Submission date range:', title: 'Query submission stat in the range' } diff --git a/config/application.rb.SAMPLE b/config/application.rb.SAMPLE --- a/config/application.rb.SAMPLE +++ b/config/application.rb.SAMPLE @@ -59,6 +59,6 @@ # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' - config.assets.precompile += ['announcement_refresh.js','effects.js','site_update.js','graders.css','problems.css'] + config.assets.precompile += ['announcement_refresh.js','effects.js','site_update.js','graders.css','problems.css','new.js'] end end diff --git a/config/locales/en.yml b/config/locales/en.yml --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -20,6 +20,7 @@ tasks: 'Tasks' submissions: 'Submissions' test: 'Test Interface' + hall_of_fame: 'Hall of Fame' help: 'Help' settings: 'Settings' log_out: 'Log out' diff --git a/config/locales/th.yml b/config/locales/th.yml --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -20,6 +20,7 @@ tasks: 'โจทย์' submissions: 'โปรแกรมที่ส่ง' test: 'ทดสอบโปรแกรม' + hall_of_fame: 'หอเกียรติยศ' help: 'ความช่วยเหลือ' settings: 'เปลี่ยนรหัสผ่าน' log_out: 'ออกจากระบบ' diff --git a/db/migrate/20140823031747_add_more_detail_to_submission.rb b/db/migrate/20140823031747_add_more_detail_to_submission.rb new file mode 100644 --- /dev/null +++ b/db/migrate/20140823031747_add_more_detail_to_submission.rb @@ -0,0 +1,7 @@ +class AddMoreDetailToSubmission < ActiveRecord::Migration + def change + add_column :submissions, :max_runtime, :float + add_column :submissions, :peak_memory, :integer + add_column :submissions, :effective_code_length, :integer + end +end diff --git a/db/seeds.rb b/db/seeds.rb --- a/db/seeds.rb +++ b/db/seeds.rb @@ -6,60 +6,67 @@ :default_value => 'false', :description => 'Only admins can log in to the system when running under single user mode.' }, - + { :key => 'ui.front.title', :value_type => 'string', :default_value => 'Grader' }, - + { :key => 'ui.front.welcome_message', :value_type => 'string', :default_value => 'Welcome!' }, - + { :key => 'ui.show_score', :value_type => 'boolean', :default_value => 'true' }, - + { :key => 'contest.time_limit', :value_type => 'string', :default_value => 'unlimited', :description => 'Time limit in format hh:mm, or "unlimited" for contests with no time limits. This config is CACHED. Restart the server before the change can take effect.' }, - + { :key => 'system.mode', :value_type => 'string', :default_value => 'standard', :description => 'Current modes are "standard", "contest", "indv-contest", and "analysis".' }, - + { :key => 'contest.name', :value_type => 'string', :default_value => 'Grader', :description => 'This name will be shown on the user header bar.' }, - + { :key => 'contest.multisites', :value_type => 'boolean', :default_value => 'false', :description => 'If the server is in contest mode and this option is true, on the log in of the admin a menu for site selections is shown.' }, - + { - :key => 'system.online_registration', + :key => 'right.user_hall_of_fame', :value_type => 'boolean', :default_value => 'false', - :description => 'This option enables online registration.' + :description => 'If true, any user can access hall of fame page.' }, - + + { + :key => 'right.user_view_submission', + :value_type => 'boolean', + :default_value => 'false', + :description => 'If true, any user can view submissions of every one.' + }, + # If Configuration['system.online_registration'] is true, the # system allows online registration, and will use these # information for sending confirmation emails. @@ -68,26 +75,33 @@ :value_type => 'string', :default_value => 'smtp.somehost.com' }, - + { :key => 'system.online_registration.from', :value_type => 'string', :default_value => 'your.email@address' }, - + { :key => 'system.admin_email', :value_type => 'string', :default_value => 'admin@admin.email' }, - + { :key => 'system.user_setting_enabled', :value_type => 'boolean', :default_value => 'true', :description => 'If this option is true, users can change their settings' }, - + + { + :key => 'system.user_setting_enabled', + :value_type => 'boolean', + :default_value => 'true', + :description => 'If this option is true, users can change their settings' + } + # If Configuration['contest.test_request.early_timeout'] is true # the user will not be able to use test request at 30 minutes # before the contest ends. @@ -115,7 +129,7 @@ :default_value => 'none', :description => "New user will be assigned to this contest automatically, if it exists. Set to 'none' if there is no default contest." } - + ] @@ -194,5 +208,16 @@ seed_root end +def seed_more_languages + Language.delete_all + Language.create( name: 'c', pretty_name: 'C', ext: 'c', common_ext: 'c' ) + Language.create( name: 'cpp', pretty_name: 'C++', ext: 'cpp', common_ext: 'cpp,cc' ) + Language.create( name: 'pas', pretty_name: 'Pascal', ext: 'pas', common_ext: 'pas' ) + Language.create( name: 'ruby', pretty_name: 'Ruby', ext: 'rb', common_ext: 'rb' ) + Language.create( name: 'python', pretty_name: 'Python', ext: 'py', common_ext: 'py' ) + Language.create( name: 'java', pretty_name: 'Java', ext: 'java', common_ext: 'java' ) +end + seed_config seed_users_and_roles +seed_more_languages